tuqin
6 天以前 5d8a5addb80d0f1663862963b76a1a4a7d9295fd
feat(hrm): 薪酬核算确认后自动给员工发送工资条通知(站内信)

配合移动端 APP:核算确认后员工手机端「消息」里能立刻收到工资条通知。

改动:
- HrmSalaryCalculationServiceImpl.confirmSalaryCalculation 中,每条核算
确认成功后调用 sendPayslipNotice 发站内信
- 新增 sendPayslipNotice:用模板 salary_payslip_notice,变量传
period 与 actualSalary,走 system 模块的 NotifyMessageSendApi
- 通知失败只记 warn 日志,不影响核算确认主流程(核心事务不因通知而回滚)

SQL(hrm_salary_notify_template_init.sql,可重复执行):
- 新建站内信模板「薪资条通知」
内容:您的 {period} 工资条已生成,实发工资 {actualSalary} 元,请打开 APP 查看详情。

验证(线上实测):
- 核算确认后自动生成站内信,变量正确替换为 2026-09 / 17858.00
- ly101 在 APP 消息列表能看到该通知,ly102 看不到(数据隔离正确)
已添加1个文件
已修改1个文件
78 ■■■■■ 文件已修改
sql/mysql/hrm_salary_notify_template_init.sql 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_salary_notify_template_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
-- =====================================================================
-- è–ªèµ„核算通知(站内信模板)
--
-- ç”¨é€”:HR åœ¨åŽå°ã€Œè–ªé…¬æ ¸ç®— â†’ ç¡®è®¤ã€åŽï¼Œç³»ç»Ÿè‡ªåŠ¨ç»™æ¯ä½å‘˜å·¥å‘ä¸€æ¡ç«™å†…ä¿¡ï¼Œ
--       å‘˜å·¥åœ¨ç§»åŠ¨ç«¯ APP çš„「消息」里即可看到;后续接入极光推送后,
--       è¿˜ä¼šé¢å¤–收到手机通知栏提醒。
--
-- æ¨¡æ¿å˜é‡ï¼š
--   {period}        æ ¸ç®—周期,如 2026-09
--   {actualSalary}  å®žå‘工资
--
-- ç‰¹æ€§ï¼šå¯é‡å¤æ‰§è¡Œ
-- =====================================================================
DELETE FROM system_notify_template WHERE code = 'salary_payslip_notice';
INSERT INTO system_notify_template
  (name, code, nickname, content, type, params, status, remark,
   creator, create_time, updater, update_time, deleted)
VALUES
  ('薪资条通知',
   'salary_payslip_notice',
   '薪酬管理',
   '您的 {period} å·¥èµ„条已生成,实发工资 {actualSalary} å…ƒï¼Œè¯·æ‰“å¼€ APP æŸ¥çœ‹è¯¦æƒ…。',
   1,
   '["period","actualSalary"]',
   0,
   '薪酬核算确认后自动发送给员工',
   '1', NOW(), '1', NOW(), b'0');
-- ---------------------------------------------------------------------
-- éªŒè¯
-- ---------------------------------------------------------------------
SELECT id, name, code, nickname, content, type, params, status
FROM system_notify_template
WHERE code = 'salary_payslip_notice';
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -32,6 +32,9 @@
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryStructureMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
import lombok.extern.slf4j.Slf4j;
import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -44,6 +47,7 @@
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -60,6 +64,7 @@
 */
@Service
@Validated
@Slf4j
public class HrmSalaryCalculationServiceImpl implements HrmSalaryCalculationService {
    /**
@@ -89,6 +94,8 @@
    private HrmNoRedisDAO noRedisDAO;
    @Resource
    private DeptApi deptApi;
    @Resource
    private NotifyMessageSendApi notifyMessageSendApi;
    @Resource
    private HrmDeductionService deductionService;
    @Resource
@@ -491,6 +498,8 @@
            salaryCalculationMapper.updateById(salaryCalculation);
            // è®°å½•周期
            period = salaryCalculation.getPeriod();
            // ç¡®è®¤åŽç»™è¯¥å‘˜å·¥å‘送工资条通知(失败不影响确认主流程)
            sendPayslipNotice(salaryCalculation);
        }
        // æ£€æŸ¥è¯¥å‘¨æœŸæ˜¯å¦æ‰€æœ‰æ ¸ç®—记录都已确认,自动生成发放台账
@@ -502,6 +511,39 @@
    /**
     * æ£€æŸ¥è¯¥å‘¨æœŸæ˜¯å¦æ‰€æœ‰æ ¸ç®—记录都已确认,如果全部确认则自动生成发放台账
     */
    /**
     * å·¥èµ„条通知的站内信模板编码
     *
     * å¯¹åº” system_notify_template.code = salary_payslip_notice
     */
    private static final String TEMPLATE_CODE_PAYSLIP = "salary_payslip_notice";
    /**
     * ç»™å‘˜å·¥å‘送工资条通知(APP ç«™å†…信)
     *
     * é€šçŸ¥å¤±è´¥åªè®°æ—¥å¿—,不影响核算确认主流程
     */
    private void sendPayslipNotice(HrmSalaryCalculationDO calculation) {
        if (calculation == null || calculation.getUserId() == null) {
            return;
        }
        try {
            NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO();
            reqDTO.setUserId(calculation.getUserId());
            reqDTO.setTemplateCode(TEMPLATE_CODE_PAYSLIP);
            Map<String, Object> params = new HashMap<>();
            params.put("period", calculation.getPeriod());
            params.put("actualSalary", calculation.getActualSalary());
            reqDTO.setTemplateParams(params);
            notifyMessageSendApi.sendSingleMessageToAdmin(reqDTO);
            log.info("[sendPayslipNotice] å·¥èµ„条通知已发送,userId={}, period={}",
                    calculation.getUserId(), calculation.getPeriod());
        } catch (Exception e) {
            log.warn("[sendPayslipNotice] å·¥èµ„条通知发送失败,userId={}, period={}",
                    calculation.getUserId(), calculation.getPeriod(), e);
        }
    }
    private void autoGeneratePaymentIfAllConfirmed(String period) {
        // æŸ¥è¯¢è¯¥å‘¨æœŸæ‰€æœ‰æ ¸ç®—记录
        List<HrmSalaryCalculationDO> allCalculations = salaryCalculationMapper.selectListByPeriod(period);