From 5d8a5addb80d0f1663862963b76a1a4a7d9295fd Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期二, 03 十一月 2026 10:11:12 +0800
Subject: [PATCH] feat(hrm): 薪酬核算确认后自动给员工发送工资条通知(站内信)
---
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 112 insertions(+), 1 deletions(-)
diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
index 8be05ab..08218c8 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
+++ b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -6,6 +6,8 @@
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryCalculationPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmOvertimeApplicationDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmEmployeeScheduleDO;
+import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmShiftDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSocialSecurityDO;
@@ -16,6 +18,8 @@
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSocialSecuritySchemeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmEmployeeScheduleMapper;
+import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmShiftMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmOvertimeApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
@@ -28,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;
@@ -39,8 +46,12 @@
import java.time.LocalDate;
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;
+import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -53,6 +64,7 @@
*/
@Service
@Validated
+@Slf4j
public class HrmSalaryCalculationServiceImpl implements HrmSalaryCalculationService {
/**
@@ -83,6 +95,8 @@
@Resource
private DeptApi deptApi;
@Resource
+ private NotifyMessageSendApi notifyMessageSendApi;
+ @Resource
private HrmDeductionService deductionService;
@Resource
private HrmLeaveApplicationMapper leaveApplicationMapper;
@@ -96,10 +110,35 @@
private HrmSalaryPaymentMapper salaryPaymentMapper;
@Resource
private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper;
+ @Resource
+ private HrmEmployeeScheduleMapper employeeScheduleMapper;
+ @Resource
+ private HrmShiftMapper shiftMapper;
@Override
public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) {
return salaryCalculationMapper.selectPage(pageReqVO);
+ }
+
+ @Override
+ public PageResult<HrmSalaryCalculationDO> getMySalaryCalculationPage(Long userId, HrmSalaryCalculationPageReqVO pageReqVO) {
+ // 寮哄埗闄愬畾涓哄綋鍓嶇櫥褰曚汉锛氬拷鐣ュ墠绔紶鍏ョ殑 userId / deptId锛岄槻姝㈣秺鏉冩煡鐪嬩粬浜哄伐璧�
+ pageReqVO.setUserId(userId);
+ pageReqVO.setDeptId(null);
+ return salaryCalculationMapper.selectPage(pageReqVO);
+ }
+
+ @Override
+ public HrmSalaryCalculationDO getMySalaryCalculation(Long id, Long userId) {
+ HrmSalaryCalculationDO calculation = salaryCalculationMapper.selectById(id);
+ if (calculation == null) {
+ return null;
+ }
+ // 涓嶅睘浜庢湰浜虹殑璁板綍涓�寰嬭涓轰笉瀛樺湪锛岄伩鍏嶉�氳繃鐚� id 瓒婃潈鏌ョ湅浠栦汉宸ヨ祫
+ if (!java.util.Objects.equals(calculation.getUserId(), userId)) {
+ return null;
+ }
+ return calculation;
}
@Override
@@ -225,6 +264,11 @@
OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary);
BigDecimal overtimePay = overtimeSalary.pay();
+ // 鎸夋帓鐝疮璁″嚭鍕ゅぉ鏁颁笌鎺掔彮鎬诲伐鏃讹紙鏃犳帓鐝椂鍑哄嫟澶╂暟鍥為��榛樿 22锛�
+ ScheduleStat scheduleStat = calculateScheduleStat(employeeId, period);
+ Integer workDays = scheduleStat.scheduledDays() > 0 ? scheduleStat.scheduledDays() : 22;
+ BigDecimal totalWorkHours = scheduleStat.totalWorkHours();
+
// 搴斿彂宸ヨ祫鎬婚
BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance)
.add(transportAllowance).add(otherAllowance).add(overtimePay);
@@ -295,8 +339,9 @@
.otherDeduction(otherDeduction)
.leaveDeduction(leaveDeduction)
.actualSalary(actualSalary)
- .workDays(22) // 榛樿宸ヤ綔鏃�
+ .workDays(workDays) // 鎸夋帓鐝疮璁″簲鍑哄嫟澶╂暟锛屾棤鎺掔彮鏃跺洖閫� 22
.overtimeHours(overtimeSalary.hours())
+ .totalWorkHours(totalWorkHours)
.status(0) // 寰呯‘璁�
.build();
}
@@ -401,6 +446,37 @@
private record OvertimeSalary(BigDecimal hours, BigDecimal pay) {}
+ private record ScheduleStat(int scheduledDays, BigDecimal totalWorkHours) {}
+
+ /**
+ * 鎸夋帓鐝〃绱鍛樺伐鐨勫嚭鍕ゅぉ鏁颁笌鎺掔彮鎬诲伐鏃躲��
+ * 浠呯粺璁℃湁鐝鐨勬帓鐝褰曪紝鐝鏍囧噯宸ユ椂涔嬪拰鍗充负鎺掔彮宸ユ椂銆�
+ */
+ private ScheduleStat calculateScheduleStat(Long employeeId, String period) {
+ YearMonth yearMonth = YearMonth.parse(period);
+ List<HrmEmployeeScheduleDO> schedules = employeeScheduleMapper.selectListByEmployeeIdAndPeriod(
+ employeeId, yearMonth.atDay(1), yearMonth.atEndOfMonth());
+ if (CollUtil.isEmpty(schedules)) {
+ return new ScheduleStat(0, BigDecimal.ZERO);
+ }
+ Set<Long> shiftIds = schedules.stream().map(HrmEmployeeScheduleDO::getShiftId)
+ .filter(Objects::nonNull).collect(Collectors.toSet());
+ Map<Long, HrmShiftDO> shiftMap = CollUtil.isEmpty(shiftIds) ? Collections.emptyMap()
+ : shiftMapper.selectBatchIds(shiftIds).stream()
+ .collect(Collectors.toMap(HrmShiftDO::getId, shift -> shift, (a, b) -> a));
+ int scheduledDays = 0;
+ BigDecimal totalWorkHours = BigDecimal.ZERO;
+ for (HrmEmployeeScheduleDO schedule : schedules) {
+ HrmShiftDO shift = schedule.getShiftId() == null ? null : shiftMap.get(schedule.getShiftId());
+ if (shift == null) {
+ continue;
+ }
+ scheduledDays++;
+ totalWorkHours = totalWorkHours.add(shift.getWorkHours() != null ? shift.getWorkHours() : BigDecimal.ZERO);
+ }
+ return new ScheduleStat(scheduledDays, totalWorkHours.setScale(2, RoundingMode.HALF_UP));
+ }
+
@Override
@Transactional(rollbackFor = Exception.class)
public void confirmSalaryCalculation(List<Long> ids) {
@@ -422,6 +498,8 @@
salaryCalculationMapper.updateById(salaryCalculation);
// 璁板綍鍛ㄦ湡
period = salaryCalculation.getPeriod();
+ // 纭鍚庣粰璇ュ憳宸ュ彂閫佸伐璧勬潯閫氱煡锛堝け璐ヤ笉褰卞搷纭涓绘祦绋嬶級
+ sendPayslipNotice(salaryCalculation);
}
// 妫�鏌ヨ鍛ㄦ湡鏄惁鎵�鏈夋牳绠楄褰曢兘宸茬‘璁わ紝鑷姩鐢熸垚鍙戞斁鍙拌处
@@ -433,6 +511,39 @@
/**
* 妫�鏌ヨ鍛ㄦ湡鏄惁鎵�鏈夋牳绠楄褰曢兘宸茬‘璁わ紝濡傛灉鍏ㄩ儴纭鍒欒嚜鍔ㄧ敓鎴愬彂鏀惧彴璐�
*/
+ /**
+ * 宸ヨ祫鏉¢�氱煡鐨勭珯鍐呬俊妯℃澘缂栫爜
+ *
+ * 瀵瑰簲 system_notify_template.code = salary_payslip_notice
+ */
+ private static final String TEMPLATE_CODE_PAYSLIP = "salary_payslip_notice";
+
+ /**
+ * 缁欏憳宸ュ彂閫佸伐璧勬潯閫氱煡锛圓PP 绔欏唴淇★級
+ *
+ * 閫氱煡澶辫触鍙鏃ュ織锛屼笉褰卞搷鏍哥畻纭涓绘祦绋�
+ */
+ 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);
--
Gitblit v1.9.3