3 天以前 b852254d90e7d1955db31db154193ec8ee84d8b3
feat(hrm): 新增员工用工性质、班组归属字段及考勤异常处理优化

- 在 HrmEmployeeDO 中添加用工性质(employmentType)和所属班组(teamId)字段
- 重构考勤异常审批监听器,支持加班申请审批通过后回写加班时长功能
- 实现加班时长计算逻辑,基于规则下班时间与实际打卡时间差值计算
- 优化补卡和加班审批通过后的考勤记录更新机制
- 新增证书和合同到期提醒功能,集成懒触发机制
- 添加培训活动和转正申请编号生成功能
- 扩展绩效考核范围功能,支持按班组生成被考核人列表
- 优化员工列表展示,新增班组名称和用工性质名称显示
- 新增转正申请和培训管理相关错误码常量定义
已添加89个文件
已修改83个文件
8071 ■■■■■ 文件已修改
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/workhour/HrmWorkHourSummaryApi.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/workhour/dto/HrmWorkHourSummaryRespDTO.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmEmploymentTypeEnum.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/pom.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/api/workhour/HrmWorkHourSummaryApiImpl.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmRegularizeApplicationController.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmRegularizeApplicationPageReqVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmRegularizeApplicationRespVO.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmRegularizeApplicationSaveReqVO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/certificate/HrmCertificateController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/certificate/vo/HrmCertificatePageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performanceappraise/HrmPerformanceAppraiseController.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performanceappraise/vo/HrmPerformanceAppraiseRespVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performanceappraise/vo/HrmPerformanceAppraiseSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmEmployeeSalaryController.java 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryStructureController.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmEmployeeSalaryPageReqVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmEmployeeSalaryRespVO.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmEmployeeSalarySaveReqVO.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructurePageReqVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureRespVO.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureSaveReqVO.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/HrmTrainingController.java 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/HrmTrainingParticipantController.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingPageReqVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingParticipantAddReqVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingParticipantRespVO.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingParticipantUpdateReqVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingRespVO.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingSaveReqVO.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/workhour/HrmWorkHourSummaryController.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/workhour/vo/HrmWorkHourSummaryPageReqVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/workhour/vo/HrmWorkHourSummaryRespVO.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmRegularizeApplicationDO.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeDO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performanceappraise/HrmPerformanceAppraiseDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/training/HrmTrainingDO.java 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/training/HrmTrainingParticipantDO.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/workhour/HrmWorkHourSummaryDO.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmRegularizeApplicationMapper.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/certificate/HrmCertificateMapper.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeContractMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/performanceappraise/HrmPerformanceAppraiseMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmSalaryStructureMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/training/HrmTrainingMapper.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/training/HrmTrainingParticipantMapper.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/workhour/HrmWorkHourSummaryMapper.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmExpiryRemindRedisDAO.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmRedisKeyConstants.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/job/HrmExpiryRemindJob.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmRegularizeApplicationService.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmRegularizeApplicationServiceImpl.java 307 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmRegularizeApplicationStatusListener.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmResignationApplicationStatusListener.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmTransferApplicationStatusListener.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/listener/HrmAttendanceExceptionStatusListener.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/performanceappraise/HrmPerformanceAppraiseService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/performanceappraise/HrmPerformanceAppraiseServiceImpl.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/remind/HrmExpiryRemindService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/remind/HrmExpiryRemindServiceImpl.java 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmEmployeeSalaryService.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmEmployeeSalaryServiceImpl.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureService.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureServiceImpl.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/training/HrmTrainingService.java 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/training/HrmTrainingServiceImpl.java 279 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/workhour/HrmWorkHourSummaryService.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/workhour/HrmWorkHourSummaryServiceImpl.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemCreateReqDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemRespDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemUpdateReqDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/enums/MdmItemTypeEnum.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/enums/MdmItemTypeEnum.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/cal/MesCalTeamApi.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/cal/dto/MesCalTeamRespDTO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/pom.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/cal/MesCalTeamApiImpl.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/MesCalScheduleDetailController.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/vo/MesCalScheduleDetailCheckReqVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/vo/MesCalScheduleDetailCheckRespVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/vo/MesCalScheduleDetailPageReqVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/vo/MesCalScheduleDetailRespVO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/vo/MesCalScheduleDetailSaveReqVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryRespVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachinerySaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/barcode/MesWmBarcodeController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/MesWmEquipmentReceiptController.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/MesWmEquipmentReceiptLineController.java 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/MesWmEquipmentReceiptPageReqVO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/MesWmEquipmentReceiptRespVO.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/MesWmEquipmentReceiptSaveReqVO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/line/MesWmEquipmentReceiptLinePageReqVO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/line/MesWmEquipmentReceiptLineRespVO.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/line/MesWmEquipmentReceiptLineSaveReqVO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/MesWmSnController.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnDetailRespVO.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnInStockPageReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnInStockReqVO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnOutStockReqVO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnRespVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/plan/vo/MesWmStockTakingPlanRespVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/plan/vo/MesWmStockTakingPlanSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/MesWmStockTakingTaskController.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/vo/MesWmStockTakingTaskRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionPageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/cal/schedule/MesCalScheduleDetailDO.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/machinery/MesDvMachineryDO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/barcode/MesWmBarcodeConfigDO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/equipmentreceipt/MesWmEquipmentReceiptDO.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/equipmentreceipt/MesWmEquipmentReceiptLineDO.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/sn/MesWmSnDO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/stocktaking/plan/MesWmStockTakingPlanDO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/stocktaking/task/MesWmStockTakingTaskDO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/transaction/MesWmTransactionDO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warehouse/MesWmWarehouseAreaDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warehouse/MesWmWarehouseDO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warehouse/MesWmWarehouseLocationDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/cal/schedule/MesCalScheduleDetailMapper.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/barcode/MesWmBarcodeConfigMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/equipmentreceipt/MesWmEquipmentReceiptLineMapper.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/equipmentreceipt/MesWmEquipmentReceiptMapper.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/sn/MesWmSnMapper.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/stocktaking/plan/MesWmStockTakingPlanMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/stocktaking/task/MesWmStockTakingTaskMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/transaction/MesWmTransactionMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesBizTypeConstants.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/cal/MesCalScheduleDetailSourceEnum.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/md/autocode/MesMdAutoCodeRuleCodeEnum.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmEquipmentReceiptStatusEnum.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmSnStatusEnum.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmStockTakingTaskStatusEnum.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/wm/MesWmStockTakingPlanAutoJob.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/schedule/MesCalScheduleDetailService.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/schedule/MesCalScheduleDetailServiceImpl.java 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/machinery/MesDvMachineryServiceImpl.java 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/barcode/MesWmBarcodeConfigService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/barcode/MesWmBarcodeConfigServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/barcode/MesWmBarcodeService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/barcode/MesWmBarcodeServiceImpl.java 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/equipmentreceipt/MesWmEquipmentReceiptLineService.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/equipmentreceipt/MesWmEquipmentReceiptLineServiceImpl.java 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/equipmentreceipt/MesWmEquipmentReceiptService.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/equipmentreceipt/MesWmEquipmentReceiptServiceImpl.java 290 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/equipmentreceipt/listener/MesWmEquipmentReceiptStatusListener.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/sn/MesWmSnService.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/sn/MesWmSnServiceImpl.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/plan/MesWmStockTakingPlanService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/plan/MesWmStockTakingPlanServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskLineServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskResultService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskResultServiceImpl.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskService.java 67 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskServiceImpl.java 243 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/listener/MesWmStockTakingTaskResultStatusListener.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/workhour/HrmWorkHourSummaryApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.hrm.api.workhour;
import cn.iocoder.yudao.module.hrm.api.workhour.dto.HrmWorkHourSummaryRespDTO;
import java.util.List;
/**
 * HRM æœˆåº¦å·¥æ—¶æ±‡æ€» API æŽ¥å£
 * <p>
 * ä¾›æˆæœ¬æ ¸ç®—等模块调用
 *
 * @author è¶…级管理员
 */
public interface HrmWorkHourSummaryApi {
    /**
     * èŽ·å–æŒ‡å®šå‘¨æœŸçš„å…¨éƒ¨å‘˜å·¥å·¥æ—¶æ±‡æ€»
     *
     * @param period æ ¸ç®—周期(yyyy-MM)
     * @return å‘˜å·¥å·¥æ—¶æ±‡æ€»
     */
    List<HrmWorkHourSummaryRespDTO> getSummaryByPeriod(String period);
    /**
     * èŽ·å–æŒ‡å®šå‘˜å·¥æŒ‡å®šå‘¨æœŸçš„å·¥æ—¶æ±‡æ€»
     *
     * @param employeeId å‘˜å·¥ID
     * @param period æ ¸ç®—周期(yyyy-MM)
     * @return å‘˜å·¥å·¥æ—¶æ±‡æ€»
     */
    HrmWorkHourSummaryRespDTO getSummaryByEmployeeAndPeriod(Long employeeId, String period);
}
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/workhour/dto/HrmWorkHourSummaryRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.hrm.api.workhour.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
 * HRM æœˆåº¦å·¥æ—¶æ±‡æ€» Response DTO
 * <p>
 * ä¾›æˆæœ¬æ ¸ç®—等模块调用,获取指定核算周期的员工月度工时(考勤口径)
 *
 * @author è¶…级管理员
 */
@Data
public class HrmWorkHourSummaryRespDTO {
    /**
     * å‘˜å·¥ID(HrmEmployeeDO#getId())
     */
    private Long employeeId;
    /**
     * å‘˜å·¥ç¼–号
     */
    private String employeeNo;
    /**
     * å‘˜å·¥å§“名
     */
    private String employeeName;
    /**
     * éƒ¨é—¨ID
     */
    private Long deptId;
    /**
     * éƒ¨é—¨åç§°
     */
    private String deptName;
    /**
     * æ ¸ç®—周期(yyyy-MM)
     */
    private String period;
    /**
     * å‡ºå‹¤å¤©æ•°
     */
    private Integer workDays;
    /**
     * æ­£å¸¸å·¥æ—¶ï¼ˆå°æ—¶ï¼‰
     */
    private BigDecimal workHours;
    /**
     * åŠ ç­å·¥æ—¶ï¼ˆå°æ—¶ï¼‰
     */
    private BigDecimal overtimeHours;
}
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
@@ -95,6 +95,7 @@
    ErrorCode PERFORMANCE_APPRAISE_RESULT_NOT_EXISTS = new ErrorCode(1_030_008_007, "考核结果不存在");
    ErrorCode PERFORMANCE_APPRAISE_RESULT_ITEM_NOT_EXISTS = new ErrorCode(1_030_008_008, "考核指标评分不存在");
    ErrorCode PERFORMANCE_APPRAISE_SCORE_FAIL_STATUS = new ErrorCode(1_030_008_009, "评分失败,原因:方案不是已发布/进行中状态");
    ErrorCode PERFORMANCE_APPRAISE_SCOPE_TEAM_EMPTY = new ErrorCode(1_030_008_010, "指定班组为空,无法生成被考核人");
    // ========== è–ªé…¬ç®¡ç† 1-030-005-000 ==========
    ErrorCode SALARY_STRUCTURE_NOT_EXISTS = new ErrorCode(1_030_005_000, "薪酬结构不存在");
@@ -123,4 +124,23 @@
    ErrorCode SALARY_PAYMENT_DELETE_FAIL = new ErrorCode(1_030_005_023, "删除失败,原因:仅待发放状态可删除,请先撤销已发放明细");
    ErrorCode SALARY_PAYMENT_HAS_PAID_DETAIL = new ErrorCode(1_030_005_024, "删除失败,原因:存在已发放明细,请先撤销");
    // ========== è½¬æ­£ç”³è¯· 1-030-006-000 ==========
    ErrorCode REGULARIZE_APPLICATION_NOT_EXISTS = new ErrorCode(1_030_006_000, "转正申请不存在");
    ErrorCode REGULARIZE_APPLICATION_UPDATE_FAIL_NOT_DRAFT = new ErrorCode(1_030_006_001, "转正申请更新失败,原因:不是草稿状态");
    ErrorCode REGULARIZE_APPLICATION_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_030_006_002, "转正申请提交失败,原因:不是草稿状态");
    ErrorCode REGULARIZE_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_030_006_003, "更新转正申请审批状态失败,原因:不是审批中状态");
    ErrorCode REGULARIZE_APPLICATION_NO_EXISTS = new ErrorCode(1_030_006_004, "生成转正申请单号重复,请重试");
    ErrorCode REGULARIZE_APPLICATION_EXISTS = new ErrorCode(1_030_006_005, "该员工已存在转正申请,请勿重复提交");
    ErrorCode REGULARIZE_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_006_006, "转正审批流程不存在");
    // ========== åŸ¹è®­ç®¡ç† 1-030-009-000 ==========
    ErrorCode TRAINING_NOT_EXISTS = new ErrorCode(1_030_009_000, "培训活动不存在");
    ErrorCode TRAINING_PARTICIPANT_NOT_EXISTS = new ErrorCode(1_030_009_001, "培训参训人员不存在");
    ErrorCode TRAINING_EMPLOYEE_LIST_EMPTY = new ErrorCode(1_030_009_002, "请选择参训员工");
    ErrorCode TRAINING_CERT_VALIDITY_REQUIRED = new ErrorCode(1_030_009_003, "启用自动发证时必须填写证书有效期(月)");
    ErrorCode TRAINING_CERT_TYPE_NOT_SUPPORT = new ErrorCode(1_030_009_004, "仅安全培训/技能培训支持自动发证");
    ErrorCode TRAINING_LOCKED = new ErrorCode(1_030_009_005, "培训已完成或已取消,不可修改");
    ErrorCode TRAINING_COMPLETE_FAIL_STATUS = new ErrorCode(1_030_009_006, "培训完成操作失败,原因:仅草稿/待开展状态可完成");
    ErrorCode TRAINING_PARTICIPANT_EXISTS = new ErrorCode(1_030_009_007, "该员工已在本次培训参训名单中,请勿重复添加");
}
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmEmploymentTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.hrm.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * ç”¨å·¥æ€§è´¨æžšä¸¾
 */
@RequiredArgsConstructor
@Getter
public enum HrmEmploymentTypeEnum implements ArrayValuable<Integer> {
    FORMAL(1, "正式"),
    LABOR(2, "劳务"),
    OUTSOURCING(3, "外包"),
    INTERNSHIP(4, "实习"),
    RETIRED_REHIRE(5, "退休返聘"),
    OTHER(6, "其他");
    private final Integer type;
    private final String name;
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(HrmEmploymentTypeEnum::getType).toArray(Integer[]::new);
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
    public static String getNameByType(Integer type) {
        return Arrays.stream(values())
                .filter(e -> e.getType().equals(type))
                .findFirst()
                .map(HrmEmploymentTypeEnum::getName)
                .orElse("");
    }
}
yudao-module-hrm/pom.xml
@@ -49,6 +49,12 @@
            <artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId>
        </dependency>
        <!-- å®šæ—¶ä»»åŠ¡ç›¸å…³ï¼šè¯ä¹¦/合同到期提醒每日任务 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-job</artifactId>
        </dependency>
        <!-- Web ç›¸å…³ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/api/workhour/HrmWorkHourSummaryApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.hrm.api.workhour;
import cn.iocoder.yudao.module.hrm.api.workhour.dto.HrmWorkHourSummaryRespDTO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.workhour.HrmWorkHourSummaryDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.workhour.HrmWorkHourSummaryMapper;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * æœˆåº¦å·¥æ—¶æ±‡æ€» API å®žçް
 *
 * @author è¶…级管理员
 */
@Service
public class HrmWorkHourSummaryApiImpl implements HrmWorkHourSummaryApi {
    @Resource
    private HrmWorkHourSummaryMapper workHourSummaryMapper;
    @Resource
    private HrmEmployeeService employeeService;
    @Resource
    private DeptApi deptApi;
    @Override
    public List<HrmWorkHourSummaryRespDTO> getSummaryByPeriod(String period) {
        return workHourSummaryMapper.selectListByPeriod(period).stream()
                .map(this::convert)
                .toList();
    }
    @Override
    public HrmWorkHourSummaryRespDTO getSummaryByEmployeeAndPeriod(Long employeeId, String period) {
        HrmWorkHourSummaryDO summary = workHourSummaryMapper.selectByEmployeeIdAndPeriod(employeeId, period);
        return summary == null ? null : convert(summary);
    }
    private HrmWorkHourSummaryRespDTO convert(HrmWorkHourSummaryDO summary) {
        HrmWorkHourSummaryRespDTO dto = new HrmWorkHourSummaryRespDTO();
        dto.setEmployeeId(summary.getEmployeeId());
        dto.setDeptId(summary.getDeptId());
        dto.setPeriod(summary.getPeriod());
        dto.setWorkDays(summary.getWorkDays());
        dto.setWorkHours(summary.getWorkHours());
        dto.setOvertimeHours(summary.getOvertimeHours());
        HrmEmployeeDO employee = employeeService.getEmployee(summary.getEmployeeId());
        if (employee != null) {
            dto.setEmployeeNo(employee.getEmployeeNo());
            dto.setEmployeeName(employee.getName());
        }
        if (summary.getDeptId() != null) {
            DeptRespDTO dept = deptApi.getDept(summary.getDeptId());
            if (dept != null) {
                dto.setDeptName(dept.getName());
            }
        }
        return dto;
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmRegularizeApplicationController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.hrm.controller.admin.approval;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmRegularizeApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmRegularizeApplicationRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmRegularizeApplicationSaveReqVO;
import cn.iocoder.yudao.module.hrm.service.approval.HrmRegularizeApplicationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - HRM è½¬æ­£ç”³è¯·")
@RestController
@RequestMapping("/hrm/regularize")
@Validated
public class HrmRegularizeApplicationController {
    @Resource
    private HrmRegularizeApplicationService regularizeApplicationService;
    @PostMapping("/create")
    @Operation(summary = "创建转正申请")
    @PreAuthorize("@ss.hasPermission('hrm:regularize:create')")
    public CommonResult<Long> createRegularizeApplication(@Valid @RequestBody HrmRegularizeApplicationSaveReqVO createReqVO) {
        return success(regularizeApplicationService.createRegularizeApplication(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新转正申请")
    @PreAuthorize("@ss.hasPermission('hrm:regularize:update')")
    public CommonResult<Boolean> updateRegularizeApplication(@Valid @RequestBody HrmRegularizeApplicationSaveReqVO updateReqVO) {
        regularizeApplicationService.updateRegularizeApplication(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除转正申请")
    @Parameter(name = "id", description = "申请ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:regularize:delete')")
    public CommonResult<Boolean> deleteRegularizeApplication(@RequestParam("id") Long id) {
        regularizeApplicationService.deleteRegularizeApplication(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得转正申请详情")
    @Parameter(name = "id", description = "申请ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:regularize:query')")
    public CommonResult<HrmRegularizeApplicationRespVO> getRegularizeApplication(@RequestParam("id") Long id) {
        return success(regularizeApplicationService.getRegularizeApplicationDetail(id));
    }
    @GetMapping("/page")
    @Operation(summary = "获得转正申请分页")
    @PreAuthorize("@ss.hasPermission('hrm:regularize:query')")
    public CommonResult<PageResult<HrmRegularizeApplicationRespVO>> getRegularizeApplicationPage(@Valid HrmRegularizeApplicationPageReqVO pageReqVO) {
        return success(regularizeApplicationService.getRegularizeApplicationPage(pageReqVO));
    }
    @PostMapping("/submit")
    @Operation(summary = "提交转正申请审批")
    @Parameter(name = "id", description = "申请ID", required = true)
    @Parameter(name = "processDefinitionKey", description = "流程定义Key", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:regularize:submit')")
    public CommonResult<Boolean> submitRegularizeApplication(@RequestParam("id") Long id,
                                                             @RequestParam("processDefinitionKey") String processDefinitionKey) {
        Long userId = SecurityFrameworkUtils.getLoginUserId();
        regularizeApplicationService.submitRegularizeApplication(id, processDefinitionKey, userId);
        return success(true);
    }
    @GetMapping("/approve-process-definition-list")
    @Operation(summary = "获取可用的审批流程定义列表")
    @PreAuthorize("@ss.hasPermission('hrm:regularize:query')")
    public CommonResult<List<Map<String, Object>>> getApproveProcessDefinitionList() {
        return success(regularizeApplicationService.getApproveProcessDefinitionList());
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmRegularizeApplicationPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.hrm.controller.admin.approval.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - è½¬æ­£ç”³è¯·åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class HrmRegularizeApplicationPageReqVO extends PageParam {
    @Schema(description = "申请单号", example = "ZZ202401001")
    private String no;
    @Schema(description = "申请人ID", example = "1")
    private Long userId;
    @Schema(description = "部门ID", example = "1")
    private Long deptId;
    @Schema(description = "当前用工性质", example = "2")
    private Integer employmentType;
    @Schema(description = "审批状态", example = "0")
    private Integer status;
    @Schema(description = "创建时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] createTime;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmRegularizeApplicationRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.hrm.controller.admin.approval.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - è½¬æ­£ç”³è¯· Response VO")
@Data
public class HrmRegularizeApplicationRespVO {
    @Schema(description = "申请ID", example = "1")
    private Long id;
    @Schema(description = "申请单号", example = "ZZ202401001")
    private String no;
    @Schema(description = "申请人ID", example = "1")
    private Long userId;
    @Schema(description = "申请人姓名", example = "张三")
    private String userName;
    @Schema(description = "部门ID", example = "1")
    private Long deptId;
    @Schema(description = "部门名称", example = "研发部")
    private String deptName;
    @Schema(description = "岗位ID", example = "1")
    private Long postId;
    @Schema(description = "岗位名称", example = "Java å·¥ç¨‹å¸ˆ")
    private String postName;
    @Schema(description = "当前用工性质", example = "2")
    private Integer employmentType;
    @Schema(description = "当前用工性质名称", example = "劳务")
    private String employmentTypeName;
    @Schema(description = "拟转正日期", example = "2024-02-01")
    private LocalDate expectedDate;
    @Schema(description = "转正原因", example = "试用期表现合格")
    private String reason;
    @Schema(description = "审批状态", example = "0")
    private Integer status;
    @Schema(description = "审批状态名称", example = "草稿")
    private String statusName;
    @Schema(description = "BPM æµç¨‹å®žä¾‹ID", example = "xxx")
    private String processInstanceId;
    @Schema(description = "审批完成时间", example = "2024-01-01 10:00:00")
    private LocalDateTime approveTime;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
    @Schema(description = "创建时间", example = "2024-01-01 09:00:00")
    private LocalDateTime createTime;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmRegularizeApplicationSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.hrm.controller.admin.approval.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
@Schema(description = "管理后台 - è½¬æ­£ç”³è¯·æ–°å¢ž/修改 Request VO")
@Data
public class HrmRegularizeApplicationSaveReqVO {
    @Schema(description = "申请ID", example = "1")
    private Long id;
    @Schema(description = "申请人ID", example = "1")
    private Long userId;
    @Schema(description = "部门ID", example = "1")
    private Long deptId;
    @Schema(description = "岗位ID", example = "1")
    private Long postId;
    @Schema(description = "当前用工性质:1-正式,2-劳务,3-外包,4-实习,5-退休返聘,6-其他", example = "2")
    private Integer employmentType;
    @Schema(description = "拟转正日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-01")
    @NotNull(message = "拟转正日期不能为空")
    private LocalDate expectedDate;
    @Schema(description = "转正原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "试用期表现合格")
    @NotEmpty(message = "转正原因不能为空")
    private String reason;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/certificate/HrmCertificateController.java
@@ -12,6 +12,7 @@
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.service.certificate.HrmCertificateService;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.hrm.service.remind.HrmExpiryRemindService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -39,6 +40,8 @@
    private HrmCertificateService certificateService;
    @Resource
    private HrmEmployeeService employeeService;
    @Resource
    private HrmExpiryRemindService expiryRemindService;
    @PostMapping("/create")
    @Operation(summary = "创建证书资质")
@@ -77,6 +80,11 @@
    @Operation(summary = "获得证书资质分页")
    @PreAuthorize("@ss.hasPermission('hrm:certificate:query')")
    public CommonResult<PageResult<HrmCertificateRespVO>> getCertificatePage(@Valid HrmCertificatePageReqVO pageReqVO) {
        // æ‡’触发:有人打开证书列表时顺带补扫即将到期证书并发送站内信提醒(best-effort)
        try {
            expiryRemindService.sendCertificateExpireRemind();
        } catch (Exception ignored) {
        }
        PageResult<HrmCertificateDO> pageResult = certificateService.getCertificatePage(pageReqVO);
        if (CollUtil.isEmpty(pageResult.getList())) {
            return success(PageResult.empty(pageResult.getTotal()));
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/certificate/vo/HrmCertificatePageReqVO.java
@@ -21,4 +21,7 @@
    @Schema(description = "证书名称", example = "高压电工特种作业操作证")
    private String certName;
    @Schema(description = "到期类型(1=即将到期,2=已过期),配合合规列表筛选", example = "1")
    private Integer expiryType;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeContractController.java
@@ -7,6 +7,7 @@
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeContractTerminateReqVO;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeContractService;
import cn.iocoder.yudao.module.hrm.service.remind.HrmExpiryRemindService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -26,6 +27,8 @@
    @Resource
    private HrmEmployeeContractService contractService;
    @Resource
    private HrmExpiryRemindService expiryRemindService;
    @PostMapping("/create")
    @Operation(summary = "创建员工合同")
@@ -71,6 +74,11 @@
    @Operation(summary = "获得员工合同分页")
    @PreAuthorize("@ss.hasPermission('hrm:employee-contract:query')")
    public CommonResult<PageResult<HrmEmployeeContractRespVO>> getContractPage(@Valid HrmEmployeeContractPageReqVO pageReqVO) {
        // æ‡’触发:有人打开合同列表时顺带补扫即将到期合同并发送站内信提醒(best-effort)
        try {
            expiryRemindService.sendContractExpireRemind();
        } catch (Exception ignored) {
        }
        return success(contractService.getContractPage(pageReqVO));
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/HrmEmployeeController.java
@@ -17,7 +17,10 @@
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeSimpleRespVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.enums.HrmEmploymentTypeEnum;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.mes.api.cal.MesCalTeamApi;
import cn.iocoder.yudao.module.mes.api.cal.dto.MesCalTeamRespDTO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.PostApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
@@ -56,6 +59,8 @@
    private PostApi postApi;
    @Resource
    private RoleApi roleApi;
    @Resource
    private MesCalTeamApi mesCalTeamApi;
    @PostMapping("/create")
    @Operation(summary = "创建员工")
@@ -121,7 +126,16 @@
                .collect(Collectors.toSet());
        Map<Long, PostRespDTO> postMap = CollUtil.isEmpty(postIds) ? Collections.emptyMap() : postApi.getPostMap(postIds);
        // 3. æ”¶é›†æ‰€æœ‰è§’色ID
        // 3. æ‰¹é‡èŽ·å–ç­ç»„ä¿¡æ¯ï¼ˆæ‰€å±žç­ç»„ï¼‰
        Set<Long> teamIds = employeeList.stream()
                .map(HrmEmployeeDO::getTeamId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, String> teamNameMap = CollUtil.isEmpty(teamIds) ? Collections.emptyMap() : mesCalTeamApi
                .getTeamList(teamIds).stream()
                .collect(Collectors.toMap(MesCalTeamRespDTO::getId, MesCalTeamRespDTO::getName, (v1, v2) -> v1));
        // 4. æ”¶é›†æ‰€æœ‰è§’色ID
        Set<Long> allRoleIds = new HashSet<>();
        for (HrmEmployeeDO employee : employeeList) {
            if (StrUtil.isNotBlank(employee.getRoleIds())) {
@@ -148,6 +162,16 @@
                respVO.setPostName(postMap.get(employee.getPostId()).getName());
            }
            // å¡«å……用工性质名称
            if (employee.getEmploymentType() != null) {
                respVO.setEmploymentTypeName(HrmEmploymentTypeEnum.getNameByType(employee.getEmploymentType()));
            }
            // å¡«å……所属班组名称
            if (employee.getTeamId() != null && teamNameMap.containsKey(employee.getTeamId())) {
                respVO.setTeamName(teamNameMap.get(employee.getTeamId()));
            }
            // è§£æž roleIds å¹¶å¡«å……角色名称
            if (StrUtil.isNotBlank(employee.getRoleIds())) {
                List<Long> roleIds = JSONUtil.toList(employee.getRoleIds(), Long.class);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeRespVO.java
@@ -39,6 +39,20 @@
    @ExcelProperty("岗位名称")
    private String postName;
    @Schema(description = "用工性质")
    @ExcelProperty("用工性质")
    private Integer employmentType;
    @Schema(description = "用工性质名称")
    private String employmentTypeName;
    @Schema(description = "所属班组ID")
    private Long teamId;
    @Schema(description = "所属班组名称")
    @ExcelProperty("所属班组")
    private String teamName;
    @Schema(description = "员工姓名")
    @ExcelProperty("员工姓名")
    private String name;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/employee/vo/HrmEmployeeSaveReqVO.java
@@ -27,6 +27,12 @@
    @NotNull(message = "岗位不能为空")
    private Long postId;
    @Schema(description = "用工性质", example = "1")
    private Integer employmentType;
    @Schema(description = "所属班组ID", example = "1")
    private Long teamId;
    @Schema(description = "员工姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
    @NotEmpty(message = "员工姓名不能为空")
    private String name;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performanceappraise/HrmPerformanceAppraiseController.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.hrm.controller.admin.performanceappraise;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -179,10 +181,21 @@
        return success(buildResultRespVO(result, employeeService.getEmployee(employeeId)));
    }
    @PostMapping("/generate-appraisees")
    @Operation(summary = "生成被考核人(按考核范围,幂等)")
    @Parameter(name = "appraiseId", description = "方案编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('hrm:performance-appraise:update')")
    public CommonResult<Integer> generateAppraisees(@RequestParam("appraiseId") Long appraiseId) {
        return success(appraiseService.generateAppraisees(appraiseId));
    }
    // ========== å†…部方法:拼装 VO ==========
    private HrmPerformanceAppraiseRespVO buildAppraiseRespVO(HrmPerformanceAppraiseDO appraise) {
        HrmPerformanceAppraiseRespVO vo = BeanUtils.toBean(appraise, HrmPerformanceAppraiseRespVO.class);
        if (StrUtil.isNotEmpty(appraise.getScopeTeamIds())) {
            vo.setScopeTeamIds(JSONUtil.toList(appraise.getScopeTeamIds(), Long.class));
        }
        vo.setStatusName(convertAppraiseStatusName(appraise.getStatus()));
        vo.setAuditStatusName(HrmAuditStatusEnum.getNameByStatus(appraise.getAuditStatus()));
        vo.setItemCount(appraiseService.getAppraiseItemList(appraise.getId()).size());
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performanceappraise/vo/HrmPerformanceAppraiseRespVO.java
@@ -8,6 +8,7 @@
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
 * HRM ç»©æ•ˆè€ƒæ ¸æ–¹æ¡ˆ Response VO
@@ -39,6 +40,9 @@
    @DictFormat(cn.iocoder.yudao.module.hrm.enums.DictTypeConstants.HRM_PERFORMANCE_SCOPE_TYPE)
    private Integer scopeType;
    @Schema(description = "指定班组编号集合(scopeType=2 æ—¶ç”Ÿæ•ˆï¼Œå…³è” MES æŽ’班组)")
    private List<Long> scopeTeamIds;
    @Schema(description = "方案状态", example = "0")
    private Integer status;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/performanceappraise/vo/HrmPerformanceAppraiseSaveReqVO.java
@@ -4,6 +4,8 @@
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/**
 * HRM ç»©æ•ˆè€ƒæ ¸æ–¹æ¡ˆ ä¿å­˜ Request VO
 */
@@ -27,6 +29,9 @@
    @Schema(description = "考核范围类型: 1-全部员工 2-指定班组", example = "1")
    private Integer scopeType;
    @Schema(description = "指定班组编号集合(scopeType=2 æ—¶å¿…填,关联 MES æŽ’班组)")
    private List<Long> scopeTeamIds;
    @Schema(description = "备注")
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmEmployeeSalaryController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmEmployeeSalaryPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmEmployeeSalaryRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmEmployeeSalarySaveReqVO;
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.HrmSalaryStructureDO;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.hrm.service.salary.HrmEmployeeSalaryService;
import cn.iocoder.yudao.module.hrm.service.salary.HrmSalaryStructureService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - HRM å‘˜å·¥è–ªé…¬æ¡£æ¡ˆ")
@RestController
@RequestMapping("/hrm/employee-salary")
@Validated
public class HrmEmployeeSalaryController {
    @Resource
    private HrmEmployeeSalaryService employeeSalaryService;
    @Resource
    private HrmEmployeeService employeeService;
    @Resource
    private HrmSalaryStructureService structureService;
    @PostMapping("/create")
    @Operation(summary = "创建员工薪酬档案")
    @PreAuthorize("@ss.hasPermission('hrm:salary:create')")
    public CommonResult<Long> createEmployeeSalary(@Valid @RequestBody HrmEmployeeSalarySaveReqVO createReqVO) {
        HrmEmployeeSalaryDO salary = BeanUtils.toBean(createReqVO, HrmEmployeeSalaryDO.class);
        return success(employeeSalaryService.createEmployeeSalary(salary));
    }
    @PutMapping("/update")
    @Operation(summary = "更新员工薪酬档案")
    @PreAuthorize("@ss.hasPermission('hrm:salary:update')")
    public CommonResult<Boolean> updateEmployeeSalary(@Valid @RequestBody HrmEmployeeSalarySaveReqVO updateReqVO) {
        HrmEmployeeSalaryDO salary = BeanUtils.toBean(updateReqVO, HrmEmployeeSalaryDO.class);
        employeeSalaryService.updateEmployeeSalary(salary);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除员工薪酬档案")
    @Parameter(name = "id", description = "档案ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:salary:delete')")
    public CommonResult<Boolean> deleteEmployeeSalary(@RequestParam("id") Long id) {
        employeeSalaryService.deleteEmployeeSalary(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获取员工薪酬档案")
    @Parameter(name = "id", description = "档案ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:salary:query')")
    public CommonResult<HrmEmployeeSalaryRespVO> getEmployeeSalary(@RequestParam("id") Long id) {
        HrmEmployeeSalaryDO salary = employeeSalaryService.getEmployeeSalary(id);
        if (salary == null) {
            return success(null);
        }
        HrmEmployeeSalaryRespVO respVO = BeanUtils.toBean(salary, HrmEmployeeSalaryRespVO.class);
        fillEmployeeInfo(respVO, salary.getUserId());
        fillStructureInfo(respVO, salary.getSalaryStructureId());
        return success(respVO);
    }
    @GetMapping("/page")
    @Operation(summary = "获取员工薪酬档案分页")
    @PreAuthorize("@ss.hasPermission('hrm:salary:query')")
    public CommonResult<PageResult<HrmEmployeeSalaryRespVO>> getEmployeeSalaryPage(
            @Valid HrmEmployeeSalaryPageReqVO pageReqVO) {
        PageResult<HrmEmployeeSalaryDO> pageResult = employeeSalaryService.getEmployeeSalaryPage(pageReqVO);
        List<HrmEmployeeSalaryDO> recordList = pageResult.getList();
        if (CollUtil.isEmpty(recordList)) {
            return success(new PageResult<>(Collections.emptyList(), pageResult.getTotal()));
        }
        // æ‰¹é‡èŽ·å–å‘˜å·¥ä¿¡æ¯
        Set<Long> employeeIds = new HashSet<>();
        Set<Long> structureIds = new HashSet<>();
        for (HrmEmployeeSalaryDO record : recordList) {
            if (record.getUserId() != null) {
                employeeIds.add(record.getUserId());
            }
            if (record.getSalaryStructureId() != null) {
                structureIds.add(record.getSalaryStructureId());
            }
        }
        Map<Long, HrmEmployeeDO> employeeMap = new HashMap<>();
        for (Long employeeId : employeeIds) {
            HrmEmployeeDO employee = employeeService.getEmployee(employeeId);
            if (employee != null) {
                employeeMap.put(employeeId, employee);
            }
        }
        Map<Long, HrmSalaryStructureDO> structureMap = new HashMap<>();
        for (Long structureId : structureIds) {
            HrmSalaryStructureDO structure = structureService.getStructure(structureId);
            if (structure != null) {
                structureMap.put(structureId, structure);
            }
        }
        List<HrmEmployeeSalaryRespVO> list = recordList.stream()
                .map(salary -> {
                    HrmEmployeeSalaryRespVO respVO = BeanUtils.toBean(salary, HrmEmployeeSalaryRespVO.class);
                    HrmEmployeeDO employee = employeeMap.get(salary.getUserId());
                    if (employee != null) {
                        respVO.setEmployeeNo(employee.getEmployeeNo());
                        respVO.setEmployeeName(employee.getName());
                    }
                    if (salary.getSalaryStructureId() != null) {
                        HrmSalaryStructureDO structure = structureMap.get(salary.getSalaryStructureId());
                        if (structure != null) {
                            respVO.setSalaryStructureName(structure.getName());
                        }
                    }
                    return respVO;
                })
                .toList();
        return success(new PageResult<>(list, pageResult.getTotal()));
    }
    @GetMapping("/list-by-user")
    @Operation(summary = "获取员工薪酬档案历史列表")
    @Parameter(name = "userId", description = "员工ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:salary:query')")
    public CommonResult<List<HrmEmployeeSalaryRespVO>> getEmployeeSalaryList(
            @RequestParam("userId") Long userId) {
        List<HrmEmployeeSalaryDO> list = employeeSalaryService.getEmployeeSalaryList(userId);
        return success(list.stream()
                .map(salary -> {
                    HrmEmployeeSalaryRespVO respVO = BeanUtils.toBean(salary, HrmEmployeeSalaryRespVO.class);
                    fillEmployeeInfo(respVO, salary.getUserId());
                    fillStructureInfo(respVO, salary.getSalaryStructureId());
                    return respVO;
                })
                .toList());
    }
    @GetMapping("/get-latest")
    @Operation(summary = "获取员工最新生效的薪酬档案")
    @Parameter(name = "userId", description = "员工ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:salary:query')")
    public CommonResult<HrmEmployeeSalaryRespVO> getLatestEmployeeSalary(
            @RequestParam("userId") Long userId) {
        HrmEmployeeSalaryDO salary = employeeSalaryService.getLatestEmployeeSalary(userId);
        if (salary == null) {
            return success(null);
        }
        HrmEmployeeSalaryRespVO respVO = BeanUtils.toBean(salary, HrmEmployeeSalaryRespVO.class);
        fillEmployeeInfo(respVO, salary.getUserId());
        fillStructureInfo(respVO, salary.getSalaryStructureId());
        return success(respVO);
    }
    private void fillEmployeeInfo(HrmEmployeeSalaryRespVO respVO, Long userId) {
        HrmEmployeeDO employee = employeeService.getEmployee(userId);
        if (employee != null) {
            respVO.setEmployeeNo(employee.getEmployeeNo());
            respVO.setEmployeeName(employee.getName());
        }
    }
    private void fillStructureInfo(HrmEmployeeSalaryRespVO respVO, Long structureId) {
        if (structureId != null) {
            HrmSalaryStructureDO structure = structureService.getStructure(structureId);
            if (structure != null) {
                respVO.setSalaryStructureName(structure.getName());
            }
        }
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryStructureController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryStructurePageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryStructureRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryStructureSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
import cn.iocoder.yudao.module.hrm.service.salary.HrmSalaryStructureService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - HRM è–ªé…¬ç»“æž„")
@RestController
@RequestMapping("/hrm/salary/structure")
@Validated
public class HrmSalaryStructureController {
    @Resource
    private HrmSalaryStructureService structureService;
    @PostMapping("/create")
    @Operation(summary = "创建薪酬结构")
    @PreAuthorize("@ss.hasPermission('hrm:salary:create')")
    public CommonResult<Long> createStructure(@Valid @RequestBody HrmSalaryStructureSaveReqVO createReqVO) {
        HrmSalaryStructureDO structure = BeanUtils.toBean(createReqVO, HrmSalaryStructureDO.class);
        return success(structureService.createStructure(structure));
    }
    @PutMapping("/update")
    @Operation(summary = "更新薪酬结构")
    @PreAuthorize("@ss.hasPermission('hrm:salary:update')")
    public CommonResult<Boolean> updateStructure(@Valid @RequestBody HrmSalaryStructureSaveReqVO updateReqVO) {
        HrmSalaryStructureDO structure = BeanUtils.toBean(updateReqVO, HrmSalaryStructureDO.class);
        structureService.updateStructure(structure);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除薪酬结构")
    @Parameter(name = "id", description = "结构ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:salary:delete')")
    public CommonResult<Boolean> deleteStructure(@RequestParam("id") Long id) {
        structureService.deleteStructure(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获取薪酬结构")
    @Parameter(name = "id", description = "结构ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:salary:query')")
    public CommonResult<HrmSalaryStructureRespVO> getStructure(@RequestParam("id") Long id) {
        HrmSalaryStructureDO structure = structureService.getStructure(id);
        return success(BeanUtils.toBean(structure, HrmSalaryStructureRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获取薪酬结构分页")
    @PreAuthorize("@ss.hasPermission('hrm:salary:query')")
    public CommonResult<PageResult<HrmSalaryStructureRespVO>> getStructurePage(HrmSalaryStructurePageReqVO pageReqVO) {
        PageResult<HrmSalaryStructureDO> pageResult = structureService.getStructurePage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, HrmSalaryStructureRespVO.class));
    }
    @GetMapping("/list")
    @Operation(summary = "获取薪酬结构列表")
    @Parameter(name = "status", description = "状态")
    @PreAuthorize("@ss.hasPermission('hrm:salary:query')")
    public CommonResult<List<HrmSalaryStructureRespVO>> getStructureList(
            @RequestParam(value = "status", required = false) Integer status) {
        List<HrmSalaryStructureDO> list = structureService.getStructureList(status);
        return success(BeanUtils.toBean(list, HrmSalaryStructureRespVO.class));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmEmployeeSalaryPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - å‘˜å·¥è–ªé…¬æ¡£æ¡ˆåˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class HrmEmployeeSalaryPageReqVO extends PageParam {
    @Schema(description = "员工ID", example = "1024")
    private Long userId;
    @Schema(description = "薪酬结构ID", example = "1")
    private Long salaryStructureId;
    @Schema(description = "状态:0-启用,1-禁用", example = "0")
    private Integer status;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmEmployeeSalaryRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - å‘˜å·¥è–ªé…¬æ¡£æ¡ˆ Response VO")
@Data
public class HrmEmployeeSalaryRespVO {
    @Schema(description = "档案ID", requiredMode = Schema.RequiredMode.REQUIRED)
    private Long id;
    @Schema(description = "员工ID")
    private Long userId;
    @Schema(description = "员工编号")
    private String employeeNo;
    @Schema(description = "员工姓名")
    private String employeeName;
    @Schema(description = "薪酬结构ID")
    private Long salaryStructureId;
    @Schema(description = "薪酬结构名称")
    private String salaryStructureName;
    @Schema(description = "基本工资")
    private BigDecimal baseSalary;
    @Schema(description = "绩效工资")
    private BigDecimal performanceSalary;
    @Schema(description = "餐补")
    private BigDecimal mealAllowance;
    @Schema(description = "交通补贴")
    private BigDecimal transportAllowance;
    @Schema(description = "其他补贴")
    private BigDecimal otherAllowance;
    @Schema(description = "生效日期")
    private LocalDate effectiveDate;
    @Schema(description = "状态:0-启用,1-禁用")
    private Integer status;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmEmployeeSalarySaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Schema(description = "管理后台 - å‘˜å·¥è–ªé…¬æ¡£æ¡ˆ æ–°å¢ž/修改 Request VO")
@Data
public class HrmEmployeeSalarySaveReqVO {
    @Schema(description = "档案ID", example = "1")
    private Long id;
    @Schema(description = "员工ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "员工不能为空")
    private Long userId;
    @Schema(description = "薪酬结构ID", example = "1")
    private Long salaryStructureId;
    @Schema(description = "基本工资", example = "8000.00")
    private BigDecimal baseSalary;
    @Schema(description = "绩效工资", example = "2000.00")
    private BigDecimal performanceSalary;
    @Schema(description = "餐补", example = "300.00")
    private BigDecimal mealAllowance;
    @Schema(description = "交通补贴", example = "200.00")
    private BigDecimal transportAllowance;
    @Schema(description = "其他补贴", example = "100.00")
    private BigDecimal otherAllowance;
    @Schema(description = "生效日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "生效日期不能为空")
    private LocalDate effectiveDate;
    @Schema(description = "状态:0-启用,1-禁用", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @NotNull(message = "状态不能为空")
    private Integer status;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructurePageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - è–ªé…¬ç»“构分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class HrmSalaryStructurePageReqVO extends PageParam {
    @Schema(description = "结构名称", example = "生产岗")
    private String name;
    @Schema(description = "状态:0-启用,1-禁用", example = "0")
    private Integer status;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - è–ªé…¬ç»“æž„ Response VO")
@Data
public class HrmSalaryStructureRespVO {
    @Schema(description = "结构ID", requiredMode = Schema.RequiredMode.REQUIRED)
    private Long id;
    @Schema(description = "结构名称")
    private String name;
    @Schema(description = "基本工资")
    private BigDecimal baseSalary;
    @Schema(description = "绩效工资占比(%)")
    private BigDecimal performanceRatio;
    @Schema(description = "加班工资倍率")
    private BigDecimal overtimePayRatio;
    @Schema(description = "社保基数")
    private BigDecimal socialSecurityBase;
    @Schema(description = "公积金基数")
    private BigDecimal housingFundBase;
    @Schema(description = "状态:0-启用,1-禁用")
    private Integer status;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - è–ªé…¬ç»“æž„ æ–°å¢ž/修改 Request VO")
@Data
public class HrmSalaryStructureSaveReqVO {
    @Schema(description = "结构ID", example = "1")
    private Long id;
    @Schema(description = "结构名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "生产岗薪酬结构")
    @NotEmpty(message = "结构名称不能为空")
    private String name;
    @Schema(description = "基本工资", example = "8000.00")
    private BigDecimal baseSalary;
    @Schema(description = "绩效工资占比(%)", example = "20")
    private BigDecimal performanceRatio;
    @Schema(description = "加班工资倍率", example = "1.5")
    private BigDecimal overtimePayRatio;
    @Schema(description = "社保基数", example = "8000.00")
    private BigDecimal socialSecurityBase;
    @Schema(description = "公积金基数", example = "8000.00")
    private BigDecimal housingFundBase;
    @Schema(description = "状态:0-启用,1-禁用", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @NotNull(message = "状态不能为空")
    private Integer status;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/HrmTrainingController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,171 @@
package cn.iocoder.yudao.module.hrm.controller.admin.training;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingParticipantRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.training.HrmTrainingDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.training.HrmTrainingParticipantDO;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.hrm.service.training.HrmTrainingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.hrm.dal.dataobject.training.HrmTrainingDO.*;
@Tag(name = "管理后台 - HRM åŸ¹è®­æ´»åЍ")
@RestController
@RequestMapping("/hrm/training")
@Validated
public class HrmTrainingController {
    @Resource
    private HrmTrainingService trainingService;
    @Resource
    private HrmEmployeeService employeeService;
    @PostMapping("/create")
    @Operation(summary = "创建培训活动(初始为草稿状态)")
    @PreAuthorize("@ss.hasPermission('hrm:training:create')")
    public CommonResult<Long> createTraining(@Valid @RequestBody HrmTrainingSaveReqVO createReqVO) {
        return success(trainingService.createTraining(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新培训活动(仅草稿/待开展可改,状态走 start/complete/cancel æµè½¬ï¼‰")
    @PreAuthorize("@ss.hasPermission('hrm:training:update')")
    public CommonResult<Boolean> updateTraining(@Valid @RequestBody HrmTrainingSaveReqVO updateReqVO) {
        trainingService.updateTraining(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除培训活动(级联删除参训人员;已完成/已取消不可删)")
    @Parameter(name = "ids", description = "编号数组", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:training:delete')")
    public CommonResult<Boolean> deleteTraining(@RequestParam("ids") List<Long> ids) {
        trainingService.deleteTraining(ids);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得培训活动详情(含参训人员列表)")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('hrm:training:query')")
    public CommonResult<HrmTrainingRespVO> getTraining(@RequestParam("id") Long id) {
        HrmTrainingDO training = trainingService.getTraining(id);
        if (training == null) {
            return success(null);
        }
        HrmTrainingRespVO respVO = BeanUtils.toBean(training, HrmTrainingRespVO.class);
        fillDisplayNames(respVO);
        respVO.setParticipants(buildParticipantList(trainingService.getParticipantsByTrainingId(id)));
        return success(respVO);
    }
    @GetMapping("/page")
    @Operation(summary = "获得培训活动分页(含参训人数)")
    @PreAuthorize("@ss.hasPermission('hrm:training:query')")
    public CommonResult<PageResult<HrmTrainingRespVO>> getTrainingPage(@Valid HrmTrainingPageReqVO pageReqVO) {
        PageResult<HrmTrainingDO> pageResult = trainingService.getTrainingPage(pageReqVO);
        if (CollUtil.isEmpty(pageResult.getList())) {
            return success(PageResult.empty(pageResult.getTotal()));
        }
        Map<Long, Integer> participantCountMap = trainingService.getParticipantCountMap(
                convertSet(pageResult.getList(), HrmTrainingDO::getId));
        return success(BeanUtils.toBean(pageResult, HrmTrainingRespVO.class, vo -> {
            fillDisplayNames(vo);
            vo.setParticipantCount(participantCountMap.getOrDefault(vo.getId(), 0));
        }));
    }
    @PostMapping("/start")
    @Operation(summary = "开始培训(草稿 â†’ å¾…开展)")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('hrm:training:update')")
    public CommonResult<Boolean> startTraining(@RequestParam("id") Long id) {
        trainingService.startTraining(id);
        return success(true);
    }
    @PostMapping("/complete")
    @Operation(summary = "完成培训(草稿/待开展 â†’ å·²å®Œæˆï¼›å¼€å¯è‡ªåŠ¨å‘è¯æ—¶å¯¹åˆæ ¼äººå‘˜è”åŠ¨å‘æ”¾è¯ä¹¦ï¼‰")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('hrm:training:update')")
    public CommonResult<Boolean> completeTraining(@RequestParam("id") Long id) {
        trainingService.completeTraining(id);
        return success(true);
    }
    @PostMapping("/cancel")
    @Operation(summary = "取消培训(草稿/待开展 â†’ å·²å–消)")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('hrm:training:update')")
    public CommonResult<Boolean> cancelTraining(@RequestParam("id") Long id) {
        trainingService.cancelTraining(id);
        return success(true);
    }
    // ========== ç§æœ‰æ–¹æ³• ==========
    private void fillDisplayNames(HrmTrainingRespVO respVO) {
        respVO.setTrainingTypeName(convertTrainingTypeName(respVO.getTrainingType()));
        respVO.setStatusName(convertStatusName(respVO.getStatus()));
    }
    private List<HrmTrainingParticipantRespVO> buildParticipantList(List<HrmTrainingParticipantDO> participants) {
        if (CollUtil.isEmpty(participants)) {
            return new ArrayList<>();
        }
        Map<Long, HrmEmployeeDO> employeeMap = employeeService.getEmployeeList(
                        convertSet(participants, HrmTrainingParticipantDO::getEmployeeId)).stream()
                .collect(java.util.stream.Collectors.toMap(HrmEmployeeDO::getId, e -> e, (a, b) -> a));
        List<HrmTrainingParticipantRespVO> voList = BeanUtils.toBean(participants, HrmTrainingParticipantRespVO.class);
        for (HrmTrainingParticipantRespVO vo : voList) {
            HrmEmployeeDO employee = employeeMap.get(vo.getEmployeeId());
            if (employee != null) {
                vo.setEmployeeName(employee.getName());
                vo.setEmployeeNo(employee.getEmployeeNo());
            }
        }
        return voList;
    }
    private String convertTrainingTypeName(Integer trainingType) {
        return switch (trainingType == null ? 0 : trainingType) {
            case TYPE_SAFETY -> "安全培训";
            case TYPE_SKILL -> "技能培训";
            case TYPE_MANAGE -> "管理培训";
            case TYPE_OTHER -> "其他";
            default -> "";
        };
    }
    private String convertStatusName(Integer status) {
        return switch (status == null ? -1 : status) {
            case STATUS_DRAFT -> "草稿";
            case STATUS_PLANNED -> "待开展";
            case STATUS_COMPLETED -> "已完成";
            case STATUS_CANCELED -> "已取消";
            default -> "";
        };
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/HrmTrainingParticipantController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.hrm.controller.admin.training;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingParticipantAddReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingParticipantRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingParticipantUpdateReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.training.HrmTrainingParticipantDO;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.hrm.service.training.HrmTrainingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - HRM åŸ¹è®­å‚训人员")
@RestController
@RequestMapping("/hrm/training-participant")
@Validated
public class HrmTrainingParticipantController {
    @Resource
    private HrmTrainingService trainingService;
    @Resource
    private HrmEmployeeService employeeService;
    @PostMapping("/add")
    @Operation(summary = "批量添加参训员工(自动跳过已在名单中的员工)")
    @PreAuthorize("@ss.hasPermission('hrm:training:update')")
    public CommonResult<Integer> addParticipants(@Valid @RequestBody HrmTrainingParticipantAddReqVO addReqVO) {
        return success(trainingService.addParticipants(addReqVO.getTrainingId(), addReqVO.getEmployeeIds()));
    }
    @PutMapping("/update")
    @Operation(summary = "更新参训记录(参加情况/考核成绩/是否合格)")
    @PreAuthorize("@ss.hasPermission('hrm:training:update')")
    public CommonResult<Boolean> updateParticipant(@Valid @RequestBody HrmTrainingParticipantUpdateReqVO updateReqVO) {
        trainingService.updateParticipant(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除参训记录")
    @Parameter(name = "ids", description = "参训记录编号数组", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:training:update')")
    public CommonResult<Boolean> deleteParticipants(@RequestParam("ids") List<Long> ids) {
        trainingService.deleteParticipants(ids);
        return success(true);
    }
    @GetMapping("/list")
    @Operation(summary = "获得某次培训的参训人员列表")
    @Parameter(name = "trainingId", description = "培训活动编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('hrm:training:query')")
    public CommonResult<List<HrmTrainingParticipantRespVO>> getParticipantsByTrainingId(
            @RequestParam("trainingId") Long trainingId) {
        List<HrmTrainingParticipantDO> participants = trainingService.getParticipantsByTrainingId(trainingId);
        return success(buildParticipantList(participants));
    }
    private List<HrmTrainingParticipantRespVO> buildParticipantList(List<HrmTrainingParticipantDO> participants) {
        if (CollUtil.isEmpty(participants)) {
            return new ArrayList<>();
        }
        Map<Long, HrmEmployeeDO> employeeMap = employeeService.getEmployeeList(
                        convertSet(participants, HrmTrainingParticipantDO::getEmployeeId)).stream()
                .collect(java.util.stream.Collectors.toMap(HrmEmployeeDO::getId, e -> e, (a, b) -> a));
        List<HrmTrainingParticipantRespVO> voList = BeanUtils.toBean(participants, HrmTrainingParticipantRespVO.class);
        for (HrmTrainingParticipantRespVO vo : voList) {
            HrmEmployeeDO employee = employeeMap.get(vo.getEmployeeId());
            if (employee != null) {
                vo.setEmployeeName(employee.getName());
                vo.setEmployeeNo(employee.getEmployeeNo());
            }
        }
        return voList;
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.hrm.controller.admin.training.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - HRM åŸ¹è®­æ´»åŠ¨åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class HrmTrainingPageReqVO extends PageParam {
    @Schema(description = "培训编号", example = "PX202409080001")
    private String trainingNo;
    @Schema(description = "培训名称", example = "2026年度电力安全培训")
    private String name;
    @Schema(description = "培训类型(1=安全培训,2=技能培训,3=管理培训,4=其他)", example = "1")
    private Integer trainingType;
    @Schema(description = "状态(0=草稿,10=待开展,20=已完成,30=已取消)", example = "10")
    private Integer status;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingParticipantAddReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.hrm.controller.admin.training.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - HRM åŸ¹è®­å‚训人员(批量添加) Request VO")
@Data
public class HrmTrainingParticipantAddReqVO {
    @Schema(description = "培训活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "培训活动编号不能为空")
    private Long trainingId;
    @Schema(description = "参训员工编号集合", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotEmpty(message = "请选择参训员工")
    private List<Long> employeeIds;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingParticipantRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.hrm.controller.admin.training.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - HRM åŸ¹è®­å‚训人员 Response VO")
@Data
public class HrmTrainingParticipantRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "培训活动编号", example = "1")
    private Long trainingId;
    @Schema(description = "员工编号", example = "1")
    private Long employeeId;
    @Schema(description = "员工工号", example = "EMP202401010001")
    private String employeeNo;
    @Schema(description = "员工姓名", example = "张三")
    private String employeeName;
    @Schema(description = "是否实际参加(0=未参加,1=参加)", example = "true")
    private Boolean attended;
    @Schema(description = "考核成绩", example = "90")
    private BigDecimal score;
    @Schema(description = "是否合格(0=不合格,1=合格)", example = "true")
    private Boolean isPass;
    @Schema(description = "联动生成的证书编号(为空表示尚未发证)", example = "1")
    private Long certificateId;
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingParticipantUpdateReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.hrm.controller.admin.training.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - HRM åŸ¹è®­å‚训人员(参加/成绩/合格)更新 Request VO")
@Data
public class HrmTrainingParticipantUpdateReqVO {
    @Schema(description = "参训记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "参训记录编号不能为空")
    private Long id;
    @Schema(description = "是否实际参加(0=未参加,1=参加)", example = "true")
    private Boolean attended;
    @Schema(description = "考核成绩", example = "90")
    private BigDecimal score;
    @Schema(description = "是否合格(0=不合格,1=合格)", example = "true")
    private Boolean isPass;
    @Schema(description = "备注", example = "备注")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
package cn.iocoder.yudao.module.hrm.controller.admin.training.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - HRM åŸ¹è®­æ´»åЍ Response VO")
@Data
public class HrmTrainingRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "培训编号", example = "PX202409080001")
    private String trainingNo;
    @Schema(description = "培训名称", example = "2026年度电力安全培训")
    private String name;
    @Schema(description = "培训类型(1=安全培训,2=技能培训,3=管理培训,4=其他)", example = "1")
    private Integer trainingType;
    @Schema(description = "培训类型名称", example = "安全培训")
    private String trainingTypeName;
    @Schema(description = "讲师", example = "å¼ å·¥")
    private String trainer;
    @Schema(description = "培训地点", example = "安全培训室")
    private String location;
    @Schema(description = "培训学时", example = "8")
    private BigDecimal hours;
    @Schema(description = "开始日期")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    private LocalDate endDate;
    @Schema(description = "是否自动发证(0=否,1=是)", example = "true")
    private Boolean issueCertificate;
    @Schema(description = "证书有效期(月)", example = "12")
    private Integer certValidityMonths;
    @Schema(description = "状态(0=草稿,10=待开展,20=已完成,30=已取消)", example = "10")
    private Integer status;
    @Schema(description = "状态名称", example = "待开展")
    private String statusName;
    @Schema(description = "参训人数", example = "5")
    private Integer participantCount;
    @Schema(description = "参训人员列表(详情接口返回)")
    private List<HrmTrainingParticipantRespVO> participants;
    @Schema(description = "备注", example = "年度例行培训")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/training/vo/HrmTrainingSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.hrm.controller.admin.training.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Schema(description = "管理后台 - HRM åŸ¹è®­æ´»åŠ¨æ–°å¢ž/修改 Request VO")
@Data
public class HrmTrainingSaveReqVO {
    @Schema(description = "编号(修改时必填)", example = "1")
    private Long id;
    @Schema(description = "培训名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026年度电力安全培训")
    @NotEmpty(message = "培训名称不能为空")
    private String name;
    @Schema(description = "培训类型(1=安全培训,2=技能培训,3=管理培训,4=其他)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "培训类型不能为空")
    private Integer trainingType;
    @Schema(description = "讲师", example = "å¼ å·¥")
    private String trainer;
    @Schema(description = "培训地点", example = "安全培训室")
    private String location;
    @Schema(description = "培训学时", example = "8")
    private BigDecimal hours;
    @Schema(description = "开始日期")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    private LocalDate endDate;
    @Schema(description = "是否自动发证(0=否,1=是;仅安全/技能培训支持)", example = "true")
    private Boolean issueCertificate;
    @Schema(description = "证书有效期(月),自动发证时必填,用于计算证书到期日", example = "12")
    private Integer certValidityMonths;
    @Schema(description = "备注", example = "年度例行培训")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/workhour/HrmWorkHourSummaryController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.hrm.controller.admin.workhour;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.workhour.vo.HrmWorkHourSummaryPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.workhour.vo.HrmWorkHourSummaryRespVO;
import cn.iocoder.yudao.module.hrm.service.workhour.HrmWorkHourSummaryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - HRM æœˆåº¦å·¥æ—¶æ±‡æ€»")
@RestController
@RequestMapping("/hrm/work-hour-summary")
@Validated
public class HrmWorkHourSummaryController {
    @Resource
    private HrmWorkHourSummaryService workHourSummaryService;
    @PostMapping("/generate-monthly")
    @Operation(summary = "生成月度工时汇总")
    @Parameter(name = "period", description = "核算周期,格式 yyyy-MM", required = true, example = "2026-08")
    @PreAuthorize("@ss.hasPermission('hrm:work-hour:generate')")
    public CommonResult<Boolean> generateMonthly(@RequestParam("period") String period) {
        workHourSummaryService.generateMonthly(period);
        return success(true);
    }
    @GetMapping("/page")
    @Operation(summary = "获得月度工时汇总分页")
    @PreAuthorize("@ss.hasPermission('hrm:work-hour:query')")
    public CommonResult<PageResult<HrmWorkHourSummaryRespVO>> getPage(
            @Valid HrmWorkHourSummaryPageReqVO pageReqVO) {
        return success(workHourSummaryService.getWorkHourSummaryPage(pageReqVO));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/workhour/vo/HrmWorkHourSummaryPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.hrm.controller.admin.workhour.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - æœˆåº¦å·¥æ—¶æ±‡æ€»åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class HrmWorkHourSummaryPageReqVO extends PageParam {
    @Schema(description = "核算周期(yyyy-MM)", example = "2026-08")
    private String period;
    @Schema(description = "员工ID", example = "1024")
    private Long employeeId;
    @Schema(description = "部门ID", example = "1")
    private Long deptId;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/workhour/vo/HrmWorkHourSummaryRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.hrm.controller.admin.workhour.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - æœˆåº¦å·¥æ—¶æ±‡æ€» Response VO")
@Data
public class HrmWorkHourSummaryRespVO {
    @Schema(description = "汇总ID")
    private Long id;
    @Schema(description = "员工ID")
    private Long employeeId;
    @Schema(description = "员工编号")
    private String employeeNo;
    @Schema(description = "员工姓名")
    private String employeeName;
    @Schema(description = "部门ID")
    private Long deptId;
    @Schema(description = "部门名称")
    private String deptName;
    @Schema(description = "核算周期(yyyy-MM)")
    private String period;
    @Schema(description = "出勤天数")
    private Integer workDays;
    @Schema(description = "正常工时(小时)")
    private BigDecimal workHours;
    @Schema(description = "加班工时(小时)")
    private BigDecimal overtimeHours;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmRegularizeApplicationDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
package cn.iocoder.yudao.module.hrm.dal.dataobject.approval;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDate;
/**
 * è½¬æ­£ç”³è¯· DO
 *
 * @author è¶…级管理员
 */
@TableName("hrm_regularize_application")
@KeySequence("hrm_regularize_application_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HrmRegularizeApplicationDO extends BaseDO {
    /**
     * ç”³è¯·ID
     */
    @TableId
    private Long id;
    /**
     * ç”³è¯·å•号
     */
    private String no;
    /**
     * ç”³è¯·äººID
     *
     * å…³è” AdminUserDO#getId()
     */
    private Long userId;
    /**
     * éƒ¨é—¨ID
     *
     * å…³è” DeptDO#getId()
     */
    private Long deptId;
    /**
     * å²—位ID
     *
     * å…³è” PostDO#getId()
     */
    private Long postId;
    /**
     * å½“前用工性质
     *
     * 1-正式,2-劳务,3-外包,4-实习,5-退休返聘,6-其他
     */
    private Integer employmentType;
    /**
     * æ‹Ÿè½¬æ­£æ—¥æœŸ
     */
    private LocalDate expectedDate;
    /**
     * è½¬æ­£åŽŸå› 
     */
    private String reason;
    /**
     * å®¡æ‰¹çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum}
     */
    private Integer status;
    /**
     * BPM æµç¨‹å®žä¾‹ID
     */
    private String processInstanceId;
    /**
     * å®¡æ‰¹å®Œæˆæ—¶é—´
     */
    private java.time.LocalDateTime approveTime;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/employee/HrmEmployeeDO.java
@@ -52,6 +52,20 @@
    private Long postId;
    /**
     * ç”¨å·¥æ€§è´¨
     *
     * 1-正式,2-劳务,3-外包,4-实习,5-退休返聘,6-其他
     */
    private Integer employmentType;
    /**
     * æ‰€å±žç­ç»„ID
     *
     * å…³è” MesCalTeamDO#getId()(MES æŽ’班组,班组/排班/绩效同源)
     */
    private Long teamId;
    /**
     * å‘˜å·¥ç¼–号
     */
    private String employeeNo;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/performanceappraise/HrmPerformanceAppraiseDO.java
@@ -51,6 +51,11 @@
    private Integer scopeType;
    /**
     * æŒ‡å®šç­ç»„编号集合(JSON æ•°ç»„,scopeType=2 æ—¶ç”Ÿæ•ˆï¼Œå…³è” MES æŽ’班组)
     */
    private String scopeTeamIds;
    /**
     * æ–¹æ¡ˆçŠ¶æ€: 0-草稿 10-已发布 20-进行中 30-已完成
     */
    private Integer status;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/training/HrmTrainingDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
package cn.iocoder.yudao.module.hrm.dal.dataobject.training;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * HRM åŸ¹è®­æ´»åЍ DO
 *
 * <p>管理安全培训、技能培训等培训计划/活动,配合 hrm_training_participant è®°å½•参训与考核结果;
 * åŸ¹è®­å®Œæˆï¼ˆstatus=已完成)时对合格人员联动生成对应证书资质,打通"培训→证书→到期提醒"合规链路。
 *
 * @author è¶…级管理员
 */
@TableName("hrm_training")
@KeySequence("hrm_training_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HrmTrainingDO extends BaseDO {
    /**
     * åŸ¹è®­ç±»åž‹ï¼šå®‰å…¨åŸ¹è®­
     */
    public static final int TYPE_SAFETY = 1;
    /**
     * åŸ¹è®­ç±»åž‹ï¼šæŠ€èƒ½åŸ¹è®­
     */
    public static final int TYPE_SKILL = 2;
    /**
     * åŸ¹è®­ç±»åž‹ï¼šç®¡ç†åŸ¹è®­
     */
    public static final int TYPE_MANAGE = 3;
    /**
     * åŸ¹è®­ç±»åž‹ï¼šå…¶ä»–
     */
    public static final int TYPE_OTHER = 4;
    /**
     * çŠ¶æ€ï¼šè‰ç¨¿
     */
    public static final int STATUS_DRAFT = 0;
    /**
     * çŠ¶æ€ï¼šå¾…å¼€å±•
     */
    public static final int STATUS_PLANNED = 10;
    /**
     * çŠ¶æ€ï¼šå·²å®Œæˆ
     */
    public static final int STATUS_COMPLETED = 20;
    /**
     * çŠ¶æ€ï¼šå·²å–æ¶ˆ
     */
    public static final int STATUS_CANCELED = 30;
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * åŸ¹è®­ç¼–号
     */
    private String trainingNo;
    /**
     * åŸ¹è®­åç§°
     */
    private String name;
    /**
     * åŸ¹è®­ç±»åž‹ï¼ˆ1=安全培训,2=技能培训,3=管理培训,4=其他)
     */
    private Integer trainingType;
    /**
     * è®²å¸ˆ
     */
    private String trainer;
    /**
     * åŸ¹è®­åœ°ç‚¹
     */
    private String location;
    /**
     * åŸ¹è®­å­¦æ—¶
     */
    private BigDecimal hours;
    /**
     * å¼€å§‹æ—¥æœŸ
     */
    private LocalDate startDate;
    /**
     * ç»“束日期
     */
    private LocalDate endDate;
    /**
     * æ˜¯å¦è‡ªåŠ¨å‘è¯ï¼ˆ0=否,1=是;仅安全/技能培训支持)
     */
    private Boolean issueCertificate;
    /**
     * è¯ä¹¦æœ‰æ•ˆæœŸï¼ˆæœˆï¼‰ï¼Œè‡ªåŠ¨å‘è¯æ—¶ç”¨äºŽè®¡ç®—è¯ä¹¦åˆ°æœŸæ—¥
     */
    private Integer certValidityMonths;
    /**
     * çŠ¶æ€ï¼ˆ0=草稿,10=待开展,20=已完成,30=已取消)
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/training/HrmTrainingParticipantDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
package cn.iocoder.yudao.module.hrm.dal.dataobject.training;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
 * HRM åŸ¹è®­å‚训人员 DO
 *
 * <p>记录某次培训的参训员工、是否参加、考核成绩与是否合格;培训联动发证后回写 certificateId,避免重复发证。
 *
 * @author è¶…级管理员
 */
@TableName("hrm_training_participant")
@KeySequence("hrm_training_participant_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HrmTrainingParticipantDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * åŸ¹è®­æ´»åŠ¨ç¼–å·
     *
     * å…³è” {@link HrmTrainingDO#getId()}
     */
    private Long trainingId;
    /**
     * å‘˜å·¥ç¼–号
     *
     * å…³è” {@link cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO#getId()}
     */
    private Long employeeId;
    /**
     * æ˜¯å¦å®žé™…参加(0=未参加,1=参加)
     */
    private Boolean attended;
    /**
     * è€ƒæ ¸æˆç»©
     */
    private BigDecimal score;
    /**
     * æ˜¯å¦åˆæ ¼ï¼ˆ0=不合格,1=合格)
     */
    private Boolean isPass;
    /**
     * è”动生成证书资质编号
     *
     * å…³è” {@link cn.iocoder.yudao.module.hrm.dal.dataobject.certificate.HrmCertificateDO#getId()},
     * ç”¨äºŽå¹‚等(同一参训人同一培训只发一次证)
     */
    private Long certificateId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/workhour/HrmWorkHourSummaryDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
package cn.iocoder.yudao.module.hrm.dal.dataobject.workhour;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
 * æœˆåº¦å·¥æ—¶æ±‡æ€» DO
 *
 * @author è¶…级管理员
 */
@TableName("hrm_workhour_summary")
@KeySequence("hrm_workhour_summary_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HrmWorkHourSummaryDO extends BaseDO {
    /**
     * æ±‡æ€»ID
     */
    @TableId
    private Long id;
    /**
     * å‘˜å·¥ID
     *
     * å…³è” HrmEmployeeDO#getId()
     */
    private Long employeeId;
    /**
     * éƒ¨é—¨ID
     */
    private Long deptId;
    /**
     * æ ¸ç®—周期(yyyy-MM)
     */
    private String period;
    /**
     * å‡ºå‹¤å¤©æ•°
     */
    private Integer workDays;
    /**
     * æ­£å¸¸å·¥æ—¶ï¼ˆå°æ—¶ï¼‰
     */
    private BigDecimal workHours;
    /**
     * åŠ ç­å·¥æ—¶ï¼ˆå°æ—¶ï¼‰
     */
    private BigDecimal overtimeHours;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmRegularizeApplicationMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.hrm.dal.mysql.approval;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmRegularizeApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmRegularizeApplicationDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * è½¬æ­£ç”³è¯· Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface HrmRegularizeApplicationMapper extends BaseMapperX<HrmRegularizeApplicationDO> {
    default PageResult<HrmRegularizeApplicationDO> selectPage(HrmRegularizeApplicationPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<HrmRegularizeApplicationDO>()
                .likeIfPresent(HrmRegularizeApplicationDO::getNo, reqVO.getNo())
                .eqIfPresent(HrmRegularizeApplicationDO::getUserId, reqVO.getUserId())
                .eqIfPresent(HrmRegularizeApplicationDO::getDeptId, reqVO.getDeptId())
                .eqIfPresent(HrmRegularizeApplicationDO::getEmploymentType, reqVO.getEmploymentType())
                .eqIfPresent(HrmRegularizeApplicationDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(HrmRegularizeApplicationDO::getCreateTime, reqVO.getCreateTime())
                .orderByDesc(HrmRegularizeApplicationDO::getId));
    }
    default HrmRegularizeApplicationDO selectByNo(String no) {
        return selectOne(HrmRegularizeApplicationDO::getNo, no);
    }
    default HrmRegularizeApplicationDO selectByProcessInstanceId(String processInstanceId) {
        return selectOne(HrmRegularizeApplicationDO::getProcessInstanceId, processInstanceId);
    }
    default HrmRegularizeApplicationDO selectByUserIdAndStatusNot(Long userId, Integer status) {
        return selectOne(new LambdaQueryWrapperX<HrmRegularizeApplicationDO>()
                .eq(HrmRegularizeApplicationDO::getUserId, userId)
                .ne(HrmRegularizeApplicationDO::getStatus, status)
                .orderByDesc(HrmRegularizeApplicationDO::getId)
                .last("LIMIT 1"));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/certificate/HrmCertificateMapper.java
@@ -7,6 +7,9 @@
import cn.iocoder.yudao.module.hrm.dal.dataobject.certificate.HrmCertificateDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDate;
import java.util.List;
/**
 * HRM è¯ä¹¦èµ„è´¨ Mapper
 *
@@ -15,12 +18,40 @@
@Mapper
public interface HrmCertificateMapper extends BaseMapperX<HrmCertificateDO> {
    /**
     * åˆ°æœŸæé†’天数
     */
    int REMIND_DAYS = 30;
    default PageResult<HrmCertificateDO> selectPage(HrmCertificatePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<HrmCertificateDO>()
        LambdaQueryWrapperX<HrmCertificateDO> query = new LambdaQueryWrapperX<HrmCertificateDO>()
                .eqIfPresent(HrmCertificateDO::getEmployeeId, reqVO.getEmployeeId())
                .eqIfPresent(HrmCertificateDO::getCertType, reqVO.getCertType())
                .likeIfPresent(HrmCertificateDO::getCertName, reqVO.getCertName())
                .orderByDesc(HrmCertificateDO::getId));
                .likeIfPresent(HrmCertificateDO::getCertName, reqVO.getCertName());
        // åˆ°æœŸç±»åž‹ç­›é€‰ï¼ˆé…åˆåˆè§„列表/待办提醒):1=即将到期,2=已过期
        if (reqVO.getExpiryType() != null) {
            LocalDate today = LocalDate.now();
            if (reqVO.getExpiryType() == 1) { // å³å°†åˆ°æœŸ
                query.between(HrmCertificateDO::getExpireDate, today, today.plusDays(REMIND_DAYS));
            } else if (reqVO.getExpiryType() == 2) { // å·²è¿‡æœŸ
                query.isNotNull(HrmCertificateDO::getExpireDate)
                        .lt(HrmCertificateDO::getExpireDate, today);
            }
        }
        return selectPage(reqVO, query.orderByDesc(HrmCertificateDO::getId));
    }
    /**
     * æŸ¥è¯¢æœ‰æ•ˆæœŸè½åœ¨ [start, end] ä¹‹é—´çš„证书(到期提醒用)
     *
     * @param start èµ·å§‹æ—¥æœŸ
     * @param end æˆªæ­¢æ—¥æœŸ
     * @return å³å°†åˆ°æœŸè¯ä¹¦åˆ—表
     */
    default List<HrmCertificateDO> selectListByExpireDateBetween(LocalDate start, LocalDate end) {
        return selectList(new LambdaQueryWrapperX<HrmCertificateDO>()
                .isNotNull(HrmCertificateDO::getExpireDate)
                .between(HrmCertificateDO::getExpireDate, start, end));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeContractMapper.java
@@ -187,4 +187,20 @@
                .orderByDesc(HrmEmployeeContractDO::getId));
    }
    /**
     * æŸ¥è¯¢å½“前生效、将在 [start, end] ä¹‹é—´åˆ°æœŸçš„固定期限合同(到期提醒用)
     *
     * @param start èµ·å§‹æ—¥æœŸ
     * @param end æˆªæ­¢æ—¥æœŸ
     * @return å³å°†åˆ°æœŸåˆåŒåˆ—表
     */
    default List<HrmEmployeeContractDO> selectListExpiringBetween(LocalDate start, LocalDate end) {
        return selectList(new LambdaQueryWrapperX<HrmEmployeeContractDO>()
                .eq(HrmEmployeeContractDO::getTerminateStatus, TERMINATE_NORMAL)
                .eq(HrmEmployeeContractDO::getIsCurrent, true)
                .le(HrmEmployeeContractDO::getStartDate, start)
                .isNotNull(HrmEmployeeContractDO::getEndDate)
                .between(HrmEmployeeContractDO::getEndDate, start, end));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.hrm.dal.mysql.employee;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
@@ -7,6 +8,7 @@
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
@@ -52,4 +54,10 @@
                .orderByAsc(HrmEmployeeDO::getName));
    }
    default List<HrmEmployeeDO> selectListByTeamIds(Collection<Long> teamIds) {
        return selectList(new LambdaQueryWrapperX<HrmEmployeeDO>()
                .inIfPresent(HrmEmployeeDO::getTeamId, CollUtil.isNotEmpty(teamIds) ? teamIds : null)
                .orderByAsc(HrmEmployeeDO::getName));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/performanceappraise/HrmPerformanceAppraiseMapper.java
@@ -21,4 +21,13 @@
                .orderByDesc(HrmPerformanceAppraiseDO::getId));
    }
    default HrmPerformanceAppraiseDO selectMonthlyCompletedByPeriod(String period) {
        return selectOne(new LambdaQueryWrapperX<HrmPerformanceAppraiseDO>()
                .eq(HrmPerformanceAppraiseDO::getPeriodType, 1)
                .eq(HrmPerformanceAppraiseDO::getPeriod, period)
                .eq(HrmPerformanceAppraiseDO::getStatus, 30)
                .orderByDesc(HrmPerformanceAppraiseDO::getId)
                .last("LIMIT 1"));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java
@@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.hrm.dal.mysql.salary;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmEmployeeSalaryPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
import org.apache.ibatis.annotations.Mapper;
@@ -36,4 +38,12 @@
                .orderByDesc(HrmEmployeeSalaryDO::getEffectiveDate));
    }
    default PageResult<HrmEmployeeSalaryDO> selectPage(HrmEmployeeSalaryPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<HrmEmployeeSalaryDO>()
                .eqIfPresent(HrmEmployeeSalaryDO::getUserId, reqVO.getUserId())
                .eqIfPresent(HrmEmployeeSalaryDO::getSalaryStructureId, reqVO.getSalaryStructureId())
                .eqIfPresent(HrmEmployeeSalaryDO::getStatus, reqVO.getStatus())
                .orderByDesc(HrmEmployeeSalaryDO::getId));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmSalaryStructureMapper.java
@@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.hrm.dal.mysql.salary;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryStructurePageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
import org.apache.ibatis.annotations.Mapper;
@@ -21,4 +23,11 @@
                .orderByAsc(HrmSalaryStructureDO::getName));
    }
    default PageResult<HrmSalaryStructureDO> selectPage(HrmSalaryStructurePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<HrmSalaryStructureDO>()
                .likeIfPresent(HrmSalaryStructureDO::getName, reqVO.getName())
                .eqIfPresent(HrmSalaryStructureDO::getStatus, reqVO.getStatus())
                .orderByDesc(HrmSalaryStructureDO::getId));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/training/HrmTrainingMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.hrm.dal.mysql.training;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.training.HrmTrainingDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * HRM åŸ¹è®­æ´»åЍ Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface HrmTrainingMapper extends BaseMapperX<HrmTrainingDO> {
    default PageResult<HrmTrainingDO> selectPage(HrmTrainingPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<HrmTrainingDO>()
                .likeIfPresent(HrmTrainingDO::getName, reqVO.getName())
                .eqIfPresent(HrmTrainingDO::getTrainingNo, reqVO.getTrainingNo())
                .eqIfPresent(HrmTrainingDO::getTrainingType, reqVO.getTrainingType())
                .eqIfPresent(HrmTrainingDO::getStatus, reqVO.getStatus())
                .orderByDesc(HrmTrainingDO::getId));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/training/HrmTrainingParticipantMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.hrm.dal.mysql.training;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.dal.dataobject.training.HrmTrainingParticipantDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * HRM åŸ¹è®­å‚训人员 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface HrmTrainingParticipantMapper extends BaseMapperX<HrmTrainingParticipantDO> {
    /**
     * æŸ¥è¯¢æŸæ¬¡åŸ¹è®­çš„全部参训人员
     *
     * @param trainingId åŸ¹è®­æ´»åŠ¨ç¼–å·
     * @return å‚训人员列表
     */
    default List<HrmTrainingParticipantDO> selectListByTrainingId(Long trainingId) {
        return selectList(new LambdaQueryWrapperX<HrmTrainingParticipantDO>()
                .eq(HrmTrainingParticipantDO::getTrainingId, trainingId)
                .orderByAsc(HrmTrainingParticipantDO::getId));
    }
    /**
     * æ‰¹é‡æŸ¥è¯¢æŒ‡å®šåŸ¹è®­çš„参训人员(分页统计参训人数用)
     *
     * @param trainingIds åŸ¹è®­æ´»åŠ¨ç¼–å·é›†åˆ
     * @return å‚训人员列表
     */
    default List<HrmTrainingParticipantDO> selectListByTrainingIds(Collection<Long> trainingIds) {
        return selectList(new LambdaQueryWrapperX<HrmTrainingParticipantDO>()
                .in(HrmTrainingParticipantDO::getTrainingId, trainingIds));
    }
    /**
     * æŸ¥è¯¢æŸæ¬¡åŸ¹è®­ä¸‹æŸä½å‘˜å·¥çš„参训记录(防重复添加用)
     *
     * @param trainingId åŸ¹è®­æ´»åŠ¨ç¼–å·
     * @param employeeId å‘˜å·¥ç¼–号
     * @return å‚训记录,无则返回 null
     */
    default HrmTrainingParticipantDO selectByTrainingIdAndEmployeeId(Long trainingId, Long employeeId) {
        return selectOne(new LambdaQueryWrapperX<HrmTrainingParticipantDO>()
                .eq(HrmTrainingParticipantDO::getTrainingId, trainingId)
                .eq(HrmTrainingParticipantDO::getEmployeeId, employeeId));
    }
    /**
     * åˆ é™¤æŸæ¬¡åŸ¹è®­çš„全部参训人员(培训删除时级联)
     *
     * @param trainingId åŸ¹è®­æ´»åŠ¨ç¼–å·
     */
    default void deleteByTrainingId(Long trainingId) {
        delete(new LambdaQueryWrapperX<HrmTrainingParticipantDO>()
                .eq(HrmTrainingParticipantDO::getTrainingId, trainingId));
    }
    /**
     * å›žå†™è”动生成的证书编号
     *
     * @param id å‚训人员编号
     * @param certificateId è¯ä¹¦èµ„质编号
     */
    default void updateCertificateId(Long id, Long certificateId) {
        update(null, new LambdaUpdateWrapper<HrmTrainingParticipantDO>()
                .eq(HrmTrainingParticipantDO::getId, id)
                .set(HrmTrainingParticipantDO::getCertificateId, certificateId));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/workhour/HrmWorkHourSummaryMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.hrm.dal.mysql.workhour;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.hrm.controller.admin.workhour.vo.HrmWorkHourSummaryPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.workhour.HrmWorkHourSummaryDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * æœˆåº¦å·¥æ—¶æ±‡æ€» Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface HrmWorkHourSummaryMapper extends BaseMapperX<HrmWorkHourSummaryDO> {
    default PageResult<HrmWorkHourSummaryDO> selectPage(HrmWorkHourSummaryPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<HrmWorkHourSummaryDO>()
                .eqIfPresent(HrmWorkHourSummaryDO::getEmployeeId, reqVO.getEmployeeId())
                .eqIfPresent(HrmWorkHourSummaryDO::getDeptId, reqVO.getDeptId())
                .eqIfPresent(HrmWorkHourSummaryDO::getPeriod, reqVO.getPeriod())
                .orderByDesc(HrmWorkHourSummaryDO::getId));
    }
    default HrmWorkHourSummaryDO selectByEmployeeIdAndPeriod(Long employeeId, String period) {
        return selectOne(new LambdaQueryWrapperX<HrmWorkHourSummaryDO>()
                .eq(HrmWorkHourSummaryDO::getEmployeeId, employeeId)
                .eq(HrmWorkHourSummaryDO::getPeriod, period)
                .last("LIMIT 1"));
    }
    default List<HrmWorkHourSummaryDO> selectListByPeriod(String period) {
        return selectList(new LambdaQueryWrapperX<HrmWorkHourSummaryDO>()
                .eq(HrmWorkHourSummaryDO::getPeriod, period)
                .orderByAsc(HrmWorkHourSummaryDO::getEmployeeId));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmExpiryRemindRedisDAO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.hrm.dal.redis;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDate;
/**
 * HRM è¯ä¹¦/合同到期提醒 Redis DAO
 *
 * <p>用于到期站内信的去重(同一业务同一到期日只提醒一次)与当天扫描门闩(避免多实例/多次并发重复扫描),
 * æ€è·¯å‚ç…§ MES è®¡é‡å™¨å…·åˆ°æœŸæé†’。
 *
 * @author è¶…级管理员
 */
@Repository
public class HrmExpiryRemindRedisDAO {
    /**
     * æé†’去重 KEY çš„过期时间(60 å¤©ï¼‰ï¼šåŒä¸€ä¸šåŠ¡è®°å½•åŒä¸€åˆ°æœŸæ—¥ 60 å¤©å†…只提醒一次
     */
    private static final Duration REMIND_TIMEOUT = Duration.ofDays(60);
    /**
     * æ‰«æé—¨é—©çš„过期时间(30 åˆ†é’Ÿï¼‰ï¼šé¿å…å¤šå®žä¾‹/多次并发重复扫描
     */
    private static final Duration SCAN_LOCK_TIMEOUT = Duration.ofMinutes(30);
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    /**
     * å°è¯•标记已提醒(同一业务记录同一到期日 60 å¤©å†…只提醒一次)
     *
     * @param biz ä¸šåŠ¡æ ‡è¯†ï¼ˆå¦‚ cert=证书资质、contract=员工合同)
     * @param bizId ä¸šåŠ¡è®°å½•ç¼–å·
     * @param expireDate åˆ°æœŸæ—¥
     * @return true è¡¨ç¤ºé¦–次标记成功,需要发送提醒
     */
    public boolean tryMarkReminded(String biz, Long bizId, LocalDate expireDate) {
        String key = HrmRedisKeyConstants.EXPIRE_REMIND + biz + ":" + bizId + ":" + expireDate;
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "1", REMIND_TIMEOUT));
    }
    /**
     * å°è¯•获取当天的扫描门闩,避免多实例并发重复扫描
     *
     * @param biz ä¸šåŠ¡æ ‡è¯†ï¼ˆå¦‚ cert=证书资质、contract=员工合同)
     * @param today å½“天日期
     * @return true è¡¨ç¤ºèŽ·å–æˆåŠŸï¼ˆå¯ä»¥æ‰§è¡Œæ‰«æï¼‰
     */
    public boolean tryAcquireScanLock(String biz, LocalDate today) {
        String key = HrmRedisKeyConstants.EXPIRE_REMIND_SCAN + biz + ":" + today;
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "1", SCAN_LOCK_TIMEOUT));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java
@@ -57,10 +57,40 @@
     */
    public static final String EMPLOYEE_CONTRACT_NO_PREFIX = "HT";
    /**
     * åŸ¹è®­æ´»åŠ¨ç¼–å·å‰ç¼€
     */
    public static final String TRAINING_NO_PREFIX = "PX";
    /**
     * è½¬æ­£ç”³è¯·ç¼–号前缀
     */
    public static final String REGULARIZE_APPLICATION_NO_PREFIX = "ZZ";
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    /**
     * ç”ŸæˆåŸ¹è®­æ´»åŠ¨ç¼–å·ï¼Œæ ¼å¼ï¼šPX + yyyyMMdd + 4位自增
     * ä¾‹å¦‚:PX202401010001
     *
     * @return åŸ¹è®­æ´»åŠ¨ç¼–å·
     */
    public String generateTrainingNo() {
        return generate(TRAINING_NO_PREFIX, 4);
    }
    /**
     * ç”Ÿæˆè½¬æ­£ç”³è¯·ç¼–号,格式:ZZ + yyyyMMdd + 4位自增
     * ä¾‹å¦‚:ZZ202401010001
     *
     * @return è½¬æ­£ç”³è¯·ç¼–号
     */
    public String generateRegularizeApplicationNo() {
        return generate(REGULARIZE_APPLICATION_NO_PREFIX, 4);
    }
    /**
     * ç”Ÿæˆå‘˜å·¥ç¼–号,格式:EMP + yyyyMMdd + 4位自增
     * ä¾‹å¦‚:EMP202401010001
     *
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmRedisKeyConstants.java
@@ -12,4 +12,14 @@
     */
    String NO = "hrm:no:";
    /**
     * åˆ°æœŸæé†’去重 KEY å‰ç¼€ï¼ˆåŽæŽ¥ ä¸šåŠ¡æ ‡è¯†:id:到期日期)
     */
    String EXPIRE_REMIND = "hrm:remind:item:";
    /**
     * åˆ°æœŸæé†’扫描门闩 KEY å‰ç¼€ï¼ˆåŽæŽ¥ ä¸šåŠ¡æ ‡è¯†:当天日期)
     */
    String EXPIRE_REMIND_SCAN = "hrm:remind:scan:";
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/job/HrmExpiryRemindJob.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.hrm.job;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.module.hrm.service.remind.HrmExpiryRemindService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
/**
 * HRM è¯ä¹¦/合同到期提醒定时任务
 *
 * <p>每日扫描即将到期(默认 30 å¤©å†…,可在 infra_config è°ƒæ•´ï¼‰çš„证书资质与劳动合同,
 * å‘在岗员工系统账号发送站内信提醒。在「基础设施 â†’ å®šæ—¶ä»»åŠ¡ã€ä¸­ä»¥ handlerName=hrmExpiryRemindJob å¯ç”¨ã€‚
 *
 * @author è¶…级管理员
 */
@Component
public class HrmExpiryRemindJob implements JobHandler {
    @Resource
    private HrmExpiryRemindService expiryRemindService;
    @Override
    public String execute(String param) {
        expiryRemindService.sendAll();
        return "HRM è¯ä¹¦/合同到期提醒执行完成";
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmRegularizeApplicationService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package cn.iocoder.yudao.module.hrm.service.approval;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmRegularizeApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmRegularizeApplicationRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmRegularizeApplicationSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmRegularizeApplicationDO;
import java.util.List;
import java.util.Map;
/**
 * è½¬æ­£ç”³è¯· Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface HrmRegularizeApplicationService {
    /**
     * å®¡æ‰¹åˆ†ç±»ç¼–码:转正审批
     */
    String APPROVE_CATEGORY_CODE = "hrm_regularize_approve";
    /**
     * åˆ›å»ºè½¬æ­£ç”³è¯·
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç”³è¯·ID
     */
    Long createRegularizeApplication(HrmRegularizeApplicationSaveReqVO createReqVO);
    /**
     * æ›´æ–°è½¬æ­£ç”³è¯·
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateRegularizeApplication(HrmRegularizeApplicationSaveReqVO updateReqVO);
    /**
     * åˆ é™¤è½¬æ­£ç”³è¯·
     *
     * @param id ç”³è¯·ID
     */
    void deleteRegularizeApplication(Long id);
    /**
     * èŽ·å–è½¬æ­£ç”³è¯·
     *
     * @param id ç”³è¯·ID
     * @return ç”³è¯·ä¿¡æ¯
     */
    HrmRegularizeApplicationDO getRegularizeApplication(Long id);
    /**
     * èŽ·å–è½¬æ­£ç”³è¯·è¯¦æƒ…
     *
     * @param id ç”³è¯·ID
     * @return ç”³è¯·è¯¦æƒ…
     */
    HrmRegularizeApplicationRespVO getRegularizeApplicationDetail(Long id);
    /**
     * èŽ·å–è½¬æ­£ç”³è¯·åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µè¯·æ±‚
     * @return åˆ†é¡µç»“æžœ
     */
    PageResult<HrmRegularizeApplicationRespVO> getRegularizeApplicationPage(HrmRegularizeApplicationPageReqVO pageReqVO);
    /**
     * æäº¤è½¬æ­£ç”³è¯·å®¡æ‰¹
     *
     * @param id ç”³è¯·ID
     * @param processDefinitionKey æµç¨‹å®šä¹‰Key
     * @param userId å‘起人ID
     */
    void submitRegularizeApplication(Long id, String processDefinitionKey, Long userId);
    /**
     * æ›´æ–°è½¬æ­£ç”³è¯·å®¡æ‰¹çŠ¶æ€
     *
     * @param id ç”³è¯·ID
     * @param status å®¡æ‰¹çŠ¶æ€
     */
    void updateRegularizeApplicationAuditStatus(Long id, Integer status);
    /**
     * æ ¹æ®æµç¨‹å®žä¾‹ID获取转正申请
     *
     * @param processInstanceId æµç¨‹å®žä¾‹ID
     * @return ç”³è¯·ä¿¡æ¯
     */
    HrmRegularizeApplicationDO getRegularizeApplicationByProcessInstanceId(String processInstanceId);
    /**
     * èŽ·å–è½¬æ­£å®¡æ‰¹æµç¨‹å®šä¹‰åˆ—è¡¨
     *
     * @return æµç¨‹å®šä¹‰åˆ—表
     */
    List<Map<String, Object>> getApproveProcessDefinitionList();
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmRegularizeApplicationServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,307 @@
package cn.iocoder.yudao.module.hrm.service.approval;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmRegularizeApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmRegularizeApplicationRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmRegularizeApplicationSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmRegularizeApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmRegularizeApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmEmploymentTypeEnum;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.PostApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
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 java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
/**
 * è½¬æ­£ç”³è¯· Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
@Slf4j
public class HrmRegularizeApplicationServiceImpl implements HrmRegularizeApplicationService {
    @Resource
    private HrmRegularizeApplicationMapper regularizeApplicationMapper;
    @Resource
    private HrmNoRedisDAO noRedisDAO;
    @Resource
    private DeptApi deptApi;
    @Resource
    private PostApi postApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createRegularizeApplication(HrmRegularizeApplicationSaveReqVO createReqVO) {
        Long userId = createReqVO.getUserId();
        if (userId == null) {
            userId = getCurrentUserId();
        }
        HrmRegularizeApplicationDO existing = regularizeApplicationMapper.selectByUserIdAndStatusNot(userId,
                HrmAuditStatusEnum.CANCEL.getStatus());
        if (existing != null) {
            throw exception(REGULARIZE_APPLICATION_EXISTS);
        }
        String no = generateUniqueNo();
        AdminUserRespDTO user = adminUserApi.getUser(userId);
        if (user == null) {
            userId = getCurrentUserId();
            user = adminUserApi.getUser(userId);
        }
        HrmRegularizeApplicationDO application = BeanUtils.toBean(createReqVO, HrmRegularizeApplicationDO.class);
        application.setNo(no);
        application.setUserId(userId);
        application.setDeptId(createReqVO.getDeptId() != null ? createReqVO.getDeptId()
                : (user != null ? user.getDeptId() : null));
        application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
        regularizeApplicationMapper.insert(application);
        return application.getId();
    }
    private Long getCurrentUserId() {
        return SecurityFrameworkUtils.getLoginUserId();
    }
    private String generateUniqueNo() {
        String no = noRedisDAO.generateRegularizeApplicationNo();
        if (regularizeApplicationMapper.selectByNo(no) != null) {
            return generateUniqueNo();
        }
        return no;
    }
    @Override
    public void updateRegularizeApplication(HrmRegularizeApplicationSaveReqVO updateReqVO) {
        HrmRegularizeApplicationDO application = validateRegularizeApplicationExists(updateReqVO.getId());
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(REGULARIZE_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        HrmRegularizeApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmRegularizeApplicationDO.class);
        regularizeApplicationMapper.updateById(updateObj);
    }
    @Override
    public void deleteRegularizeApplication(Long id) {
        HrmRegularizeApplicationDO application = validateRegularizeApplicationExists(id);
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(REGULARIZE_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        regularizeApplicationMapper.deleteById(id);
    }
    private HrmRegularizeApplicationDO validateRegularizeApplicationExists(Long id) {
        HrmRegularizeApplicationDO application = regularizeApplicationMapper.selectById(id);
        if (application == null) {
            throw exception(REGULARIZE_APPLICATION_NOT_EXISTS);
        }
        return application;
    }
    @Override
    public HrmRegularizeApplicationDO getRegularizeApplication(Long id) {
        return regularizeApplicationMapper.selectById(id);
    }
    @Override
    public HrmRegularizeApplicationRespVO getRegularizeApplicationDetail(Long id) {
        HrmRegularizeApplicationDO application = getRegularizeApplication(id);
        if (application == null) {
            return null;
        }
        HrmRegularizeApplicationRespVO respVO = BeanUtils.toBean(application, HrmRegularizeApplicationRespVO.class);
        fillUserNames(respVO, application);
        if (application.getDeptId() != null) {
            DeptRespDTO dept = deptApi.getDept(application.getDeptId());
            if (dept != null) {
                respVO.setDeptName(dept.getName());
            }
        }
        if (application.getPostId() != null) {
            Map<Long, PostRespDTO> postMap = postApi.getPostMap(Collections.singleton(application.getPostId()));
            PostRespDTO post = postMap.get(application.getPostId());
            if (post != null) {
                respVO.setPostName(post.getName());
            }
        }
        respVO.setEmploymentTypeName(HrmEmploymentTypeEnum.getNameByType(application.getEmploymentType()));
        respVO.setStatusName(HrmAuditStatusEnum.getNameByStatus(application.getStatus()));
        return respVO;
    }
    private void fillUserNames(HrmRegularizeApplicationRespVO respVO, HrmRegularizeApplicationDO application) {
        if (application.getUserId() != null) {
            AdminUserRespDTO user = adminUserApi.getUser(application.getUserId());
            if (user != null) {
                respVO.setUserName(user.getNickname());
            }
        }
    }
    @Override
    public PageResult<HrmRegularizeApplicationRespVO> getRegularizeApplicationPage(HrmRegularizeApplicationPageReqVO pageReqVO) {
        PageResult<HrmRegularizeApplicationDO> pageResult = regularizeApplicationMapper.selectPage(pageReqVO);
        if (pageResult.getList().isEmpty()) {
            return new PageResult<>(Collections.emptyList(), pageResult.getTotal());
        }
        Set<Long> userIds = pageResult.getList().stream()
                .map(HrmRegularizeApplicationDO::getUserId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
        Set<Long> deptIds = pageResult.getList().stream()
                .map(HrmRegularizeApplicationDO::getDeptId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(deptIds);
        Set<Long> postIds = pageResult.getList().stream()
                .map(HrmRegularizeApplicationDO::getPostId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, PostRespDTO> postMap = postApi.getPostMap(postIds);
        List<HrmRegularizeApplicationRespVO> list = pageResult.getList().stream().map(app -> {
            HrmRegularizeApplicationRespVO vo = BeanUtils.toBean(app, HrmRegularizeApplicationRespVO.class);
            if (app.getUserId() != null && userMap.containsKey(app.getUserId())) {
                vo.setUserName(userMap.get(app.getUserId()).getNickname());
            }
            if (app.getDeptId() != null && deptMap.containsKey(app.getDeptId())) {
                vo.setDeptName(deptMap.get(app.getDeptId()).getName());
            }
            if (app.getPostId() != null && postMap.containsKey(app.getPostId())) {
                vo.setPostName(postMap.get(app.getPostId()).getName());
            }
            vo.setEmploymentTypeName(HrmEmploymentTypeEnum.getNameByType(app.getEmploymentType()));
            vo.setStatusName(HrmAuditStatusEnum.getNameByStatus(app.getStatus()));
            return vo;
        }).collect(Collectors.toList());
        return new PageResult<>(list, pageResult.getTotal());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitRegularizeApplication(Long id, String processDefinitionKey, Long userId) {
        HrmRegularizeApplicationDO application = validateRegularizeApplicationExists(id);
        if (ObjUtil.notEqual(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(REGULARIZE_APPLICATION_SUBMIT_FAIL_NOT_DRAFT);
        }
        validateApproveCategoryAndProcessDefinition();
        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
        if (processDefinition == null) {
            throw exception(REGULARIZE_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
                .setProcessDefinitionKey(processDefinitionKey).setBusinessKey(String.valueOf(id)));
        regularizeApplicationMapper.updateById(HrmRegularizeApplicationDO.builder()
                .id(id)
                .processInstanceId(processInstanceId)
                .status(HrmAuditStatusEnum.PROCESS.getStatus())
                .build());
    }
    private void validateApproveCategoryAndProcessDefinition() {
        List<String> codes = Collections.singletonList(APPROVE_CATEGORY_CODE);
        Map<String, ?> categoryMap = bpmCategoryService.getCategoryMap(codes);
        if (!categoryMap.containsKey(APPROVE_CATEGORY_CODE)) {
            throw exception(REGULARIZE_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
        if (CollUtil.isEmpty(definitionInfoList)) {
            throw exception(REGULARIZE_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
        List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
        boolean hasActiveDefinition = processDefinitions.stream().anyMatch(pd -> !pd.isSuspended());
        if (!hasActiveDefinition) {
            throw exception(REGULARIZE_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
    }
    @Override
    public List<Map<String, Object>> getApproveProcessDefinitionList() {
        validateApproveCategoryAndProcessDefinition();
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
        List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
        Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
        for (ProcessDefinition pd : processDefinitions) {
            if (pd.isSuspended()) continue;
            ProcessDefinition existing = latestVersionMap.get(pd.getKey());
            if (existing == null || pd.getVersion() > existing.getVersion()) {
                latestVersionMap.put(pd.getKey(), pd);
            }
        }
        List<Map<String, Object>> result = new ArrayList<>();
        for (ProcessDefinition pd : latestVersionMap.values()) {
            Map<String, Object> item = new HashMap<>();
            item.put("id", pd.getId());
            item.put("key", pd.getKey());
            item.put("name", pd.getName());
            item.put("version", pd.getVersion());
            result.add(item);
        }
        return result;
    }
    @Override
    public void updateRegularizeApplicationAuditStatus(Long id, Integer status) {
        HrmRegularizeApplicationDO application = validateRegularizeApplicationExists(id);
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) {
            log.error("[updateRegularizeApplicationAuditStatus][application({}) ä¸å¤„于审批中,无法更新审批结果({})]", id, status);
            throw exception(REGULARIZE_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
        }
        regularizeApplicationMapper.updateById(HrmRegularizeApplicationDO.builder()
                .id(id)
                .status(status)
                .approveTime(java.time.LocalDateTime.now())
                .build());
    }
    @Override
    public HrmRegularizeApplicationDO getRegularizeApplicationByProcessInstanceId(String processInstanceId) {
        return regularizeApplicationMapper.selectByProcessInstanceId(processInstanceId);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmRegularizeApplicationStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
package cn.iocoder.yudao.module.hrm.service.approval.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmRegularizeApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmRegularizeApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.service.approval.HrmRegularizeApplicationService;
import cn.iocoder.yudao.module.hrm.util.HrmAuditStatusUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
/**
 * è½¬æ­£ç”³è¯·å®¡æ‰¹çš„结果监听器实现类
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class HrmRegularizeApplicationStatusListener implements ApplicationListener<BpmProcessInstanceStatusEvent> {
    /**
     * å‘˜å·¥çŠ¶æ€ï¼šåœ¨èŒ
     */
    private static final Integer EMPLOYEE_STATUS_ACTIVE = 1;
    /**
     * ç”¨å·¥æ€§è´¨ï¼šæ­£å¼
     */
    private static final Integer EMPLOYMENT_TYPE_FORMAL = 1;
    @Resource
    private HrmRegularizeApplicationService regularizeApplicationService;
    @Resource
    private HrmRegularizeApplicationMapper regularizeApplicationMapper;
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Override
    public void onApplicationEvent(BpmProcessInstanceStatusEvent event) {
        // é€šè¿‡ businessKey æŸ¥è¯¢è½¬æ­£ç”³è¯·
        Long applicationId;
        try {
            applicationId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            return;
        }
        HrmRegularizeApplicationDO application = regularizeApplicationMapper.selectById(applicationId);
        if (application == null) {
            return;
        }
        // åªæœ‰å®¡æ‰¹ä¸­çŠ¶æ€çš„è½¬æ­£ç”³è¯·æ‰å¤„ç†
        if (!HrmAuditStatusEnum.PROCESS.getStatus().equals(application.getStatus())) {
            log.warn("[onApplicationEvent] è½¬æ­£ç”³è¯·({}) ä¸å¤„于审批中状态,当前状态: {}", applicationId, application.getStatus());
            return;
        }
        log.info("[onApplicationEvent] è½¬æ­£ç”³è¯·({}) å®¡æ‰¹çŠ¶æ€æ›´æ–°: {}", applicationId, event.getStatus());
        // æ›´æ–°å®¡æ‰¹çŠ¶æ€
        Integer auditStatus = HrmAuditStatusUtils.convertBpmResultToAuditStatus(event.getStatus());
        regularizeApplicationService.updateRegularizeApplicationAuditStatus(applicationId, auditStatus);
        // å®¡æ‰¹é€šè¿‡ï¼šå›žå†™å‘˜å·¥ä¸»æ¡£ï¼ˆçŠ¶æ€=在职,用工性质=正式,转正日期 = æ‹Ÿè½¬æ­£æ—¥æœŸæˆ–当天)
        if (HrmAuditStatusEnum.APPROVE.getStatus().equals(auditStatus)) {
            writeBackEmployee(application);
        }
    }
    private void writeBackEmployee(HrmRegularizeApplicationDO application) {
        HrmEmployeeDO employee = employeeMapper.selectByUserId(application.getUserId());
        if (employee == null) {
            log.warn("[writeBackEmployee] è½¬æ­£ç”³è¯·({}) ç”¨æˆ·({}) æœªæ‰¾åˆ°å‘˜å·¥æ¡£æ¡ˆï¼Œè·³è¿‡å›žå†™", application.getId(), application.getUserId());
            return;
        }
        LocalDate regularDate = application.getExpectedDate() != null ? application.getExpectedDate() : LocalDate.now();
        employeeMapper.updateById(HrmEmployeeDO.builder()
                .id(employee.getId())
                .employeeStatus(EMPLOYEE_STATUS_ACTIVE)
                .employmentType(EMPLOYMENT_TYPE_FORMAL)
                .regularDate(regularDate)
                .build());
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmResignationApplicationStatusListener.java
@@ -2,7 +2,9 @@
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmResignationApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmResignationApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.service.approval.HrmResignationApplicationService;
import cn.iocoder.yudao.module.hrm.util.HrmAuditStatusUtils;
@@ -20,10 +22,17 @@
@Slf4j
public class HrmResignationApplicationStatusListener implements ApplicationListener<BpmProcessInstanceStatusEvent> {
    /**
     * å‘˜å·¥çŠ¶æ€ï¼šç¦»èŒ
     */
    private static final Integer EMPLOYEE_STATUS_RESIGNED = 3;
    @Resource
    private HrmResignationApplicationService resignationApplicationService;
    @Resource
    private HrmResignationApplicationMapper resignationApplicationMapper;
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Override
    public void onApplicationEvent(BpmProcessInstanceStatusEvent event) {
@@ -50,6 +59,25 @@
        // æ›´æ–°å®¡æ‰¹çŠ¶æ€
        Integer auditStatus = HrmAuditStatusUtils.convertBpmResultToAuditStatus(event.getStatus());
        resignationApplicationService.updateResignationApplicationAuditStatus(applicationId, auditStatus);
        // å®¡æ‰¹é€šè¿‡ï¼šå›žå†™å‘˜å·¥ä¸»æ¡£ï¼ˆçŠ¶æ€=离职,离职日期 = å®žé™…离职日期或预计离职日期)
        if (HrmAuditStatusEnum.APPROVE.getStatus().equals(auditStatus)) {
            writeBackEmployee(application);
        }
    }
    private void writeBackEmployee(HrmResignationApplicationDO application) {
        HrmEmployeeDO employee = employeeMapper.selectByUserId(application.getUserId());
        if (employee == null) {
            log.warn("[writeBackEmployee] ç¦»èŒç”³è¯·({}) ç”¨æˆ·({}) æœªæ‰¾åˆ°å‘˜å·¥æ¡£æ¡ˆï¼Œè·³è¿‡å›žå†™", application.getId(), application.getUserId());
            return;
        }
        java.time.LocalDate leaveDate = application.getActualDate() != null
                ? application.getActualDate() : application.getExpectedDate();
        employeeMapper.updateById(HrmEmployeeDO.builder()
                .id(employee.getId())
                .employeeStatus(EMPLOYEE_STATUS_RESIGNED)
                .leaveDate(leaveDate)
                .build());
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmTransferApplicationStatusListener.java
@@ -2,7 +2,9 @@
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmTransferApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmTransferApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.service.approval.HrmTransferApplicationService;
import cn.iocoder.yudao.module.hrm.util.HrmAuditStatusUtils;
@@ -24,6 +26,8 @@
    private HrmTransferApplicationService transferApplicationService;
    @Resource
    private HrmTransferApplicationMapper transferApplicationMapper;
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Override
    public void onApplicationEvent(BpmProcessInstanceStatusEvent event) {
@@ -50,6 +54,23 @@
        // æ›´æ–°å®¡æ‰¹çŠ¶æ€
        Integer auditStatus = HrmAuditStatusUtils.convertBpmResultToAuditStatus(event.getStatus());
        transferApplicationService.updateTransferApplicationAuditStatus(applicationId, auditStatus);
        // å®¡æ‰¹é€šè¿‡ï¼šå›žå†™å‘˜å·¥ä¸»æ¡£ï¼ˆéƒ¨é—¨ = ç›®æ ‡éƒ¨é—¨ï¼Œå²—位 = ç›®æ ‡å²—位)
        if (HrmAuditStatusEnum.APPROVE.getStatus().equals(auditStatus)) {
            writeBackEmployee(application);
        }
    }
    private void writeBackEmployee(HrmTransferApplicationDO application) {
        HrmEmployeeDO employee = employeeMapper.selectByUserId(application.getUserId());
        if (employee == null) {
            log.warn("[writeBackEmployee] è°ƒå²—申请({}) ç”¨æˆ·({}) æœªæ‰¾åˆ°å‘˜å·¥æ¡£æ¡ˆï¼Œè·³è¿‡å›žå†™", application.getId(), application.getUserId());
            return;
        }
        employeeMapper.updateById(HrmEmployeeDO.builder()
                .id(employee.getId())
                .deptId(application.getTargetDeptId())
                .postId(application.getTargetPostId())
                .build());
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java
@@ -64,4 +64,11 @@
     */
    void supplementClockIn(HrmAttendanceExceptionDO exception);
    /**
     * åŠ ç­å¼‚å¸¸å®¡æ‰¹é€šè¿‡åŽå›žå†™å½“æ—¥è€ƒå‹¤è®°å½•çš„åŠ ç­æ—¶é•¿
     *
     * @param exception è€ƒå‹¤å¼‚常申请(type=加班)
     */
    void recordOvertime(HrmAttendanceExceptionDO exception);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
@@ -173,8 +173,59 @@
                    .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
            record.setWorkHours(workHours);
        }
        // 7. è®¡ç®—加班时长:超过规则下班时间的部分
        record.setOvertimeHours(computeOvertimeHours(rule.getWorkEndTime(), clockTimeOnly));
        attendanceRecordMapper.updateById(record);
        return record;
    }
    /**
     * è®¡ç®—加班时长(小时):clockTimeOnly æ™šäºŽ workEndTime çš„部分,否则为 0
     */
    private BigDecimal computeOvertimeHours(LocalTime workEndTime, LocalTime clockTimeOnly) {
        if (workEndTime == null || clockTimeOnly == null || !clockTimeOnly.isAfter(workEndTime)) {
            return BigDecimal.ZERO;
        }
        return BigDecimal.valueOf(Duration.between(workEndTime, clockTimeOnly).toMinutes())
                .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void recordOvertime(HrmAttendanceExceptionDO exception) {
        // ä»…处理加班类型
        if (!HrmAttendanceExceptionTypeEnum.OVERTIME.getType().equals(exception.getType())
                || exception.getExceptionTime() == null) {
            return;
        }
        // 1. å–员工考勤规则,用规则下班时间到异常时间(实际下班点)折算加班时长;无规则则无法折算,跳过
        BigDecimal overtimeHours = BigDecimal.ZERO;
        HrmEmployeeDO employee = employeeService.getEmployee(exception.getUserId());
        HrmAttendanceRuleDO rule = employee == null ? null
                : attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
        if (rule != null) {
            overtimeHours = computeOvertimeHours(rule.getWorkEndTime(), exception.getExceptionTime().toLocalTime());
        }
        if (overtimeHours.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        // 2. æŸ¥è¯¢å½“日考勤记录,不存在则补建
        HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(exception.getUserId(), exception.getExceptionDate());
        if (record == null) {
            record = HrmAttendanceRecordDO.builder()
                    .userId(exception.getUserId())
                    .deptId(employee != null ? employee.getDeptId() : exception.getDeptId())
                    .date(exception.getExceptionDate())
                    .dataSource(HrmAttendanceDataSourceEnum.SUPPLEMENT.getType())
                    .build();
            record.setOvertimeHours(overtimeHours);
            attendanceRecordMapper.insert(record);
            return;
        }
        // 3. å·²æœ‰è®°å½•则累加加班时长
        BigDecimal existed = record.getOvertimeHours() != null ? record.getOvertimeHours() : BigDecimal.ZERO;
        record.setOvertimeHours(existed.add(overtimeHours));
        attendanceRecordMapper.updateById(record);
    }
    @Override
@@ -187,11 +238,13 @@
        LocalDate exceptionDate = exception.getExceptionDate();
        LocalDateTime exceptionTime = exception.getExceptionTime();
        Long userId = exception.getUserId();
        HrmEmployeeDO employee = employeeService.getEmployee(userId);
        HrmAttendanceRuleDO rule = employee == null ? null
                : attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
        // æŸ¥è¯¢è€ƒå‹¤è®°å½•
        HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, exceptionDate);
        if (record == null) {
            // å¦‚果没有记录,创建一条
            HrmEmployeeDO employee = employeeService.getEmployee(userId);
            record = HrmAttendanceRecordDO.builder()
                    .userId(userId)
                    .deptId(employee != null ? employee.getDeptId() : exception.getDeptId())
@@ -210,6 +263,8 @@
            // ä¸‹ç­è¡¥å¡
            record.setClockOutTime(exceptionTime);
            record.setClockOutType(HrmAttendanceClockTypeEnum.NORMAL.getType());
            // ä¸‹ç­è¡¥å¡åŒæ—¶æŠ˜ç®—加班时长(超过规则下班时间部分)
            record.setOvertimeHours(computeOvertimeHours(rule == null ? null : rule.getWorkEndTime(), time));
        }
        // æ›´æ–°æ•°æ®æ¥æºä¸ºè¡¥å¡å®¡æ‰¹
        record.setDataSource(HrmAttendanceDataSourceEnum.SUPPLEMENT.getType());
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/listener/HrmAttendanceExceptionStatusListener.java
@@ -56,11 +56,15 @@
        // æ›´æ–°å®¡æ‰¹çŠ¶æ€
        attendanceExceptionService.updateAttendanceExceptionAuditStatus(exceptionId, auditStatus);
        // è¡¥å¡å®¡æ‰¹é€šè¿‡åŽï¼Œæ›´æ–°è€ƒå‹¤è®°å½•
        if (HrmAttendanceExceptionTypeEnum.SUPPLEMENT.getType().equals(exception.getType())
                && HrmAuditStatusEnum.APPROVE.getStatus().equals(auditStatus)) {
            log.info("[onApplicationEvent] è¡¥å¡ç”³è¯·({}) å®¡æ‰¹é€šè¿‡ï¼Œå¼€å§‹æ›´æ–°è€ƒå‹¤è®°å½•", exceptionId);
            attendanceRecordService.supplementClockIn(exception);
        // è¡¥å¡/加班审批通过后,更新考勤记录(补卡回填上下班时间,加班回写加班时长)
        if (HrmAuditStatusEnum.APPROVE.getStatus().equals(auditStatus)) {
            if (HrmAttendanceExceptionTypeEnum.SUPPLEMENT.getType().equals(exception.getType())) {
                log.info("[onApplicationEvent] è¡¥å¡ç”³è¯·({}) å®¡æ‰¹é€šè¿‡ï¼Œå¼€å§‹æ›´æ–°è€ƒå‹¤è®°å½•", exceptionId);
                attendanceRecordService.supplementClockIn(exception);
            } else if (HrmAttendanceExceptionTypeEnum.OVERTIME.getType().equals(exception.getType())) {
                log.info("[onApplicationEvent] åŠ ç­ç”³è¯·({}) å®¡æ‰¹é€šè¿‡ï¼Œå¼€å§‹å›žå†™åŠ ç­æ—¶é•¿", exceptionId);
                attendanceRecordService.recordOvertime(exception);
            }
        }
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -6,6 +6,9 @@
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.enums.HrmEmploymentTypeEnum;
import cn.iocoder.yudao.module.mes.api.cal.MesCalTeamApi;
import cn.iocoder.yudao.module.mes.api.cal.dto.MesCalTeamRespDTO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEducationSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeEmergencyContactSaveReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.employee.vo.HrmEmployeeImportExcelVO;
@@ -81,6 +84,8 @@
    private AdminUserApi adminUserApi;
    @Resource
    private RoleApi roleApi;
    @Resource
    private MesCalTeamApi mesCalTeamApi;
    @Resource
    @Lazy // è‡ªæ³¨å…¥éœ€å»¶è¿ŸåŠ è½½ï¼Œå¦åˆ™ bean è¢«ä»£ç†åŒ…装后注入原始版本导致启动失败
    private HrmEmployeeService self;
@@ -285,6 +290,17 @@
                respVO.setRoleNames(roles.stream().map(RoleRespDTO::getName).collect(Collectors.joining(",")));
            }
        }
        // å¡«å……用工性质名称
        if (employee.getEmploymentType() != null) {
            respVO.setEmploymentTypeName(HrmEmploymentTypeEnum.getNameByType(employee.getEmploymentType()));
        }
        // å¡«å……所属班组名称
        if (employee.getTeamId() != null) {
            List<MesCalTeamRespDTO> teams = mesCalTeamApi.getTeamList(Collections.singleton(employee.getTeamId()));
            if (CollUtil.isNotEmpty(teams)) {
                respVO.setTeamName(teams.get(0).getName());
            }
        }
        return respVO;
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/performanceappraise/HrmPerformanceAppraiseService.java
@@ -76,4 +76,12 @@
     */
    List<HrmPerformanceAppraiseResultItemDO> getAppraiseResultItems(Long resultId);
    /**
     * ç”Ÿæˆè¢«è€ƒæ ¸äººï¼ˆæŒ‰è€ƒæ ¸èŒƒå›´ï¼šå…¨éƒ¨åœ¨èŒå‘˜å·¥ / æŒ‡å®šç­ç»„员工),已存在结果的不重复生成
     *
     * @param appraiseId è€ƒæ ¸æ–¹æ¡ˆç¼–号
     * @return æœ¬æ¬¡æ–°ç”Ÿæˆçš„被考核人数
     */
    Integer generateAppraisees(Long appraiseId);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/performanceappraise/HrmPerformanceAppraiseServiceImpl.java
@@ -2,6 +2,8 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
@@ -11,7 +13,9 @@
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.hrm.controller.admin.performanceappraise.vo.*;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.performanceappraise.*;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.performanceappraise.*;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants;
@@ -67,6 +71,17 @@
    private static final Integer SCORER_SUPERIOR = 2;
    /**
     * è€ƒæ ¸èŒƒå›´ç±»åž‹ï¼š1 å…¨éƒ¨å‘˜å·¥ / 2 æŒ‡å®šç­ç»„
     */
    private static final Integer SCOPE_ALL = 1;
    private static final Integer SCOPE_TEAM = 2;
    /**
     * åœ¨èŒå‘˜å·¥çŠ¶æ€
     */
    private static final Integer EMPLOYEE_STATUS_ACTIVE = 1;
    /**
     * BPM ç»©æ•ˆè€ƒæ ¸å®¡æ‰¹åˆ†ç±»ç¼–码
     */
    private static final String APPRAISE_APPROVE_CATEGORY_CODE = "performance_appraise_approve";
@@ -80,6 +95,8 @@
    @Resource
    private HrmPerformanceAppraiseResultItemMapper resultItemMapper;
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Resource
    private BpmCategoryService bpmCategoryService;
@@ -92,6 +109,8 @@
        HrmPerformanceAppraiseDO appraise = BeanUtils.toBean(createReqVO, HrmPerformanceAppraiseDO.class, req -> req
                .setStatus(STATUS_DRAFT)
                .setAuditStatus(HrmAuditStatusEnum.DRAFT.getStatus()));
        appraise.setScopeTeamIds(CollUtil.isEmpty(createReqVO.getScopeTeamIds())
                ? null : JSONUtil.toJsonStr(createReqVO.getScopeTeamIds()));
        appraiseMapper.insert(appraise);
        return appraise.getId();
    }
@@ -104,6 +123,8 @@
                .setStatus(oldAppraise.getStatus())
                .setAuditStatus(oldAppraise.getAuditStatus())
                .setProcessInstanceId(oldAppraise.getProcessInstanceId()));
        updateObj.setScopeTeamIds(CollUtil.isEmpty(updateReqVO.getScopeTeamIds())
                ? null : JSONUtil.toJsonStr(updateReqVO.getScopeTeamIds()));
        appraiseMapper.updateById(updateObj);
    }
@@ -391,6 +412,44 @@
        return resultItemMapper.selectListByResultId(resultId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Integer generateAppraisees(Long appraiseId) {
        HrmPerformanceAppraiseDO appraise = validateAppraiseExists(appraiseId);
        // 1. æŒ‰è€ƒæ ¸èŒƒå›´ç­›é€‰å‘˜å·¥
        List<HrmEmployeeDO> employees;
        if (Objects.equals(appraise.getScopeType(), SCOPE_TEAM)) {
            List<Long> teamIds = StrUtil.isEmpty(appraise.getScopeTeamIds())
                    ? Collections.emptyList() : JSONUtil.toList(appraise.getScopeTeamIds(), Long.class);
            if (CollUtil.isEmpty(teamIds)) {
                throw exception(ErrorCodeConstants.PERFORMANCE_APPRAISE_SCOPE_TEAM_EMPTY);
            }
            employees = employeeMapper.selectListByTeamIds(teamIds);
        } else {
            employees = employeeMapper.selectListByStatus(EMPLOYEE_STATUS_ACTIVE);
        }
        if (CollUtil.isEmpty(employees)) {
            return 0;
        }
        // 2. å·²å­˜åœ¨ç»“果的不再生成(幂等)
        List<HrmPerformanceAppraiseResultDO> existing = resultMapper.selectList(
                new LambdaQueryWrapperX<HrmPerformanceAppraiseResultDO>()
                        .eq(HrmPerformanceAppraiseResultDO::getAppraiseId, appraiseId));
        Set<Long> existingEmployeeIds = convertSet(existing, HrmPerformanceAppraiseResultDO::getEmployeeId);
        // 3. ç”Ÿæˆç©ºç™½ç»“果行
        int count = 0;
        for (HrmEmployeeDO employee : employees) {
            if (existingEmployeeIds.contains(employee.getId())) {
                continue;
            }
            resultMapper.insert(new HrmPerformanceAppraiseResultDO()
                    .setAppraiseId(appraiseId)
                    .setEmployeeId(employee.getId()));
            count++;
        }
        return count;
    }
    // ========== ç§æœ‰æ–¹æ³• ==========
    private Integer calcGrade(BigDecimal finalScore) {
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/remind/HrmExpiryRemindService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.hrm.service.remind;
/**
 * HRM è¯ä¹¦/合同到期提醒 Service æŽ¥å£
 *
 * <p>对即将到期的证书资质、劳动合同向持证/在岗员工发送站内信提醒,
 * æ»¡è¶³ç”µåŠ›è¡Œä¸šå®‰å…¨åŸ¹è®­åˆæ ¼è¯ã€ç‰¹ç§ä½œä¸šè¯ä¹¦ã€ä»Žä¸šèµ„è´¨ç­‰åˆè§„ç»­æœŸè¦æ±‚ã€‚
 *
 * @author è¶…级管理员
 */
public interface HrmExpiryRemindService {
    /**
     * å‘送证书资质到期提醒(即将到期 [today, today+N])
     */
    void sendCertificateExpireRemind();
    /**
     * å‘送劳动合同到期提醒(即将到期 [today, today+N])
     */
    void sendContractExpireRemind();
    /**
     * å‘送全部到期提醒(先证书后合同),供每日定时任务调用
     */
    void sendAll();
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/remind/HrmExpiryRemindServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,217 @@
package cn.iocoder.yudao.module.hrm.service.remind;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.hrm.dal.dataobject.certificate.HrmCertificateDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeContractDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.certificate.HrmCertificateMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeContractMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmExpiryRemindRedisDAO;
import cn.iocoder.yudao.module.hrm.enums.HrmEmployeeStatusEnum;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
/**
 * HRM è¯ä¹¦/合同到期提醒 Service å®žçŽ°ç±»
 *
 * <p>参照 MES è®¡é‡å™¨å…·åˆ°æœŸæé†’实现:Redis æ—¥é”é˜²å¤šå®žä¾‹å¹¶å‘重复扫描、按业务记录+到期日去重,
 * å‘员工系统账号(hrm_employee.userId)发送站内信;提醒天数从 infra_config è¯»å–,默认 30 å¤©ã€‚
 *
 * @author è¶…级管理员
 */
@Slf4j
@Service
public class HrmExpiryRemindServiceImpl implements HrmExpiryRemindService {
    /**
     * ä¸šåŠ¡æ ‡è¯†ï¼šè¯ä¹¦èµ„è´¨
     */
    private static final String BIZ_CERT = "cert";
    /**
     * ä¸šåŠ¡æ ‡è¯†ï¼šå‘˜å·¥åˆåŒ
     */
    private static final String BIZ_CONTRACT = "contract";
    /**
     * è¯ä¹¦åˆ°æœŸæé†’天数配置 KEY(infra_config)
     */
    private static final String CERT_REMIND_DAYS_KEY = "hrm.certificate.remind-days";
    /**
     * åˆåŒåˆ°æœŸæé†’天数配置 KEY(infra_config)
     */
    private static final String CONTRACT_REMIND_DAYS_KEY = "hrm.contract.remind-days";
    /**
     * æé†’天数默认值
     */
    private static final int DEFAULT_REMIND_DAYS = 30;
    /**
     * è¯ä¹¦åˆ°æœŸæé†’站内信模板编号
     */
    private static final String CERT_REMIND_TEMPLATE = "hrm_certificate_expire_remind";
    /**
     * åˆåŒåˆ°æœŸæé†’站内信模板编号
     */
    private static final String CONTRACT_REMIND_TEMPLATE = "hrm_contract_expire_remind";
    @Resource
    private HrmCertificateMapper certificateMapper;
    @Resource
    private HrmEmployeeContractMapper contractMapper;
    @Resource
    private HrmExpiryRemindRedisDAO expiryRemindRedisDAO;
    @Resource
    private ConfigApi configApi;
    @Resource
    private NotifyMessageSendApi notifyMessageSendApi;
    @Resource
    private HrmEmployeeService employeeService;
    @Override
    public void sendAll() {
        sendCertificateExpireRemind();
        sendContractExpireRemind();
    }
    @Override
    public void sendCertificateExpireRemind() {
        try {
            LocalDate today = LocalDate.now();
            if (!expiryRemindRedisDAO.tryAcquireScanLock(BIZ_CERT, today)) {
                return;
            }
            int remindDays = getRemindDays(CERT_REMIND_DAYS_KEY);
            List<HrmCertificateDO> list = certificateMapper.selectListByExpireDateBetween(
                    today, today.plusDays(remindDays));
            if (CollUtil.isEmpty(list)) {
                return;
            }
            Map<Long, HrmEmployeeDO> employeeMap = buildEmployeeMap(
                    convertSet(list, HrmCertificateDO::getEmployeeId));
            for (HrmCertificateDO certificate : list) {
                if (certificate.getExpireDate() == null) {
                    continue;
                }
                HrmEmployeeDO employee = employeeMap.get(certificate.getEmployeeId());
                if (!canNotify(employee)) {
                    continue;
                }
                if (!expiryRemindRedisDAO.tryMarkReminded(BIZ_CERT, certificate.getId(), certificate.getExpireDate())) {
                    continue;
                }
                sendCertificateRemind(certificate, employee);
            }
        } catch (Exception ex) {
            // best-effort:提醒失败不影响列表查询/定时任务整体
            log.error("[sendCertificateExpireRemind][发送证书到期提醒失败]", ex);
        }
    }
    @Override
    public void sendContractExpireRemind() {
        try {
            LocalDate today = LocalDate.now();
            if (!expiryRemindRedisDAO.tryAcquireScanLock(BIZ_CONTRACT, today)) {
                return;
            }
            int remindDays = getRemindDays(CONTRACT_REMIND_DAYS_KEY);
            List<HrmEmployeeContractDO> list = contractMapper.selectListExpiringBetween(
                    today, today.plusDays(remindDays));
            if (CollUtil.isEmpty(list)) {
                return;
            }
            Map<Long, HrmEmployeeDO> employeeMap = buildEmployeeMap(
                    convertSet(list, HrmEmployeeContractDO::getEmployeeId));
            for (HrmEmployeeContractDO contract : list) {
                if (contract.getEndDate() == null) {
                    continue;
                }
                HrmEmployeeDO employee = employeeMap.get(contract.getEmployeeId());
                if (!canNotify(employee)) {
                    continue;
                }
                if (!expiryRemindRedisDAO.tryMarkReminded(BIZ_CONTRACT, contract.getId(), contract.getEndDate())) {
                    continue;
                }
                sendContractRemind(contract, employee);
            }
        } catch (Exception ex) {
            // best-effort:提醒失败不影响列表查询/定时任务整体
            log.error("[sendContractExpireRemind][发送合同到期提醒失败]", ex);
        }
    }
    private boolean canNotify(HrmEmployeeDO employee) {
        // ä»…向有系统账号且在岗/试用员工发送提醒;离职员工(账号多已停用)不打扰
        return employee != null && employee.getUserId() != null
                && !HrmEmployeeStatusEnum.LEAVE.getStatus().equals(employee.getEmployeeStatus());
    }
    private void sendCertificateRemind(HrmCertificateDO certificate, HrmEmployeeDO employee) {
        Map<String, Object> params = new HashMap<>();
        params.put("employeeName", StrUtil.nullToEmpty(employee.getName()));
        params.put("certTypeName", StrUtil.nullToEmpty(convertCertTypeName(certificate.getCertType())));
        params.put("certName", StrUtil.nullToEmpty(certificate.getCertName()));
        params.put("expireDate", StrUtil.nullToEmpty(certificate.getExpireDate() == null ? null : certificate.getExpireDate().toString()));
        notifyMessageSendApi.sendSingleMessageToAdmin(new NotifySendSingleToUserReqDTO()
                .setUserId(employee.getUserId())
                .setTemplateCode(CERT_REMIND_TEMPLATE)
                .setTemplateParams(params));
    }
    private void sendContractRemind(HrmEmployeeContractDO contract, HrmEmployeeDO employee) {
        Map<String, Object> params = new HashMap<>();
        params.put("employeeName", StrUtil.nullToEmpty(employee.getName()));
        params.put("contractNo", StrUtil.nullToEmpty(contract.getContractNo()));
        params.put("endDate", StrUtil.nullToEmpty(contract.getEndDate() == null ? null : contract.getEndDate().toString()));
        notifyMessageSendApi.sendSingleMessageToAdmin(new NotifySendSingleToUserReqDTO()
                .setUserId(employee.getUserId())
                .setTemplateCode(CONTRACT_REMIND_TEMPLATE)
                .setTemplateParams(params));
    }
    private Map<Long, HrmEmployeeDO> buildEmployeeMap(java.util.Collection<Long> employeeIds) {
        if (CollUtil.isEmpty(employeeIds)) {
            return Map.of();
        }
        return employeeService.getEmployeeList(employeeIds).stream()
                .collect(java.util.stream.Collectors.toMap(HrmEmployeeDO::getId, e -> e, (a, b) -> a));
    }
    private int getRemindDays(String configKey) {
        String value = configApi.getConfigValueByKey(configKey);
        if (StrUtil.isBlank(value)) {
            return DEFAULT_REMIND_DAYS;
        }
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException ex) {
            log.warn("[getRemindDays][配置 {} çš„值 {} éžæ³•,使用默认值 {}]", configKey, value, DEFAULT_REMIND_DAYS);
            return DEFAULT_REMIND_DAYS;
        }
    }
    private String convertCertTypeName(Integer certType) {
        return switch (certType == null ? 0 : certType) {
            case 1 -> "特种作业证书";
            case 2 -> "从业资质";
            case 3 -> "技能等级证书";
            case 4 -> "安全培训合格证";
            default -> "证书";
        };
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmEmployeeSalaryService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.hrm.service.salary;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmEmployeeSalaryPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
import java.util.List;
/**
 * å‘˜å·¥è–ªé…¬æ¡£æ¡ˆ Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface HrmEmployeeSalaryService {
    /**
     * åˆ›å»ºå‘˜å·¥è–ªé…¬æ¡£æ¡ˆ
     *
     * @param salary æ¡£æ¡ˆä¿¡æ¯
     * @return æ¡£æ¡ˆID
     */
    Long createEmployeeSalary(HrmEmployeeSalaryDO salary);
    /**
     * æ›´æ–°å‘˜å·¥è–ªé…¬æ¡£æ¡ˆ
     *
     * @param salary æ¡£æ¡ˆä¿¡æ¯
     */
    void updateEmployeeSalary(HrmEmployeeSalaryDO salary);
    /**
     * åˆ é™¤å‘˜å·¥è–ªé…¬æ¡£æ¡ˆ
     *
     * @param id æ¡£æ¡ˆID
     */
    void deleteEmployeeSalary(Long id);
    /**
     * èŽ·å–å‘˜å·¥è–ªé…¬æ¡£æ¡ˆ
     *
     * @param id æ¡£æ¡ˆID
     * @return æ¡£æ¡ˆä¿¡æ¯
     */
    HrmEmployeeSalaryDO getEmployeeSalary(Long id);
    /**
     * èŽ·å–å‘˜å·¥è–ªé…¬æ¡£æ¡ˆåˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µè¯·æ±‚
     * @return åˆ†é¡µç»“æžœ
     */
    PageResult<HrmEmployeeSalaryDO> getEmployeeSalaryPage(HrmEmployeeSalaryPageReqVO pageReqVO);
    /**
     * èŽ·å–å‘˜å·¥è–ªé…¬æ¡£æ¡ˆåŽ†å²åˆ—è¡¨
     *
     * @param userId å‘˜å·¥ID
     * @return æ¡£æ¡ˆåˆ—表
     */
    List<HrmEmployeeSalaryDO> getEmployeeSalaryList(Long userId);
    /**
     * èŽ·å–å‘˜å·¥æœ€æ–°ç”Ÿæ•ˆçš„è–ªé…¬æ¡£æ¡ˆ
     *
     * @param userId å‘˜å·¥ID
     * @return æ¡£æ¡ˆä¿¡æ¯
     */
    HrmEmployeeSalaryDO getLatestEmployeeSalary(Long userId);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmEmployeeSalaryServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.hrm.service.salary;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmEmployeeSalaryPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.EMPLOYEE_SALARY_NOT_EXISTS;
/**
 * å‘˜å·¥è–ªé…¬æ¡£æ¡ˆ Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class HrmEmployeeSalaryServiceImpl implements HrmEmployeeSalaryService {
    @Resource
    private HrmEmployeeSalaryMapper employeeSalaryMapper;
    @Override
    public Long createEmployeeSalary(HrmEmployeeSalaryDO salary) {
        employeeSalaryMapper.insert(salary);
        return salary.getId();
    }
    @Override
    public void updateEmployeeSalary(HrmEmployeeSalaryDO salary) {
        validateEmployeeSalaryExists(salary.getId());
        employeeSalaryMapper.updateById(salary);
    }
    @Override
    public void deleteEmployeeSalary(Long id) {
        validateEmployeeSalaryExists(id);
        employeeSalaryMapper.deleteById(id);
    }
    @Override
    public HrmEmployeeSalaryDO getEmployeeSalary(Long id) {
        return employeeSalaryMapper.selectById(id);
    }
    @Override
    public PageResult<HrmEmployeeSalaryDO> getEmployeeSalaryPage(HrmEmployeeSalaryPageReqVO pageReqVO) {
        return employeeSalaryMapper.selectPage(pageReqVO);
    }
    @Override
    public List<HrmEmployeeSalaryDO> getEmployeeSalaryList(Long userId) {
        return employeeSalaryMapper.selectListByUserId(userId);
    }
    @Override
    public HrmEmployeeSalaryDO getLatestEmployeeSalary(Long userId) {
        return employeeSalaryMapper.selectLatestByUserId(userId);
    }
    private void validateEmployeeSalaryExists(Long id) {
        if (employeeSalaryMapper.selectById(id) == null) {
            throw exception(EMPLOYEE_SALARY_NOT_EXISTS);
        }
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -5,22 +5,30 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
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.attendance.HrmAttendanceSummaryDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.performanceappraise.HrmPerformanceAppraiseDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.performanceappraise.HrmPerformanceAppraiseResultDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSocialSecurityDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmLeaveTypeConfigDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryCalculationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryPaymentDetailDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryPaymentDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSocialSecuritySchemeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceSummaryMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.performanceappraise.HrmPerformanceAppraiseMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.performanceappraise.HrmPerformanceAppraiseResultMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSocialSecurityMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmLeaveTypeConfigMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryCalculationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryPaymentDetailMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryPaymentMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryStructureMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSocialSecuritySchemeMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -63,6 +71,24 @@
    private static final BigDecimal DEFAULT_UNEMPLOYMENT_PERSONAL_RATIO = new BigDecimal("0.005");
    private static final BigDecimal DEFAULT_HOUSING_FUND_PERSONAL_RATIO = new BigDecimal("0.12");
    /**
     * åŠ ç­å·¥èµ„å€çŽ‡é»˜è®¤å€¼ï¼ˆè–ªé…¬ç»“æž„æœªé…ç½®æ—¶ä½¿ç”¨ï¼‰
     */
    private static final BigDecimal DEFAULT_OVERTIME_PAY_RATIO = new BigDecimal("1.5");
    /**
     * é»˜è®¤åº”出勤天数(无考勤汇总时的兜底值)
     */
    private static final int DEFAULT_WORK_DAYS = 22;
    /**
     * ç»©æ•ˆå·¥èµ„档级系数:1-优秀 2-良好 3-合格 4-待改进
     */
    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_EXCELLENT = new BigDecimal("1.2");
    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_GOOD = new BigDecimal("1.0");
    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_PASS = new BigDecimal("0.8");
    private static final BigDecimal PERFORMANCE_GRADE_FACTOR_IMPROVE = new BigDecimal("0.6");
    @Resource
    private HrmSalaryCalculationMapper salaryCalculationMapper;
    @Resource
@@ -87,6 +113,14 @@
    private HrmSalaryPaymentMapper salaryPaymentMapper;
    @Resource
    private HrmSalaryPaymentDetailMapper salaryPaymentDetailMapper;
    @Resource
    private HrmAttendanceSummaryMapper attendanceSummaryMapper;
    @Resource
    private HrmPerformanceAppraiseMapper performanceAppraiseMapper;
    @Resource
    private HrmPerformanceAppraiseResultMapper performanceAppraiseResultMapper;
    @Resource
    private HrmSalaryStructureMapper salaryStructureMapper;
    @Override
    public PageResult<HrmSalaryCalculationDO> getSalaryCalculationPage(HrmSalaryCalculationPageReqVO pageReqVO) {
@@ -204,7 +238,21 @@
        BigDecimal mealAllowance = employeeSalary.getMealAllowance() != null ? employeeSalary.getMealAllowance() : BigDecimal.ZERO;
        BigDecimal transportAllowance = employeeSalary.getTransportAllowance() != null ? employeeSalary.getTransportAllowance() : BigDecimal.ZERO;
        BigDecimal otherAllowance = employeeSalary.getOtherAllowance() != null ? employeeSalary.getOtherAllowance() : BigDecimal.ZERO;
        // ===== è€ƒå‹¤é“¾è·¯ï¼šåº”出勤天数 / åŠ ç­æ—¶é•¿ / ç¼ºå‹¤æ‰£æ¬¾ =====
        HrmAttendanceSummaryDO attendanceSummary = attendanceSummaryMapper.selectByUserIdAndYearMonth(employeeId, period);
        int workDays = attendanceSummary != null && attendanceSummary.getWorkDays() != null
                ? attendanceSummary.getWorkDays() : DEFAULT_WORK_DAYS;
        BigDecimal overtimeHours = attendanceSummary != null && attendanceSummary.getOvertimeHours() != null
                ? attendanceSummary.getOvertimeHours() : BigDecimal.ZERO;
        // ===== åŠ ç­å·¥èµ„ = æ—¶è–ª Ã— åŠ ç­æ—¶é•¿ Ã— å€çŽ‡ï¼ˆæ—¶è–ª = åŸºæœ¬å·¥èµ„ Ã· (应出勤天 Ã— 8)) =====
        BigDecimal overtimePay = BigDecimal.ZERO;
        if (overtimeHours.compareTo(BigDecimal.ZERO) > 0 && workDays > 0) {
            BigDecimal hourlyRate = baseSalary.divide(BigDecimal.valueOf(workDays * 8L), 4, RoundingMode.HALF_UP);
            overtimePay = hourlyRate.multiply(overtimeHours).multiply(getOvertimePayRatio(employeeSalary, employee))
                    .setScale(2, RoundingMode.HALF_UP);
        }
        // ===== ç»©æ•ˆå·¥èµ„:按当月绩效结果档级系数折算(无结果按全额) =====
        performanceSalary = calculatePerformanceSalary(performanceSalary, employeeId, period);
        // åº”发工资总额
        BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance)
@@ -238,8 +286,8 @@
        // å…¬ç§¯é‡‘扣除 = åŸºæ•° Ã— å…¬ç§¯é‡‘个人比例
        BigDecimal housingFundDeduction = housingFundBase.multiply(housingFundPersonalRatio).setScale(2, RoundingMode.HALF_UP);
        // è®¡ç®—请假扣款
        BigDecimal leaveDeduction = calculateLeaveDeduction(employee, period, baseSalary);
        // è®¡ç®—缺勤扣款(考勤口径:缺勤天数 = åº”出勤 âˆ’ å®žé™…出勤;无考勤汇总时回退到请假申请口径)
        BigDecimal leaveDeduction = calculateDeduction(attendanceSummary, baseSalary, workDays, employee, period);
        // è®¡ç®—个税扣除
        // åº”纳税所得额 = åº”发工资 - ç¤¾ä¿æ‰£é™¤ - å…¬ç§¯é‡‘扣除 - èµ·å¾ç‚¹(5000)
@@ -276,13 +324,66 @@
                .otherDeduction(otherDeduction)
                .leaveDeduction(leaveDeduction)
                .actualSalary(actualSalary)
                .workDays(22) // é»˜è®¤å·¥ä½œæ—¥
                .overtimeHours(BigDecimal.ZERO)
                .workDays(workDays) // åŠ¨æ€åº”å‡ºå‹¤å¤©æ•°ï¼ˆæ¥è‡ªè€ƒå‹¤æ±‡æ€»ï¼‰
                .overtimeHours(overtimeHours) // åŠ ç­æ—¶é•¿ï¼ˆæ¥è‡ªè€ƒå‹¤æ±‡æ€»ï¼‰
                .status(0) // å¾…确认
                .build();
    }
    /**
     * è®¡ç®—缺勤扣款:优先按考勤汇总(缺勤天数 = åº”出勤 âˆ’ å®žé™…出勤);无汇总时回退到请假申请口径
     */
    private BigDecimal calculateDeduction(HrmAttendanceSummaryDO attendanceSummary, BigDecimal baseSalary,
                                          int workDays, HrmEmployeeDO employee, String period) {
        if (attendanceSummary != null && workDays > 0 && attendanceSummary.getActualDays() != null) {
            int absentDays = Math.max(0, workDays - attendanceSummary.getActualDays());
            if (absentDays == 0) {
                return BigDecimal.ZERO;
            }
            BigDecimal daySalary = baseSalary.divide(BigDecimal.valueOf(workDays), 4, RoundingMode.HALF_UP);
            return daySalary.multiply(BigDecimal.valueOf(absentDays)).setScale(2, RoundingMode.HALF_UP);
        }
        return calculateLeaveDeduction(employee, period, baseSalary);
    }
    /**
     * è®¡ç®—绩效工资:按当月已完成且匹配核算周期的月度绩效考核结果的档级系数折算
     */
    private BigDecimal calculatePerformanceSalary(BigDecimal performanceSalary, Long employeeId, String period) {
        HrmPerformanceAppraiseDO appraise = performanceAppraiseMapper.selectMonthlyCompletedByPeriod(period);
        if (appraise == null) {
            return performanceSalary;
        }
        HrmPerformanceAppraiseResultDO result = performanceAppraiseResultMapper
                .selectByAppraiseIdAndEmployeeId(appraise.getId(), employeeId);
        if (result == null || result.getGrade() == null) {
            return performanceSalary;
        }
        BigDecimal factor = switch (result.getGrade()) {
            case 1 -> PERFORMANCE_GRADE_FACTOR_EXCELLENT;
            case 2 -> PERFORMANCE_GRADE_FACTOR_GOOD;
            case 3 -> PERFORMANCE_GRADE_FACTOR_PASS;
            case 4 -> PERFORMANCE_GRADE_FACTOR_IMPROVE;
            default -> BigDecimal.ONE;
        };
        return performanceSalary.multiply(factor).setScale(2, RoundingMode.HALF_UP);
    }
    /**
     * èŽ·å–åŠ ç­å·¥èµ„å€çŽ‡ï¼ˆè–ªé…¬ç»“æž„æœªé…ç½®æ—¶é»˜è®¤ 1.5)
     */
    private BigDecimal getOvertimePayRatio(HrmEmployeeSalaryDO employeeSalary, HrmEmployeeDO employee) {
        Long structureId = employeeSalary.getSalaryStructureId() != null
                ? employeeSalary.getSalaryStructureId() : employee.getSalaryStructureId();
        if (structureId == null) {
            return DEFAULT_OVERTIME_PAY_RATIO;
        }
        HrmSalaryStructureDO structure = salaryStructureMapper.selectById(structureId);
        return structure != null && structure.getOvertimePayRatio() != null
                ? structure.getOvertimePayRatio() : DEFAULT_OVERTIME_PAY_RATIO;
    }
    /**
     * è®¡ç®—请假扣款
     *
     * @param employee   å‘˜å·¥ä¿¡æ¯
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.hrm.service.salary;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryStructurePageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
import java.util.List;
/**
 * è–ªé…¬ç»“æž„ Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface HrmSalaryStructureService {
    /**
     * åˆ›å»ºè–ªé…¬ç»“æž„
     *
     * @param structure ç»“构信息
     * @return ç»“æž„ID
     */
    Long createStructure(HrmSalaryStructureDO structure);
    /**
     * æ›´æ–°è–ªé…¬ç»“æž„
     *
     * @param structure ç»“构信息
     */
    void updateStructure(HrmSalaryStructureDO structure);
    /**
     * åˆ é™¤è–ªé…¬ç»“æž„
     *
     * @param id ç»“æž„ID
     */
    void deleteStructure(Long id);
    /**
     * èŽ·å–è–ªé…¬ç»“æž„
     *
     * @param id ç»“æž„ID
     * @return ç»“构信息
     */
    HrmSalaryStructureDO getStructure(Long id);
    /**
     * èŽ·å–è–ªé…¬ç»“æž„åˆ—è¡¨
     *
     * @param status çŠ¶æ€ï¼ˆå¯é€‰ï¼‰
     * @return ç»“构列表
     */
    List<HrmSalaryStructureDO> getStructureList(Integer status);
    /**
     * èŽ·å–è–ªé…¬ç»“æž„åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µè¯·æ±‚
     * @return åˆ†é¡µç»“æžœ
     */
    PageResult<HrmSalaryStructureDO> getStructurePage(HrmSalaryStructurePageReqVO pageReqVO);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.hrm.service.salary;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmSalaryStructurePageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryStructureMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.SALARY_STRUCTURE_NOT_EXISTS;
/**
 * è–ªé…¬ç»“æž„ Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class HrmSalaryStructureServiceImpl implements HrmSalaryStructureService {
    @Resource
    private HrmSalaryStructureMapper structureMapper;
    @Override
    public Long createStructure(HrmSalaryStructureDO structure) {
        structureMapper.insert(structure);
        return structure.getId();
    }
    @Override
    public void updateStructure(HrmSalaryStructureDO structure) {
        validateStructureExists(structure.getId());
        structureMapper.updateById(structure);
    }
    @Override
    public void deleteStructure(Long id) {
        validateStructureExists(id);
        structureMapper.deleteById(id);
    }
    @Override
    public HrmSalaryStructureDO getStructure(Long id) {
        return structureMapper.selectById(id);
    }
    @Override
    public List<HrmSalaryStructureDO> getStructureList(Integer status) {
        return structureMapper.selectListByStatus(status);
    }
    @Override
    public PageResult<HrmSalaryStructureDO> getStructurePage(HrmSalaryStructurePageReqVO pageReqVO) {
        return structureMapper.selectPage(pageReqVO);
    }
    private void validateStructureExists(Long id) {
        if (structureMapper.selectById(id) == null) {
            throw exception(SALARY_STRUCTURE_NOT_EXISTS);
        }
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/training/HrmTrainingService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,121 @@
package cn.iocoder.yudao.module.hrm.service.training;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingParticipantUpdateReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.training.HrmTrainingDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.training.HrmTrainingParticipantDO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
 * HRM åŸ¹è®­æ´»åЍ Service æŽ¥å£
 *
 * <p>管理安全培训、技能培训等培训计划与参训/考核结果;培训完成时对合格人员联动生成证书资质。
 *
 * @author è¶…级管理员
 */
public interface HrmTrainingService {
    /**
     * åˆ›å»ºåŸ¹è®­æ´»åЍ
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return åŸ¹è®­æ´»åЍID
     */
    Long createTraining(HrmTrainingSaveReqVO createReqVO);
    /**
     * æ›´æ–°åŸ¹è®­æ´»åŠ¨ï¼ˆä»…è‰ç¨¿/待开展可改,且不改状态)
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateTraining(HrmTrainingSaveReqVO updateReqVO);
    /**
     * åˆ é™¤åŸ¹è®­æ´»åŠ¨ï¼ˆçº§è”åˆ é™¤å‚è®­äººå‘˜ï¼›å·²å®Œæˆ/已取消不可删)
     *
     * @param ids åŸ¹è®­æ´»åЍID集合
     */
    void deleteTraining(Collection<Long> ids);
    /**
     * èŽ·å¾—åŸ¹è®­æ´»åŠ¨
     *
     * @param id åŸ¹è®­æ´»åЍID
     * @return åŸ¹è®­æ´»åЍ
     */
    HrmTrainingDO getTraining(Long id);
    /**
     * èŽ·å¾—åŸ¹è®­æ´»åŠ¨åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return åŸ¹è®­æ´»åŠ¨åˆ†é¡µ
     */
    PageResult<HrmTrainingDO> getTrainingPage(HrmTrainingPageReqVO pageReqVO);
    /**
     * å¼€å§‹åŸ¹è®­ï¼ˆè‰ç¨¿ â†’ å¾…开展)
     *
     * @param id åŸ¹è®­æ´»åЍID
     */
    void startTraining(Long id);
    /**
     * å®ŒæˆåŸ¹è®­ï¼ˆå¾…开展/草稿 â†’ å·²å®Œæˆï¼‰ï¼Œå®Œæˆæ—¶å¯¹åˆæ ¼äººå‘˜è”动自动发证
     *
     * @param id åŸ¹è®­æ´»åЍID
     */
    void completeTraining(Long id);
    /**
     * å–消培训(草稿/待开展 â†’ å·²å–消)
     *
     * @param id åŸ¹è®­æ´»åЍID
     */
    void cancelTraining(Long id);
    /**
     * æ·»åŠ å‚è®­å‘˜å·¥ï¼ˆè‡ªåŠ¨è·³è¿‡å·²åœ¨åå•ä¸­çš„å‘˜å·¥ï¼‰
     *
     * @param trainingId åŸ¹è®­æ´»åЍID
     * @param employeeIds å‘˜å·¥ID集合
     * @return å®žé™…新增数量
     */
    int addParticipants(Long trainingId, Collection<Long> employeeIds);
    /**
     * æ›´æ–°å‚训记录(参加情况/成绩/是否合格)
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateParticipant(HrmTrainingParticipantUpdateReqVO updateReqVO);
    /**
     * åˆ é™¤å‚训记录
     *
     * @param ids å‚训记录ID集合
     */
    void deleteParticipants(Collection<Long> ids);
    /**
     * èŽ·å¾—æŸæ¬¡åŸ¹è®­çš„å…¨éƒ¨å‚è®­è®°å½•
     *
     * @param trainingId åŸ¹è®­æ´»åЍID
     * @return å‚训记录列表
     */
    List<HrmTrainingParticipantDO> getParticipantsByTrainingId(Long trainingId);
    /**
     * æ‰¹é‡ç»Ÿè®¡å„培训活动的参训人数(分页列表展示用)
     *
     * @param trainingIds åŸ¹è®­æ´»åЍID集合
     * @return åŸ¹è®­æ´»åЍID â†’ å‚训人数
     */
    Map<Long, Integer> getParticipantCountMap(Collection<Long> trainingIds);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/training/HrmTrainingServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,279 @@
package cn.iocoder.yudao.module.hrm.service.training;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingParticipantUpdateReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.training.vo.HrmTrainingSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.certificate.HrmCertificateDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.training.HrmTrainingDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.training.HrmTrainingParticipantDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.certificate.HrmCertificateMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.training.HrmTrainingMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.training.HrmTrainingParticipantMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.dal.dataobject.training.HrmTrainingDO.*;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.*;
/**
 * HRM åŸ¹è®­æ´»åЍ Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class HrmTrainingServiceImpl implements HrmTrainingService {
    @Resource
    private HrmTrainingMapper trainingMapper;
    @Resource
    private HrmTrainingParticipantMapper participantMapper;
    @Resource
    private HrmCertificateMapper certificateMapper;
    @Resource
    private HrmNoRedisDAO noRedisDAO;
    @Override
    public Long createTraining(HrmTrainingSaveReqVO createReqVO) {
        validateIssueCertificate(createReqVO.getTrainingType(), createReqVO.getIssueCertificate(),
                createReqVO.getCertValidityMonths());
        HrmTrainingDO training = BeanUtils.toBean(createReqVO, HrmTrainingDO.class);
        training.setId(null);
        training.setTrainingNo(noRedisDAO.generateTrainingNo());
        training.setStatus(STATUS_DRAFT);
        trainingMapper.insert(training);
        return training.getId();
    }
    @Override
    public void updateTraining(HrmTrainingSaveReqVO updateReqVO) {
        HrmTrainingDO training = validateTrainingExists(updateReqVO.getId());
        ensureMutable(training);
        validateIssueCertificate(updateReqVO.getTrainingType(), updateReqVO.getIssueCertificate(),
                updateReqVO.getCertValidityMonths());
        HrmTrainingDO update = BeanUtils.toBean(updateReqVO, HrmTrainingDO.class);
        update.setTrainingNo(training.getTrainingNo()); // ç¼–号不可改
        update.setStatus(null); // çŠ¶æ€èµ° start/complete/cancel æµè½¬ï¼Œä¸åœ¨æ­¤ä¿®æ”¹
        trainingMapper.updateById(update);
    }
    @Override
    public void deleteTraining(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return;
        }
        for (Long id : ids) {
            HrmTrainingDO training = validateTrainingExists(id);
            ensureMutable(training);
            participantMapper.deleteByTrainingId(id);
            trainingMapper.deleteById(id);
        }
    }
    @Override
    public HrmTrainingDO getTraining(Long id) {
        return trainingMapper.selectById(id);
    }
    @Override
    public PageResult<HrmTrainingDO> getTrainingPage(HrmTrainingPageReqVO pageReqVO) {
        return trainingMapper.selectPage(pageReqVO);
    }
    @Override
    public void startTraining(Long id) {
        HrmTrainingDO training = validateTrainingExists(id);
        if (training.getStatus() != null && !training.getStatus().equals(STATUS_DRAFT)) {
            throw exception(TRAINING_COMPLETE_FAIL_STATUS); // ä»…草稿可开始
        }
        trainingMapper.updateById(HrmTrainingDO.builder().id(id).status(STATUS_PLANNED).build());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void completeTraining(Long id) {
        HrmTrainingDO training = validateTrainingExists(id);
        if (training.getStatus() == null
                || (!training.getStatus().equals(STATUS_DRAFT) && !training.getStatus().equals(STATUS_PLANNED))) {
            throw exception(TRAINING_COMPLETE_FAIL_STATUS);
        }
        trainingMapper.updateById(HrmTrainingDO.builder().id(id).status(STATUS_COMPLETED).build());
        // è”动发证:培训完成后对合格人员自动生成证书(幂等,二次完成不重复发)
        if (Boolean.TRUE.equals(training.getIssueCertificate())) {
            issueCertificatesForPassed(training);
        }
    }
    @Override
    public void cancelTraining(Long id) {
        HrmTrainingDO training = validateTrainingExists(id);
        if (training.getStatus() == null || training.getStatus().equals(STATUS_COMPLETED)
                || training.getStatus().equals(STATUS_CANCELED)) {
            throw exception(TRAINING_LOCKED);
        }
        trainingMapper.updateById(HrmTrainingDO.builder().id(id).status(STATUS_CANCELED).build());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int addParticipants(Long trainingId, Collection<Long> employeeIds) {
        HrmTrainingDO training = validateTrainingExists(trainingId);
        ensureMutable(training);
        if (CollUtil.isEmpty(employeeIds)) {
            return 0;
        }
        int count = 0;
        for (Long employeeId : employeeIds) {
            if (participantMapper.selectByTrainingIdAndEmployeeId(trainingId, employeeId) != null) {
                continue; // å·²åœ¨åå•中,跳过不重复添加
            }
            HrmTrainingParticipantDO participant = HrmTrainingParticipantDO.builder()
                    .trainingId(trainingId)
                    .employeeId(employeeId)
                    .attended(false)
                    .isPass(false)
                    .build();
            participantMapper.insert(participant);
            count++;
        }
        return count;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateParticipant(HrmTrainingParticipantUpdateReqVO updateReqVO) {
        HrmTrainingParticipantDO participant = validateParticipantExists(updateReqVO.getId());
        HrmTrainingDO training = validateTrainingExists(participant.getTrainingId());
        ensureMutable(training);
        HrmTrainingParticipantDO update = HrmTrainingParticipantDO.builder()
                .id(participant.getId())
                .attended(updateReqVO.getAttended())
                .score(updateReqVO.getScore())
                .isPass(updateReqVO.getIsPass())
                .remark(updateReqVO.getRemark())
                .build();
        participantMapper.updateById(update);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteParticipants(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return;
        }
        for (Long id : ids) {
            HrmTrainingParticipantDO participant = validateParticipantExists(id);
            HrmTrainingDO training = validateTrainingExists(participant.getTrainingId());
            ensureMutable(training);
            participantMapper.deleteById(id);
        }
    }
    @Override
    public List<HrmTrainingParticipantDO> getParticipantsByTrainingId(Long trainingId) {
        return participantMapper.selectListByTrainingId(trainingId);
    }
    @Override
    public Map<Long, Integer> getParticipantCountMap(Collection<Long> trainingIds) {
        if (CollUtil.isEmpty(trainingIds)) {
            return Collections.emptyMap();
        }
        return participantMapper.selectListByTrainingIds(trainingIds).stream()
                .collect(Collectors.groupingBy(HrmTrainingParticipantDO::getTrainingId, Collectors.summingInt(p -> 1)));
    }
    // ========== ç§æœ‰æ–¹æ³• ==========
    private void issueCertificatesForPassed(HrmTrainingDO training) {
        if (training.getCertValidityMonths() == null || training.getCertValidityMonths() <= 0) {
            throw exception(TRAINING_CERT_VALIDITY_REQUIRED);
        }
        List<HrmTrainingParticipantDO> participants = participantMapper.selectListByTrainingId(training.getId());
        if (CollUtil.isEmpty(participants)) {
            return;
        }
        Integer certType = convertToCertificateType(training.getTrainingType());
        LocalDate issueDate = LocalDate.now();
        LocalDate expireDate = issueDate.plusMonths(training.getCertValidityMonths());
        for (HrmTrainingParticipantDO participant : participants) {
            if (!Boolean.TRUE.equals(participant.getIsPass()) || participant.getCertificateId() != null) {
                continue;
            }
            HrmCertificateDO certificate = HrmCertificateDO.builder()
                    .employeeId(participant.getEmployeeId())
                    .certType(certType)
                    .certName(training.getName())
                    .issueDate(issueDate)
                    .expireDate(expireDate)
                    .remark("由培训【" + training.getTrainingNo() + "】" + training.getName() + "完成自动发放")
                    .build();
            certificateMapper.insert(certificate);
            participantMapper.updateCertificateId(participant.getId(), certificate.getId());
        }
    }
    /**
     * åŸ¹è®­ç±»åž‹ â†’ è¯ä¹¦èµ„质类型映射:安全培训→4 å®‰å…¨åŸ¹è®­åˆæ ¼è¯ï¼ŒæŠ€èƒ½åŸ¹è®­â†’3 æŠ€èƒ½ç­‰çº§è¯ä¹¦
     */
    private Integer convertToCertificateType(Integer trainingType) {
        return switch (trainingType == null ? 0 : trainingType) {
            case TYPE_SAFETY -> 4;
            case TYPE_SKILL -> 3;
            default -> null;
        };
    }
    private void validateIssueCertificate(Integer trainingType, Boolean issueCertificate, Integer certValidityMonths) {
        if (Boolean.TRUE.equals(issueCertificate)) {
            if (trainingType == null
                    || (!trainingType.equals(TYPE_SAFETY) && !trainingType.equals(TYPE_SKILL))) {
                throw exception(TRAINING_CERT_TYPE_NOT_SUPPORT);
            }
            if (certValidityMonths == null || certValidityMonths <= 0) {
                throw exception(TRAINING_CERT_VALIDITY_REQUIRED);
            }
        }
    }
    private HrmTrainingDO validateTrainingExists(Long id) {
        HrmTrainingDO training = trainingMapper.selectById(id);
        if (training == null) {
            throw exception(TRAINING_NOT_EXISTS);
        }
        return training;
    }
    private HrmTrainingParticipantDO validateParticipantExists(Long id) {
        HrmTrainingParticipantDO participant = participantMapper.selectById(id);
        if (participant == null) {
            throw exception(TRAINING_PARTICIPANT_NOT_EXISTS);
        }
        return participant;
    }
    /**
     * å·²å®Œæˆ/已取消的培训不允许再修改
     */
    private void ensureMutable(HrmTrainingDO training) {
        if (training.getStatus() != null
                && (training.getStatus().equals(STATUS_COMPLETED) || training.getStatus().equals(STATUS_CANCELED))) {
            throw exception(TRAINING_LOCKED);
        }
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/workhour/HrmWorkHourSummaryService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.hrm.service.workhour;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.workhour.vo.HrmWorkHourSummaryPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.workhour.vo.HrmWorkHourSummaryRespVO;
/**
 * æœˆåº¦å·¥æ—¶æ±‡æ€» Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface HrmWorkHourSummaryService {
    /**
     * ç”ŸæˆæŒ‡å®šå‘¨æœŸçš„æœˆåº¦å·¥æ—¶æ±‡æ€»ï¼ˆè€ƒå‹¤å£å¾„,按员工 upsert)
     *
     * @param period æ ¸ç®—周期(yyyy-MM)
     */
    void generateMonthly(String period);
    /**
     * èŽ·å–æœˆåº¦å·¥æ—¶æ±‡æ€»åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µè¯·æ±‚
     * @return åˆ†é¡µç»“æžœ
     */
    PageResult<HrmWorkHourSummaryRespVO> getWorkHourSummaryPage(HrmWorkHourSummaryPageReqVO pageReqVO);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/workhour/HrmWorkHourSummaryServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
package cn.iocoder.yudao.module.hrm.service.workhour;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.workhour.vo.HrmEmployeeWorkHourReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.workhour.vo.HrmWorkHourSummaryPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.workhour.vo.HrmWorkHourSummaryRespVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.workhour.HrmWorkHourSummaryDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceRecordMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.workhour.HrmWorkHourSummaryMapper;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.YearMonth;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
 * æœˆåº¦å·¥æ—¶æ±‡æ€» Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class HrmWorkHourSummaryServiceImpl implements HrmWorkHourSummaryService {
    @Resource
    private HrmWorkHourSummaryMapper workHourSummaryMapper;
    @Resource
    private HrmAttendanceRecordMapper attendanceRecordMapper;
    @Resource
    private HrmEmployeeService employeeService;
    @Resource
    private DeptApi deptApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void generateMonthly(String period) {
        YearMonth yearMonth = YearMonth.parse(period);
        HrmEmployeeWorkHourReqVO reqVO = new HrmEmployeeWorkHourReqVO();
        reqVO.setStartDate(yearMonth.atDay(1));
        reqVO.setEndDate(yearMonth.atEndOfMonth());
        for (Map<String, Object> row : attendanceRecordMapper.selectWorkHourSummary(reqVO)) {
            Long employeeId = toLong(row.get("user_id"));
            if (employeeId == null) {
                continue;
            }
            HrmEmployeeDO employee = employeeService.getEmployee(employeeId);
            HrmWorkHourSummaryDO summary = workHourSummaryMapper.selectByEmployeeIdAndPeriod(employeeId, period);
            if (summary == null) {
                summary = new HrmWorkHourSummaryDO();
                summary.setEmployeeId(employeeId);
                summary.setPeriod(period);
            }
            summary.setDeptId(employee != null ? employee.getDeptId() : null);
            summary.setWorkDays(toInteger(row.get("work_days")));
            summary.setWorkHours(toDecimal(row.get("work_hours")));
            summary.setOvertimeHours(toDecimal(row.get("overtime_hours")));
            if (summary.getId() == null) {
                workHourSummaryMapper.insert(summary);
            } else {
                workHourSummaryMapper.updateById(summary);
            }
        }
    }
    @Override
    public PageResult<HrmWorkHourSummaryRespVO> getWorkHourSummaryPage(HrmWorkHourSummaryPageReqVO pageReqVO) {
        PageResult<HrmWorkHourSummaryDO> pageResult = workHourSummaryMapper.selectPage(pageReqVO);
        if (CollUtil.isEmpty(pageResult.getList())) {
            return new PageResult<>(Collections.emptyList(), pageResult.getTotal());
        }
        List<HrmWorkHourSummaryRespVO> list = pageResult.getList().stream()
                .map(this::convert)
                .toList();
        return new PageResult<>(list, pageResult.getTotal());
    }
    private HrmWorkHourSummaryRespVO convert(HrmWorkHourSummaryDO summary) {
        HrmWorkHourSummaryRespVO respVO = new HrmWorkHourSummaryRespVO();
        respVO.setId(summary.getId());
        respVO.setEmployeeId(summary.getEmployeeId());
        respVO.setDeptId(summary.getDeptId());
        respVO.setPeriod(summary.getPeriod());
        respVO.setWorkDays(summary.getWorkDays());
        respVO.setWorkHours(summary.getWorkHours());
        respVO.setOvertimeHours(summary.getOvertimeHours());
        respVO.setRemark(summary.getRemark());
        respVO.setCreateTime(summary.getCreateTime());
        HrmEmployeeDO employee = employeeService.getEmployee(summary.getEmployeeId());
        if (employee != null) {
            respVO.setEmployeeNo(employee.getEmployeeNo());
            respVO.setEmployeeName(employee.getName());
        }
        if (summary.getDeptId() != null) {
            DeptRespDTO dept = deptApi.getDept(summary.getDeptId());
            if (dept != null) {
                respVO.setDeptName(dept.getName());
            }
        }
        return respVO;
    }
    private Long toLong(Object value) {
        return value instanceof Number ? ((Number) value).longValue() : null;
    }
    private Integer toInteger(Object value) {
        return value instanceof Number ? ((Number) value).intValue() : 0;
    }
    private BigDecimal toDecimal(Object value) {
        return value instanceof Number ? new BigDecimal(value.toString()) : BigDecimal.ZERO;
    }
}
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemCreateReqDTO.java
@@ -72,6 +72,9 @@
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "是否序列号管理", example = "false")
    private Boolean isSerialManaged;
    @Schema(description = "重量(kg)", example = "0.5")
    private BigDecimal weight;
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemRespDTO.java
@@ -81,6 +81,9 @@
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "是否序列号管理", example = "false")
    private Boolean isSerialManaged;
    @Schema(description = "是否高值物料", example = "false")
    private Boolean highValue;
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemUpdateReqDTO.java
@@ -75,6 +75,9 @@
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "是否序列号管理", example = "false")
    private Boolean isSerialManaged;
    @Schema(description = "重量(kg)", example = "0.5")
    private BigDecimal weight;
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/enums/MdmItemTypeEnum.java
@@ -14,7 +14,8 @@
    MATERIAL(1, "物料"),
    PRODUCT(2, "产品"),
    SEMI_FINISHED(3, "半成品");
    SEMI_FINISHED(3, "半成品"),
    EQUIPMENT(4, "设备");
    /**
     * ç±»åž‹ç¼–码
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java
@@ -78,6 +78,9 @@
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "是否序列号管理", example = "false")
    private Boolean isSerialManaged;
    @Schema(description = "是否高值物料", example = "false")
    private Boolean isHighValue;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/enums/MdmItemTypeEnum.java
@@ -14,7 +14,8 @@
    MATERIAL(1, "物料"),
    PRODUCT(2, "产品"),
    SEMI_FINISHED(3, "半成品");
    SEMI_FINISHED(3, "半成品"),
    EQUIPMENT(4, "设备");
    /**
     * ç±»åž‹ç¼–码
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/cal/MesCalTeamApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.mes.api.cal;
import cn.iocoder.yudao.module.mes.api.cal.dto.MesCalTeamRespDTO;
import java.util.Collection;
import java.util.List;
/**
 * MES ç­ç»„ API æŽ¥å£
 *
 * <p>供 HRM ç­‰æ¨¡å—引用,按班组 ID æ‰¹é‡æŸ¥è¯¢ç­ç»„编码/名称,用于员工主档「所属班组」回显。
 *
 * @author è¶…级管理员
 */
public interface MesCalTeamApi {
    /**
     * æŒ‰ç­ç»„ ID é›†åˆæŸ¥è¯¢ç­ç»„列表
     *
     * @param teamIds ç­ç»„ ID é›†åˆï¼ˆä¸ºç©ºè¿”回空列表)
     * @return ç­ç»„列表
     */
    List<MesCalTeamRespDTO> getTeamList(Collection<Long> teamIds);
}
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/cal/dto/MesCalTeamRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mes.api.cal.dto;
import lombok.Data;
/**
 * MES ç­ç»„ Response DTO
 *
 * @author è¶…级管理员
 */
@Data
public class MesCalTeamRespDTO {
    /**
     * ç­ç»„编号
     */
    private Long id;
    /**
     * ç­ç»„编码
     */
    private String code;
    /**
     * ç­ç»„名称
     */
    private String name;
}
yudao-module-mes/pom.xml
@@ -103,6 +103,18 @@
            <artifactId>yudao-spring-boot-starter-excel</artifactId>
        </dependency>
        <!-- æ¡ç /二维码生成(zxing) -->
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>core</artifactId>
            <version>3.3.3</version>
        </dependency>
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>javase</artifactId>
            <version>3.3.3</version>
        </dependency>
        <!-- Test æµ‹è¯•相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/cal/MesCalTeamApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.api.cal;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.mes.api.cal.dto.MesCalTeamRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.team.MesCalTeamDO;
import cn.iocoder.yudao.module.mes.dal.mysql.cal.team.MesCalTeamMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
 * MES ç­ç»„ API å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesCalTeamApiImpl implements MesCalTeamApi {
    @Resource
    private MesCalTeamMapper teamMapper;
    @Override
    public List<MesCalTeamRespDTO> getTeamList(Collection<Long> teamIds) {
        if (CollUtil.isEmpty(teamIds)) {
            return Collections.emptyList();
        }
        List<MesCalTeamDO> teams = teamMapper.selectByIds(teamIds);
        List<MesCalTeamRespDTO> list = new ArrayList<>(teams.size());
        for (MesCalTeamDO team : teams) {
            MesCalTeamRespDTO dto = new MesCalTeamRespDTO();
            dto.setId(team.getId());
            dto.setCode(team.getCode());
            dto.setName(team.getName());
            list.add(dto);
        }
        return list;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/MesCalScheduleDetailController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
package cn.iocoder.yudao.module.mes.controller.admin.cal.schedule;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.schedule.MesCalScheduleDetailDO;
import cn.iocoder.yudao.module.mes.service.cal.schedule.MesCalScheduleDetailService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES å‘˜å·¥çº§æŽ’班明细")
@RestController
@RequestMapping("/mes/cal/schedule-detail")
@Validated
public class MesCalScheduleDetailController {
    @Resource
    private MesCalScheduleDetailService scheduleDetailService;
    @PostMapping("/generate")
    @Operation(summary = "按班组排班生成员工级排班明细")
    @Parameter(name = "planId", description = "排班计划编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mes:cal-schedule-detail:generate')")
    public CommonResult<Boolean> generateDetails(@RequestParam("planId") Long planId) {
        scheduleDetailService.generateDetails(planId);
        return success(true);
    }
    @PutMapping("/update")
    @Operation(summary = "更新员工级排班明细(人工微调)")
    @PreAuthorize("@ss.hasPermission('mes:cal-schedule-detail:update')")
    public CommonResult<Boolean> updateDetail(@Valid @RequestBody MesCalScheduleDetailSaveReqVO updateReqVO) {
        scheduleDetailService.updateDetail(updateReqVO);
        return success(true);
    }
    @GetMapping("/page")
    @Operation(summary = "获得员工级排班明细分页")
    @PreAuthorize("@ss.hasPermission('mes:cal-schedule-detail:query')")
    public CommonResult<PageResult<MesCalScheduleDetailRespVO>> getDetailPage(
            @Valid MesCalScheduleDetailPageReqVO pageReqVO) {
        PageResult<MesCalScheduleDetailDO> pageResult = scheduleDetailService.getDetailPage(pageReqVO);
        List<MesCalScheduleDetailRespVO> list = BeanUtils.toBean(pageResult.getList(), MesCalScheduleDetailRespVO.class);
        return success(new PageResult<>(list, pageResult.getTotal()));
    }
    @GetMapping("/list")
    @Operation(summary = "获得员工级排班明细列表")
    @PreAuthorize("@ss.hasPermission('mes:cal-schedule-detail:query')")
    public CommonResult<List<MesCalScheduleDetailRespVO>> getDetailList(
            @Valid MesCalScheduleDetailPageReqVO pageReqVO) {
        return success(BeanUtils.toBean(scheduleDetailService.getDetailList(pageReqVO), MesCalScheduleDetailRespVO.class));
    }
    @GetMapping("/check")
    @Operation(summary = "员工排班合规校验(连续工时/休息间隔)")
    @PreAuthorize("@ss.hasPermission('mes:cal-schedule-detail:query')")
    public CommonResult<List<MesCalScheduleDetailCheckRespVO>> checkCompliance(
            @Valid MesCalScheduleDetailCheckReqVO checkReqVO) {
        return success(scheduleDetailService.checkCompliance(checkReqVO));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/vo/MesCalScheduleDetailCheckReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - MES å‘˜å·¥çº§æŽ’班明细 åˆè§„校验 Request VO")
@Data
public class MesCalScheduleDetailCheckReqVO {
    @NotNull(message = "班组成员用户编号不能为空")
    private Long userId;
    @Schema(description = "起始日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime startDay;
    @Schema(description = "结束日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime endDay;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/vo/MesCalScheduleDetailCheckRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES å‘˜å·¥çº§æŽ’班明细 åˆè§„校验结果项 VO")
@Data
public class MesCalScheduleDetailCheckRespVO {
    @Schema(description = "日期")
    private LocalDateTime day;
    @Schema(description = "班次名称")
    private String shiftName;
    @Schema(description = "开始时间(HH:mm)")
    private String startTime;
    @Schema(description = "结束时间(HH:mm)")
    private String endTime;
    @Schema(description = "违规类型: 1-单日疑似连续工时超长 2-相邻班次间休息不足", example = "1")
    private Integer issueType;
    @Schema(description = "合规校验问题描述,为空表示该班次合规")
    private String issue;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/vo/MesCalScheduleDetailPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - MES å‘˜å·¥çº§æŽ’班明细 åˆ†é¡µ/列表 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class MesCalScheduleDetailPageReqVO extends SortablePageParam {
    @Schema(description = "排班计划编号", example = "1")
    private Long planId;
    @Schema(description = "班组编号", example = "1")
    private Long teamId;
    @Schema(description = "班组成员用户编号", example = "1")
    private Long userId;
    @Schema(description = "开始日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime startDay;
    @Schema(description = "结束日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime endDay;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/vo/MesCalScheduleDetailRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES å‘˜å·¥çº§æŽ’班明细 Response VO")
@Data
public class MesCalScheduleDetailRespVO {
    @Schema(description = "编号")
    private Long id;
    @Schema(description = "排班计划编号")
    private Long planId;
    @Schema(description = "班组编号")
    private Long teamId;
    @Schema(description = "班组成员用户编号")
    private Long userId;
    @Schema(description = "日期")
    private LocalDateTime day;
    @Schema(description = "班次编号")
    private Long shiftId;
    @Schema(description = "班次名称")
    private String shiftName;
    @Schema(description = "开始时间(HH:mm)")
    private String startTime;
    @Schema(description = "结束时间(HH:mm)")
    private String endTime;
    @Schema(description = "来源: 1-自动生成 2-人工调整")
    private Integer source;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/cal/schedule/vo/MesCalScheduleDetailSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - MES å‘˜å·¥çº§æŽ’班明细 äººå·¥è°ƒæ•´ Request VO")
@Data
public class MesCalScheduleDetailSaveReqVO {
    @NotNull(message = "明细编号不能为空")
    private Long id;
    @Schema(description = "班次编号")
    private Long shiftId;
    @Schema(description = "班次名称")
    private String shiftName;
    @Schema(description = "开始时间(HH:mm)")
    private String startTime;
    @Schema(description = "结束时间(HH:mm)")
    private String endTime;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachineryRespVO.java
@@ -85,6 +85,10 @@
    @ExcelProperty("工作站")
    private String workstationName;
    @Schema(description = "对应物料编号", example = "1000")
    @ExcelProperty("物料编号")
    private Long itemId;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/machinery/vo/MesDvMachinerySaveReqVO.java
@@ -61,4 +61,7 @@
    @Schema(description = "工作站编号", example = "300")
    private Long workstationId;
    @Schema(description = "对应物料编号(由系统在创建设备时自动生成,无需前端传入)", example = "1000")
    private Long itemId;
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/barcode/MesWmBarcodeController.java
@@ -25,6 +25,7 @@
import java.io.IOException;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -95,13 +96,37 @@
    @Operation(summary = "生成条码内容")
    @Parameter(name = "bizType", description = "业务类型", required = true, example = "1")
    @Parameter(name = "bizCode", description = "业务编码", required = true, example = "WO202403070001")
    @Parameter(name = "bizName", description = "业务名称", required = false, example = "一号车间")
    @PreAuthorize("@ss.hasPermission('mes:wm-barcode:query')")
    public CommonResult<String> generateBarcodeContent(@RequestParam("bizType") @NotNull(message = "业务类型不能为空") Integer bizType,
                                                        @RequestParam("bizCode") @NotBlank(message = "业务编码不能为空") String bizCode) {
        String content = barcodeService.generateBarcodeContent(bizType, bizCode);
                                                        @RequestParam("bizCode") @NotBlank(message = "业务编码不能为空") String bizCode,
                                                        @RequestParam(value = "bizName", required = false) String bizName) {
        String content = barcodeService.generateBarcodeContent(bizType, bizCode, bizName);
        return success(content);
    }
    @PostMapping("/render")
    @Operation(summary = "生成条码/二维码图片", description = "服务端 zxing æˆå›¾ï¼Œè¿”回 PNG Base64 ä¾›å³æ—¶é¢„览;不落库")
    @Parameter(name = "content", description = "条码内容", required = true)
    @Parameter(name = "format", description = "条码格式(1 äºŒç»´ç  / 2 EAN13 / 3 CODE39 / 4 UPC-A / ç©ºé»˜è®¤ CODE128)", required = false)
    @Parameter(name = "width", description = "图片宽度", required = false)
    @Parameter(name = "height", description = "图片高度", required = false)
    @PreAuthorize("@ss.hasPermission('mes:wm-barcode:query')")
    public CommonResult<String> renderBarcodeImage(@RequestParam("content") @NotBlank(message = "条码内容不能为空") String content,
                                                   @RequestParam(value = "format", required = false) Integer format,
                                                   @RequestParam(value = "width", required = false) Integer width,
                                                   @RequestParam(value = "height", required = false) Integer height) {
        return success(barcodeService.renderBarcodeImage(content, format, width, height));
    }
    @PostMapping("/parse")
    @Operation(summary = "解析条码内容", description = "按条码内容精确命中或模板反解,返回业务字段")
    @Parameter(name = "content", description = "扫描到的条码内容", required = true)
    @PreAuthorize("@ss.hasPermission('mes:wm-barcode:query')")
    public CommonResult<Map<String, Object>> parseBarcodeContent(@RequestParam("content") @NotBlank(message = "条码内容不能为空") String content) {
        return success(barcodeService.parseBarcodeContent(content));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出条码清单 Excel")
    @PreAuthorize("@ss.hasPermission('mes:wm-barcode:export')")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/MesWmEquipmentReceiptController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.equipmentreceipt.MesWmEquipmentReceiptDO;
import cn.iocoder.yudao.module.mes.service.wm.equipmentreceipt.MesWmEquipmentReceiptService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES è®¾å¤‡å…¥åº“单")
@RestController
@RequestMapping("/mes/wm/equipment-receipt")
@Validated
public class MesWmEquipmentReceiptController {
    @Resource
    private MesWmEquipmentReceiptService equipmentReceiptService;
    @PostMapping("/create")
    @Operation(summary = "创建设备入库单")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:create')")
    public CommonResult<Long> create(@Valid @RequestBody MesWmEquipmentReceiptSaveReqVO reqVO) {
        return success(equipmentReceiptService.createEquipmentReceipt(reqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "修改设备入库单")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody MesWmEquipmentReceiptSaveReqVO reqVO) {
        equipmentReceiptService.updateEquipmentReceipt(reqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除设备入库单")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:delete')")
    public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
        equipmentReceiptService.deleteEquipmentReceipt(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得设备入库单")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:query')")
    public CommonResult<MesWmEquipmentReceiptRespVO> get(@RequestParam("id") Long id) {
        return success(BeanUtils.toBean(equipmentReceiptService.getEquipmentReceipt(id), MesWmEquipmentReceiptRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获得设备入库单分页")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:query')")
    public CommonResult<PageResult<MesWmEquipmentReceiptRespVO>> page(@Valid MesWmEquipmentReceiptPageReqVO reqVO) {
        return success(BeanUtils.toBean(equipmentReceiptService.getEquipmentReceiptPage(reqVO), MesWmEquipmentReceiptRespVO.class));
    }
    @PutMapping("/submit-approval")
    @Operation(summary = "提交设备入库单审批")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:submit')")
    public CommonResult<Boolean> submitApproval(@RequestParam("id") Long id,
                                                  @RequestParam("processDefinitionKey") String processDefinitionKey) {
        equipmentReceiptService.submitEquipmentReceiptApproval(id, processDefinitionKey);
        return success(true);
    }
    @GetMapping("/approval-process-list")
    @Operation(summary = "获得设备入库审批流程定义列表")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:query')")
    public CommonResult<List<Map<String, Object>>> approvalProcessList() {
        return success(equipmentReceiptService.getEquipmentReceiptApprovalProcessDefinitionList());
    }
    @PutMapping("/finish")
    @Operation(summary = "执行设备入库")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:finish')")
    public CommonResult<Boolean> finish(@RequestParam("id") Long id) {
        equipmentReceiptService.finishEquipmentReceipt(id);
        return success(true);
    }
    @PutMapping("/cancel")
    @Operation(summary = "取消设备入库单")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:cancel')")
    public CommonResult<Boolean> cancel(@RequestParam("id") Long id) {
        equipmentReceiptService.cancelEquipmentReceipt(id);
        return success(true);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/MesWmEquipmentReceiptLineController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.line.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.equipmentreceipt.MesWmEquipmentReceiptLineDO;
import cn.iocoder.yudao.module.mes.service.dv.machinery.MesDvMachineryService;
import cn.iocoder.yudao.module.mes.service.wm.equipmentreceipt.MesWmEquipmentReceiptLineService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - MES è®¾å¤‡å…¥åº“单行")
@RestController
@RequestMapping("/mes/wm/equipment-receipt-line")
@Validated
public class MesWmEquipmentReceiptLineController {
    @Resource
    private MesWmEquipmentReceiptLineService lineService;
    @Resource
    private MesDvMachineryService machineryService;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @PostMapping("/create")
    @Operation(summary = "创建设备入库单行")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:create')")
    public CommonResult<Long> create(@Valid @RequestBody MesWmEquipmentReceiptLineSaveReqVO reqVO) {
        return success(lineService.createEquipmentReceiptLine(reqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "修改设备入库单行")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody MesWmEquipmentReceiptLineSaveReqVO reqVO) {
        lineService.updateEquipmentReceiptLine(reqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除设备入库单行")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:delete')")
    public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
        lineService.deleteEquipmentReceiptLine(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得设备入库单行")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:query')")
    public CommonResult<MesWmEquipmentReceiptLineRespVO> get(@RequestParam("id") Long id) {
        MesWmEquipmentReceiptLineDO line = lineService.getEquipmentReceiptLine(id);
        return success(buildResp(Collections.singletonList(line)).get(0));
    }
    @GetMapping("/list-by-receipt-id")
    @Operation(summary = "获得设备入库单行列表")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:query')")
    public CommonResult<List<MesWmEquipmentReceiptLineRespVO>> listByReceiptId(@RequestParam("receiptId") Long receiptId) {
        return success(buildResp(lineService.getEquipmentReceiptLineListByReceiptId(receiptId)));
    }
    @GetMapping("/page")
    @Operation(summary = "获得设备入库单行分页")
    @PreAuthorize("@ss.hasPermission('mes:wm:equipment-receipt:query')")
    public CommonResult<PageResult<MesWmEquipmentReceiptLineRespVO>> page(@Valid MesWmEquipmentReceiptLinePageReqVO reqVO) {
        PageResult<MesWmEquipmentReceiptLineDO> result = lineService.getEquipmentReceiptLinePage(reqVO);
        return success(new PageResult<>(buildResp(result.getList()), result.getTotal()));
    }
    private List<MesWmEquipmentReceiptLineRespVO> buildResp(List<MesWmEquipmentReceiptLineDO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        Map<Long, MesDvMachineryDO> machineryMap = machineryService.getMachineryMap(convertSet(list, MesWmEquipmentReceiptLineDO::getMachineryId));
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(convertSet(list, MesWmEquipmentReceiptLineDO::getItemId));
        Map<Long, MdmUnitMeasureRespDTO> unitMap = mdmUnitMeasureApi.getUnitMeasureList(
                convertSet(itemMap.values(), MdmItemRespDTO::getUnitMeasureId)).getCheckedData()
                .stream().collect(Collectors.toMap(MdmUnitMeasureRespDTO::getId, e -> e));
        return BeanUtils.toBean(list, MesWmEquipmentReceiptLineRespVO.class, vo -> {
            MesDvMachineryDO machinery = machineryMap.get(vo.getMachineryId());
            if (machinery != null) {
                vo.setMachineryCode(machinery.getCode()).setMachineryName(machinery.getName());
            }
            MdmItemRespDTO item = itemMap.get(vo.getItemId());
            if (item != null) {
                vo.setItemCode(item.getCode()).setItemName(item.getName()).setSpecification(item.getSpecification());
                MdmUnitMeasureRespDTO unit = unitMap.get(item.getUnitMeasureId());
                if (unit != null) {
                    vo.setUnitMeasureName(unit.getName());
                }
            }
        });
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/MesWmEquipmentReceiptPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - MES è®¾å¤‡å…¥åº“单分页 Request VO")
@Data
public class MesWmEquipmentReceiptPageReqVO extends PageParam {
    @Schema(description = "单据编码", example = "ER20260908001")
    private String code;
    @Schema(description = "单据名称", example = "设备入库")
    private String name;
    @Schema(description = "入库来源", example = "外购")
    private String source;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "入库日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] receiptDate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/MesWmEquipmentReceiptRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.line.MesWmEquipmentReceiptLineRespVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - MES è®¾å¤‡å…¥åº“单 Response VO")
@Data
public class MesWmEquipmentReceiptRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "单据编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ER20260908001")
    private String code;
    @Schema(description = "单据名称", example = "设备入库")
    private String name;
    @Schema(description = "入库日期")
    private LocalDateTime receiptDate;
    @Schema(description = "入库来源", example = "外购")
    private String source;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "BPM æµç¨‹å®žä¾‹ç¼–号")
    private String processInstanceId;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    @Schema(description = "明细行")
    private List<MesWmEquipmentReceiptLineRespVO> lines;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/MesWmEquipmentReceiptSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - MES è®¾å¤‡å…¥åº“单新增/修改 Request VO")
@Data
public class MesWmEquipmentReceiptSaveReqVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "单据编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ER20260908001")
    @NotEmpty(message = "单据编码不能为空")
    private String code;
    @Schema(description = "单据名称", example = "设备入库")
    private String name;
    @Schema(description = "入库日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "入库日期不能为空")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime receiptDate;
    @Schema(description = "入库来源", example = "外购")
    private String source;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/line/MesWmEquipmentReceiptLinePageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.line;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - MES è®¾å¤‡å…¥åº“单行分页 Request VO")
@Data
public class MesWmEquipmentReceiptLinePageReqVO extends PageParam {
    @Schema(description = "入库单编号", example = "1")
    private Long receiptId;
    @Schema(description = "设备台账编号", example = "1")
    private Long machineryId;
    @Schema(description = "设备物料编号", example = "1")
    private Long itemId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/line/MesWmEquipmentReceiptLineRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.line;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES è®¾å¤‡å…¥åº“单行 Response VO")
@Data
public class MesWmEquipmentReceiptLineRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "入库单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long receiptId;
    @Schema(description = "设备台账编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long machineryId;
    @Schema(description = "设备编码")
    private String machineryCode;
    @Schema(description = "设备名称")
    private String machineryName;
    @Schema(description = "设备物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long itemId;
    @Schema(description = "物料编码")
    private String itemCode;
    @Schema(description = "物料名称")
    private String itemName;
    @Schema(description = "规格型号")
    private String specification;
    @Schema(description = "计量单位名称")
    private String unitMeasureName;
    @Schema(description = "入库数量", example = "1.00")
    private BigDecimal quantity;
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/equipmentreceipt/vo/line/MesWmEquipmentReceiptLineSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.line;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - MES è®¾å¤‡å…¥åº“单行新增/修改 Request VO")
@Data
public class MesWmEquipmentReceiptLineSaveReqVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "入库单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "入库单编号不能为空")
    private Long receiptId;
    @Schema(description = "设备台账编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "设备不能为空")
    private Long machineryId;
    @Schema(description = "设备物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "设备物料不能为空")
    private Long itemId;
    @Schema(description = "入库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1.00")
    @NotNull(message = "入库数量不能为空")
    @DecimalMin(value = "0.01", message = "入库数量不能小于 0.01")
    private BigDecimal quantity;
    @Schema(description = "备注", example = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/MesWmSnController.java
@@ -8,8 +8,12 @@
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnDetailRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnGroupRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnInStockPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnInStockReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnOutStockReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnRespVO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
@@ -92,6 +96,39 @@
        return success(true);
    }
    @PostMapping("/in-stock")
    @Operation(summary = "入库登记 SN", description = "按 SN å»ºç«‹å•件入库事务并入台,需物料已启用序列号管理")
    @PreAuthorize("@ss.hasPermission('mes:wm-sn:update')")
    public CommonResult<Boolean> registerSnInStock(@Valid @RequestBody MesWmSnInStockReqVO reqVO) {
        snService.registerSnInStock(reqVO);
        return success(true);
    }
    @PostMapping("/out-stock")
    @Operation(summary = "出库核销 SN", description = "按 SN å»ºç«‹å•件出库事务并核销")
    @PreAuthorize("@ss.hasPermission('mes:wm-sn:update')")
    public CommonResult<Boolean> outboundSn(@Valid @RequestBody MesWmSnOutStockReqVO reqVO) {
        snService.outboundSn(reqVO);
        return success(true);
    }
    @GetMapping("/detail")
    @Operation(summary = "获得 SN å•件明细", description = "返回状态、在库位置及最近一笔库存事务")
    @Parameter(name = "code", description = "SN ç ", required = true, example = "SN20260305000001")
    @PreAuthorize("@ss.hasPermission('mes:wm-sn:query')")
    public CommonResult<MesWmSnDetailRespVO> getSnDetail(@RequestParam("code") @NotBlank(message = "SN ç ä¸èƒ½ä¸ºç©º") String code) {
        return success(snService.getSnDetail(code));
    }
    @GetMapping("/in-stock-page")
    @Operation(summary = "SN ç çŠ¶æ€åˆ†é¡µ", description = "按状态/物料/库位等维度分页查询 SN(支持盘点核对)")
    @PreAuthorize("@ss.hasPermission('mes:wm-sn:query')")
    public CommonResult<PageResult<MesWmSnRespVO>> getSnInStockPage(@Valid MesWmSnInStockPageReqVO reqVO) {
        PageResult<MesWmSnRespVO> pageResult = BeanUtils.toBean(snService.getSnInStockPage(reqVO), MesWmSnRespVO.class);
        buildSnItemVO(pageResult.getList());
        return success(pageResult);
    }
    @GetMapping("/group-export-excel")
    @Operation(summary = "导出 SN ç åˆ†ç»„ Excel")
    @PreAuthorize("@ss.hasPermission('mes:wm-sn:export')")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnDetailRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - SN ç å•件明细 Response VO")
@Data
public class MesWmSnDetailRespVO {
    @Schema(description = "编号", example = "1")
    private Long id;
    @Schema(description = "SN ç ", example = "SN20260305000001")
    private String code;
    @Schema(description = "物料编号", example = "1")
    private Long itemId;
    @Schema(description = "批次号", example = "BATCH001")
    private String batchCode;
    @Schema(description = "状态(1-在库 2-已出库)", example = "1")
    private Integer status;
    @Schema(description = "在库仓库 ID", example = "5")
    private Long warehouseId;
    @Schema(description = "在库库区 ID", example = "11")
    private Long locationId;
    @Schema(description = "在库库位 ID", example = "10")
    private Long areaId;
    // ==================== æœ€è¿‘一笔库存事务(去向/位置来源) ====================
    @Schema(description = "最近事务类型(1-入库 2-出库)", example = "1")
    private Integer lastTransactionType;
    @Schema(description = "最近事务类型名称", example = "入库")
    private String lastTransactionTypeName;
    @Schema(description = "最近事务变动数量", example = "1")
    private BigDecimal lastTransactionQuantity;
    @Schema(description = "最近事务业务类型", example = "109")
    private Integer lastTransactionBizType;
    @Schema(description = "最近事务业务单号", example = "SN20260305000001")
    private String lastTransactionBizCode;
    @Schema(description = "最近事务发生时间", example = "2026-09-08 10:00:00")
    private LocalDateTime lastTransactionTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnInStockPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - SN ç åœ¨åº“/状态分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesWmSnInStockPageReqVO extends PageParam {
    @Schema(description = "SN ç ", example = "SN20260305000001")
    private String code;
    @Schema(description = "物料编号", example = "1")
    private Long itemId;
    @Schema(description = "批次号", example = "BATCH001")
    private String batchCode;
    @Schema(description = "仓库 ID", example = "5")
    private Long warehouseId;
    @Schema(description = "库区 ID", example = "11")
    private Long locationId;
    @Schema(description = "库位 ID", example = "10")
    private Long areaId;
    @Schema(description = "状态(1-在库 2-已出库)", example = "1")
    private Integer status;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnInStockReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - SN ç å…¥åº“登记 Request VO")
@Data
public class MesWmSnInStockReqVO {
    @Schema(description = "SN ç ", requiredMode = Schema.RequiredMode.REQUIRED, example = "SN20260305000001")
    @NotBlank(message = "SN ç ä¸èƒ½ä¸ºç©º")
    private String code;
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "物料编号不能为空")
    private Long itemId;
    @Schema(description = "仓库 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
    @NotNull(message = "仓库不能为空")
    private Long warehouseId;
    @Schema(description = "库区 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
    @NotNull(message = "库区不能为空")
    private Long locationId;
    @Schema(description = "库位 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
    @NotNull(message = "库位不能为空")
    private Long areaId;
    @Schema(description = "批次号", example = "BATCH001")
    private String batchCode;
    @Schema(description = "备注", example = "扫码入库")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnOutStockReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - SN ç å‡ºåº“核销 Request VO")
@Data
public class MesWmSnOutStockReqVO {
    @Schema(description = "SN ç ", requiredMode = Schema.RequiredMode.REQUIRED, example = "SN20260305000001")
    @NotBlank(message = "SN ç ä¸èƒ½ä¸ºç©º")
    private String code;
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "物料编号不能为空")
    private Long itemId;
    @Schema(description = "备注", example = "扫码出库")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/sn/vo/MesWmSnRespVO.java
@@ -48,6 +48,18 @@
    @Schema(description = "生产工单编号", example = "1")
    private Long workOrderId;
    @Schema(description = "状态(1-在库 2-已出库)", example = "1")
    private Integer status;
    @Schema(description = "在库仓库 ID", example = "5")
    private Long warehouseId;
    @Schema(description = "在库库区 ID", example = "11")
    private Long locationId;
    @Schema(description = "在库库位 ID", example = "10")
    private Long areaId;
    @Schema(description = "创建时间")
    @ExcelProperty("生成时间")
    private LocalDateTime createTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/plan/vo/MesWmStockTakingPlanRespVO.java
@@ -44,6 +44,18 @@
    @ExcelProperty("是否冻结库存")
    private Boolean frozen;
    @Schema(description = "是否定期自动生成盘点任务", example = "false")
    @ExcelProperty("是否定期自动生成")
    private Boolean autoGenerate;
    @Schema(description = "自动生成周期 cron è¡¨è¾¾å¼", example = "0 0 8 * * ?")
    @ExcelProperty("自动生成周期")
    private String generateCron;
    @Schema(description = "上次自动生成时间")
    @ExcelProperty("上次自动生成时间")
    private LocalDateTime lastGenerateTime;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @ExcelProperty("状态")
    private Integer status;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/plan/vo/MesWmStockTakingPlanSaveReqVO.java
@@ -40,6 +40,12 @@
    @NotNull(message = "是否冻结库存不能为空")
    private Boolean frozen;
    @Schema(description = "是否定期自动生成盘点任务", example = "false")
    private Boolean autoGenerate;
    @Schema(description = "自动生成周期 cron è¡¨è¾¾å¼ï¼Œå¼€å¯è‡ªåŠ¨ç”Ÿæˆæ—¶å¿…å¡«ï¼Œå¦‚ 0 0 8 * * ?", example = "0 0 8 * * ?")
    private String generateCron;
    @Schema(description = "备注", example = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/MesWmStockTakingTaskController.java
@@ -113,14 +113,24 @@
                buildTaskRespVOList(pageResult.getList()));
    }
    @PutMapping("/finish")
    @Operation(summary = "完成盘点任务")
    @PreAuthorize("@ss.hasPermission('mes:wm-stock-taking-task:finish')")
    public CommonResult<Boolean> finishStockTakingTask(@Valid @RequestBody MesWmStockTakingTaskFinishReqVO reqVO) {
        stockTakingTaskService.finishStockTakingTask(reqVO.getId());
    @PutMapping("/submit-approval")
    @Operation(summary = "提交盘点结果审批")
    @Parameter(name = "id", description = "编号", required = true)
    @Parameter(name = "processDefinitionKey", description = "流程定义 Key", required = true)
    @PreAuthorize("@ss.hasPermission('mes:wm-stock-taking-task:update')")
    public CommonResult<Boolean> submitStockTakingTaskResultApproval(@RequestParam("id") Long id,
                                                                     @RequestParam("processDefinitionKey") String processDefinitionKey) {
        stockTakingTaskService.submitStockTakingResultApproval(id, processDefinitionKey);
        return success(true);
    }
    @GetMapping("/approval-process-list")
    @Operation(summary = "获得盘点结果审批流程定义列表")
    @PreAuthorize("@ss.hasPermission('mes:wm-stock-taking-task:query')")
    public CommonResult<List<Map<String, Object>>> getStockTakingTaskResultApprovalProcessDefinitionList() {
        return success(stockTakingTaskService.getStockTakingTaskResultApprovalProcessDefinitionList());
    }
    @PutMapping("/cancel")
    @Operation(summary = "取消盘点任务")
    @Parameter(name = "id", description = "编号", required = true)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/stocktaking/task/vo/MesWmStockTakingTaskRespVO.java
@@ -70,6 +70,9 @@
    @ExcelProperty("状态")
    private Integer status;
    @Schema(description = "BPM æµç¨‹å®žä¾‹ç¼–号,非空表示盘点结果已提交审批", example = "xxx")
    private String processInstanceId;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java
@@ -153,6 +153,16 @@
                return "外协发料";
            case MesBizTypeConstants.WM_OUTSOURCE_RECPT:
                return "外协入库";
            case MesBizTypeConstants.WM_STOCK_TAKING:
                return "盘点调整";
            case MesBizTypeConstants.WM_SN:
                return "SN å‡ºå…¥åº“";
            case MesBizTypeConstants.WM_ITEM_RECEIPT_CANCEL:
                return "采购入库红冲";
            case MesBizTypeConstants.DV_MACHINERY:
                return "设备台账入库";
            case MesBizTypeConstants.DV_EQUIPMENT_RECEIPT:
                return "设备入库";
            default:
                return "未知类型(" + bizType + ")";
        }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionPageReqVO.java
@@ -47,6 +47,9 @@
    @Schema(description = "库存记录ID", example = "1")
    private Long materialStockId;
    @Schema(description = "SN ç ", example = "SN20260305000001")
    private String snCode;
    @Schema(description = "开始时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime startTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmTransactionRespVO.java
@@ -51,6 +51,9 @@
    @Schema(description = "批次号", example = "BATCH001")
    private String batchCode;
    @Schema(description = "SN ç ", example = "SN20260305000001")
    private String snCode;
    // ========== åº“存位置 ==========
    @Schema(description = "仓库ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/cal/schedule/MesCalScheduleDetailDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.cal.schedule;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanShiftDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.team.MesCalTeamDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * MES å‘˜å·¥çº§æŽ’班明细 DO
 *
 * ç”±ç­ç»„排班(mes_cal_team_shift)+ ç­ç»„成员(mes_cal_team_member)展开到"员工×日×班次",
 * ä½œä¸ºå·¥æ—¶ç»Ÿè®¡ä¸Žå€¼å®ˆåˆè§„的基准。
 *
 * @author è¶…级管理员
 */
@TableName("mes_cal_schedule_detail")
@KeySequence("mes_cal_schedule_detail_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesCalScheduleDetailDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * æŽ’班计划编号
     */
    private Long planId;
    /**
     * ç­ç»„编号
     *
     * å…³è” {@link MesCalTeamDO#getId()}
     */
    private Long teamId;
    /**
     * ç­ç»„成员用户编号
     *
     * å…³è” system_users.id(mes_cal_team_member.userId)
     */
    private Long userId;
    /**
     * æ—¥æœŸ
     */
    private LocalDateTime day;
    /**
     * ç­æ¬¡ç¼–号
     *
     * å…³è” {@link MesCalPlanShiftDO#getId()}
     */
    private Long shiftId;
    /**
     * ç­æ¬¡åç§°ï¼ˆå†—余)
     */
    private String shiftName;
    /**
     * å¼€å§‹æ—¶é—´ï¼ˆHH:mm æ ¼å¼ï¼Œå†—余)
     */
    private String startTime;
    /**
     * ç»“束时间(HH:mm æ ¼å¼ï¼Œå†—余)
     */
    private String endTime;
    /**
     * æ¥æº: 1-自动生成 2-人工调整
     */
    private Integer source;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/machinery/MesDvMachineryDO.java
@@ -94,5 +94,12 @@
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO#getId()}
     */
    private Long workstationId;
    /**
     * å¯¹åº”物料编号
     *
     * å…³è” MDM ç‰©æ–™ {@link cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO#getId()}
     * è®¾å¤‡åˆ›å»ºæ—¶è‡ªåŠ¨åˆ›å»ºå¯¹åº”ç‰©æ–™ï¼Œå¹¶ä»¥æ­¤å…³è”è®¾å¤‡ä»“åº“å­˜
     */
    private Long itemId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/barcode/MesWmBarcodeConfigDO.java
@@ -26,6 +26,10 @@
     * å†…容格式模板占位符
     */
    public static final String PLACEHOLDER_BUSINESS_CODE = "{BUSINESSCODE}";
    /**
     * å†…容格式模板占位符
     */
    public static final String PLACEHOLDER_BUSINESS_NAME = "{BUSINESSNAME}";
    /**
     * ç¼–号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/equipmentreceipt/MesWmEquipmentReceiptDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.wm.equipmentreceipt;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
/**
 * MES è®¾å¤‡å…¥åº“单 DO
 */
@TableName("mes_wm_equipment_receipt")
@KeySequence("mes_wm_equipment_receipt_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesWmEquipmentReceiptDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * å•据编码
     */
    private String code;
    /**
     * å•据名称
     */
    private String name;
    /**
     * å…¥åº“日期
     */
    private LocalDateTime receiptDate;
    /**
     * å…¥åº“来源
     */
    private String source;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.wm.MesWmEquipmentReceiptStatusEnum}
     */
    private Integer status;
    /**
     * BPM æµç¨‹å®žä¾‹ç¼–号
     */
    private String processInstanceId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/equipmentreceipt/MesWmEquipmentReceiptLineDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.wm.equipmentreceipt;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
 * MES è®¾å¤‡å…¥åº“单行 DO
 */
@TableName("mes_wm_equipment_receipt_line")
@KeySequence("mes_wm_equipment_receipt_line_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesWmEquipmentReceiptLineDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * å…¥åº“单编号
     *
     * å…³è” {@link MesWmEquipmentReceiptDO#getId()}
     */
    private Long receiptId;
    /**
     * è®¾å¤‡å°è´¦ç¼–号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO#getId()}
     */
    private Long machineryId;
    /**
     * è®¾å¤‡ç‰©æ–™ç¼–号(设备台账对应的 MDM ç‰©æ–™ï¼Œmdm_item.id)
     */
    private Long itemId;
    /**
     * å…¥åº“数量
     */
    private BigDecimal quantity;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/sn/MesWmSnDO.java
@@ -3,6 +3,11 @@
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmSnStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -53,5 +58,39 @@
     * å…³è” {@link MesProWorkOrderDO#getId()}
     */
    private Long workOrderId;
    /**
     * æ‰¹æ¬¡ ID
     *
     * å…³è” {@link MesWmBatchDO#getId()}
     */
    private Long batchId;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link MesWmSnStatusEnum}
     */
    private Integer status;
    /**
     * åœ¨åº“仓库 ID
     *
     * å…³è” {@link MesWmWarehouseDO#getId()}
     */
    private Long warehouseId;
    /**
     * åœ¨åº“库区 ID
     *
     * å…³è” {@link MesWmWarehouseLocationDO#getId()}
     */
    private Long locationId;
    /**
     * åœ¨åº“库位 ID
     *
     * å…³è” {@link MesWmWarehouseAreaDO#getId()}
     */
    private Long areaId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/stocktaking/plan/MesWmStockTakingPlanDO.java
@@ -68,6 +68,25 @@
    private Boolean frozen;
    /**
     * æ˜¯å¦å®šæœŸè‡ªåŠ¨ç”Ÿæˆç›˜ç‚¹ä»»åŠ¡
     *
     * ä¸º true æ—¶ç”±åŽå°å®šæ—¶ä»»åŠ¡æŒ‰ {@link #generateCron} å‘¨æœŸè‡ªåŠ¨åˆ›å»ºç›˜ç‚¹ä»»åŠ¡
     */
    private Boolean autoGenerate;
    /**
     * è‡ªåŠ¨ç”Ÿæˆå‘¨æœŸ cron è¡¨è¾¾å¼
     *
     * autoGenerate ä¸º true æ—¶å¿…填,Quartz 6 æ®µ cron,如 {@code 0 0 8 * * ?} è¡¨ç¤ºæ¯å¤© 8 ç‚¹
     */
    private String generateCron;
    /**
     * ä¸Šæ¬¡è‡ªåŠ¨ç”Ÿæˆæ—¶é—´
     *
     * å®šæ—¶ä»»åŠ¡æ®æ­¤åˆ¤æ–­å‘¨æœŸæ˜¯å¦åˆ°æœŸï¼Œé¿å…åŒä¸€å‘¨æœŸé‡å¤ç”Ÿæˆ
     */
    private LocalDateTime lastGenerateTime;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link CommonStatusEnum}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/stocktaking/task/MesWmStockTakingTaskDO.java
@@ -93,6 +93,13 @@
    private Integer status;
    /**
     * BPM æµç¨‹å®žä¾‹ç¼–号
     *
     * éžç©ºè¡¨ç¤ºç›˜ç‚¹ç»“果已提交 BPM å®¡æ‰¹ï¼Œæ­£åœ¨ç­‰å¾…审批结果
     */
    private String processInstanceId;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/transaction/MesWmTransactionDO.java
@@ -107,6 +107,10 @@
     * å…³è” {@link MesWmBatchDO#getCode()}
     */
    private String batchCode;
    /**
     * SN ç ï¼ˆåºåˆ—号管理物料单品进出)
     */
    private String snCode;
    // ==================== åº“存位置 ====================
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warehouse/MesWmWarehouseAreaDO.java
@@ -28,6 +28,11 @@
    public static final String WIP_VIRTUAL_AREA = "WIP_VIRTUAL_AREA";
    /**
     * è®¾å¤‡ä»“默认库位编码(配合 {@link MesWmWarehouseDO#EQUIPMENT_WAREHOUSE} ä½¿ç”¨ï¼‰
     */
    public static final String EQUIPMENT_AREA = "SYS-EQUIP-BIN-01";
    /**
     * ç¼–号
     */
    @TableId
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warehouse/MesWmWarehouseDO.java
@@ -32,6 +32,13 @@
    public static final String WIP_VIRTUAL_WAREHOUSE = "WIP_VIRTUAL_WAREHOUSE";
    /**
     * è®¾å¤‡ä»“编码
     *
     * è®¾å¤‡åˆ›å»ºæ—¶è‡ªåŠ¨å…¥åº“çš„ä¸“ç”¨ä»“åº“ï¼Œå¯¹åº”åº“å­˜äº‹åŠ¡ä»“åº“ç»´åº¦ã€‚
     */
    public static final String EQUIPMENT_WAREHOUSE = "SYS-EQUIPMENT";
    /**
     * ç¼–号
     */
    @TableId
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/warehouse/MesWmWarehouseLocationDO.java
@@ -27,6 +27,11 @@
    public static final String WIP_VIRTUAL_LOCATION = "WIP_VIRTUAL_LOCATION";
    /**
     * è®¾å¤‡ä»“通用库区编码(配合 {@link MesWmWarehouseDO#EQUIPMENT_WAREHOUSE} ä½¿ç”¨ï¼‰
     */
    public static final String EQUIPMENT_LOCATION = "SYS-EQUIP-LOC-01";
    /**
     * ç¼–号
     */
    @TableId
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/cal/schedule/MesCalScheduleDetailMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.mes.dal.mysql.cal.schedule;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo.MesCalScheduleDetailPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.schedule.MesCalScheduleDetailDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MES å‘˜å·¥çº§æŽ’班明细 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesCalScheduleDetailMapper extends BaseMapperX<MesCalScheduleDetailDO> {
    default PageResult<MesCalScheduleDetailDO> selectPage(MesCalScheduleDetailPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesCalScheduleDetailDO>()
                .eqIfPresent(MesCalScheduleDetailDO::getPlanId, reqVO.getPlanId())
                .eqIfPresent(MesCalScheduleDetailDO::getTeamId, reqVO.getTeamId())
                .eqIfPresent(MesCalScheduleDetailDO::getUserId, reqVO.getUserId())
                .geIfPresent(MesCalScheduleDetailDO::getDay, reqVO.getStartDay())
                .leIfPresent(MesCalScheduleDetailDO::getDay, reqVO.getEndDay())
                .orderByAsc(MesCalScheduleDetailDO::getDay));
    }
    default List<MesCalScheduleDetailDO> selectList(MesCalScheduleDetailPageReqVO reqVO) {
        return selectList(new LambdaQueryWrapperX<MesCalScheduleDetailDO>()
                .eqIfPresent(MesCalScheduleDetailDO::getPlanId, reqVO.getPlanId())
                .eqIfPresent(MesCalScheduleDetailDO::getTeamId, reqVO.getTeamId())
                .eqIfPresent(MesCalScheduleDetailDO::getUserId, reqVO.getUserId())
                .geIfPresent(MesCalScheduleDetailDO::getDay, reqVO.getStartDay())
                .leIfPresent(MesCalScheduleDetailDO::getDay, reqVO.getEndDay())
                .orderByAsc(MesCalScheduleDetailDO::getDay));
    }
    default List<MesCalScheduleDetailDO> selectListByPlanId(Long planId) {
        return selectList(MesCalScheduleDetailDO::getPlanId, planId);
    }
    default List<MesCalScheduleDetailDO> selectListByUserIdBetween(Long userId,
                                                                   java.time.LocalDateTime startDay,
                                                                   java.time.LocalDateTime endDay) {
        return selectList(new LambdaQueryWrapperX<MesCalScheduleDetailDO>()
                .eq(MesCalScheduleDetailDO::getUserId, userId)
                .geIfPresent(MesCalScheduleDetailDO::getDay, startDay)
                .leIfPresent(MesCalScheduleDetailDO::getDay, endDay)
                .orderByAsc(MesCalScheduleDetailDO::getDay));
    }
    default void deleteByPlanId(Long planId) {
        delete(MesCalScheduleDetailDO::getPlanId, planId);
    }
    default void deleteByTeamId(Long teamId) {
        delete(MesCalScheduleDetailDO::getTeamId, teamId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/barcode/MesWmBarcodeConfigMapper.java
@@ -1,11 +1,14 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.barcode;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.barcode.vo.config.MesWmBarcodeConfigPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.barcode.MesWmBarcodeConfigDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * MES æ¡ç é…ç½® Mapper
@@ -29,4 +32,10 @@
        return selectOne(MesWmBarcodeConfigDO::getBizType, bizType);
    }
    default List<MesWmBarcodeConfigDO> selectEnableList() {
        return selectList(new LambdaQueryWrapperX<MesWmBarcodeConfigDO>()
                .eq(MesWmBarcodeConfigDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
                .orderByAsc(MesWmBarcodeConfigDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/equipmentreceipt/MesWmEquipmentReceiptLineMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.equipmentreceipt;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.line.MesWmEquipmentReceiptLinePageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.equipmentreceipt.MesWmEquipmentReceiptLineDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * MES è®¾å¤‡å…¥åº“单行 Mapper
 */
@Mapper
public interface MesWmEquipmentReceiptLineMapper extends BaseMapperX<MesWmEquipmentReceiptLineDO> {
    default PageResult<MesWmEquipmentReceiptLineDO> selectPage(MesWmEquipmentReceiptLinePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmEquipmentReceiptLineDO>()
                .eqIfPresent(MesWmEquipmentReceiptLineDO::getReceiptId, reqVO.getReceiptId())
                .eqIfPresent(MesWmEquipmentReceiptLineDO::getMachineryId, reqVO.getMachineryId())
                .eqIfPresent(MesWmEquipmentReceiptLineDO::getItemId, reqVO.getItemId())
                .orderByDesc(MesWmEquipmentReceiptLineDO::getId));
    }
    default List<MesWmEquipmentReceiptLineDO> selectListByReceiptId(Long receiptId) {
        return selectList(MesWmEquipmentReceiptLineDO::getReceiptId, receiptId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/equipmentreceipt/MesWmEquipmentReceiptMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.equipmentreceipt;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.MesWmEquipmentReceiptPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.equipmentreceipt.MesWmEquipmentReceiptDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES è®¾å¤‡å…¥åº“单 Mapper
 */
@Mapper
public interface MesWmEquipmentReceiptMapper extends BaseMapperX<MesWmEquipmentReceiptDO> {
    default PageResult<MesWmEquipmentReceiptDO> selectPage(MesWmEquipmentReceiptPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmEquipmentReceiptDO>()
                .likeIfPresent(MesWmEquipmentReceiptDO::getCode, reqVO.getCode())
                .likeIfPresent(MesWmEquipmentReceiptDO::getName, reqVO.getName())
                .likeIfPresent(MesWmEquipmentReceiptDO::getSource, reqVO.getSource())
                .eqIfPresent(MesWmEquipmentReceiptDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MesWmEquipmentReceiptDO::getReceiptDate, reqVO.getReceiptDate())
                .orderByDesc(MesWmEquipmentReceiptDO::getId));
    }
    default MesWmEquipmentReceiptDO selectByCode(String code) {
        return selectOne(MesWmEquipmentReceiptDO::getCode, code);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/sn/MesWmSnMapper.java
@@ -5,8 +5,11 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnGroupRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnInStockPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.sn.MesWmSnDO;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmSnStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@@ -51,4 +54,35 @@
                .eq(MesWmSnDO::getUuid, uuid));
    }
    default MesWmSnDO selectByCode(String code) {
        return selectOne(MesWmSnDO::getCode, code);
    }
    /**
     * å‡ºåº“核销:置为已出库并清空在库库位
     */
    default void markOutStockAndClearLocation(Long id) {
        update(null, new LambdaUpdateWrapper<MesWmSnDO>()
                .eq(MesWmSnDO::getId, id)
                .set(MesWmSnDO::getStatus, MesWmSnStatusEnum.OUT_STOCK.getStatus())
                .set(MesWmSnDO::getWarehouseId, null)
                .set(MesWmSnDO::getLocationId, null)
                .set(MesWmSnDO::getAreaId, null));
    }
    /**
     * æŒ‰ SN çŠ¶æ€/库位/物料等维度分页查询
     */
    default PageResult<MesWmSnDO> selectStatusPage(MesWmSnInStockPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmSnDO>()
                .likeIfPresent(MesWmSnDO::getCode, reqVO.getCode())
                .eqIfPresent(MesWmSnDO::getItemId, reqVO.getItemId())
                .likeIfPresent(MesWmSnDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesWmSnDO::getWarehouseId, reqVO.getWarehouseId())
                .eqIfPresent(MesWmSnDO::getLocationId, reqVO.getLocationId())
                .eqIfPresent(MesWmSnDO::getAreaId, reqVO.getAreaId())
                .eqIfPresent(MesWmSnDO::getStatus, reqVO.getStatus())
                .orderByDesc(MesWmSnDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/stocktaking/plan/MesWmStockTakingPlanMapper.java
@@ -1,11 +1,14 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.stocktaking.plan;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.plan.vo.MesWmStockTakingPlanPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.plan.MesWmStockTakingPlanDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
@@ -20,6 +23,19 @@
        return selectOne(MesWmStockTakingPlanDO::getCode, code);
    }
    /**
     * æŸ¥è¯¢å¯ç”¨ä¸”开启定期自动生成的盘点方案
     *
     * @return ç›˜ç‚¹æ–¹æ¡ˆåˆ—表
     */
    default List<MesWmStockTakingPlanDO> selectEnableAutoGenerateList() {
        return selectList(new LambdaQueryWrapperX<MesWmStockTakingPlanDO>()
                .eq(MesWmStockTakingPlanDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
                .eq(MesWmStockTakingPlanDO::getAutoGenerate, Boolean.TRUE)
                .isNotNull(MesWmStockTakingPlanDO::getGenerateCron)
                .orderByAsc(MesWmStockTakingPlanDO::getId));
    }
    default PageResult<MesWmStockTakingPlanDO> selectPage(MesWmStockTakingPlanPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmStockTakingPlanDO>()
                .likeIfPresent(MesWmStockTakingPlanDO::getCode, reqVO.getCode())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/stocktaking/task/MesWmStockTakingTaskMapper.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo.MesWmStockTakingTaskPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.task.MesWmStockTakingTaskDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
/**
@@ -19,6 +20,19 @@
        return selectOne(MesWmStockTakingTaskDO::getCode, code);
    }
    /**
     * æ¸…空 BPM æµç¨‹å®žä¾‹ç¼–号
     *
     * ç”¨äºŽç›˜ç‚¹ç»“果审批被驳回/取消后置空,任务回到可重新提交结果审批的状态
     *
     * @param id ç›˜ç‚¹ä»»åŠ¡ç¼–å·
     */
    default void clearProcessInstanceId(Long id) {
        update(null, new LambdaUpdateWrapper<MesWmStockTakingTaskDO>()
                .eq(MesWmStockTakingTaskDO::getId, id)
                .set(MesWmStockTakingTaskDO::getProcessInstanceId, null));
    }
    default PageResult<MesWmStockTakingTaskDO> selectPage(MesWmStockTakingTaskPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesWmStockTakingTaskDO>()
                .likeIfPresent(MesWmStockTakingTaskDO::getCode, reqVO.getCode())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/transaction/MesWmTransactionMapper.java
@@ -24,12 +24,27 @@
                .eqIfPresent(MesWmTransactionDO::getBizType, reqVO.getBizType())
                .likeIfPresent(MesWmTransactionDO::getBizCode, reqVO.getBizCode())
                .eqIfPresent(MesWmTransactionDO::getMaterialStockId, reqVO.getMaterialStockId())
                .eqIfPresent(MesWmTransactionDO::getSnCode, reqVO.getSnCode())
                .geIfPresent(MesWmTransactionDO::getTransactionTime, reqVO.getStartTime())
                .leIfPresent(MesWmTransactionDO::getTransactionTime, reqVO.getEndTime())
                .orderByDesc(MesWmTransactionDO::getTransactionTime));
    }
    /**
     * æŒ‰ SN ç æŸ¥è¯¢æœ€è¿‘一笔库存事务
     *
     * @param snCode SN ç 
     * @return æœ€è¿‘一笔事务,不存在返回 null
     */
    default MesWmTransactionDO selectLatestBySnCode(String snCode) {
        return selectOne(new LambdaQueryWrapperX<MesWmTransactionDO>()
                .eq(MesWmTransactionDO::getSnCode, snCode)
                .orderByDesc(MesWmTransactionDO::getTransactionTime)
                .orderByDesc(MesWmTransactionDO::getId)
                .last("LIMIT 1"));
    }
    /**
     * æŸ¥æ‰¾è™šæ‹Ÿçº¿è¾¹åº“库存记录对应的原始出库仓库信息
     * <p>
     * é€šè¿‡è™šæ‹Ÿçº¿è¾¹åº“çš„ materialStockId,找到首次入库事务(IN),
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -135,6 +135,9 @@
    ErrorCode CAL_TEAM_SHIFT_NOT_EXISTS = new ErrorCode(1_040_201_200, "班组排班记录不存在");
    ErrorCode CAL_TEAM_SHIFT_GENERATE_TEAM_NOT_ENOUGH = new ErrorCode(1_040_201_201, "班组数量不满足轮班方式要求");
    ErrorCode CAL_TEAM_SHIFT_GENERATE_SHIFT_NOT_ENOUGH = new ErrorCode(1_040_201_202, "班次数量不满足轮班方式要求");
    // ========== MES æ—¥åŽ†æŽ’ç­-员工级排班明细(1-040-201-300) ==========
    ErrorCode CAL_SCHEDULE_DETAIL_NOT_EXISTS = new ErrorCode(1_040_201_300, "员工排班明细不存在");
    ErrorCode CAL_SCHEDULE_DETAIL_GENERATE_TEAM_MEMBER_EMPTY = new ErrorCode(1_040_201_301, "该计划参与的班组均无班组成员,无法生成员工排班明细");
    // ========== MES æ—¥åŽ†æŽ’ç­-排班计划(1-040-202-000) ==========
    ErrorCode CAL_PLAN_NOT_EXISTS = new ErrorCode(1_040_202_000, "排班计划不存在");
@@ -168,6 +171,8 @@
    ErrorCode DV_MACHINERY_HAS_MAINTEN_RECORD = new ErrorCode(1_040_301_005, "设备已关联保养记录,无法删除");
    ErrorCode DV_MACHINERY_HAS_REPAIR = new ErrorCode(1_040_301_006, "设备已关联维修工单,无法删除");
    ErrorCode DV_MACHINERY_WORKSTATION_NOT_EXISTS = new ErrorCode(1_040_301_008, "工作站编码不存在");
    ErrorCode DV_MACHINERY_HAS_ITEM = new ErrorCode(1_040_301_009, "设备已自动生成物料并入设备仓,禁止删除;如需移除请通过库存调整先处理库存");
    ErrorCode DV_MACHINERY_EQUIP_MASTER_MISSING = new ErrorCode(1_040_301_010, "设备入库联动所需主数据未初始化(设备仓或“设备”分类/“台”计量单位缺失),请联系管理员初始化");
    // ========== MES è®¾å¤‡ç®¡ç†-设备数采(1-040-308-000) ==========
    ErrorCode DV_TELEMETRY_NOT_EXISTS = new ErrorCode(1_040_308_000, "数采遥测数据不存在");
@@ -669,6 +674,9 @@
    ErrorCode WM_STOCK_TAKING_PLAN_PARAM_NOT_EXISTS = new ErrorCode(1_040_714_104, "盘点方案参数不存在");
    ErrorCode WM_STOCK_TAKING_PLAN_PARAM_EMPTY = new ErrorCode(1_040_714_105, "盘点方案参数不能为空,请先配置盘点参数");
    ErrorCode WM_STOCK_TAKING_PLAN_DYNAMIC_TIME_INVALID = new ErrorCode(1_040_714_106, "动态盘点方案必须设置开始时间和结束时间,且结束时间必须晚于开始时间");
    ErrorCode WM_STOCK_TAKING_PLAN_AUTO_GENERATE_CRON_REQUIRED = new ErrorCode(1_040_714_107, "开启定期自动生成时必须配置生成周期 cron è¡¨è¾¾å¼");
    ErrorCode WM_STOCK_TAKING_PLAN_AUTO_GENERATE_CRON_INVALID = new ErrorCode(1_040_714_108, "生成周期 cron è¡¨è¾¾å¼ä¸åˆæ³•");
    ErrorCode WM_STOCK_TAKING_PLAN_NOT_AUTO_GENERATE = new ErrorCode(1_040_714_109, "该盘点方案未开启定期自动生成");
    ErrorCode WM_STOCK_TAKING_TASK_NOT_EXISTS = new ErrorCode(1_040_714_110, "盘点任务不存在");
    ErrorCode WM_STOCK_TAKING_TASK_CODE_DUPLICATE = new ErrorCode(1_040_714_111, "盘点任务编码已存在");
    ErrorCode WM_STOCK_TAKING_TASK_NOT_PREPARE = new ErrorCode(1_040_714_112, "只有草稿状态的盘点任务才允许此操作");
@@ -681,6 +689,8 @@
    ErrorCode WM_STOCK_TAKING_TASK_RESULT_NOT_EXISTS = new ErrorCode(1_040_714_119, "盘点结果不存在");
    ErrorCode WM_STOCK_TAKING_TASK_LINE_ALREADY_TAKEN = new ErrorCode(1_040_714_120, "该盘点清单行已有盘点记录,不能重复盘点");
    ErrorCode WM_STOCK_TAKING_TASK_NOT_FINISHED = new ErrorCode(1_040_714_121, "只有已完成状态的盘点任务才能处理差异");
    ErrorCode WM_STOCK_TAKING_TASK_NO_RESULT = new ErrorCode(1_040_714_122, "盘点任务至少需要一条盘点结果才允许提交结果审批");
    ErrorCode WM_STOCK_TAKING_TASK_ALREADY_SUBMIT_APPROVAL = new ErrorCode(1_040_714_123, "盘点结果审批已提交,请勿重复提交");
    // ========== MES ä»“库管理-销售出库单(1-040-714-000) ==========
    ErrorCode WM_PRODUCT_SALES_NOT_EXISTS = new ErrorCode(1_040_714_000, "销售出库单不存在");
@@ -763,6 +773,27 @@
    ErrorCode BARCODE_BIZ_TYPE_NOT_EXISTS = new ErrorCode(1_040_731_003, "业务类型不能为空");
    ErrorCode BARCODE_BIZ_CODE_NOT_EXISTS = new ErrorCode(1_040_731_004, "业务编码不能为空");
    ErrorCode BARCODE_CONFIG_NOT_EXISTS = new ErrorCode(1_040_731_005, "条码配置不存在");
    ErrorCode BARCODE_FORMAT_INVALID = new ErrorCode(1_040_731_006, "条码格式不支持渲染");
    ErrorCode BARCODE_CONTENT_EMPTY = new ErrorCode(1_040_731_007, "条码内容不能为空");
    // ========== MES ä»“库管理-SN ç ï¼ˆ1-040-732-000) ==========
    ErrorCode WM_SN_NOT_EXISTS = new ErrorCode(1_040_732_000, "SN ç ä¸å­˜åœ¨");
    ErrorCode WM_SN_ITEM_NOT_SERIAL = new ErrorCode(1_040_732_001, "物料未启用序列号管理,无法登记 SN");
    ErrorCode WM_SN_ALREADY_IN_STOCK = new ErrorCode(1_040_732_002, "SN å·²åœ¨åº“,请先出库再登记入库");
    ErrorCode WM_SN_NOT_IN_STOCK = new ErrorCode(1_040_732_003, "SN ä¸åœ¨åº“,无法出库");
    ErrorCode WM_SN_ITEM_MISMATCH = new ErrorCode(1_040_732_004, "SN ä¸Žç‰©æ–™ä¸åŒ¹é…");
    ErrorCode WM_SN_LOCATION_MISMATCH = new ErrorCode(1_040_732_005, "SN ä¸åœ¨æŒ‡å®šåº“位在库,无法出库");
    // ========== MES è®¾å¤‡å…¥åº“单(1-040-733-000) ==========
    ErrorCode WM_EQUIP_RECEIPT_NOT_EXISTS = new ErrorCode(1_040_733_000, "设备入库单不存在");
    ErrorCode WM_EQUIP_RECEIPT_CODE_DUPLICATE = new ErrorCode(1_040_733_001, "设备入库单编码已存在");
    ErrorCode WM_EQUIP_RECEIPT_STATUS_NOT_PREPARE = new ErrorCode(1_040_733_002, "只有草稿状态才允许此操作");
    ErrorCode WM_EQUIP_RECEIPT_STATUS_NOT_APPROVED = new ErrorCode(1_040_733_003, "只有已审批状态才允许执行入库");
    ErrorCode WM_EQUIP_RECEIPT_NO_LINE = new ErrorCode(1_040_733_004, "至少需要一条行项目");
    ErrorCode WM_EQUIP_RECEIPT_CANCEL_NOT_ALLOWED = new ErrorCode(1_040_733_005, "已完成或已取消的入库单不允许取消");
    ErrorCode WM_EQUIP_RECEIPT_LINE_NOT_EXISTS = new ErrorCode(1_040_733_100, "设备入库单行不存在");
    ErrorCode WM_EQUIP_RECEIPT_MACHINERY_NOT_VALID = new ErrorCode(1_040_733_101, "所选设备未关联物料,无法入库");
    ErrorCode WM_EQUIP_RECEIPT_QUANTITY_INVALID = new ErrorCode(1_040_733_102, "入库数量必须大于 0");
    // ========== MES ä»“库管理-装箱单(1-040-740-000) ==========
    ErrorCode WM_PACKAGE_NOT_EXISTS = new ErrorCode(1_040_740_000, "装箱单不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesBizTypeConstants.java
@@ -66,6 +66,7 @@
    // ========== DV è®¾å¤‡æ¨¡å— [400, 500) ==========
    public static final int DV_MACHINERY = 400; // è®¾å¤‡ï¼šMesDvMachineryDO
    public static final int DV_EQUIPMENT_RECEIPT = 401; // è®¾å¤‡å…¥åº“单:MesWmEquipmentReceiptDO
    // ========== TM å·¥è£…夹具模块 [500, 600) ==========
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/cal/MesCalScheduleDetailSourceEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.enums.cal;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES å‘˜å·¥æŽ’班明细来源枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesCalScheduleDetailSourceEnum implements ArrayValuable<Integer> {
    AUTO(1, "自动生成"),
    MANUAL(2, "人工调整");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesCalScheduleDetailSourceEnum::getSource).toArray(Integer[]::new);
    /**
     * æ¥æºå€¼
     */
    private final Integer source;
    /**
     * æ¥æºå
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/md/autocode/MesMdAutoCodeRuleCodeEnum.java
@@ -47,6 +47,7 @@
    WM_SALES_NOTICE_CODE("WM_SALES_NOTICE_CODE", "发货通知单编码"),
    WM_RETURN_SALES_CODE("WM_RETURN_SALES_CODE", "销售退货单编码"),
    WM_MISC_ISSUE_CODE("WM_MISC_ISSUE_CODE", "杂项出库单编码"),
    WM_EQUIP_RECEIPT_CODE("WM_EQUIP_RECEIPT_CODE", "设备入库单编码"),
    PRO_ROUTE_CODE("PRO_ROUTE_CODE", "工艺路线编码"),
    PRO_FEEDBACK_CODE("PRO_FEEDBACK_CODE", "生产报工单编码"),
    PRO_WORK_ORDER_CODE("PRO_WORK_ORDER_CODE", "生产工单编码"),
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmEquipmentReceiptStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.mes.enums.wm;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import cn.iocoder.yudao.module.mes.enums.MesOrderStatusConstants;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES è®¾å¤‡å…¥åº“单状态枚举
 */
@Getter
@AllArgsConstructor
public enum MesWmEquipmentReceiptStatusEnum implements ArrayValuable<Integer> {
    /**
     * è‰ç¨¿
     */
    PREPARE(MesOrderStatusConstants.PREPARE, "草稿"),
    /**
     * å®¡æ‰¹ä¸­
     */
    APPROVING(MesOrderStatusConstants.APPROVING, "审批中"),
    /**
     * å·²å®¡æ‰¹
     */
    APPROVED(MesOrderStatusConstants.APPROVED, "已审批"),
    /**
     * å·²å®Œæˆ
     */
    FINISHED(MesOrderStatusConstants.FINISHED, "已完成"),
    /**
     * å·²å–消
     */
    CANCELED(MesOrderStatusConstants.CANCELLED, "已取消");
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(MesWmEquipmentReceiptStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * çŠ¶æ€å€¼
     */
    private final Integer status;
    /**
     * çŠ¶æ€å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmSnStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.enums.wm;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES SN ç çŠ¶æ€æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesWmSnStatusEnum implements ArrayValuable<Integer> {
    IN_STOCK(1, "在库"),
    OUT_STOCK(2, "已出库");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesWmSnStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * çŠ¶æ€å€¼
     */
    private final Integer status;
    /**
     * çŠ¶æ€å
     */
    private final String label;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmStockTakingTaskStatusEnum.java
@@ -23,15 +23,16 @@
     */
    PREPARE(MesOrderStatusConstants.PREPARE, "草稿"),
    /**
     * å®¡æ‰¹ä¸­
     * ç›˜ç‚¹ä¸­ï¼ˆå«ç›˜ç‚¹ç»“果审批中)
     *
     * å¯¹åº” MesWmStockTakingTaskService#submitStockTakingTask æ–¹æ³•
     * å¯¹åº” MesWmStockTakingTaskService#submitStockTakingTask å¼€å§‹ç›˜ç‚¹ã€
     * #submitStockTakingResultApproval æäº¤ç»“果审批
     */
    APPROVING(MesOrderStatusConstants.APPROVING, "审批中"),
    APPROVING(MesOrderStatusConstants.APPROVING, "盘点中"),
    /**
     * å·²å®Œæˆ
     *
     * å¯¹åº” MesWmStockTakingTaskService#finishStockTakingTask æ–¹æ³•
     * ç›˜ç‚¹ç»“果审批通过后,由结果审批回调置为已完成并自动核销差异
     */
    FINISHED(MesOrderStatusConstants.FINISHED, "已完成"),
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/job/wm/MesWmStockTakingPlanAutoJob.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,101 @@
package cn.iocoder.yudao.module.mes.job.wm;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.plan.MesWmStockTakingPlanDO;
import cn.iocoder.yudao.module.mes.service.wm.stocktaking.plan.MesWmStockTakingPlanService;
import cn.iocoder.yudao.module.mes.service.wm.stocktaking.task.MesWmStockTakingTaskService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.quartz.CronExpression;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
 * MES å®šæœŸç›˜ç‚¹è‡ªåŠ¨ç”Ÿæˆ Job
 *
 * <p>扫描启用且开启定期自动生成的盘点方案,按各方案配置的生成周期 cron åˆ¤æ–­æ˜¯å¦åˆ°æœŸï¼Œ
 * åˆ°æœŸåˆ™æŒ‰æ–¹æ¡ˆè‡ªåŠ¨åˆ›å»ºç›˜ç‚¹ä»»åŠ¡ï¼Œé¿å…äººå·¥é€æœŸå»ºå•ã€‚åŽå°è°ƒåº¦ cron å»ºè®®é…ç½®ä¸ºæ¯åˆ†é’Ÿæ‰§è¡Œä¸€æ¬¡ã€‚
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class MesWmStockTakingPlanAutoJob implements JobHandler {
    @Resource
    private MesWmStockTakingPlanService stockTakingPlanService;
    @Resource
    private MesWmStockTakingTaskService stockTakingTaskService;
    @Override
    @TenantJob
    public String execute(String param) {
        // 1. èŽ·å–å¯ç”¨ä¸”å¼€å¯å®šæœŸè‡ªåŠ¨ç”Ÿæˆçš„æ–¹æ¡ˆ
        List<MesWmStockTakingPlanDO> plans = stockTakingPlanService.getEnableAutoGeneratePlanList();
        if (CollUtil.isEmpty(plans)) {
            return "定期盘点:无启用的自动生成方案,跳过";
        }
        // 2. é€ä¸ªæ–¹æ¡ˆåˆ¤æ–­å‘¨æœŸæ˜¯å¦åˆ°æœŸï¼Œåˆ°æœŸåˆ™è‡ªåŠ¨ç”Ÿæˆä»»åŠ¡
        Date now = new Date();
        int successCount = 0;
        List<String> errors = new ArrayList<>();
        for (MesWmStockTakingPlanDO plan : plans) {
            try {
                if (shouldGenerate(plan, now)) {
                    Long taskId = stockTakingTaskService.createStockTakingTaskFromPlan(plan.getId());
                    successCount++;
                    log.info("[execute] æ–¹æ¡ˆ({}) è‡ªåŠ¨ç”Ÿæˆç›˜ç‚¹ä»»åŠ¡({}) æˆåŠŸ", plan.getId(), taskId);
                }
            } catch (Exception e) {
                log.error("[execute] æ–¹æ¡ˆ({}) è‡ªåŠ¨ç”Ÿæˆç›˜ç‚¹ä»»åŠ¡å¤±è´¥", plan.getId(), e);
                errors.add(String.format("方案[%s]:%s", plan.getName(), e.toString()));
            }
        }
        // 3. æ±‡æ€»æ‰§è¡Œç»“æžœ
        String msg = String.format("定期盘点:扫描 %s ä¸ªæ–¹æ¡ˆï¼Œæœ¬æ¬¡ç”Ÿæˆ %s ä¸ªä»»åŠ¡", plans.size(), successCount);
        if (CollUtil.isNotEmpty(errors)) {
            msg += ",失败明细:" + String.join(";", errors);
        }
        return msg;
    }
    /**
     * åˆ¤æ–­æ–¹æ¡ˆå½“前周期是否到期
     *
     * <p>从未生成过则仅记录基线时间并从下一周期开始触发(刚启用不立即生成);
     * å¦åˆ™æŒ‰ã€Œä¸Šæ¬¡ç”Ÿæˆæ—¶é—´ + cron」计算下一次触发时间,落在当前时间之前即为到期。
     *
     * @param plan ç›˜ç‚¹æ–¹æ¡ˆ
     * @param now  å½“前时间
     * @return æ˜¯å¦åˆ°æœŸ
     */
    private boolean shouldGenerate(MesWmStockTakingPlanDO plan, Date now) {
        CronExpression cron;
        try {
            cron = new CronExpression(plan.getGenerateCron());
        } catch (Exception e) {
            log.warn("[shouldGenerate] æ–¹æ¡ˆ({}) cron è¡¨è¾¾å¼({}) ä¸åˆæ³•,跳过",
                    plan.getId(), plan.getGenerateCron());
            return false;
        }
        LocalDateTime lastTime = plan.getLastGenerateTime();
        if (lastTime == null) {
            // é¦–次扫描:仅记录基线时间,不立即生成
            stockTakingPlanService.updateStockTakingPlanLastGenerateTime(plan.getId(), LocalDateTime.now());
            return false;
        }
        Date last = Date.from(lastTime.atZone(ZoneId.systemDefault()).toInstant());
        Date next = cron.getNextValidTimeAfter(last);
        return next != null && !next.after(now);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/schedule/MesCalScheduleDetailService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.mes.service.cal.schedule;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo.MesCalScheduleDetailCheckReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo.MesCalScheduleDetailCheckRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo.MesCalScheduleDetailPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo.MesCalScheduleDetailSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.schedule.MesCalScheduleDetailDO;
import java.util.List;
/**
 * MES å‘˜å·¥çº§æŽ’班明细 Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface MesCalScheduleDetailService {
    /**
     * æŒ‰ç­ç»„排班生成员工级排班明细(幂等:先清理该计划旧明细再展开班组成员)
     *
     * @param planId æŽ’班计划编号
     */
    void generateDetails(Long planId);
    /**
     * èŽ·å¾—å‘˜å·¥çº§æŽ’ç­æ˜Žç»†åˆ†é¡µ
     *
     * @param reqVO æŸ¥è¯¢æ¡ä»¶
     * @return åˆ†é¡µç»“æžœ
     */
    PageResult<MesCalScheduleDetailDO> getDetailPage(MesCalScheduleDetailPageReqVO reqVO);
    /**
     * èŽ·å¾—å‘˜å·¥çº§æŽ’ç­æ˜Žç»†åˆ—è¡¨
     *
     * @param reqVO æŸ¥è¯¢æ¡ä»¶
     * @return æ˜Žç»†åˆ—表
     */
    List<MesCalScheduleDetailDO> getDetailList(MesCalScheduleDetailPageReqVO reqVO);
    /**
     * æ›´æ–°å‘˜å·¥çº§æŽ’班明细(人工微调,source ç½®ä¸º 2-人工调整)
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateDetail(MesCalScheduleDetailSaveReqVO updateReqVO);
    /**
     * å‘˜å·¥æŽ’班合规校验(单日连续工时超长、相邻班次间休息不足)
     *
     * @param reqVO æ ¡éªŒæ¡ä»¶
     * @return æ ¡éªŒç»“果列表,item.issue ä¸ºç©ºè¡¨ç¤ºè¯¥ç­æ¬¡åˆè§„
     */
    List<MesCalScheduleDetailCheckRespVO> checkCompliance(MesCalScheduleDetailCheckReqVO reqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/cal/schedule/MesCalScheduleDetailServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,225 @@
package cn.iocoder.yudao.module.mes.service.cal.schedule;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo.MesCalScheduleDetailCheckReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo.MesCalScheduleDetailCheckRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo.MesCalScheduleDetailPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.cal.schedule.vo.MesCalScheduleDetailSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.plan.MesCalPlanShiftDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.schedule.MesCalScheduleDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.team.MesCalTeamMemberDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.team.MesCalTeamShiftDO;
import cn.iocoder.yudao.module.mes.dal.mysql.cal.plan.MesCalPlanShiftMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.cal.schedule.MesCalScheduleDetailMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.cal.team.MesCalTeamMemberMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.cal.team.MesCalTeamShiftMapper;
import cn.iocoder.yudao.module.mes.enums.cal.MesCalScheduleDetailSourceEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_SCHEDULE_DETAIL_GENERATE_TEAM_MEMBER_EMPTY;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.CAL_SCHEDULE_DETAIL_NOT_EXISTS;
/**
 * MES å‘˜å·¥çº§æŽ’班明细 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
@Slf4j
public class MesCalScheduleDetailServiceImpl implements MesCalScheduleDetailService {
    /**
     * åˆè§„校验阈值:单班疑似连续工时上限(小时)
     */
    private static final long MAX_CONTINUOUS_WORK_HOURS = 12;
    /**
     * åˆè§„校验阈值:相邻班次最少休息间隔(小时)
     */
    private static final long MIN_REST_HOURS = 8;
    private static final int MINUTES_PER_DAY = 24 * 60;
    @Resource
    private MesCalScheduleDetailMapper scheduleDetailMapper;
    @Resource
    private MesCalTeamShiftMapper teamShiftMapper;
    @Resource
    private MesCalTeamMemberMapper teamMemberMapper;
    @Resource
    private MesCalPlanShiftMapper planShiftMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void generateDetails(Long planId) {
        // 1. æŸ¥è¯¢è¯¥è®¡åˆ’下的班组排班(班组 Ã— æ—¥æœŸ Ã— ç­æ¬¡ï¼‰
        List<MesCalTeamShiftDO> teamShifts = teamShiftMapper.selectListByPlanId(planId);
        if (teamShifts.isEmpty()) {
            scheduleDetailMapper.deleteByPlanId(planId);
            throw exception(CAL_SCHEDULE_DETAIL_GENERATE_TEAM_MEMBER_EMPTY);
        }
        Set<Long> teamIds = teamShifts.stream().map(MesCalTeamShiftDO::getTeamId).collect(Collectors.toSet());
        // 2. åŠ è½½ç­æ¬¡å®šä¹‰ï¼ˆç­æ¬¡åç§°/起止时间)与班组成员
        Map<Long, MesCalPlanShiftDO> shiftMap = planShiftMapper.selectListByPlanId(planId).stream()
                .collect(Collectors.toMap(MesCalPlanShiftDO::getId, shift -> shift, (a, b) -> a));
        List<MesCalTeamMemberDO> members = teamMemberMapper.selectListByTeamIds(teamIds);
        if (members.isEmpty()) {
            scheduleDetailMapper.deleteByPlanId(planId);
            throw exception(CAL_SCHEDULE_DETAIL_GENERATE_TEAM_MEMBER_EMPTY);
        }
        Map<Long, List<MesCalTeamMemberDO>> memberMap = members.stream()
                .collect(Collectors.groupingBy(MesCalTeamMemberDO::getTeamId));
        // 3. å±•开为员工级明细(员工 Ã— æ—¥æœŸ Ã— ç­æ¬¡ï¼‰ï¼Œå…ˆæ¸…理旧数据保证幂等
        scheduleDetailMapper.deleteByPlanId(planId);
        List<MesCalScheduleDetailDO> details = new ArrayList<>();
        for (MesCalTeamShiftDO teamShift : teamShifts) {
            MesCalPlanShiftDO shift = shiftMap.get(teamShift.getShiftId());
            if (shift == null) {
                continue;
            }
            for (MesCalTeamMemberDO member : memberMap.getOrDefault(teamShift.getTeamId(), Collections.emptyList())) {
                details.add(MesCalScheduleDetailDO.builder()
                        .planId(planId)
                        .teamId(teamShift.getTeamId())
                        .userId(member.getUserId())
                        .day(teamShift.getDay())
                        .shiftId(teamShift.getShiftId())
                        .shiftName(shift.getName())
                        .startTime(shift.getStartTime())
                        .endTime(shift.getEndTime())
                        .source(MesCalScheduleDetailSourceEnum.AUTO.getSource())
                        .build());
            }
        }
        if (!details.isEmpty()) {
            scheduleDetailMapper.insertBatch(details);
        }
    }
    @Override
    public PageResult<MesCalScheduleDetailDO> getDetailPage(MesCalScheduleDetailPageReqVO reqVO) {
        return scheduleDetailMapper.selectPage(reqVO);
    }
    @Override
    public List<MesCalScheduleDetailDO> getDetailList(MesCalScheduleDetailPageReqVO reqVO) {
        return scheduleDetailMapper.selectList(reqVO);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateDetail(MesCalScheduleDetailSaveReqVO updateReqVO) {
        MesCalScheduleDetailDO detail = scheduleDetailMapper.selectById(updateReqVO.getId());
        if (detail == null) {
            throw exception(CAL_SCHEDULE_DETAIL_NOT_EXISTS);
        }
        MesCalScheduleDetailDO updateObj = MesCalScheduleDetailDO.builder()
                .id(updateReqVO.getId())
                .shiftId(updateReqVO.getShiftId())
                .shiftName(updateReqVO.getShiftName())
                .startTime(updateReqVO.getStartTime())
                .endTime(updateReqVO.getEndTime())
                .remark(updateReqVO.getRemark())
                .source(MesCalScheduleDetailSourceEnum.MANUAL.getSource())
                .build();
        scheduleDetailMapper.updateById(updateObj);
    }
    @Override
    public List<MesCalScheduleDetailCheckRespVO> checkCompliance(MesCalScheduleDetailCheckReqVO reqVO) {
        List<MesCalScheduleDetailDO> details =
                scheduleDetailMapper.selectListByUserIdBetween(reqVO.getUserId(), reqVO.getStartDay(), reqVO.getEndDay());
        List<MesCalScheduleDetailCheckRespVO> result = new ArrayList<>();
        for (int i = 0; i < details.size(); i++) {
            MesCalScheduleDetailDO detail = details.get(i);
            MesCalScheduleDetailCheckRespVO respVO = new MesCalScheduleDetailCheckRespVO();
            respVO.setDay(detail.getDay());
            respVO.setShiftName(detail.getShiftName());
            respVO.setStartTime(detail.getStartTime());
            respVO.setEndTime(detail.getEndTime());
            // æ ¡éªŒ 1:单班疑似连续工时超长
            Long duration = shiftDurationMinutes(detail.getStartTime(), detail.getEndTime());
            if (duration != null && duration > MAX_CONTINUOUS_WORK_HOURS * 60) {
                respVO.setIssueType(1);
                respVO.setIssue("单班疑似连续工时超长(" + formatMinutes(duration) + "),可能违反连续工时上限");
            }
            // æ ¡éªŒ 2:与前一班次间隔休息不足(仅在同一天或相邻天才比较)
            if (i > 0) {
                MesCalScheduleDetailDO prev = details.get(i - 1);
                Long rest = restMinutesBetween(prev, detail);
                if (rest != null && rest < MIN_REST_HOURS * 60) {
                    respVO.setIssueType(2);
                    respVO.setIssue("与前一个班次间隔仅 " + formatMinutes(rest) + ",休息时间不足");
                }
            }
            result.add(respVO);
        }
        return result;
    }
    /**
     * è®¡ç®—单班工时(分钟),跨午夜班次自动+24h;时间格式非法时返回 null
     */
    private Long shiftDurationMinutes(String startTime, String endTime) {
        Integer start = toMinutes(startTime);
        Integer end = toMinutes(endTime);
        if (start == null || end == null) {
            return null;
        }
        int duration = end - start;
        if (duration < 0) {
            duration += MINUTES_PER_DAY;
        }
        return (long) duration;
    }
    /**
     * è®¡ç®—相邻两天班次之间的休息间隔(分钟);
     * ç­æ¬¡è‹¥è·¨åˆå¤œï¼Œç»“束分钟数按超过 MINUTES_PER_DAY è®¡ï¼ˆå³ day + ç­æ¬¡æ—¶é•¿ï¼‰ï¼Œ
     * ä¸‹ä¸€ç­æ¬¡å¼€å§‹åˆ†é’ŸæŒ‰æ–°çš„一天(+MINUTES_PER_DAY)计。
     */
    private Long restMinutesBetween(MesCalScheduleDetailDO prev, MesCalScheduleDetailDO next) {
        Integer prevStart = toMinutes(prev.getStartTime());
        Integer prevEnd = toMinutes(prev.getEndTime());
        Integer nextStart = toMinutes(next.getStartTime());
        if (prevStart == null || prevEnd == null || nextStart == null) {
            return null;
        }
        int prevDuration = prevEnd - prevStart;
        if (prevDuration < 0) {
            prevDuration += MINUTES_PER_DAY;
        }
        int prevEndMinutes = prevStart + prevDuration; // ç›¸å¯¹ prev.day é›¶ç‚¹ï¼Œå¯èƒ½è¶…过一天
        int nextStartMinutes = MINUTES_PER_DAY + nextStart; // ä¸‹ä¸€ç­æ¬¡åœ¨æ–°çš„一天
        return (long) (nextStartMinutes - prevEndMinutes);
    }
    private Integer toMinutes(String time) {
        if (time == null) {
            return null;
        }
        String[] parts = time.split(":");
        if (parts.length != 2) {
            return null;
        }
        try {
            return Integer.parseInt(parts[0].trim()) * 60 + Integer.parseInt(parts[1].trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    private String formatMinutes(long minutes) {
        long hours = minutes / 60;
        long mins = minutes % 60;
        return mins == 0 ? hours + " å°æ—¶" : hours + " å°æ—¶ " + mins + " åˆ†é’Ÿ";
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/machinery/MesDvMachineryServiceImpl.java
@@ -3,30 +3,51 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemCreateReqDTO;
import cn.iocoder.yudao.module.mdm.api.unit.MdmUnitMeasureApi;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mdm.enums.MdmItemTypeEnum;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemApi;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSyncReqDTO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.machinery.vo.MesDvMachineryImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.machinery.vo.MesDvMachineryImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.machinery.vo.MesDvMachineryPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.machinery.vo.MesDvMachinerySaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryTypeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkshopDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.BarcodeBizTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.dv.checkplan.MesDvCheckPlanMachineryService;
import cn.iocoder.yudao.module.mes.service.dv.checkrecord.MesDvCheckRecordService;
import cn.iocoder.yudao.module.mes.service.dv.maintenrecord.MesDvMaintenRecordService;
import cn.iocoder.yudao.module.mes.service.dv.repair.MesDvRepairService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkshopService;
import cn.iocoder.yudao.module.mes.service.wm.barcode.MesWmBarcodeService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
@@ -43,6 +64,15 @@
@Service
@Validated
public class MesDvMachineryServiceImpl implements MesDvMachineryService {
    /**
     * è®¾å¤‡è”动物料的类型编码(mes_md_item_type / mdm_item_category ä¸­â€œè®¾å¤‡â€è¡Œçš„ code)
     */
    private static final String EQUIPMENT_ITEM_TYPE_CODE = "EQUIPMENT";
    /**
     * è®¾å¤‡è”动物料的计量单位编码(mes_md_unit_measure / mdm_unit_measure ä¸­â€œå°â€è¡Œçš„ code)
     */
    private static final String EQUIPMENT_UNIT_CODE = "TAI";
    @Resource
    private MesDvMachineryMapper machineryMapper;
@@ -71,7 +101,30 @@
    @Lazy
    private MesDvRepairService repairService;
    // ========== è®¾å¤‡ç‰©æ–™è”动依赖 ==========
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private MesMdItemApi mesMdItemApi;
    @Resource
    @Lazy
    private MesMdItemTypeService mesMdItemTypeService;
    @Resource
    @Lazy
    private MesMdUnitMeasureService mesMdUnitMeasureService;
    @Resource
    private MesWmTransactionService wmTransactionService;
    @Resource
    private MesWmWarehouseService wmWarehouseService;
    @Resource
    private MesWmWarehouseLocationService wmLocationService;
    @Resource
    private MesWmWarehouseAreaService wmAreaService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createMachinery(MesDvMachinerySaveReqVO createReqVO) {
        // æ ¡éªŒè®¾å¤‡ç±»åž‹å­˜åœ¨
        machineryTypeService.getMachineryType(createReqVO.getMachineryTypeId());
@@ -80,13 +133,20 @@
        // æ ¡éªŒç¼–码唯一
        validateMachineryCodeUnique(null, createReqVO.getCode());
        // æ’å…¥
        // åˆ›å»ºå¯¹åº” MDM ç‰©æ–™å¹¶åŒæ­¥ MES ç‰©æ–™ï¼ˆåŒä¸€äº‹åŠ¡ï¼Œå¤±è´¥æ•´ä½“å›žæ»šï¼‰
        Long itemId = createEquipmentMaterial(createReqVO.getCode(), createReqVO.getName(),
                createReqVO.getSpecification(), createReqVO.getRemark());
        // æ’入设备台账
        MesDvMachineryDO machinery = BeanUtils.toBean(createReqVO, MesDvMachineryDO.class);
        machinery.setItemId(itemId);
        machineryMapper.insert(machinery);
        // è‡ªåŠ¨ç”Ÿæˆæ¡ç 
        barcodeService.autoGenerateBarcode(BarcodeBizTypeEnum.MACHINERY.getValue(),
                machinery.getId(), machinery.getCode(), machinery.getName());
        // è‡ªåŠ¨å…¥è®¾å¤‡ä»“ï¼ˆåº“å­˜ + æµæ°´ï¼‰
        createEquipmentStock(itemId, machinery.getId(), machinery.getCode());
        return machinery.getId();
    }
@@ -94,6 +154,7 @@
    public void updateMachinery(MesDvMachinerySaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateMachineryExists(updateReqVO.getId());
        MesDvMachineryDO existMachinery = machineryMapper.selectById(updateReqVO.getId());
        // æ ¡éªŒè®¾å¤‡ç±»åž‹å­˜åœ¨
        machineryTypeService.getMachineryType(updateReqVO.getMachineryTypeId());
        // æ ¡éªŒè½¦é—´å­˜åœ¨
@@ -103,6 +164,8 @@
        // æ›´æ–°
        MesDvMachineryDO updateObj = BeanUtils.toBean(updateReqVO, MesDvMachineryDO.class);
        // ç‰©æ–™å…³è”由创建时自动生成,更新不允许清空(前端也不传入 itemId)
        updateObj.setItemId(existMachinery.getItemId());
        machineryMapper.updateById(updateObj);
    }
@@ -110,6 +173,11 @@
    public void deleteMachinery(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateMachineryExists(id);
        // æ ¡éªŒå·²ç”Ÿæˆç‰©æ–™å¹¶å…¥è®¾å¤‡ä»“:避免库存/流水失去业务主体,禁止删除
        MesDvMachineryDO machinery = machineryMapper.selectById(id);
        if (machinery.getItemId() != null) {
            throw exception(DV_MACHINERY_HAS_ITEM);
        }
        // æ ¡éªŒå…³è”数据
        if (checkPlanMachineryService.getCheckPlanMachineryCountByMachineryId(id) > 0) {
            throw exception(DV_MACHINERY_HAS_CHECK_PLAN);
@@ -279,10 +347,15 @@
                machinery.setMachineryTypeId(machineryType.getId());
                machinery.setWorkshopId(workshop.getId());
                machinery.setWorkstationId(workstationId);
                // åˆ›å»ºå¯¹åº” MDM ç‰©æ–™å¹¶åŒæ­¥ MES ç‰©æ–™ï¼ˆåŒä¸€äº‹åŠ¡ï¼‰
                machinery.setItemId(createEquipmentMaterial(machinery.getCode(), machinery.getName(),
                        machinery.getSpecification(), machinery.getRemark()));
                machineryMapper.insert(machinery);
                // è‡ªåŠ¨ç”Ÿæˆæ¡ç 
                barcodeService.autoGenerateBarcode(BarcodeBizTypeEnum.MACHINERY.getValue(),
                        machinery.getId(), machinery.getCode(), machinery.getName());
                // è‡ªåŠ¨å…¥è®¾å¤‡ä»“ï¼ˆåº“å­˜ + æµæ°´ï¼‰
                createEquipmentStock(machinery.getItemId(), machinery.getId(), machinery.getCode());
                respVO.getCreateCodes().add(importItem.getCode());
            } else if (updateSupport) {
                // 3.5.2 æ›´æ–°
@@ -291,6 +364,8 @@
                updateObj.setMachineryTypeId(machineryType.getId());
                updateObj.setWorkshopId(workshop.getId());
                updateObj.setWorkstationId(workstationId);
                // ç‰©æ–™å…³è”由创建时自动生成,更新不允许清空
                updateObj.setItemId(existMachinery.getItemId());
                machineryMapper.updateById(updateObj);
                respVO.getUpdateCodes().add(importItem.getCode());
            } else {
@@ -301,4 +376,77 @@
        return respVO;
    }
    /**
     * åˆ›å»ºè®¾å¤‡å¯¹åº”物料:先在 MDM å»ºä¸»æ•°æ®ç‰©æ–™ï¼Œå†æ˜¾å¼åŒæ­¥ MES ç‰©æ–™ï¼ˆä¸¤å¥—主数据 id å¯¹é½ï¼‰ã€‚
     *
     * @return MDM ç‰©æ–™ç¼–号(= MES ç‰©æ–™ç¼–号)
     */
    private Long createEquipmentMaterial(String code, String name, String specification, String remark) {
        // 1. è§£æžâ€œè®¾å¤‡â€ç±»åž‹ï¼ˆMES ä¾§ï¼‰ä¸Žâ€œå°â€å•位(MDM ä¾§ + MES ä¾§ï¼‰ï¼Œç¼ºå¤±åˆ™æç¤ºä¸»æ•°æ®æœªåˆå§‹åŒ–
        MesMdItemTypeDO itemType = mesMdItemTypeService.getItemTypeByCode(EQUIPMENT_ITEM_TYPE_CODE);
        if (itemType == null) {
            throw exception(DV_MACHINERY_EQUIP_MASTER_MISSING);
        }
        if (mesMdUnitMeasureService.getUnitMeasureByCode(EQUIPMENT_UNIT_CODE) == null) {
            throw exception(DV_MACHINERY_EQUIP_MASTER_MISSING);
        }
        CommonResult<MdmUnitMeasureRespDTO> unitResult = mdmUnitMeasureApi.getUnitMeasureByCode(EQUIPMENT_UNIT_CODE);
        MdmUnitMeasureRespDTO mdmUnit = unitResult.isSuccess() ? unitResult.getData() : null;
        if (mdmUnit == null) {
            throw exception(DV_MACHINERY_EQUIP_MASTER_MISSING);
        }
        // 2. åˆ›å»º MDM ç‰©æ–™ï¼›åˆ†ç±»ä½¿ç”¨â€œè®¾å¤‡â€åˆ†ç±»ï¼ˆå…¶ id ä¸Ž MES ç±»åž‹ id å¯¹é½ï¼Œç”±åˆå§‹åŒ–脚本保证)
        MdmItemCreateReqDTO createReqDTO = new MdmItemCreateReqDTO();
        createReqDTO.setCode(code);
        createReqDTO.setName(name);
        createReqDTO.setSpecification(specification);
        createReqDTO.setRemark(remark);
        createReqDTO.setCategoryId(itemType.getId());
        createReqDTO.setUnitMeasureId(mdmUnit.getId());
        createReqDTO.setItemType(MdmItemTypeEnum.EQUIPMENT.getType());
        createReqDTO.setStatus(0);
        createReqDTO.setIsBatchManaged(false);
        Long mdmItemId = mdmItemApi.createItem(createReqDTO).getCheckedData();
        // 3. æ˜¾å¼åŒæ­¥ MES ç‰©æ–™ï¼ˆMdmItemCreateReqDTO æ—  syncMes å¼€å…³ï¼Œä¸ä¼šè‡ªåŠ¨åŒæ­¥ï¼‰
        MesMdItemSyncReqDTO syncReqDTO = new MesMdItemSyncReqDTO();
        syncReqDTO.setCode(code);
        syncReqDTO.setName(name);
        syncReqDTO.setSpecification(specification);
        syncReqDTO.setRemark(remark);
        syncReqDTO.setUnitCode(EQUIPMENT_UNIT_CODE);
        syncReqDTO.setItemTypeCode(EQUIPMENT_ITEM_TYPE_CODE);
        syncReqDTO.setBatchFlag(false);
        syncReqDTO.setSafeStockFlag(false);
        syncReqDTO.setStatus(0);
        syncReqDTO.setMdmItemId(mdmItemId);
        mesMdItemApi.createItem(syncReqDTO);
        return mdmItemId;
    }
    /**
     * è®¾å¤‡è‡ªåŠ¨å…¥è®¾å¤‡ä»“ï¼šåˆ›å»ºä¸€æ¡å…¥åº“æ•°é‡ä¸º 1 çš„库存事务,仓库/库区/库位取系统内置设备仓。
     * åº“存行、数量与流水统一经 {@link MesWmTransactionService},禁止直接写库存表。
     */
    private void createEquipmentStock(Long itemId, Long machineryId, String machineryCode) {
        MesWmWarehouseDO warehouse = wmWarehouseService.getWarehouseByCode(MesWmWarehouseDO.EQUIPMENT_WAREHOUSE);
        MesWmWarehouseLocationDO location = wmLocationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.EQUIPMENT_LOCATION);
        MesWmWarehouseAreaDO area = wmAreaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.EQUIPMENT_AREA);
        if (warehouse == null || location == null || area == null) {
            throw exception(DV_MACHINERY_EQUIP_MASTER_MISSING);
        }
        wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.IN.getType())
                .setItemId(itemId)
                .setQuantity(BigDecimal.ONE)
                .setWarehouseId(warehouse.getId())
                .setLocationId(location.getId())
                .setAreaId(area.getId())
                .setBizType(MesBizTypeConstants.DV_MACHINERY)
                .setBizId(machineryId)
                .setBizCode(machineryCode)
                .setBizLineId(machineryId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/barcode/MesWmBarcodeConfigService.java
@@ -6,6 +6,8 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.barcode.MesWmBarcodeConfigDO;
import jakarta.validation.Valid;
import java.util.List;
/**
 * MES æ¡ç é…ç½® Service æŽ¥å£
 *
@@ -67,4 +69,11 @@
     */
    MesWmBarcodeConfigDO validateBarcodeConfigByBizType(Integer bizType);
    /**
     * èŽ·å–å¯ç”¨çŠ¶æ€çš„æ¡ç é…ç½®åˆ—è¡¨ï¼ˆç”¨äºŽæ‰«ç è§£æžæ—¶åæŸ¥æ¨¡æ¿ï¼‰
     *
     * @return å¯ç”¨çŠ¶æ€çš„æ¡ç é…ç½®åˆ—è¡¨
     */
    List<MesWmBarcodeConfigDO> getEnableBarcodeConfigList();
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/barcode/MesWmBarcodeConfigServiceImpl.java
@@ -12,6 +12,8 @@
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -109,4 +111,9 @@
        return config;
    }
    @Override
    public List<MesWmBarcodeConfigDO> getEnableBarcodeConfigList() {
        return barcodeConfigMapper.selectEnableList();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/barcode/MesWmBarcodeService.java
@@ -7,6 +7,7 @@
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.Map;
/**
 * MES æ¡ç æ¸…单 Service æŽ¥å£
@@ -85,9 +86,32 @@
     *
     * @param bizType ä¸šåŠ¡ç±»åž‹
     * @param bizCode ä¸šåŠ¡ç¼–ç 
     * @param bizName ä¸šåŠ¡åç§°
     * @return ç”Ÿæˆçš„æ¡ç å†…容
     */
    String generateBarcodeContent(Integer bizType, String bizCode);
    String generateBarcodeContent(Integer bizType, String bizCode, String bizName);
    /**
     * æ¸²æŸ“条码/二维码图片(服务端 zxing æˆå›¾ï¼Œå³æ—¶é¢„览用 Base64)
     *
     * @param content æ¡ç å†…容
     * @param format æ¡ç æ ¼å¼ {@link cn.iocoder.yudao.module.mes.enums.wm.BarcodeFormatEnum}
     * @param width å›¾ç‰‡å®½åº¦
     * @param height å›¾ç‰‡é«˜åº¦
     * @return PNG å›¾ç‰‡ Base64 data-URL
     */
    String renderBarcodeImage(String content, Integer format, Integer width, Integer height);
    /**
     * è§£æžæ¡ç å†…容(服务端扫码解析)
     *
     * <p>优先按条码内容精确命中已有条码记录,返回业务对象信息;
     * å¦åˆ™éåŽ†å¯ç”¨çŠ¶æ€çš„æ¡ç é…ç½®ï¼ŒæŒ‰ contentFormat æ¨¡æ¿åè§£å‡ºå„占位符字段。
     *
     * @param content æ‰«æåˆ°çš„æ¡ç å†…容
     * @return è§£æžç»“果(matched/bizType/bizCode ç­‰å­—段)
     */
    Map<String, Object> parseBarcodeContent(String content);
    /**
     * æ ¹æ®æ¡ç é…ç½®ç¼–号获取相关联的条码数量
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/barcode/MesWmBarcodeServiceImpl.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.mes.service.wm.barcode;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
@@ -11,11 +12,26 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.barcode.MesWmBarcodeConfigDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.barcode.MesWmBarcodeDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.barcode.MesWmBarcodeMapper;
import cn.iocoder.yudao.module.mes.enums.wm.BarcodeFormatEnum;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -27,7 +43,13 @@
 */
@Service
@Validated
@Slf4j
public class MesWmBarcodeServiceImpl implements MesWmBarcodeService {
    /**
     * å†…容格式模板中的占位符识别正则
     */
    private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{[A-Za-z0-9_]+\\}");
    @Resource
    private MesWmBarcodeMapper barcodeMapper;
@@ -43,7 +65,8 @@
        // 2. ç”Ÿæˆæ¡ç å†…容,并校验唯一性
        String content = generateAndValidateContent(createReqVO.getId(),
                createReqVO.getContent(), config.getContentFormat(), createReqVO.getBizCode());
                createReqVO.getContent(), config.getContentFormat(), createReqVO.getBizCode(),
                createReqVO.getBizName());
        // 3. ä¿å­˜æ¡ç è®°å½•
        MesWmBarcodeDO barcode = BeanUtils.toBean(createReqVO, MesWmBarcodeDO.class).setContent(content)
@@ -53,7 +76,7 @@
    }
    @Override
    public String generateBarcodeContent(Integer bizType, String bizCode) {
    public String generateBarcodeContent(Integer bizType, String bizCode, String bizName) {
        // 1.1 æ ¡éªŒå‚æ•°
        if (bizType == null) {
            throw exception(BARCODE_BIZ_TYPE_NOT_EXISTS);
@@ -65,7 +88,7 @@
        MesWmBarcodeConfigDO config = barcodeConfigService.validateBarcodeConfigByBizType(bizType);
        // 2. ç”Ÿæˆæ¡ç å†…容
        return generateBarcodeContent(config.getContentFormat(), bizCode);
        return generateBarcodeContent(config.getContentFormat(), bizCode, bizName);
    }
    @Override
@@ -78,7 +101,8 @@
        // 2. ç”Ÿæˆæ¡ç å†…容,并校验唯一性
        String content = generateAndValidateContent(updateReqVO.getId(),
                updateReqVO.getContent(), config.getContentFormat(), updateReqVO.getBizCode());
                updateReqVO.getContent(), config.getContentFormat(), updateReqVO.getBizCode(),
                updateReqVO.getBizName());
        // 3. æ›´æ–°ï¼ˆåˆ·æ–° configId å’Œ format,确保与最新配置一致)
        MesWmBarcodeDO updateObj = BeanUtils.toBean(updateReqVO, MesWmBarcodeDO.class)
@@ -131,10 +155,10 @@
     * @param bizCode ä¸šåŠ¡ç¼–ç 
     * @return æœ€ç»ˆæ¡ç å†…容
     */
    private String generateAndValidateContent(Long id, String content, String contentFormat, String bizCode) {
    private String generateAndValidateContent(Long id, String content, String contentFormat, String bizCode, String bizName) {
        // 1. å¦‚果前端未传递内容,则自动生成
        if (StrUtil.isBlank(content)) {
            content = generateBarcodeContent(contentFormat, bizCode);
            content = generateBarcodeContent(contentFormat, bizCode, bizName);
        }
        // 2. æ ¡éªŒæ¡ç å†…容唯一性(排除自身)
        if (StrUtil.isNotBlank(content)) {
@@ -151,13 +175,144 @@
     *
     * @param contentFormat å†…容格式模板
     * @param bizCode ä¸šåŠ¡ç¼–ç 
     * @param bizName ä¸šåŠ¡åç§°
     * @return æ¡ç å†…容
     */
    private String generateBarcodeContent(String contentFormat, String bizCode) {
    private String generateBarcodeContent(String contentFormat, String bizCode, String bizName) {
        if (StrUtil.isBlank(contentFormat)) {
            return bizCode;
        }
        return contentFormat.replace(MesWmBarcodeConfigDO.PLACEHOLDER_BUSINESS_CODE, bizCode);
        // 1. æ›¿æ¢å·²æ”¯æŒçš„占位符
        String content = contentFormat
                .replace(MesWmBarcodeConfigDO.PLACEHOLDER_BUSINESS_CODE, StrUtil.nullToEmpty(bizCode))
                .replace(MesWmBarcodeConfigDO.PLACEHOLDER_BUSINESS_NAME, StrUtil.nullToEmpty(bizName));
        // 2. æ¸…空模板中其它未填充的未知占位符,避免 {XXX} æ®‹ç•™å½±å“æ‰«ç 
        return PLACEHOLDER_PATTERN.matcher(content).replaceAll("");
    }
    @Override
    public String renderBarcodeImage(String content, Integer format, Integer width, Integer height) {
        if (StrUtil.isBlank(content)) {
            throw exception(BARCODE_CONTENT_EMPTY);
        }
        int w = width != null && width > 0 ? width : 200;
        int h = height != null && height > 0 ? height : 200;
        try {
            BitMatrix bitMatrix = new MultiFormatWriter().encode(content, toZxingFormat(format), w, h);
            BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ImageIO.write(image, "png", out);
            return "data:image/png;base64," + Base64.encode(out.toByteArray());
        } catch (Exception e) {
            log.error("[renderBarcodeImage] æ¡ç æ¸²æŸ“失败,content={}, format={}", content, format, e);
            throw new RuntimeException("条码渲染失败");
        }
    }
    /**
     * å°†é¡¹ç›®æ¡ç æ ¼å¼æžšä¸¾å€¼æ˜ å°„为 zxing æ¡ç æ ¼å¼
     *
     * @param format é¡¹ç›®æ¡ç æ ¼å¼ {@link BarcodeFormatEnum}
     * @return zxing æ¡ç æ ¼å¼
     */
    private BarcodeFormat toZxingFormat(Integer format) {
        if (format == null) {
            return BarcodeFormat.CODE_128;
        }
        switch (format) {
            case 1: // BarcodeFormatEnum.QR_CODE
                return BarcodeFormat.QR_CODE;
            case 2: // BarcodeFormatEnum.EAN13
                return BarcodeFormat.EAN_13;
            case 3: // BarcodeFormatEnum.CODE39
                return BarcodeFormat.CODE_39;
            case 4: // BarcodeFormatEnum.UPC_A
                return BarcodeFormat.UPC_A;
            default:
                throw exception(BARCODE_FORMAT_INVALID);
        }
    }
    @Override
    public Map<String, Object> parseBarcodeContent(String content) {
        if (StrUtil.isBlank(content)) {
            throw exception(BARCODE_CONTENT_EMPTY);
        }
        // 1. ä¼˜å…ˆæŒ‰æ¡ç å†…容精确命中已有条码记录
        MesWmBarcodeDO barcode = barcodeMapper.selectByContent(content);
        if (barcode != null) {
            Map<String, Object> result = new LinkedHashMap<>();
            result.put("matched", true);
            result.put("barcodeId", barcode.getId());
            result.put("configId", barcode.getConfigId());
            result.put("format", barcode.getFormat());
            result.put("bizType", barcode.getBizType());
            result.put("bizId", barcode.getBizId());
            result.put("bizCode", barcode.getBizCode());
            result.put("bizName", barcode.getBizName());
            result.put("content", barcode.getContent());
            return result;
        }
        // 2. æœªå‘½ä¸­æ—¶ï¼ŒéåŽ†å¯ç”¨çŠ¶æ€çš„é…ç½®æŒ‰å†…å®¹æ ¼å¼æ¨¡æ¿åè§£
        List<MesWmBarcodeConfigDO> configs = barcodeConfigService.getEnableBarcodeConfigList();
        for (MesWmBarcodeConfigDO config : configs) {
            Map<String, Object> fields = matchByTemplate(config.getContentFormat(), content);
            if (fields != null) {
                Map<String, Object> result = new LinkedHashMap<>();
                result.put("matched", false);
                result.put("configId", config.getId());
                result.put("format", config.getFormat());
                result.put("bizType", config.getBizType());
                result.put("bizCode", fields.get(MesWmBarcodeConfigDO.PLACEHOLDER_BUSINESS_CODE));
                result.put("bizName", fields.get(MesWmBarcodeConfigDO.PLACEHOLDER_BUSINESS_NAME));
                result.put("fields", fields);
                return result;
            }
        }
        // 3. å‡æœªå‘½ä¸­
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("matched", false);
        result.put("content", content);
        return result;
    }
    /**
     * æŒ‰å†…容格式模板反解条码内容
     *
     * @param contentFormat å†…容格式模板
     * @param content å¾…解析的条码内容
     * @return å ä½ç¬¦ -> å®žé™…值的映射;不匹配返回 null
     */
    private Map<String, Object> matchByTemplate(String contentFormat, String content) {
        if (StrUtil.isBlank(contentFormat) || !contentFormat.contains("{")) {
            return null;
        }
        // 1. å°†æ¨¡æ¿è½¬ä¸ºæ­£åˆ™ï¼šå­—面量转义,占位符转为捕获组
        List<String> placeholders = new ArrayList<>();
        StringBuilder regexSb = new StringBuilder();
        Matcher matcher = PLACEHOLDER_PATTERN.matcher(contentFormat);
        int lastEnd = 0;
        while (matcher.find()) {
            regexSb.append(Pattern.quote(contentFormat.substring(lastEnd, matcher.start())));
            placeholders.add(matcher.group());
            regexSb.append("(.+?)");
            lastEnd = matcher.end();
        }
        if (placeholders.isEmpty()) {
            return null;
        }
        regexSb.append(Pattern.quote(contentFormat.substring(lastEnd)));
        // 2. å…¨é‡åŒ¹é…å¹¶æå–各占位符值
        Matcher contentMatcher = Pattern.compile(regexSb.toString()).matcher(content);
        if (!contentMatcher.matches()) {
            return null;
        }
        Map<String, Object> fields = new LinkedHashMap<>();
        for (int i = 0; i < placeholders.size(); i++) {
            fields.put(placeholders.get(i), contentMatcher.group(i + 1));
        }
        return fields;
    }
    @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/equipmentreceipt/MesWmEquipmentReceiptLineService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.mes.service.wm.equipmentreceipt;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.line.MesWmEquipmentReceiptLinePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.line.MesWmEquipmentReceiptLineSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.equipmentreceipt.MesWmEquipmentReceiptLineDO;
import jakarta.validation.Valid;
import java.util.List;
/**
 * MES è®¾å¤‡å…¥åº“单行 Service æŽ¥å£
 */
public interface MesWmEquipmentReceiptLineService {
    /**
     * åˆ›å»ºè®¾å¤‡å…¥åº“单行
     */
    Long createEquipmentReceiptLine(@Valid MesWmEquipmentReceiptLineSaveReqVO createReqVO);
    /**
     * æ›´æ–°è®¾å¤‡å…¥åº“单行
     */
    void updateEquipmentReceiptLine(@Valid MesWmEquipmentReceiptLineSaveReqVO updateReqVO);
    /**
     * åˆ é™¤è®¾å¤‡å…¥åº“单行
     */
    void deleteEquipmentReceiptLine(Long id);
    /**
     * æŒ‰å…¥åº“单编号删除行(级联删除)
     */
    void deleteByReceiptId(Long receiptId);
    /**
     * èŽ·å¾—è®¾å¤‡å…¥åº“å•è¡Œ
     */
    MesWmEquipmentReceiptLineDO getEquipmentReceiptLine(Long id);
    /**
     * æŒ‰å…¥åº“单编号获得行列表
     */
    List<MesWmEquipmentReceiptLineDO> getEquipmentReceiptLineListByReceiptId(Long receiptId);
    /**
     * èŽ·å¾—è®¾å¤‡å…¥åº“å•è¡Œåˆ†é¡µ
     */
    PageResult<MesWmEquipmentReceiptLineDO> getEquipmentReceiptLinePage(MesWmEquipmentReceiptLinePageReqVO pageReqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/equipmentreceipt/MesWmEquipmentReceiptLineServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,119 @@
package cn.iocoder.yudao.module.mes.service.wm.equipmentreceipt;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.line.MesWmEquipmentReceiptLinePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.line.MesWmEquipmentReceiptLineSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.equipmentreceipt.MesWmEquipmentReceiptLineDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.equipmentreceipt.MesWmEquipmentReceiptLineMapper;
import cn.iocoder.yudao.module.mes.service.dv.machinery.MesDvMachineryService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
 * MES è®¾å¤‡å…¥åº“单行 Service å®žçŽ°ç±»
 */
@Service
@Validated
public class MesWmEquipmentReceiptLineServiceImpl implements MesWmEquipmentReceiptLineService {
    @Resource
    private MesWmEquipmentReceiptLineMapper equipmentReceiptLineMapper;
    @Resource
    private MesDvMachineryService machineryService;
    @Resource
    @Lazy
    private MesWmEquipmentReceiptService equipmentReceiptService;
    @Override
    public Long createEquipmentReceiptLine(MesWmEquipmentReceiptLineSaveReqVO createReqVO) {
        // æ ¡éªŒå…¥åº“单可编辑
        equipmentReceiptService.validateEquipmentReceiptEditable(createReqVO.getReceiptId());
        // æ ¡éªŒè®¾å¤‡å­˜åœ¨ä¸”已关联物料
        validateMachineryHasItem(createReqVO.getMachineryId());
        // æ ¡éªŒæ•°é‡
        validateQuantity(createReqVO.getQuantity());
        MesWmEquipmentReceiptLineDO line = BeanUtils.toBean(createReqVO, MesWmEquipmentReceiptLineDO.class);
        equipmentReceiptLineMapper.insert(line);
        return line.getId();
    }
    @Override
    public void updateEquipmentReceiptLine(MesWmEquipmentReceiptLineSaveReqVO updateReqVO) {
        // æ ¡éªŒè¡Œå­˜åœ¨
        validateEquipmentReceiptLineExists(updateReqVO.getId());
        // æ ¡éªŒå…¥åº“单可编辑
        equipmentReceiptService.validateEquipmentReceiptEditable(updateReqVO.getReceiptId());
        // æ ¡éªŒè®¾å¤‡å­˜åœ¨ä¸”已关联物料
        validateMachineryHasItem(updateReqVO.getMachineryId());
        // æ ¡éªŒæ•°é‡
        validateQuantity(updateReqVO.getQuantity());
        MesWmEquipmentReceiptLineDO updateObj = BeanUtils.toBean(updateReqVO, MesWmEquipmentReceiptLineDO.class);
        equipmentReceiptLineMapper.updateById(updateObj);
    }
    @Override
    public void deleteEquipmentReceiptLine(Long id) {
        // æ ¡éªŒè¡Œå­˜åœ¨
        MesWmEquipmentReceiptLineDO line = validateEquipmentReceiptLineExists(id);
        // æ ¡éªŒå…¥åº“单可编辑
        equipmentReceiptService.validateEquipmentReceiptEditable(line.getReceiptId());
        equipmentReceiptLineMapper.deleteById(id);
    }
    @Override
    public void deleteByReceiptId(Long receiptId) {
        equipmentReceiptLineMapper.delete(MesWmEquipmentReceiptLineDO::getReceiptId, receiptId);
    }
    @Override
    public MesWmEquipmentReceiptLineDO getEquipmentReceiptLine(Long id) {
        return equipmentReceiptLineMapper.selectById(id);
    }
    @Override
    public List<MesWmEquipmentReceiptLineDO> getEquipmentReceiptLineListByReceiptId(Long receiptId) {
        return equipmentReceiptLineMapper.selectListByReceiptId(receiptId);
    }
    @Override
    public PageResult<MesWmEquipmentReceiptLineDO> getEquipmentReceiptLinePage(MesWmEquipmentReceiptLinePageReqVO pageReqVO) {
        return equipmentReceiptLineMapper.selectPage(pageReqVO);
    }
    private MesWmEquipmentReceiptLineDO validateEquipmentReceiptLineExists(Long id) {
        MesWmEquipmentReceiptLineDO line = equipmentReceiptLineMapper.selectById(id);
        if (line == null) {
            throw exception(WM_EQUIP_RECEIPT_LINE_NOT_EXISTS);
        }
        return line;
    }
    private void validateMachineryHasItem(Long machineryId) {
        machineryService.validateMachineryExists(machineryId);
        MesDvMachineryDO machinery = machineryService.getMachinery(machineryId);
        if (machinery == null || machinery.getItemId() == null) {
            throw exception(WM_EQUIP_RECEIPT_MACHINERY_NOT_VALID);
        }
    }
    private void validateQuantity(BigDecimal quantity) {
        if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(WM_EQUIP_RECEIPT_QUANTITY_INVALID);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/equipmentreceipt/MesWmEquipmentReceiptService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.mes.service.wm.equipmentreceipt;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.MesWmEquipmentReceiptPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.MesWmEquipmentReceiptSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.equipmentreceipt.MesWmEquipmentReceiptDO;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
public interface MesWmEquipmentReceiptService {
    String APPROVE_CATEGORY_CODE = "equipment_receipt_approve";
    Long createEquipmentReceipt(@Valid MesWmEquipmentReceiptSaveReqVO createReqVO);
    void updateEquipmentReceipt(@Valid MesWmEquipmentReceiptSaveReqVO updateReqVO);
    void deleteEquipmentReceipt(Long id);
    MesWmEquipmentReceiptDO getEquipmentReceipt(Long id);
    PageResult<MesWmEquipmentReceiptDO> getEquipmentReceiptPage(MesWmEquipmentReceiptPageReqVO pageReqVO);
    void submitEquipmentReceiptApproval(Long id, String processDefinitionKey);
    void updateEquipmentReceiptAuditStatus(Long id, Integer bpmResult);
    List<Map<String, Object>> getEquipmentReceiptApprovalProcessDefinitionList();
    void finishEquipmentReceipt(Long id);
    void cancelEquipmentReceipt(Long id);
    MesWmEquipmentReceiptDO validateEquipmentReceiptExists(Long id);
    void validateEquipmentReceiptEditable(Long id);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/equipmentreceipt/MesWmEquipmentReceiptServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,290 @@
package cn.iocoder.yudao.module.mes.service.wm.equipmentreceipt;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.MesWmEquipmentReceiptPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.equipmentreceipt.vo.MesWmEquipmentReceiptSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.equipmentreceipt.MesWmEquipmentReceiptDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.equipmentreceipt.MesWmEquipmentReceiptLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.equipmentreceipt.MesWmEquipmentReceiptMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmEquipmentReceiptStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
 * MES è®¾å¤‡å…¥åº“单 Service å®žçŽ°ç±»
 */
@Service
@Validated
@Slf4j
public class MesWmEquipmentReceiptServiceImpl implements MesWmEquipmentReceiptService {
    @Resource
    private MesWmEquipmentReceiptMapper equipmentReceiptMapper;
    @Resource
    @Lazy
    private MesWmEquipmentReceiptLineService equipmentReceiptLineService;
    @Resource
    private MesWmTransactionService wmTransactionService;
    @Resource
    private MesWmWarehouseService wmWarehouseService;
    @Resource
    private MesWmWarehouseLocationService wmLocationService;
    @Resource
    private MesWmWarehouseAreaService wmAreaService;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
    public Long createEquipmentReceipt(MesWmEquipmentReceiptSaveReqVO createReqVO) {
        // æ ¡éªŒç¼–码唯一
        validateCodeUnique(null, createReqVO.getCode());
        MesWmEquipmentReceiptDO receipt = BeanUtils.toBean(createReqVO, MesWmEquipmentReceiptDO.class);
        receipt.setStatus(MesWmEquipmentReceiptStatusEnum.PREPARE.getStatus());
        equipmentReceiptMapper.insert(receipt);
        return receipt.getId();
    }
    @Override
    public void updateEquipmentReceipt(MesWmEquipmentReceiptSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        validateEquipmentReceiptExistsAndPrepare(updateReqVO.getId());
        // æ ¡éªŒç¼–码唯一
        validateCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
        MesWmEquipmentReceiptDO updateObj = BeanUtils.toBean(updateReqVO, MesWmEquipmentReceiptDO.class);
        equipmentReceiptMapper.updateById(updateObj);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteEquipmentReceipt(Long id) {
        // æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        validateEquipmentReceiptExistsAndPrepare(id);
        // çº§è”删除行
        equipmentReceiptLineService.deleteByReceiptId(id);
        // åˆ é™¤ä¸»è¡¨
        equipmentReceiptMapper.deleteById(id);
    }
    @Override
    public MesWmEquipmentReceiptDO getEquipmentReceipt(Long id) {
        return equipmentReceiptMapper.selectById(id);
    }
    @Override
    public PageResult<MesWmEquipmentReceiptDO> getEquipmentReceiptPage(MesWmEquipmentReceiptPageReqVO pageReqVO) {
        return equipmentReceiptMapper.selectPage(pageReqVO);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitEquipmentReceiptApproval(Long id, String processDefinitionKey) {
        // æ ¡éªŒå­˜åœ¨ + è‰ç¨¿çŠ¶æ€
        validateEquipmentReceiptExistsAndPrepare(id);
        // æ ¡éªŒè‡³å°‘有一条行
        List<MesWmEquipmentReceiptLineDO> lines = equipmentReceiptLineService.getEquipmentReceiptLineListByReceiptId(id);
        if (CollUtil.isEmpty(lines)) {
            throw exception(WM_EQUIP_RECEIPT_NO_LINE);
        }
        // åˆ›å»º BPM æµç¨‹å®žä¾‹
        String processInstanceId = processInstanceApi.createProcessInstance(getCurrentUserId(),
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
        // æäº¤å®¡æ‰¹ï¼ˆè‰ç¨¿ â†’ å®¡æ‰¹ä¸­ï¼‰
        equipmentReceiptMapper.updateById(new MesWmEquipmentReceiptDO()
                .setId(id)
                .setStatus(MesWmEquipmentReceiptStatusEnum.APPROVING.getStatus())
                .setProcessInstanceId(processInstanceId));
    }
    @Override
    public List<Map<String, Object>> getEquipmentReceiptApprovalProcessDefinitionList() {
        return getProcessDefinitionListByCategory(APPROVE_CATEGORY_CODE);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateEquipmentReceiptAuditStatus(Long id, Integer bpmResult) {
        // 1. æ ¡éªŒå­˜åœ¨
        MesWmEquipmentReceiptDO receipt = validateEquipmentReceiptExists(id);
        if (!MesWmEquipmentReceiptStatusEnum.APPROVING.getStatus().equals(receipt.getStatus())) {
            log.warn("[updateEquipmentReceiptAuditStatus] è®¾å¤‡å…¥åº“单({}) ä¸å¤„于审批中状态", id);
            return;
        }
        // 2. æ ¹æ®å®¡æ‰¹ç»“果更新状态
        Integer newStatus = convertBpmResultToStatus(bpmResult);
        if (newStatus != null) {
            equipmentReceiptMapper.updateById(new MesWmEquipmentReceiptDO().setId(id).setStatus(newStatus));
        }
    }
    private Integer convertBpmResultToStatus(Integer bpmResult) {
        if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
            return MesWmEquipmentReceiptStatusEnum.APPROVED.getStatus();
        }
        if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) {
            return MesWmEquipmentReceiptStatusEnum.PREPARE.getStatus();
        }
        if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
            return MesWmEquipmentReceiptStatusEnum.CANCELED.getStatus();
        }
        return null;
    }
    private List<Map<String, Object>> getProcessDefinitionListByCategory(String categoryCode) {
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(categoryCode);
        if (CollUtil.isEmpty(definitionInfoList)) {
            return Collections.emptyList();
        }
        // è¿‡æ»¤æ¿€æ´»çŠ¶æ€ï¼Œä¿ç•™æœ€æ–°ç‰ˆæœ¬
        Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
        for (BpmProcessDefinitionInfoDO info : definitionInfoList) {
            ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId());
            if (pd == null || pd.isSuspended()) {
                continue;
            }
            ProcessDefinition existing = latestVersionMap.get(pd.getKey());
            if (existing == null || pd.getVersion() > existing.getVersion()) {
                latestVersionMap.put(pd.getKey(), pd);
            }
        }
        List<Map<String, Object>> result = new ArrayList<>();
        for (ProcessDefinition pd : latestVersionMap.values()) {
            Map<String, Object> item = new HashMap<>();
            item.put("id", pd.getId());
            item.put("key", pd.getKey());
            item.put("name", pd.getName());
            result.add(item);
        }
        return result;
    }
    private Long getCurrentUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof Long) {
            return (Long) authentication.getPrincipal();
        }
        return 1L; // é»˜è®¤ç”¨æˆ·
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void finishEquipmentReceipt(Long id) {
        // æ ¡éªŒå­˜åœ¨
        MesWmEquipmentReceiptDO receipt = validateEquipmentReceiptExists(id);
        if (ObjUtil.notEqual(MesWmEquipmentReceiptStatusEnum.APPROVED.getStatus(), receipt.getStatus())) {
            throw exception(WM_EQUIP_RECEIPT_STATUS_NOT_APPROVED);
        }
        // 2. éåŽ†æ‰€æœ‰è¡Œï¼Œå›ºå®šè®¾å¤‡ä»“ä¸‰å…ƒç»„ï¼Œåˆ›å»º IN åº“存事务
        createEquipmentStockTransaction(receipt);
        // 3. æ‰§è¡Œå…¥åº“(已审批 â†’ å·²å®Œæˆï¼‰
        equipmentReceiptMapper.updateById(new MesWmEquipmentReceiptDO()
                .setId(id).setStatus(MesWmEquipmentReceiptStatusEnum.FINISHED.getStatus()));
    }
    private void createEquipmentStockTransaction(MesWmEquipmentReceiptDO receipt) {
        // å›ºå®šä½¿ç”¨è®¾å¤‡ä»“三元组
        MesWmWarehouseDO warehouse = wmWarehouseService.getWarehouseByCode(MesWmWarehouseDO.EQUIPMENT_WAREHOUSE);
        MesWmWarehouseLocationDO location = wmLocationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.EQUIPMENT_LOCATION);
        MesWmWarehouseAreaDO area = wmAreaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.EQUIPMENT_AREA);
        List<MesWmEquipmentReceiptLineDO> lines = equipmentReceiptLineService.getEquipmentReceiptLineListByReceiptId(receipt.getId());
        wmTransactionService.createTransactionList(convertList(lines, line -> new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.IN.getType())
                .setItemId(line.getItemId())
                .setQuantity(line.getQuantity() == null ? BigDecimal.ONE : line.getQuantity())
                .setWarehouseId(warehouse.getId()).setLocationId(location.getId()).setAreaId(area.getId())
                .setBizType(MesBizTypeConstants.DV_EQUIPMENT_RECEIPT).setBizId(receipt.getId())
                .setBizCode(receipt.getCode()).setBizLineId(line.getId())));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelEquipmentReceipt(Long id) {
        // æ ¡éªŒå­˜åœ¨
        MesWmEquipmentReceiptDO receipt = validateEquipmentReceiptExists(id);
        // å·²å®Œæˆå’Œå·²å–消状态不允许取消
        if (ObjUtil.equal(MesWmEquipmentReceiptStatusEnum.FINISHED.getStatus(), receipt.getStatus())
                || ObjUtil.equal(MesWmEquipmentReceiptStatusEnum.CANCELED.getStatus(), receipt.getStatus())) {
            throw exception(WM_EQUIP_RECEIPT_CANCEL_NOT_ALLOWED);
        }
        // å–消
        equipmentReceiptMapper.updateById(new MesWmEquipmentReceiptDO()
                .setId(id).setStatus(MesWmEquipmentReceiptStatusEnum.CANCELED.getStatus()));
    }
    @Override
    public MesWmEquipmentReceiptDO validateEquipmentReceiptExists(Long id) {
        MesWmEquipmentReceiptDO receipt = equipmentReceiptMapper.selectById(id);
        if (receipt == null) {
            throw exception(WM_EQUIP_RECEIPT_NOT_EXISTS);
        }
        return receipt;
    }
    private void validateCodeUnique(Long id, String code) {
        MesWmEquipmentReceiptDO receipt = equipmentReceiptMapper.selectByCode(code);
        if (receipt == null) {
            return;
        }
        if (id == null || ObjUtil.notEqual(id, receipt.getId())) {
            throw exception(WM_EQUIP_RECEIPT_CODE_DUPLICATE);
        }
    }
    private MesWmEquipmentReceiptDO validateEquipmentReceiptExistsAndPrepare(Long id) {
        MesWmEquipmentReceiptDO receipt = validateEquipmentReceiptExists(id);
        if (ObjUtil.notEqual(MesWmEquipmentReceiptStatusEnum.PREPARE.getStatus(), receipt.getStatus())) {
            throw exception(WM_EQUIP_RECEIPT_STATUS_NOT_PREPARE);
        }
        return receipt;
    }
    @Override
    public void validateEquipmentReceiptEditable(Long id) {
        validateEquipmentReceiptExistsAndPrepare(id);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/equipmentreceipt/listener/MesWmEquipmentReceiptStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.mes.service.wm.equipmentreceipt.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.mes.service.wm.equipmentreceipt.MesWmEquipmentReceiptService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
 * è®¾å¤‡å…¥åº“单 BPM å®¡æ‰¹çŠ¶æ€ç›‘å¬å™¨
 */
@Component
@Slf4j
public class MesWmEquipmentReceiptStatusListener
        extends cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener {
    @Resource
    private MesWmEquipmentReceiptService equipmentReceiptService;
    @Override
    protected String getProcessDefinitionKey() {
        return MesWmEquipmentReceiptService.APPROVE_CATEGORY_CODE;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Long id;
        try {
            id = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            log.warn("[onEvent] businessKey({}) ä¸æ˜¯æœ‰æ•ˆçš„设备入库单编号", event.getBusinessKey());
            return;
        }
        log.info("[onEvent] æ”¶åˆ°BPM审批事件,equipmentReceiptId={}, status={}", id, event.getStatus());
        try {
            equipmentReceiptService.updateEquipmentReceiptAuditStatus(id, event.getStatus());
            log.info("[onEvent] è®¾å¤‡å…¥åº“单({}) å®¡æ‰¹çŠ¶æ€æ›´æ–°å®Œæˆ", id);
        } catch (Exception e) {
            log.error("[onEvent] æ›´æ–°è®¾å¤‡å…¥åº“单审批状态失败,equipmentReceiptId={}", id, e);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/sn/MesWmSnService.java
@@ -3,6 +3,10 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnGroupRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnDetailRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnInStockPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnInStockReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnOutStockReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.sn.MesWmSnDO;
@@ -45,4 +49,34 @@
     */
    void deleteSnByUuid(String uuid);
}
    /**
     * å…¥åº“登记:按 SN å»ºç«‹åº“存事务并入台(单件入库)
     *
     * @param reqVO å…¥åº“登记请求
     */
    void registerSnInStock(MesWmSnInStockReqVO reqVO);
    /**
     * å‡ºåº“核销:按 SN å»ºç«‹å‡ºåº“事务并核销(单件出库)
     *
     * @param reqVO å‡ºåº“核销请求
     */
    void outboundSn(MesWmSnOutStockReqVO reqVO);
    /**
     * èŽ·å¾— SN å•件明细(状态、在库位置、最近一笔库存事务)
     *
     * @param snCode SN ç 
     * @return SN å•件明细
     */
    MesWmSnDetailRespVO getSnDetail(String snCode);
    /**
     * æŒ‰çŠ¶æ€/物料/库位等维度分页查询 SN ç ï¼ˆåœ¨åº“核对)
     *
     * @param reqVO åˆ†é¡µæŸ¥è¯¢
     * @return SN ç åˆ†é¡µ
     */
    PageResult<MesWmSnDO> getSnInStockPage(MesWmSnInStockPageReqVO reqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/sn/MesWmSnServiceImpl.java
@@ -2,28 +2,48 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnDetailRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnGroupRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnInStockPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnInStockReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnOutStockReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.sn.vo.MesWmSnPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.sn.MesWmSnDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.sn.MesWmSnMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.transaction.MesWmTransactionMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.BarcodeBizTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmSnStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.wm.barcode.MesWmBarcodeService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
 * MES SN ç  Service å®žçŽ°ç±»
@@ -37,6 +57,8 @@
    @Resource
    private MesWmSnMapper snMapper;
    @Resource
    private MesWmTransactionMapper transactionMapper;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
@@ -46,6 +68,14 @@
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesWmBarcodeService barcodeService;
    @Resource
    private MesWmTransactionService transactionService;
    @Resource
    private MesWmWarehouseService warehouseService;
    @Resource
    private MesWmWarehouseLocationService locationService;
    @Resource
    private MesWmWarehouseAreaService areaService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -99,4 +129,100 @@
        snMapper.deleteByUuid(uuid);
    }
}
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void registerSnInStock(MesWmSnInStockReqVO reqVO) {
        // 1. æ ¡éªŒ SN å­˜åœ¨ä¸”物料匹配
        MesWmSnDO sn = validateSnExists(reqVO.getCode());
        validateSnItemMatch(sn, reqVO.getItemId());
        // 2. æ ¡éªŒç‰©æ–™å¯ç”¨åºåˆ—号管理
        MesMdmItemDO item = mdmItemService.validateItemExists(reqVO.getItemId());
        if (!Boolean.TRUE.equals(item.getIsSerialManaged())) {
            throw exception(WM_SN_ITEM_NOT_SERIAL);
        }
        // 3. çŠ¶æ€æ ¡éªŒï¼šå·²åœ¨åº“åˆ™æ‹’ç»é‡å¤ç™»è®°
        if (ObjUtil.equal(sn.getStatus(), MesWmSnStatusEnum.IN_STOCK.getStatus())) {
            throw exception(WM_SN_ALREADY_IN_STOCK);
        }
        // 4. æ ¡éªŒåº“存位置存在(冻结与混放规则由事务内部兜底)
        warehouseService.validateWarehouseExists(reqVO.getWarehouseId());
        locationService.validateWarehouseLocationExists(reqVO.getLocationId());
        areaService.validateWarehouseAreaExists(reqVO.getAreaId());
        // 5. åˆ›å»ºå•件入库事务(+1)
        transactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.IN.getType())
                .setBizType(MesBizTypeConstants.WM_SN)
                .setBizId(sn.getId()).setBizCode(sn.getCode()).setBizLineId(sn.getId())
                .setItemId(sn.getItemId()).setQuantity(BigDecimal.ONE)
                .setBatchId(sn.getBatchId()).setBatchCode(sn.getBatchCode())
                .setSnCode(sn.getCode())
                .setWarehouseId(reqVO.getWarehouseId()).setLocationId(reqVO.getLocationId()).setAreaId(reqVO.getAreaId()));
        // 6. æ›´æ–° SN:在库 + è®°å½•库位
        snMapper.updateById(new MesWmSnDO().setId(sn.getId())
                .setStatus(MesWmSnStatusEnum.IN_STOCK.getStatus())
                .setWarehouseId(reqVO.getWarehouseId()).setLocationId(reqVO.getLocationId()).setAreaId(reqVO.getAreaId())
                .setRemark(reqVO.getRemark()));
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void outboundSn(MesWmSnOutStockReqVO reqVO) {
        // 1. æ ¡éªŒ SN å­˜åœ¨ä¸”物料匹配
        MesWmSnDO sn = validateSnExists(reqVO.getCode());
        validateSnItemMatch(sn, reqVO.getItemId());
        // 2. çŠ¶æ€æ ¡éªŒï¼šå¿…é¡»åœ¨åº“ä¸”è®°å½•è¿‡åº“ä½
        if (!ObjUtil.equal(sn.getStatus(), MesWmSnStatusEnum.IN_STOCK.getStatus())
                || sn.getWarehouseId() == null || sn.getLocationId() == null || sn.getAreaId() == null) {
            throw exception(WM_SN_NOT_IN_STOCK);
        }
        // 3. åˆ›å»ºå•件出库事务(-1),按 SN åœ¨åº“位置精确定位库存记录
        transactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.OUT.getType())
                .setBizType(MesBizTypeConstants.WM_SN)
                .setBizId(sn.getId()).setBizCode(sn.getCode()).setBizLineId(sn.getId())
                .setItemId(sn.getItemId()).setQuantity(BigDecimal.ONE.negate())
                .setBatchId(sn.getBatchId()).setBatchCode(sn.getBatchCode())
                .setSnCode(sn.getCode())
                .setWarehouseId(sn.getWarehouseId()).setLocationId(sn.getLocationId()).setAreaId(sn.getAreaId()));
        // 4. æ›´æ–° SN:已出库 + æ¸…空在库位置
        snMapper.markOutStockAndClearLocation(sn.getId());
        snMapper.updateById(new MesWmSnDO().setId(sn.getId()).setRemark(reqVO.getRemark()));
    }
    @Override
    public MesWmSnDetailRespVO getSnDetail(String snCode) {
        MesWmSnDO sn = validateSnExists(snCode);
        MesWmSnDetailRespVO resp = BeanUtils.toBean(sn, MesWmSnDetailRespVO.class);
        MesWmTransactionDO transaction = transactionMapper.selectLatestBySnCode(snCode);
        if (transaction != null) {
            MesWmTransactionTypeEnum typeEnum = MesWmTransactionTypeEnum.valueOf(transaction.getType());
            resp.setLastTransactionType(transaction.getType());
            resp.setLastTransactionTypeName(typeEnum != null ? typeEnum.getName() : null);
            resp.setLastTransactionQuantity(transaction.getQuantity());
            resp.setLastTransactionBizType(transaction.getBizType());
            resp.setLastTransactionBizCode(transaction.getBizCode());
            resp.setLastTransactionTime(transaction.getTransactionTime());
        }
        return resp;
    }
    @Override
    public PageResult<MesWmSnDO> getSnInStockPage(MesWmSnInStockPageReqVO reqVO) {
        return snMapper.selectStatusPage(reqVO);
    }
    private MesWmSnDO validateSnExists(String code) {
        MesWmSnDO sn = snMapper.selectByCode(code);
        if (sn == null) {
            throw exception(WM_SN_NOT_EXISTS);
        }
        return sn;
    }
    private void validateSnItemMatch(MesWmSnDO sn, Long itemId) {
        if (ObjUtil.notEqual(sn.getItemId(), itemId)) {
            throw exception(WM_SN_ITEM_MISMATCH);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/plan/MesWmStockTakingPlanService.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.plan.MesWmStockTakingPlanDO;
import jakarta.validation.Valid;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -107,4 +108,21 @@
     */
    PageResult<MesWmStockTakingPlanDO> getStockTakingPlanPage(MesWmStockTakingPlanPageReqVO pageReqVO);
    /**
     * èŽ·å–å¯ç”¨ä¸”å¼€å¯å®šæœŸè‡ªåŠ¨ç”Ÿæˆçš„ç›˜ç‚¹æ–¹æ¡ˆåˆ—è¡¨
     *
     * <p>供后台定时任务扫描:按各方案配置的 cron å‘¨æœŸè‡ªåŠ¨åˆ›å»ºç›˜ç‚¹ä»»åŠ¡ã€‚
     *
     * @return ç›˜ç‚¹æ–¹æ¡ˆåˆ—表
     */
    List<MesWmStockTakingPlanDO> getEnableAutoGeneratePlanList();
    /**
     * æ›´æ–°ç›˜ç‚¹æ–¹æ¡ˆä¸Šæ¬¡è‡ªåŠ¨ç”Ÿæˆæ—¶é—´
     *
     * @param id ç›˜ç‚¹æ–¹æ¡ˆç¼–号
     * @param time ä¸Šæ¬¡è‡ªåŠ¨ç”Ÿæˆæ—¶é—´
     */
    void updateStockTakingPlanLastGenerateTime(Long id, LocalDateTime time);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/plan/MesWmStockTakingPlanServiceImpl.java
@@ -2,6 +2,7 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -12,10 +13,12 @@
import cn.iocoder.yudao.module.mes.dal.mysql.wm.stocktaking.plan.MesWmStockTakingPlanMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockTakingTypeEnum;
import jakarta.annotation.Resource;
import org.quartz.CronExpression;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -122,6 +125,16 @@
        return stockTakingPlanMapper.selectPage(pageReqVO);
    }
    @Override
    public List<MesWmStockTakingPlanDO> getEnableAutoGeneratePlanList() {
        return stockTakingPlanMapper.selectEnableAutoGenerateList();
    }
    @Override
    public void updateStockTakingPlanLastGenerateTime(Long id, LocalDateTime time) {
        stockTakingPlanMapper.updateById(new MesWmStockTakingPlanDO().setId(id).setLastGenerateTime(time));
    }
    private void validatePlanCodeUnique(Long id, String code) {
        MesWmStockTakingPlanDO plan = stockTakingPlanMapper.selectByCode(code);
        if (plan == null) {
@@ -164,6 +177,16 @@
                throw exception(WM_STOCK_TAKING_PLAN_DYNAMIC_TIME_INVALID);
            }
        }
        // æ ¡éªŒå®šæœŸè‡ªåŠ¨ç”Ÿæˆé…ç½®ï¼šå¼€å¯è‡ªåŠ¨ç”Ÿæˆæ—¶ cron å¿…填且合法
        if (Boolean.TRUE.equals(saveReqVO.getAutoGenerate())) {
            if (StrUtil.isBlank(saveReqVO.getGenerateCron())) {
                throw exception(WM_STOCK_TAKING_PLAN_AUTO_GENERATE_CRON_REQUIRED);
            }
            if (!CronExpression.isValidExpression(saveReqVO.getGenerateCron())) {
                throw exception(WM_STOCK_TAKING_PLAN_AUTO_GENERATE_CRON_INVALID);
            }
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskLineServiceImpl.java
@@ -21,6 +21,7 @@
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.stocktaking.plan.MesWmStockTakingPlanParamService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@@ -52,6 +53,10 @@
    private MesWmMaterialStockService materialStockService;
    @Resource
    private MesWmStockTakingTaskService stockTakingTaskService;
    // ä¸Ž ResultService ç›¸äº’引用(ResultServiceImpl å·²æ³¨å…¥ LineService),须用 @Lazy æ‰“破循环
    @Lazy
    @Resource
    private MesWmStockTakingTaskResultService stockTakingTaskResultService;
    @Resource
    private cn.iocoder.yudao.module.mdm.api.item.MdmItemApi mdmItemApi;
@@ -74,7 +79,8 @@
        // 3. æ‰¹é‡ç”Ÿæˆæ˜Žç»†è¡Œ
        List<MesWmStockTakingTaskLineDO> lines = convertList(stocks, stock -> MesWmStockTakingTaskLineDO.builder()
                .taskId(task.getId()).materialStockId(stock.getId()).itemId(stock.getItemId())
                .batchId(stock.getBatchId()).quantity(stock.getQuantity()).takingQuantity(BigDecimal.ZERO)
                .batchId(stock.getBatchId()).batchCode(stock.getBatchCode())
                .quantity(stock.getQuantity()).takingQuantity(BigDecimal.ZERO)
                .warehouseId(stock.getWarehouseId()).locationId(stock.getLocationId()).areaId(stock.getAreaId())
                .status(MesWmStockTakingTaskLineStatusEnum.LOSS.getStatus()).build());
        stockTakingTaskLineMapper.insertBatch(lines);
@@ -244,6 +250,8 @@
        if (CollUtil.isEmpty(list)) {
            return respVO;
        }
        // 0. æ ¡éªŒç›˜ç‚¹ä»»åŠ¡å¤„äºŽç›˜ç‚¹ä¸­çŠ¶æ€ï¼ˆä¸Žäººå·¥å½•å…¥ç›˜ç‚¹ç»“æžœå£å¾„ä¸€è‡´ï¼Œä»…ç›˜ç‚¹ä¸­å¯å¯¼å…¥ï¼‰
        stockTakingTaskService.validateStockTakingTaskExistsAndApproving(taskId);
        // 1. èŽ·å–ç›˜ç‚¹ä»»åŠ¡è¡Œ
        List<MesWmStockTakingTaskLineDO> lines = stockTakingTaskLineMapper.selectListByTaskId(taskId);
        if (CollUtil.isEmpty(lines)) {
@@ -283,6 +291,8 @@
                continue;
            }
            updateStockTakingTaskLineTakingQuantity(matched.getId(), row.getTakingQuantity());
            // åŒæ­¥å†™ç›˜ç‚¹ç»“果,保证差异核销来源(task_result)一致
            stockTakingTaskResultService.upsertResultForImport(matched, row.getTakingQuantity());
            respVO.setSuccessCount(respVO.getSuccessCount() + 1);
        }
        return respVO;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskResultService.java
@@ -3,8 +3,10 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo.result.MesWmStockTakingTaskResultPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.stocktaking.task.vo.result.MesWmStockTakingTaskResultSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.task.MesWmStockTakingTaskLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.task.MesWmStockTakingTaskResultDO;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -67,4 +69,15 @@
     */
    List<MesWmStockTakingTaskResultDO> getStockTakingTaskResultListByTaskId(Long taskId);
    /**
     * Excel å¯¼å…¥åœºæ™¯åŒæ­¥ç›˜ç‚¹ç»“æžœ
     *
     * <p>若盘点行已存在盘点结果则覆盖其实盘数量;否则按盘点行维度新建一条盘点结果,
     * ä½¿çº¯å¯¼å…¥çš„实盘数据也进入 {@code task_result},与人工/扫码录入口径一致,保证完成时差异核销有据可依。
     *
     * @param line           ç›˜ç‚¹ä»»åŠ¡è¡Œï¼ˆæ‰¿è½½è´¦é¢æ•°é‡ä¸Žåº“å­˜ç»´åº¦ï¼‰
     * @param takingQuantity å®žç›˜æ•°é‡
     */
    void upsertResultForImport(MesWmStockTakingTaskLineDO line, BigDecimal takingQuantity);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskResultServiceImpl.java
@@ -15,6 +15,7 @@
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_STOCK_TAKING_TASK_LINE_ALREADY_TAKEN;
@@ -132,6 +133,29 @@
        return stockTakingTaskResultMapper.selectListByTaskId(taskId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void upsertResultForImport(MesWmStockTakingTaskLineDO line, BigDecimal takingQuantity) {
        // 1. æŸ¥è¯¢è¯¥ä»»åŠ¡ä¸‹æ˜¯å¦å·²å­˜åœ¨è¯¥è¡Œçš„ç›˜ç‚¹ç»“æžœ
        MesWmStockTakingTaskResultDO existing = stockTakingTaskResultMapper.selectListByTaskId(line.getTaskId()).stream()
                .filter(result -> Objects.equals(result.getLineId(), line.getId()))
                .findFirst().orElse(null);
        if (existing != null) {
            // 2. å·²å­˜åœ¨ï¼šè¦†ç›–实盘数量(重复导入以最后一次为准)
            existing.setTakingQuantity(takingQuantity);
            stockTakingTaskResultMapper.updateById(existing);
            return;
        }
        // 3. ä¸å­˜åœ¨ï¼šæŒ‰ç›˜ç‚¹è¡Œç»´åº¦æ–°å»ºï¼Œè´¦é¢æ•°é‡å–行账面数量,保证核销差额口径一致
        MesWmStockTakingTaskResultDO result = MesWmStockTakingTaskResultDO.builder()
                .taskId(line.getTaskId()).lineId(line.getId()).materialStockId(line.getMaterialStockId())
                .itemId(line.getItemId()).batchId(line.getBatchId()).batchCode(line.getBatchCode())
                .warehouseId(line.getWarehouseId()).locationId(line.getLocationId()).areaId(line.getAreaId())
                .quantity(line.getQuantity()).takingQuantity(takingQuantity)
                .build();
        stockTakingTaskResultMapper.insert(result);
    }
    /**
     * æ ¡éªŒç›˜ç‚¹ç»“果是否存在
     *
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskService.java
@@ -6,12 +6,20 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.task.MesWmStockTakingTaskDO;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
/**
 * MES ç›˜ç‚¹ä»»åŠ¡ Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface MesWmStockTakingTaskService {
    /**
     * ç›˜ç‚¹ç»“果审批 BPM åˆ†ç±»ç¼–码
     */
    String APPROVE_CATEGORY_CODE = "stock_taking_result_approve";
    /**
     * åˆ›å»ºç›˜ç‚¹ä»»åŠ¡
@@ -36,18 +44,47 @@
    void deleteStockTakingTask(Long id);
    /**
     * æäº¤ç›˜ç‚¹ä»»åŠ¡
     * å¼€å§‹ç›˜ç‚¹ï¼ˆè‰ç¨¿ â†’ ç›˜ç‚¹ä¸­ï¼‰
     *
     * <p>仅草稿状态可执行:校验任务行非空后置为盘点中,若开启冻结则冻结任务行的库存。
     * ç›˜ç‚¹ä¸­å¯å½•入实盘数量、Excel å¯¼å…¥å®žç›˜ã€‚
     *
     * @param id ç›˜ç‚¹ä»»åŠ¡ç¼–å·
     */
    void submitStockTakingTask(Long id);
    /**
     * å®Œæˆç›˜ç‚¹ä»»åŠ¡
     * æäº¤ç›˜ç‚¹ç»“果审批(盘点中 â†’ ç›˜ç‚¹ä¸­ + åˆ›å»º BPM æµç¨‹å®žä¾‹ï¼‰
     *
     * @param id ç›˜ç‚¹ä»»åŠ¡ç¼–å·
     * <p>录入/导入完成后提交结果审批:校验盘点中存在且至少一条盘点结果、且结果审批未提交过,
     * åˆ›å»º BPM æµç¨‹å®žä¾‹å¹¶å›žå†™ {@code processInstanceId}。审批通过后才执行差异核销并置为已完成。
     *
     * @param id                  ç›˜ç‚¹ä»»åŠ¡ç¼–å·
     * @param processDefinitionKey BPM æµç¨‹å®šä¹‰ Key
     */
    void finishStockTakingTask(Long id);
    void submitStockTakingResultApproval(Long id, String processDefinitionKey);
    /**
     * æ›´æ–°ç›˜ç‚¹ä»»åŠ¡ç»“æžœå®¡æ‰¹çŠ¶æ€ï¼ˆBPM å®¡æ‰¹å›žè°ƒï¼‰
     *
     * <p>仅盘点中状态处理:
     * <ul>
     *     <li>审批通过:解冻库存,同事务自动核销盘盈/盘亏并置为已完成;</li>
     *     <li>审批驳回:清空流程实例编号,任务保持盘点中,可修改实盘后重新提交;</li>
     *     <li>审批取消:解冻库存并置为已取消。</li>
     * </ul>
     *
     * @param id        ç›˜ç‚¹ä»»åŠ¡ç¼–å·
     * @param bpmResult BPM å®¡æ‰¹ç»“果(通过/驳回/取消)
     */
    void updateStockTakingTaskResultAuditStatus(Long id, Integer bpmResult);
    /**
     * èŽ·å–ç›˜ç‚¹ç»“æžœå®¡æ‰¹å¯ç”¨çš„ BPM æµç¨‹å®šä¹‰åˆ—表
     *
     * @return æµç¨‹å®šä¹‰åˆ—表(id/key/name)
     */
    List<Map<String, Object>> getStockTakingTaskResultApprovalProcessDefinitionList();
    /**
     * å–消盘点任务
@@ -55,6 +92,17 @@
     * @param id ç›˜ç‚¹ä»»åŠ¡ç¼–å·
     */
    void cancelStockTakingTask(Long id);
    /**
     * ç”±ç›˜ç‚¹æ–¹æ¡ˆæŒ‰å‘¨æœŸè‡ªåŠ¨ç”Ÿæˆç›˜ç‚¹ä»»åŠ¡
     *
     * <p>供后台定时任务调用:基于已启用且开启自动生成的方案创建草稿盘点任务并生成明细行,
     * ä»»åŠ¡ç¼–ç è‡ªåŠ¨ç”Ÿæˆã€‚åˆ›å»ºæˆåŠŸåŽå›žå†™æ–¹æ¡ˆä¸Šæ¬¡è‡ªåŠ¨ç”Ÿæˆæ—¶é—´ï¼Œé¿å…åŒä¸€å‘¨æœŸé‡å¤ç”Ÿæˆã€‚
     *
     * @param planId ç›˜ç‚¹æ–¹æ¡ˆç¼–号
     * @return ç›˜ç‚¹ä»»åŠ¡ç¼–å·
     */
    Long createStockTakingTaskFromPlan(Long planId);
    /**
     * èŽ·å¾—ç›˜ç‚¹ä»»åŠ¡
@@ -95,16 +143,5 @@
     * @return ç›˜ç‚¹ä»»åŠ¡åˆ†é¡µç»“æžœ
     */
    PageResult<MesWmStockTakingTaskDO> getStockTakingTaskPage(MesWmStockTakingTaskPageReqVO pageReqVO);
    /**
     * å¤„理盘点差异,调整库存
     *
     * è¯¥æ–¹æ³•会比较盘点结果与库存台账的差异:
     * - ç›˜ç›ˆï¼šåº“存增加,创建入库事务
     * - ç›˜äºï¼šåº“存减少,创建出库事务
     *
     * @param id ç›˜ç‚¹ä»»åŠ¡ç¼–å·
     */
    void processStockTakingDifference(Long id);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/MesWmStockTakingTaskServiceImpl.java
@@ -9,26 +9,40 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.task.MesWmStockTakingTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.task.MesWmStockTakingTaskLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.task.MesWmStockTakingTaskResultDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stocktaking.plan.MesWmStockTakingPlanDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.stocktaking.task.MesWmStockTakingTaskMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockTakingTaskStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.stocktaking.plan.MesWmStockTakingPlanService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.List;
import java.time.LocalDateTime;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
@@ -38,10 +52,16 @@
 */
@Service
@Validated
@Slf4j
public class MesWmStockTakingTaskServiceImpl implements MesWmStockTakingTaskService {
    @Resource
    private MesWmStockTakingTaskMapper stockTakingTaskMapper;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Resource
    private MesWmMaterialStockService materialStockService;
@@ -56,6 +76,9 @@
    @Resource
    private MesWmTransactionService wmTransactionService;
    @Resource
    private MesMdAutoCodeRecordService mesMdAutoCodeRecordService;
    @Resource
    private AdminUserApi adminUserApi;
@@ -82,6 +105,38 @@
            stockTakingTaskLineService.generateStockTakingLines(task, true);
        }
        return task.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createStockTakingTaskFromPlan(Long planId) {
        // 1. æ ¡éªŒæ–¹æ¡ˆå­˜åœ¨ã€å·²å¯ç”¨ä¸”开启定期自动生成
        MesWmStockTakingPlanDO plan = stockTakingPlanService.validateStockTakingPlanEnabled(planId);
        if (!Boolean.TRUE.equals(plan.getAutoGenerate())) {
            throw exception(WM_STOCK_TAKING_PLAN_NOT_AUTO_GENERATE);
        }
        // 2. è‡ªåŠ¨ç”Ÿæˆä»»åŠ¡ç¼–ç ï¼ˆè§„åˆ™ï¼šç›˜ç‚¹ä»»åŠ¡ç¼–ç ï¼‰
        String code = mesMdAutoCodeRecordService.generateAutoCode(
                MesMdAutoCodeRuleCodeEnum.WM_STOCK_TAKING_CODE.getCode());
        // 3. å¤ç”¨åˆ›å»ºé€»è¾‘生成草稿任务(内部校验编码唯一、方案可用,并按方案生成盘点明细行)
        Long taskId = createStockTakingTask(new MesWmStockTakingTaskSaveReqVO()
                .setCode(code)
                .setName(plan.getName())
                .setTakingDate(LocalDateTime.now())
                .setType(plan.getType())
                .setUserId(getCurrentUserId())
                .setPlanId(planId)
                .setStartTime(plan.getStartTime())
                .setEndTime(plan.getEndTime())
                .setBlindFlag(plan.getBlindFlag())
                .setFrozen(plan.getFrozen())
                .setRemark("方案定期自动生成"));
        // 4. å›žå†™æœ¬æ¬¡ç”Ÿæˆæ—¶é—´ï¼Œé¿å…åŒä¸€å‘¨æœŸé‡å¤ç”Ÿæˆï¼ˆä¸Žåˆ›å»ºåŒäº‹åŠ¡ï¼‰
        stockTakingPlanService.updateStockTakingPlanLastGenerateTime(planId, LocalDateTime.now());
        return taskId;
    }
    @Override
@@ -143,19 +198,114 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void finishStockTakingTask(Long id) {
        // 1. æ ¡éªŒä»»åŠ¡å­˜åœ¨ä¸”ä¸ºå®¡æ‰¹ä¸­çŠ¶æ€
    public void submitStockTakingResultApproval(Long id, String processDefinitionKey) {
        // 1.1 æ ¡éªŒä»»åŠ¡å­˜åœ¨ä¸”å¤„äºŽç›˜ç‚¹ä¸­çŠ¶æ€
        MesWmStockTakingTaskDO task = validateStockTakingTaskExistsAndApproving(id);
        // 2. æ›´æ–°ä»»åŠ¡çŠ¶æ€ä¸ºå·²å®Œæˆ
        stockTakingTaskMapper.updateById(new MesWmStockTakingTaskDO().setId(id)
                .setStatus(MesWmStockTakingTaskStatusEnum.FINISHED.getStatus()));
        // 3. è§£å†»åº“å­˜
        if (Boolean.TRUE.equals(task.getFrozen())) {
            List<MesWmStockTakingTaskLineDO> lines = stockTakingTaskLineService.getStockTakingTaskLineListByTaskId(id);
            updateMaterialStockFrozen(lines, false);
        // 1.2 æ ¡éªŒç»“果审批未提交过(避免重复提交)
        if (task.getProcessInstanceId() != null) {
            throw exception(WM_STOCK_TAKING_TASK_ALREADY_SUBMIT_APPROVAL);
        }
        // 1.3 æ ¡éªŒè‡³å°‘有一条盘点结果
        List<MesWmStockTakingTaskResultDO> results = stockTakingTaskResultService.getStockTakingTaskResultListByTaskId(id);
        if (CollUtil.isEmpty(results)) {
            throw exception(WM_STOCK_TAKING_TASK_NO_RESULT);
        }
        // 2. åˆ›å»º BPM æµç¨‹å®žä¾‹
        String processInstanceId = processInstanceApi.createProcessInstance(getCurrentUserId(),
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
        // 3. å›žå†™æµç¨‹å®žä¾‹ç¼–号(任务保持盘点中,等待审批)
        stockTakingTaskMapper.updateById(new MesWmStockTakingTaskDO()
                .setId(id).setProcessInstanceId(processInstanceId));
    }
    @Override
    public List<Map<String, Object>> getStockTakingTaskResultApprovalProcessDefinitionList() {
        return getProcessDefinitionListByCategory(APPROVE_CATEGORY_CODE);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateStockTakingTaskResultAuditStatus(Long id, Integer bpmResult) {
        // 1. æ ¡éªŒä»»åŠ¡å­˜åœ¨
        MesWmStockTakingTaskDO task = validateStockTakingTaskExists(id);
        if (!MesWmStockTakingTaskStatusEnum.APPROVING.getStatus().equals(task.getStatus())) {
            log.warn("[updateStockTakingTaskResultAuditStatus] ç›˜ç‚¹ä»»åŠ¡({}) ä¸å¤„于盘点中状态,忽略审批事件", id);
            return;
        }
        // 2. å®¡æ‰¹é€šè¿‡ï¼šå…ˆç½®ä¸ºå·²å®Œæˆï¼Œå†è§£å†»å¹¶åŒäº‹åŠ¡è‡ªåŠ¨æ ¸é”€å·®å¼‚
        if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
            // 2.1 å…ˆç½®ä¸ºå·²å®Œæˆï¼šä»»åŠ¡çŠ¶æ€å˜æ›´åŽï¼ŒåŽç»­é‡å¤çš„å®¡æ‰¹äº‹ä»¶è¯»å–åˆ° FINISHED å³è¢«å¿½ç•¥ï¼Œé¿å…é‡å¤æ ¸é”€
            stockTakingTaskMapper.updateById(new MesWmStockTakingTaskDO().setId(id)
                    .setStatus(MesWmStockTakingTaskStatusEnum.FINISHED.getStatus()));
            // 2.2 è§£å†»åº“存:须在差异核销前解冻,否则核销事务会被库存冻结校验拦截
            if (Boolean.TRUE.equals(task.getFrozen())) {
                updateMaterialStockFrozen(stockTakingTaskLineService.getStockTakingTaskLineListByTaskId(id), false);
            }
            // 2.3 è‡ªåŠ¨æ ¸é”€ç›˜ç‚¹å·®å¼‚ï¼šç›˜ç›ˆå…¥åº“ / ç›˜äºå‡ºåº“,同事务内更新库存台账;任一步失败整体回滚,任务回退为盘点中
            doProcessStockTakingDifference(task);
            return;
        }
        // 3. å®¡æ‰¹é©³å›žï¼šæ¸…空流程实例编号,任务保持盘点中,可修改实盘后重新提交结果审批
        if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) {
            stockTakingTaskMapper.clearProcessInstanceId(id);
            return;
        }
        // 4. å®¡æ‰¹å–消:解冻库存并置为已取消
        if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
            if (Boolean.TRUE.equals(task.getFrozen())) {
                updateMaterialStockFrozen(stockTakingTaskLineService.getStockTakingTaskLineListByTaskId(id), false);
            }
            stockTakingTaskMapper.clearProcessInstanceId(id);
            stockTakingTaskMapper.updateById(new MesWmStockTakingTaskDO().setId(id)
                    .setStatus(MesWmStockTakingTaskStatusEnum.CANCELED.getStatus()));
        }
    }
    /**
     * èŽ·å–åˆ†ç±»ä¸‹çš„å¯ç”¨ BPM æµç¨‹å®šä¹‰ï¼ˆè¿‡æ»¤æŒ‚起流程,同 Key ä¿ç•™æœ€æ–°ç‰ˆæœ¬ï¼‰
     *
     * @param categoryCode æµç¨‹åˆ†ç±»ç¼–码
     * @return æµç¨‹å®šä¹‰åˆ—表(id/key/name)
     */
    private List<Map<String, Object>> getProcessDefinitionListByCategory(String categoryCode) {
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(categoryCode);
        if (CollUtil.isEmpty(definitionInfoList)) {
            return Collections.emptyList();
        }
        // è¿‡æ»¤æ¿€æ´»çŠ¶æ€ï¼Œä¿ç•™æœ€æ–°ç‰ˆæœ¬
        Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
        for (BpmProcessDefinitionInfoDO info : definitionInfoList) {
            ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId());
            if (pd == null || pd.isSuspended()) {
                continue;
            }
            ProcessDefinition existing = latestVersionMap.get(pd.getKey());
            if (existing == null || pd.getVersion() > existing.getVersion()) {
                latestVersionMap.put(pd.getKey(), pd);
            }
        }
        List<Map<String, Object>> result = new ArrayList<>();
        for (ProcessDefinition pd : latestVersionMap.values()) {
            Map<String, Object> item = new HashMap<>();
            item.put("id", pd.getId());
            item.put("key", pd.getKey());
            item.put("name", pd.getName());
            result.add(item);
        }
        return result;
    }
    private Long getCurrentUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof Long) {
            return (Long) authentication.getPrincipal();
        }
        return 1L; // é»˜è®¤ç”¨æˆ·
    }
    @Override
@@ -168,6 +318,8 @@
        }
        task.setStatus(MesWmStockTakingTaskStatusEnum.CANCELED.getStatus());
        stockTakingTaskMapper.updateById(task);
        // å–消后清空流程实例编号,避免残留审批标记影响后续重新开始盘点
        stockTakingTaskMapper.clearProcessInstanceId(id);
        if (Boolean.TRUE.equals(task.getFrozen())) {
            updateMaterialStockFrozen(stockTakingTaskLineService.getStockTakingTaskLineListByTaskId(id), false);
        }
@@ -229,20 +381,26 @@
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void processStockTakingDifference(Long id) {
        // 1. æ ¡éªŒä»»åŠ¡å­˜åœ¨ä¸”å·²å®Œæˆ
        MesWmStockTakingTaskDO task = validateStockTakingTaskExists(id);
        if (!MesWmStockTakingTaskStatusEnum.FINISHED.getStatus().equals(task.getStatus())) {
            throw exception(WM_STOCK_TAKING_TASK_NOT_FINISHED);
        }
        // 2. èŽ·å–ç›˜ç‚¹ç»“æžœåˆ—è¡¨
        List<MesWmStockTakingTaskResultDO> results = stockTakingTaskResultService.getStockTakingTaskResultListByTaskId(id);
    /**
     * è‡ªåŠ¨æ ¸é”€ç›˜ç‚¹å·®å¼‚ï¼šæŒ‰ç›˜ç‚¹ç»“æžœé€è¡Œç”Ÿæˆ IN/OUT åº“存事务更新库存台账
     *
     * <p>仅由 {@link #updateStockTakingTaskResultAuditStatus} çš„审批通过分支在同事务内调用;
     * å¤±è´¥æ•´ä½“回滚,任务回退为盘点中状态,避免重复调整。
     *
     * @param task å·²ç½®ä¸ºå·²å®Œæˆã€å·²è§£å†»çš„盘点任务
     */
    private void doProcessStockTakingDifference(MesWmStockTakingTaskDO task) {
        // 1. èŽ·å–ç›˜ç‚¹ç»“æžœåˆ—è¡¨
        List<MesWmStockTakingTaskResultDO> results = stockTakingTaskResultService.getStockTakingTaskResultListByTaskId(task.getId());
        if (CollUtil.isEmpty(results)) {
            return; // æ— ç›˜ç‚¹ç»“果,无需处理
        }
        // 2. å»ºç«‹ è¡Œå· -> ç›˜ç‚¹è¡Œ æ˜ å°„:以盘点行(库存快照)为准回填库存编号与维度,
        //    é¿å…å‡ºåº“核销按 (物料+位置+批次) ç»„合键匹配到其它供应商/库位的账面行而扣错库存
        Map<Long, MesWmStockTakingTaskLineDO> lineMap = convertMap(
                stockTakingTaskLineService.getStockTakingTaskLineListByTaskId(task.getId()),
                MesWmStockTakingTaskLineDO::getId);
        // 3. éåŽ†ç»“æžœï¼Œå¤„ç†å·®å¼‚
        for (MesWmStockTakingTaskResultDO result : results) {
@@ -253,37 +411,30 @@
                continue;
            }
            // è®¡ç®—差异:盘点数量 - è´¦é¢æ•°é‡
            // è®¡ç®—差异:盘点数量 - è´¦é¢æ•°é‡ï¼ˆç›˜ç›ˆä¸ºæ­£ã€ç›˜äºä¸ºè´Ÿï¼‰
            BigDecimal difference = takingQuantity.subtract(bookQuantity);
            if (difference.compareTo(BigDecimal.ZERO) == 0) {
                continue; // æ— å·®å¼‚,跳过
            }
            // 4. åˆ›å»ºåº“存调整事务
            MesWmStockTakingTaskLineDO line = result.getLineId() != null ? lineMap.get(result.getLineId()) : null;
            // 4. åˆ›å»ºåº“存调整事务:盘盈传正数入库;盘亏直接传负数出库(createTransaction æ ¡éªŒå‡ºåº“数量必须为负)
            MesWmTransactionSaveReqDTO transaction = new MesWmTransactionSaveReqDTO()
                    .setItemId(result.getItemId())
                    .setBatchId(result.getBatchId())
                    .setBatchCode(result.getBatchCode())
                    .setWarehouseId(result.getWarehouseId())
                    .setLocationId(result.getLocationId())
                    .setAreaId(result.getAreaId())
                    .setType(difference.compareTo(BigDecimal.ZERO) > 0
                            ? MesWmTransactionTypeEnum.IN.getType() : MesWmTransactionTypeEnum.OUT.getType())
                    .setQuantity(difference)
                    .setItemId(line != null ? line.getItemId() : result.getItemId())
                    .setBatchId(line != null ? line.getBatchId() : result.getBatchId())
                    .setBatchCode(line != null ? line.getBatchCode() : result.getBatchCode())
                    .setWarehouseId(line != null ? line.getWarehouseId() : result.getWarehouseId())
                    .setLocationId(line != null ? line.getLocationId() : result.getLocationId())
                    .setAreaId(line != null ? line.getAreaId() : result.getAreaId())
                    .setMaterialStockId(line != null ? line.getMaterialStockId() : result.getMaterialStockId())
                    .setBizId(task.getId())
                    .setBizCode(task.getCode())
                    .setBizLineId(result.getLineId());
            if (difference.compareTo(BigDecimal.ZERO) > 0) {
                // ç›˜ç›ˆï¼šåº“存增加
                transaction.setType(MesWmTransactionTypeEnum.IN.getType())
                        .setQuantity(difference)
                        .setBizType(MesBizTypeConstants.WM_STOCK_TAKING);
            } else {
                // ç›˜äºï¼šåº“存减少
                transaction.setType(MesWmTransactionTypeEnum.OUT.getType())
                        .setQuantity(difference.negate()) // å‡ºåº“数量传正数,但内部会转为负数
                        .setBizType(MesBizTypeConstants.WM_STOCK_TAKING);
            }
                    .setBizLineId(result.getLineId())
                    .setBizType(MesBizTypeConstants.WM_STOCK_TAKING);
            wmTransactionService.createTransaction(transaction);
        }
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stocktaking/task/listener/MesWmStockTakingTaskResultStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.mes.service.wm.stocktaking.task.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.mes.service.wm.stocktaking.task.MesWmStockTakingTaskService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
 * MES ç›˜ç‚¹ç»“果审批 BPM çŠ¶æ€ç›‘å¬å™¨
 *
 * ç›‘听盘点结果审批流程实例状态变更事件:审批通过后解冻库存并自动核销盘点差异置为已完成;
 * é©³å›žåŽä»»åŠ¡ä¿æŒç›˜ç‚¹ä¸­å¯ä¿®æ”¹å®žç›˜é‡æ–°æäº¤ï¼›å–æ¶ˆåˆ™è§£å†»å¹¶ç½®ä¸ºå·²å–æ¶ˆ
 */
@Component
@Slf4j
public class MesWmStockTakingTaskResultStatusListener
        extends cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener {
    @Resource
    private MesWmStockTakingTaskService stockTakingTaskService;
    @Override
    protected String getProcessDefinitionKey() {
        return MesWmStockTakingTaskService.APPROVE_CATEGORY_CODE;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Long id;
        try {
            id = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            log.warn("[onEvent] businessKey({}) ä¸æ˜¯æœ‰æ•ˆçš„盘点任务编号", event.getBusinessKey());
            return;
        }
        log.info("[onEvent] æ”¶åˆ°ç›˜ç‚¹ç»“果审批事件,stockTakingTaskId={}, status={}", id, event.getStatus());
        try {
            stockTakingTaskService.updateStockTakingTaskResultAuditStatus(id, event.getStatus());
            log.info("[onEvent] ç›˜ç‚¹ä»»åŠ¡({}) ç»“果审批状态更新完成", id);
        } catch (Exception e) {
            log.error("[onEvent] æ›´æ–°ç›˜ç‚¹ä»»åŠ¡ç»“æžœå®¡æ‰¹çŠ¶æ€å¤±è´¥ï¼ŒstockTakingTaskId={}", id, e);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java
@@ -128,7 +128,7 @@
        // 3. æ’入事务流水
        MesWmTransactionDO transaction = MesWmTransactionDO.builder()
                .type(reqDTO.getType()).quantity(reqDTO.getQuantity()).transactionTime(LocalDateTime.now())
                .itemId(reqDTO.getItemId()).batchId(reqDTO.getBatchId()).batchCode(reqDTO.getBatchCode())
                .itemId(reqDTO.getItemId()).batchId(reqDTO.getBatchId()).batchCode(reqDTO.getBatchCode()).snCode(reqDTO.getSnCode())
                .warehouseId(reqDTO.getWarehouseId()).locationId(reqDTO.getLocationId()).areaId(reqDTO.getAreaId())
                .bizType(reqDTO.getBizType()).bizId(reqDTO.getBizId()).bizCode(reqDTO.getBizCode()).bizLineId(reqDTO.getBizLineId())
                .materialStockId(materialStock.getId()).relatedTransactionId(reqDTO.getRelatedTransactionId())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/dto/MesWmTransactionSaveReqDTO.java
@@ -69,6 +69,10 @@
     * æ‰¹æ¬¡å·
     */
    private String batchCode;
    /**
     * SN ç ï¼ˆåºåˆ—号管理物料单品进出)
     */
    private String snCode;
    // ===== åº“存位置 =====