hsy
4 小时以前 44840942aba889088a86d2b3d93856530bb2dc25
Merge branch 'dev_business' into fix/hsy/20260811
已添加36个文件
已修改58个文件
3106 ■■■■■ 文件已修改
docs/projectSql/ruoyi-vue-pro.sql 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_module_init.sql 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_overtime_application.sql 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_salary_structure_menu.sql 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_salary_structure_overtime_ratio.sql 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java 49 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestPageReqVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpAuditStatus.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseOrderStatusListener.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmOvertimeTypeEnum.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmOvertimeApplicationController.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationPageReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationRespVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationSaveReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordImportExcelVO.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordImportReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordRespVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmLeaveTypeConfigController.java 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryStructureController.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmLeaveTypeConfigCreateReqVO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureRespVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureSaveReqVO.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmOvertimeApplicationDO.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryStructureDO.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmOvertimeApplicationMapper.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmSalaryStructureMapper.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationServiceImpl.java 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmOvertimeApplicationStatusListener.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmLeaveTypeConfigService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmLeaveTypeConfigServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java 99 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureServiceImpl.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemImportExcelVO.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemImportRespVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/MdmUnitMeasureController.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureImportExcelVO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureImportRespVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/enums/ErrorCodeConstants.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/unit/MdmUnitMeasureService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/unit/MdmUnitMeasureServiceImpl.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/MesMdItemTypeController.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/type/MesMdItemTypeImportExcelVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/type/MesMdItemTypeImportRespVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/MesPdDocumentController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/vo/MesPdDocumentRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/MesWmReturnVendorController.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnvendor/MesWmReturnVendorDO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/dv/metering/MesDvMeteringRemindRedisDAO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindServiceImpl.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemTypeService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemTypeServiceImpl.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/document/MesPdDocumentServiceImpl.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorServiceImpl.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/vo/SrmSupplierCertificatePageReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/mysql/SrmSupplierCertificateMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/ErrorCodeConstants.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderBidServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/SystemCompanyConfigController.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/vo/SystemCompanyConfigRespVO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/vo/SystemCompanyConfigSaveReqVO.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/companyconfig/SystemCompanyConfigDO.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/companyconfig/SystemCompanyConfigMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/companyconfig/SystemCompanyConfigService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/companyconfig/SystemCompanyConfigServiceImpl.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-wtxc.yaml 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/projectSql/ruoyi-vue-pro.sql
@@ -4903,7 +4903,9 @@
  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '结构名称',
  `base_salary` decimal(12, 2) NOT NULL COMMENT '基本工资',
  `performance_ratio` decimal(4, 2) NULL DEFAULT 0.00 COMMENT '绩效工资占比(%)',
  `overtime_pay_ratio` decimal(4, 2) NULL DEFAULT 1.50 COMMENT '加班工资倍率',
  `overtime_pay_ratio` decimal(4, 2) NULL DEFAULT 1.50 COMMENT '工作日加班工资倍率',
  `rest_day_overtime_pay_ratio` decimal(4, 2) NULL DEFAULT 2.00 COMMENT '休息日加班工资倍率',
  `legal_holiday_overtime_pay_ratio` decimal(4, 2) NULL DEFAULT 3.00 COMMENT '法定节假日加班工资倍率',
  `social_security_base` decimal(12, 2) NULL DEFAULT NULL COMMENT '社保基数',
  `housing_fund_base` decimal(12, 2) NULL DEFAULT NULL COMMENT '公积金基数',
  `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-启用,1-禁用',
sql/mysql/hrm_module_init.sql
@@ -274,7 +274,9 @@
    `name` varchar(100) NOT NULL COMMENT '结构名称',
    `base_salary` decimal(12,2) NOT NULL COMMENT '基本工资',
    `performance_ratio` decimal(4,2) DEFAULT 0 COMMENT '绩效工资占比(%)',
    `overtime_pay_ratio` decimal(4,2) DEFAULT 1.5 COMMENT '加班工资倍率',
    `overtime_pay_ratio` decimal(4,2) DEFAULT 1.5 COMMENT '工作日加班工资倍率',
    `rest_day_overtime_pay_ratio` decimal(4,2) DEFAULT 2.0 COMMENT '休息日加班工资倍率',
    `legal_holiday_overtime_pay_ratio` decimal(4,2) DEFAULT 3.0 COMMENT '法定节假日加班工资倍率',
    `social_security_base` decimal(12,2) DEFAULT NULL COMMENT '社保基数',
    `housing_fund_base` decimal(12,2) DEFAULT NULL COMMENT '公积金基数',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-启用,1-禁用',
@@ -586,4 +588,4 @@
-- =============================================
-- æ‰§è¡Œå®Œæˆæç¤º
-- =============================================
SELECT 'HRM äººåŠ›èµ„æºæ¨¡å— SQL åˆå§‹åŒ–完成!' AS result;
SELECT 'HRM äººåŠ›èµ„æºæ¨¡å— SQL åˆå§‹åŒ–完成!' AS result;
sql/mysql/hrm_overtime_application.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
-- HRM åŠ ç­ç”³è¯·ï¼šååŒåŠžå…¬å®¡æ‰¹ + è–ªé…¬æ ¸ç®—数据源
CREATE TABLE IF NOT EXISTS `hrm_overtime_application` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '申请ID',
    `no` varchar(32) NOT NULL COMMENT '申请单号',
    `user_id` bigint NOT NULL COMMENT '系统用户ID',
    `employee_id` bigint NOT NULL COMMENT '员工ID',
    `dept_id` bigint NOT NULL COMMENT '部门ID',
    `overtime_date` date NOT NULL COMMENT '加班日期',
    `start_time` time NOT NULL COMMENT '开始时间',
    `end_time` time NOT NULL COMMENT '结束时间',
    `duration` decimal(6,1) NOT NULL COMMENT '扣除用餐后的加班时长(0.5小时取整)',
    `overtime_type` tinyint NOT NULL COMMENT '1-工作日,2-休息日,3-法定节假日',
    `cross_day` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否跨天',
    `meal_deduction` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否扣除用餐时间',
    `meal_deduction_hours` decimal(4,1) NOT NULL DEFAULT 0 COMMENT '用餐扣除小时数',
    `reason` varchar(500) NOT NULL COMMENT '加班原因',
    `related_type` tinyint DEFAULT NULL COMMENT '关联类型:1-项目,2-工单',
    `related_code` varchar(100) DEFAULT NULL COMMENT '项目/工单编号',
    `work_content` varchar(1000) DEFAULT NULL COMMENT '工作内容简述',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '0-草稿,10-审批中,20-通过,30-驳回,40-撤销',
    `process_instance_id` varchar(64) DEFAULT NULL COMMENT '协同办公流程实例ID',
    `approve_time` datetime DEFAULT NULL COMMENT '审批完成时间',
    `creator` varchar(64) DEFAULT '' COMMENT '创建者',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` varchar(64) DEFAULT '' COMMENT '更新者',
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_hrm_overtime_no` (`no`),
    KEY `idx_hrm_overtime_user_date_status` (`user_id`, `overtime_date`, `status`),
    KEY `idx_hrm_overtime_process_instance` (`process_instance_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='HRM加班申请表';
-- ååŒåŠžå…¬æµç¨‹åˆ†ç±»å·²åœ¨ bpm/manager/category ä¸­ç»´æŠ¤ï¼Œåˆ†ç±»ç¼–码为 overtime_approve。
-- ä»éœ€åœ¨è¯¥åˆ†ç±»ä¸‹å»ºç«‹å¹¶å‘布流程定义 key=hrm_overtime。
-- äººäº‹å®¡æ‰¹èœå•及按钮权限
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 6007, '加班申请', '', 2, 4, 6002, 'overtime', 'ep:timer', 'hrm/overtime/index', 'HrmOvertimeApplication', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 6007);
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 60071, '加班申请查询', 'hrm:overtime:query', 3, 1, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60071);
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 60072, '加班申请新增', 'hrm:overtime:create', 3, 2, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60072);
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 60073, '加班申请修改', 'hrm:overtime:update', 3, 3, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60073);
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 60074, '加班申请删除', 'hrm:overtime:delete', 3, 4, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60074);
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 60075, '加班申请提交', 'hrm:overtime:submit', 3, 5, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60075);
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 60076, '加班申请撤销', 'hrm:overtime:cancel', 3, 6, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60076);
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 60077, '加班申请管理', 'hrm:overtime:manage', 3, 7, 6007, '', '', '', NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0' FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `system_menu` WHERE `id` = 60077);
INSERT INTO `system_role_menu` (`role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 1, menu_id, '1', NOW(), '1', NOW(), b'0' FROM (
    SELECT 6007 menu_id UNION ALL SELECT 60071 UNION ALL SELECT 60072 UNION ALL
    SELECT 60073 UNION ALL SELECT 60074 UNION ALL SELECT 60075 UNION ALL SELECT 60076 UNION ALL SELECT 60077
) ids WHERE NOT EXISTS (SELECT 1 FROM `system_role_menu` rm WHERE rm.`role_id` = 1 AND rm.`menu_id` = ids.menu_id);
SELECT 'HRM åŠ ç­ç”³è¯·åˆå§‹åŒ–å®Œæˆï¼›è¯·åœ¨ååŒåŠžå…¬å‘å¸ƒ hrm_overtime æµç¨‹' AS result;
sql/mysql/hrm_salary_structure_menu.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
-- HRM è–ªé…¬ç»“构配置菜单与权限(可重复执行)
INSERT INTO `system_menu`
(`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`,
 `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
(6025, '薪酬结构', '', 2, 3, 6020, 'structure', 'ep:set-up',
 'hrm/salary/structure/index', 'HrmSalaryStructure', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(60251, '薪酬结构查询', 'hrm:salary-structure:query', 3, 1, 6025, '', '', '', NULL,
 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(60252, '薪酬结构新增', 'hrm:salary-structure:create', 3, 2, 6025, '', '', '', NULL,
 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(60253, '薪酬结构修改', 'hrm:salary-structure:update', 3, 3, 6025, '', '', '', NULL,
 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(60254, '薪酬结构删除', 'hrm:salary-structure:delete', 3, 4, 6025, '', '', '', NULL,
 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0')
ON DUPLICATE KEY UPDATE
`name` = VALUES(`name`), `permission` = VALUES(`permission`), `parent_id` = VALUES(`parent_id`),
`path` = VALUES(`path`), `icon` = VALUES(`icon`), `component` = VALUES(`component`),
`component_name` = VALUES(`component_name`), `status` = VALUES(`status`), `deleted` = b'0';
INSERT INTO `system_role_menu`
(`role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
SELECT 1, menu_id, '1', NOW(), '1', NOW(), b'0'
FROM (
    SELECT 6025 AS menu_id UNION ALL SELECT 60251 UNION ALL SELECT 60252
    UNION ALL SELECT 60253 UNION ALL SELECT 60254
) menu_ids
WHERE NOT EXISTS (
    SELECT 1 FROM `system_role_menu` role_menu
    WHERE role_menu.`role_id` = 1 AND role_menu.`menu_id` = menu_ids.menu_id AND role_menu.`deleted` = b'0'
);
SELECT 'HRM è–ªé…¬ç»“构菜单初始化完成' AS result;
sql/mysql/hrm_salary_structure_overtime_ratio.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
-- è–ªé…¬ç»“构增加休息日、法定节假日加班工资倍率
-- å·²æœ‰çš„工作日倍率继续使用 overtime_pay_ratio å­—段
ALTER TABLE `hrm_salary_structure`
    ADD COLUMN `rest_day_overtime_pay_ratio` decimal(4,2) DEFAULT 2.00
        COMMENT '休息日加班工资倍率' AFTER `overtime_pay_ratio`,
    ADD COLUMN `legal_holiday_overtime_pay_ratio` decimal(4,2) DEFAULT 3.00
        COMMENT '法定节假日加班工资倍率' AFTER `rest_day_overtime_pay_ratio`;
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
@@ -27,6 +27,7 @@
import org.flowable.task.api.TaskInfo;
import tools.jackson.databind.JsonNode;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@@ -245,6 +246,11 @@
                || !BpmModelFormTypeEnum.NORMAL.getType().equals(processDefinitionInfo.getFormType())) {
            return null;
        }
        // åŽ†å²æµç¨‹å®šä¹‰ã€ä¸šåŠ¡è¡¨å•æµç¨‹å¯èƒ½æ²¡æœ‰å†—ä½™ formFields。此时没有可展示的摘要,
        // ä¸èƒ½å› ä¸ºå•条流程数据缺少表单字段配置导致整个“我的流程/待办”分页查询失败。
        if (ObjectUtil.isEmpty(processDefinitionInfo.getFormFields())) {
            return Collections.emptyList();
        }
        // è§£æžè¡¨å•配置
        Map<String, BpmFormFieldVO> formFieldsMap = new LinkedHashMap<>();
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java
@@ -1,23 +1,18 @@
package cn.iocoder.yudao.module.bpm.service.message;
import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.module.bpm.convert.message.BpmMessageConvert;
import cn.iocoder.yudao.module.bpm.enums.message.BpmMessageEnum;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO;
import cn.iocoder.yudao.module.system.api.sms.SmsSendApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
 * BPM æ¶ˆæ¯ Service å®žçŽ°ç±»
 *
 * æ³¨æ„ï¼šå·²å…³é—­çŸ­ä¿¡é€šçŸ¥åŠŸèƒ½ï¼Œç›¸å…³æ–¹æ³•ä»…è®°å½•æ—¥å¿—ï¼Œä¸å†è°ƒç”¨çŸ­ä¿¡å‘é€æŽ¥å£ï¼Œ
 * ä»¥é¿å…â€œçŸ­ä¿¡æ¨¡æ¿ä¸å­˜åœ¨â€ç­‰é—®é¢˜ã€‚
 *
 * @author è¶…级管理员
 */
@@ -26,54 +21,24 @@
@Slf4j
public class BpmMessageServiceImpl implements BpmMessageService {
    @Resource
    private SmsSendApi smsSendApi;
    @Resource
    private WebProperties webProperties;
    @Override
    public void sendMessageWhenProcessInstanceApprove(BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO) {
        Map<String, Object> templateParams = new HashMap<>();
        templateParams.put("processInstanceName", reqDTO.getProcessInstanceName());
        templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId()));
        smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getStartUserId(),
                BpmMessageEnum.PROCESS_INSTANCE_APPROVE.getSmsTemplateCode(), templateParams));
        log.info("[sendMessageWhenProcessInstanceApprove][已关闭短信通知,跳过发送][processInstanceId: {}]", reqDTO.getProcessInstanceId());
    }
    @Override
    public void sendMessageWhenProcessInstanceReject(BpmMessageSendWhenProcessInstanceRejectReqDTO reqDTO) {
        Map<String, Object> templateParams = new HashMap<>();
        templateParams.put("processInstanceName", reqDTO.getProcessInstanceName());
        templateParams.put("reason", reqDTO.getReason());
        templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId()));
        smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getStartUserId(),
                BpmMessageEnum.PROCESS_INSTANCE_REJECT.getSmsTemplateCode(), templateParams));
        log.info("[sendMessageWhenProcessInstanceReject][已关闭短信通知,跳过发送][processInstanceId: {}]", reqDTO.getProcessInstanceId());
    }
    @Override
    public void sendMessageWhenTaskAssigned(BpmMessageSendWhenTaskCreatedReqDTO reqDTO) {
        Map<String, Object> templateParams = new HashMap<>();
        templateParams.put("processInstanceName", reqDTO.getProcessInstanceName());
        templateParams.put("taskName", reqDTO.getTaskName());
        templateParams.put("startUserNickname", reqDTO.getStartUserNickname());
        templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId()));
        smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(),
                BpmMessageEnum.TASK_ASSIGNED.getSmsTemplateCode(), templateParams));
        log.info("[sendMessageWhenTaskAssigned][已关闭短信通知,跳过发送][processInstanceId: {}]", reqDTO.getProcessInstanceId());
    }
    @Override
    public void sendMessageWhenTaskTimeout(BpmMessageSendWhenTaskTimeoutReqDTO reqDTO) {
        Map<String, Object> templateParams = new HashMap<>();
        templateParams.put("processInstanceName", reqDTO.getProcessInstanceName());
        templateParams.put("taskName", reqDTO.getTaskName());
        templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId()));
        smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(),
                BpmMessageEnum.TASK_TIMEOUT.getSmsTemplateCode(), templateParams));
    }
    private String getProcessInstanceDetailUrl(String taskId) {
        return webProperties.getAdminUi().getUrl() + "/bpm/process-instance/detail?id=" + taskId;
        log.info("[sendMessageWhenTaskTimeout][已关闭短信通知,跳过发送][processInstanceId: {}]", reqDTO.getProcessInstanceId());
    }
}
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtilsTest.java
@@ -92,6 +92,16 @@
    }
    @Test
    public void testGetSummary_normalFormWithoutFields_returnEmpty() {
        // å‡†å¤‡å‚数:业务表单或历史流程定义可能没有冗余 formFields。
        BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionInfo(null, null);
        // è°ƒç”¨ & æ–­è¨€ï¼šç¼ºå°‘摘要字段时返回空摘要,不能影响整个分页接口。
        assertEquals(Collections.emptyList(),
                FlowableUtils.getSummary(processDefinitionInfo, Collections.emptyMap()));
    }
    @Test
    public void testGetSummary_notNormalForm_returnNull() {
        // å‡†å¤‡å‚æ•°
        BpmProcessDefinitionInfoDO processDefinitionInfo = BpmProcessDefinitionInfoDO.builder()
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseOrderController.java
@@ -13,6 +13,7 @@
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderItemDO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseOrderSourceTypeEnum;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
@@ -40,7 +41,9 @@
import java.util.Map;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -170,6 +173,23 @@
        return success(purchaseOrderService.confirmReceipt(id));
    }
    @PostMapping("/reverse-approve")
    @Operation(summary = "反审批(协同办公已办任务中撤回已审核订单,直接重新进入待办审批)")
    @Parameter(name = "processInstanceId", description = "流程实例编号", required = true)
    @PreAuthorize("@ss.hasPermission('erp:purchase-order:update-status')")
    public CommonResult<Boolean> reverseApprove(@RequestParam("processInstanceId") String processInstanceId) {
        // 1. æ ¹æ®æµç¨‹å®žä¾‹ç¼–号找到采购订单
        ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.getPurchaseOrderByProcessInstanceId(processInstanceId);
        if (purchaseOrder == null) {
            throw exception(PURCHASE_ORDER_NOT_EXISTS);
        }
        // 2. åå®¡æ ¸ï¼šå·²å®¡æ ¸ -> æœªå®¡æ ¸ï¼ˆå«å…¥åº“/退货校验)
        purchaseOrderService.updatePurchaseOrderStatus(purchaseOrder.getId(), ErpAuditStatus.PROCESS.getStatus());
        // 3. è‡ªåŠ¨é‡æ–°æäº¤å®¡æ‰¹ -> è®¢å•直接回到协同办公待办任务
        purchaseOrderService.submitPurchaseOrder(purchaseOrder.getId(), "purchase_order_approve", getLoginUserId());
        return success(true);
    }
    private PageResult<ErpPurchaseOrderRespVO> buildPurchaseOrderVOPageResult(PageResult<ErpPurchaseOrderDO> pageResult) {
        if (CollUtil.isEmpty(pageResult.getList())) {
            return PageResult.empty(pageResult.getTotal());
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestPageReqVO.java
@@ -9,7 +9,7 @@
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - ERP é‡‡è´­ç”³è¯·åˆ†é¡µ Request VO")
@Data
@@ -33,7 +33,7 @@
    private Integer status;
    @Schema(description = "申请时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] requestTime;
    @Schema(description = "产品编号", example = "1")
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseOrderMapper.java
@@ -74,6 +74,10 @@
        return selectOne(ErpPurchaseOrderDO::getNo, no);
    }
    default ErpPurchaseOrderDO selectByProcessInstanceId(String processInstanceId) {
        return selectOne(ErpPurchaseOrderDO::getProcessInstanceId, processInstanceId);
    }
    default List<ErpPurchaseOrderDO> selectList(Long supplierId, Integer status) {
        return selectList(new LambdaQueryWrapperX<ErpPurchaseOrderDO>()
                .eqIfPresent(ErpPurchaseOrderDO::getSupplierId, supplierId)
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpAuditStatus.java
@@ -20,7 +20,8 @@
    PROCESS(10, "未审核"), // æœªå®¡æ ¸/草稿
    APPROVING(15, "审批中"), // å®¡æ‰¹ä¸­
    APPROVE(20, "已审核"), // å®¡æ ¸é€šè¿‡
    CANCEL(30, "已作废"); // å·²ä½œåºŸ/已撤销
    CANCEL(30, "已作废"), // å·²ä½œåºŸ/已撤销
    REJECT(35, "已驳回"); // å®¡æ‰¹è¢«é©³å›ž
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(ErpAuditStatus::getStatus).toArray(Integer[]::new);
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderService.java
@@ -98,6 +98,14 @@
    ErpPurchaseOrderDO getPurchaseOrder(Long id);
    /**
     * æ ¹æ® BPM æµç¨‹å®žä¾‹ç¼–号查询采购订单(用于协同办公反审批)
     *
     * @param processInstanceId æµç¨‹å®žä¾‹ç¼–号
     * @return é‡‡è´­è®¢å•
     */
    ErpPurchaseOrderDO getPurchaseOrderByProcessInstanceId(String processInstanceId);
    /**
     * æ ¡éªŒé‡‡è´­è®¢å•,已经审核通过
     *
     * @param id ç¼–号
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -41,6 +41,7 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
/**
@@ -56,7 +57,11 @@
    /**
     * BPM é‡‡è´­è®¢å•审批分类编码
     */
    private static final String PURCHASE_ORDER_APPROVE_CATEGORY_CODE = "purchase_request_approve";
    private static final String PURCHASE_ORDER_APPROVE_CATEGORY_CODE = "purchase_order_approve";
    /**
     * BPM é‡‡è´­è®¢å•审批流程定义 Key(创建订单后自动提交到协同办公审批)
     */
    private static final String PURCHASE_ORDER_APPROVE_PROCESS_DEFINITION_KEY = "purchase_order_approve";
    @Resource
    private ErpPurchaseOrderMapper purchaseOrderMapper;
@@ -119,6 +124,10 @@
        // 2.3 ç»‘定附件
        storageAttachmentApi.bindAttachments("file", "erp_purchase_order",
                purchaseOrder.getId(), createReqVO.getBlobIds());
        // 3. åˆ›å»ºåŽè‡ªåŠ¨æäº¤å®¡æ‰¹åˆ°ååŒåŠžå…¬ï¼ˆå®¡æ‰¹åœ¨ååŒåŠžå…¬å®Œæˆï¼Œä¸åœ¨æœ¬é¡µé¢å®¡æ‰¹ï¼‰
        submitPurchaseOrder(purchaseOrder.getId(), PURCHASE_ORDER_APPROVE_PROCESS_DEFINITION_KEY, getLoginUserId());
        return purchaseOrder.getId();
    }
@@ -288,7 +297,7 @@
        if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
            return ErpAuditStatus.APPROVE.getStatus();
        } else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) {
            return ErpAuditStatus.PROCESS.getStatus();
            return ErpAuditStatus.REJECT.getStatus();
        } else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
            return ErpAuditStatus.CANCEL.getStatus();
        }
@@ -406,6 +415,11 @@
    }
    @Override
    public ErpPurchaseOrderDO getPurchaseOrderByProcessInstanceId(String processInstanceId) {
        return purchaseOrderMapper.selectByProcessInstanceId(processInstanceId);
    }
    @Override
    public ErpPurchaseOrderDO validatePurchaseOrder(Long id) {
        ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id);
        if (ObjectUtil.notEqual(purchaseOrder.getStatus(), ErpAuditStatus.APPROVE.getStatus())) {
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseOrderStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.erp.service.purchase.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseOrderService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
 * é‡‡è´­è®¢å•审批状态监听器
 *
 * ç›‘听 BPM æµç¨‹å®žä¾‹çŠ¶æ€å˜æ›´äº‹ä»¶ï¼Œæ›´æ–°é‡‡è´­è®¢å•çš„å®¡æ‰¹çŠ¶æ€ï¼ˆååŒåŠžå…¬å®¡æ‰¹åŽå›žå†™ï¼‰
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class ErpPurchaseOrderStatusListener extends BpmProcessInstanceStatusEventListener {
    /**
     * é‡‡è´­è®¢å•审批流程定义 Key
     */
    private static final String PROCESS_DEFINITION_KEY = "purchase_order_approve";
    @Resource
    private ErpPurchaseOrderService purchaseOrderService;
    @Override
    protected String getProcessDefinitionKey() {
        return PROCESS_DEFINITION_KEY;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Long orderId;
        try {
            orderId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            log.warn("[onEvent] businessKey({}) ä¸æ˜¯æœ‰æ•ˆçš„采购订单编号", event.getBusinessKey());
            return;
        }
        log.info("[onEvent] æ”¶åˆ°BPM审批事件,orderId={}, status={}", orderId, event.getStatus());
        try {
            purchaseOrderService.updatePurchaseOrderAuditStatus(orderId, event.getStatus());
            log.info("[onEvent] é‡‡è´­è®¢å•({}) å®¡æ‰¹çŠ¶æ€æ›´æ–°å®Œæˆ", orderId);
        } catch (Exception e) {
            log.error("[onEvent] æ›´æ–°é‡‡è´­è®¢å•审批状态失败,orderId={}", orderId, e);
        }
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -21,6 +21,7 @@
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsCreateReqDTO;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
@@ -50,6 +51,11 @@
@Validated
@Slf4j
public class ErpSaleOrderServiceImpl implements ErpSaleOrderService {
    /**
     * MPS çŠ¶æ€ï¼šå·²å–æ¶ˆï¼ˆå¯¹åº” MES æ¨¡å— MesProMpsStatusEnum.CANCELED çš„值 3)
     */
    private static final Integer MPS_STATUS_CANCELED = 3;
    @Resource
    private ErpSaleOrderMapper saleOrderMapper;
@@ -205,6 +211,13 @@
            if (item.getNeedProduction() == null || item.getNeedProduction() != 1) {
                continue;
            }
            // é¿å…å®¡æ‰¹/反审批重复操作导致同一个销售订单号产生多条 MPS
            MesProMpsRespDTO existingMps = mpsApi.getMpsBySaleOrderItemId(item.getId());
            if (existingMps != null && ObjectUtil.notEqual(existingMps.getStatus(), MPS_STATUS_CANCELED)) {
                log.info("[createMpsForSaleOrder] é”€å”®è®¢å•明细({}) å·²å­˜åœ¨ MPS({}),状态={},跳过创建",
                        item.getId(), existingMps.getId(), existingMps.getStatus());
                continue;
            }
            try {
                Long mpsId = createMpsForSaleOrderItem(saleOrder, item);
                if (mpsId != null) {
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
@@ -55,6 +55,15 @@
    ErrorCode RESIGNATION_APPLICATION_EXISTS = new ErrorCode(1_030_003_005, "该员工已存在离职申请,请勿重复提交");
    ErrorCode RESIGNATION_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_003_006, "离职审批流程不存在");
    // ========== åŠ ç­ç”³è¯· 1-030-008-000 ==========
    ErrorCode OVERTIME_APPLICATION_NOT_EXISTS = new ErrorCode(1_030_008_000, "加班申请不存在");
    ErrorCode OVERTIME_APPLICATION_UPDATE_FAIL_NOT_DRAFT = new ErrorCode(1_030_008_001, "加班申请更新失败,原因:不是草稿状态");
    ErrorCode OVERTIME_APPLICATION_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_030_008_002, "加班申请提交失败,原因:不是草稿状态");
    ErrorCode OVERTIME_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_030_008_003, "更新加班申请审批状态失败,原因:不是审批中状态");
    ErrorCode OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_008_004, "加班审批流程不存在,请先在协同办公中配置并发布流程");
    ErrorCode OVERTIME_APPLICATION_EMPLOYEE_NOT_EXISTS = new ErrorCode(1_030_008_005, "当前账号未关联员工档案");
    ErrorCode OVERTIME_APPLICATION_TIME_INVALID = new ErrorCode(1_030_008_006, "加班时段或扣除时长不合法");
    // ========== ç”¨æˆ·æ•°æ®äº¤æŽ¥ 1-030-007-000 ==========
    ErrorCode USER_HANDOVER_NOT_EXISTS = new ErrorCode(1_030_007_000, "用户交接记录不存在");
    ErrorCode USER_HANDOVER_ALREADY_EXISTS = new ErrorCode(1_030_007_001, "该离职申请已存在交接记录");
@@ -93,6 +102,7 @@
    ErrorCode EMPLOYEE_SOCIAL_SECURITY_NOT_EXISTS = new ErrorCode(1_030_005_009, "员工社保公积金档案不存在");
    ErrorCode SOCIAL_SECURITY_SCHEME_NOT_EXISTS = new ErrorCode(1_030_005_010, "社保公积金方案不存在");
    ErrorCode LEAVE_TYPE_CONFIG_NOT_EXISTS = new ErrorCode(1_030_005_011, "请假类型扣款配置不存在");
    ErrorCode LEAVE_TYPE_CONFIG_TYPE_EXISTS = new ErrorCode(1_030_005_022, "该请假类型扣款配置已存在");
    ErrorCode SALARY_PAYMENT_EXISTS = new ErrorCode(1_030_005_012, "该周期薪资发放台账已存在");
    ErrorCode SALARY_CALCULATION_NO_CONFIRMED = new ErrorCode(1_030_005_013, "该周期没有已确认的薪酬核算记录");
    ErrorCode SALARY_PAYMENT_ALREADY_PAY = new ErrorCode(1_030_005_014, "薪资发放台账已发放,请勿重复操作");
@@ -106,5 +116,7 @@
    ErrorCode SALARY_PAYMENT_REVOKE_FAIL = new ErrorCode(1_030_005_022, "撤销发放失败,原因:该明细未发放");
    ErrorCode SALARY_PAYMENT_DELETE_FAIL = new ErrorCode(1_030_005_023, "删除失败,原因:仅待发放状态可删除,请先撤销已发放明细");
    ErrorCode SALARY_PAYMENT_HAS_PAID_DETAIL = new ErrorCode(1_030_005_024, "删除失败,原因:存在已发放明细,请先撤销");
    ErrorCode SALARY_STRUCTURE_NAME_EXISTS = new ErrorCode(1_030_005_025, "薪酬结构名称已存在");
    ErrorCode SALARY_STRUCTURE_IN_USE = new ErrorCode(1_030_005_026, "薪酬结构正在被员工使用,无法删除");
}
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmOvertimeTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.hrm.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.math.BigDecimal;
import java.util.Arrays;
/** åŠ ç­ç±»åž‹åŠæ³•å®šå·¥èµ„å€çŽ‡ã€‚ */
@Getter
@RequiredArgsConstructor
public enum HrmOvertimeTypeEnum {
    WORKDAY(1, "工作日加班", new BigDecimal("1.5")),
    REST_DAY(2, "休息日加班", new BigDecimal("2.0")),
    LEGAL_HOLIDAY(3, "法定节假日加班", new BigDecimal("3.0"));
    private final Integer type;
    private final String name;
    private final BigDecimal salaryMultiplier;
    public static HrmOvertimeTypeEnum valueOfType(Integer type) {
        return Arrays.stream(values()).filter(item -> item.type.equals(type)).findFirst().orElse(null);
    }
    public static String getName(Integer type) {
        HrmOvertimeTypeEnum value = valueOfType(type);
        return value == null ? "" : value.name;
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/HrmOvertimeApplicationController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
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.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.*;
import cn.iocoder.yudao.module.hrm.service.approval.HrmOvertimeApplicationService;
import io.swagger.v3.oas.annotations.Operation;
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/overtime")
@Validated
public class HrmOvertimeApplicationController {
    @Resource private HrmOvertimeApplicationService service;
    @Resource private SecurityFrameworkService securityFrameworkService;
    @PostMapping("/create") @PreAuthorize("@ss.hasPermission('hrm:overtime:create')")
    public CommonResult<Long> create(@Valid @RequestBody HrmOvertimeApplicationSaveReqVO reqVO) {
        return success(service.create(reqVO, SecurityFrameworkUtils.getLoginUserId()));
    }
    @PutMapping("/update") @PreAuthorize("@ss.hasPermission('hrm:overtime:update')")
    public CommonResult<Boolean> update(@Valid @RequestBody HrmOvertimeApplicationSaveReqVO reqVO) {
        service.update(reqVO, SecurityFrameworkUtils.getLoginUserId()); return success(true);
    }
    @DeleteMapping("/delete") @PreAuthorize("@ss.hasPermission('hrm:overtime:delete')")
    public CommonResult<Boolean> delete(@RequestParam Long id) {
        service.delete(id, SecurityFrameworkUtils.getLoginUserId()); return success(true);
    }
    @GetMapping("/get") @PreAuthorize("@ss.hasAnyPermissions('hrm:overtime:query', 'bpm:task:query')")
    public CommonResult<HrmOvertimeApplicationRespVO> get(@RequestParam Long id) {
        return success(service.getDetail(id, SecurityFrameworkUtils.getLoginUserId(), canManage()));
    }
    @GetMapping("/current-applicant") @PreAuthorize("@ss.hasPermission('hrm:overtime:create')")
    public CommonResult<HrmOvertimeApplicationRespVO> currentApplicant() {
        return success(service.getCurrentApplicant(SecurityFrameworkUtils.getLoginUserId()));
    }
    @GetMapping("/page") @PreAuthorize("@ss.hasPermission('hrm:overtime:query')")
    public CommonResult<PageResult<HrmOvertimeApplicationRespVO>> page(@Valid HrmOvertimeApplicationPageReqVO reqVO) {
        if (!canManage()) {
            reqVO.setUserId(SecurityFrameworkUtils.getLoginUserId());
        }
        return success(service.getPage(reqVO));
    }
    @PostMapping("/submit") @PreAuthorize("@ss.hasPermission('hrm:overtime:submit')")
    public CommonResult<Boolean> submit(@RequestParam Long id, @RequestParam String processDefinitionKey) {
        service.submit(id, processDefinitionKey, SecurityFrameworkUtils.getLoginUserId()); return success(true);
    }
    @PostMapping("/cancel") @PreAuthorize("@ss.hasPermission('hrm:overtime:cancel')")
    public CommonResult<Boolean> cancel(@RequestParam Long id) {
        service.cancel(id, SecurityFrameworkUtils.getLoginUserId()); return success(true);
    }
    @GetMapping("/approve-process-definition-list") @PreAuthorize("@ss.hasPermission('hrm:overtime:query')")
    public CommonResult<List<Map<String, Object>>> processes() { return success(service.getApproveProcessDefinitionList()); }
    private boolean canManage() {
        return securityFrameworkService.hasPermission("hrm:overtime:manage");
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.hrm.controller.admin.approval.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDate;
@Data
@EqualsAndHashCode(callSuper = true)
public class HrmOvertimeApplicationPageReqVO extends PageParam {
    private String no;
    private Long userId;
    private Long deptId;
    private LocalDate overtimeDate;
    private Integer overtimeType;
    private Integer status;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.hrm.controller.admin.approval.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.time.LocalTime;
@Schema(description = "管理后台 - åŠ ç­ç”³è¯· Response VO")
@Data
public class HrmOvertimeApplicationRespVO {
    private Long id;
    private String no;
    private Long userId;
    private Long employeeId;
    private String employeeName;
    private String employeeNo;
    private Long deptId;
    private String deptName;
    private String postName;
    private LocalDate overtimeDate;
    private LocalTime startTime;
    private LocalTime endTime;
    private BigDecimal duration;
    private Integer overtimeType;
    private String overtimeTypeName;
    private Boolean crossDay;
    private Boolean mealDeduction;
    private BigDecimal mealDeductionHours;
    private String reason;
    private Integer relatedType;
    private String relatedCode;
    private String workContent;
    private Integer status;
    private String statusName;
    private String processInstanceId;
    private LocalDateTime approveTime;
    private LocalDateTime createTime;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmOvertimeApplicationSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.hrm.controller.admin.approval.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalTime;
@Schema(description = "管理后台 - åŠ ç­ç”³è¯·æ–°å¢ž/修改 Request VO")
@Data
public class HrmOvertimeApplicationSaveReqVO {
    private Long id;
    @NotNull(message = "加班日期不能为空")
    private LocalDate overtimeDate;
    @NotNull(message = "开始时间不能为空")
    private LocalTime startTime;
    @NotNull(message = "结束时间不能为空")
    private LocalTime endTime;
    @NotNull(message = "加班时长不能为空")
    @DecimalMin(value = "0.5", message = "加班时长不能小于 0.5 å°æ—¶")
    private BigDecimal duration;
    @NotNull(message = "加班类型不能为空")
    private Integer overtimeType;
    private Boolean crossDay;
    private Boolean mealDeduction;
    private BigDecimal mealDeductionHours;
    @NotBlank(message = "加班原因不能为空")
    private String reason;
    private Integer relatedType;
    private String relatedCode;
    private String workContent;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java
@@ -7,6 +7,8 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordImportExcelVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordImportReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordRespVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
@@ -142,6 +144,7 @@
            HrmEmployeeDO employee = employeeMap.get(respVO.getUserId());
            if (employee != null) {
                respVO.setUserName(employee.getName());
                respVO.setPhone(employee.getPhone());
            }
            // éƒ¨é—¨åç§°
            DeptRespDTO dept = deptMap.get(respVO.getDeptId());
@@ -166,6 +169,7 @@
            HrmEmployeeDO employee = employeeService.getEmployee(attendanceRecord.getUserId());
            if (employee != null) {
                respVO.setUserName(employee.getName());
                respVO.setPhone(employee.getPhone());
            }
        }
        if (attendanceRecord.getDeptId() != null) {
@@ -177,6 +181,104 @@
        return success(respVO);
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "下载考勤记录导入模板")
    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:import')")
    public void importTemplate(HttpServletResponse response) throws IOException {
        List<HrmAttendanceRecordImportExcelVO> list = Collections.singletonList(
                HrmAttendanceRecordImportExcelVO.builder()
                        .userName("张三")
                        .phone("13800138000")
                        .date(java.time.LocalDate.of(2026, 1, 15))
                        .clockInTime(java.time.LocalDateTime.of(2026, 1, 15, 8, 30))
                        .clockOutTime(java.time.LocalDateTime.of(2026, 1, 15, 17, 30))
                        .clockInType(1)
                        .clockOutType(1)
                        .location("办公区")
                        .remark("示例数据")
                        .build()
        );
        ExcelUtils.write(response, "考勤导入模板.xls", "考勤记录", HrmAttendanceRecordImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入考勤记录")
    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:import')")
    public CommonResult<String> importExcel(@Valid HrmAttendanceRecordImportReqVO importReqVO) throws IOException {
        List<HrmAttendanceRecordImportExcelVO> list = ExcelUtils.read(importReqVO.getFile(), HrmAttendanceRecordImportExcelVO.class);
        if (CollUtil.isEmpty(list)) {
            return success("没有可导入的数据");
        }
        int successCount = 0;
        Set<String> missingNames = new LinkedHashSet<>();
        Set<String> duplicateNames = new LinkedHashSet<>();
        Set<String> invalidRows = new LinkedHashSet<>();
        for (HrmAttendanceRecordImportExcelVO item : list) {
            if (item.getDate() == null) {
                invalidRows.add((item.getUserName() == null || item.getUserName().isBlank()) ? "未填姓名的行" : "考勤日期为空(" + item.getUserName().trim() + ")");
                continue;
            }
            if (item.getUserName() == null || item.getUserName().isBlank()) {
                invalidRows.add("考勤日期为 " + item.getDate() + " ä½†æœªå¡«å‘˜å·¥å§“名");
                continue;
            }
            List<HrmEmployeeDO> employees = employeeService.getEmployeeListByName(item.getUserName().trim());
            if (employees.isEmpty()) {
                missingNames.add(item.getUserName().trim());
                continue;
            }
            HrmEmployeeDO employee = employees.get(0);
            // å­˜åœ¨å¤šä¸ªåŒåå‘˜å·¥æ—¶ï¼Œç”¨æ‰‹æœºå·åŒºåˆ†
            if (employees.size() > 1) {
                String phone = item.getPhone() != null ? item.getPhone().trim() : null;
                List<HrmEmployeeDO> matchedByPhone = new ArrayList<>();
                if (phone != null && !phone.isEmpty()) {
                    for (HrmEmployeeDO emp : employees) {
                        if (phone.equals(emp.getPhone())) {
                            matchedByPhone.add(emp);
                        }
                    }
                }
                if (matchedByPhone.size() == 1) {
                    employee = matchedByPhone.get(0);
                } else {
                    duplicateNames.add(item.getUserName().trim());
                    continue;
                }
            }
            HrmAttendanceRecordDO record = attendanceRecordService.getAttendanceRecordByUserIdAndDate(employee.getId(), item.getDate());
            if (record == null) {
                record = HrmAttendanceRecordDO.builder()
                        .userId(employee.getId()).deptId(employee.getDeptId()).date(item.getDate())
                        .clockInTime(item.getClockInTime()).clockOutTime(item.getClockOutTime())
                        .clockInType(item.getClockInType()).clockOutType(item.getClockOutType())
                        .location(item.getLocation()).remark(item.getRemark())
                        .dataSource(2).build();
                attendanceRecordService.createImportedRecord(record);
            } else if (Boolean.TRUE.equals(importReqVO.getUpdateSupport())) {
                record.setClockInTime(item.getClockInTime());
                record.setClockOutTime(item.getClockOutTime());
                record.setClockInType(item.getClockInType());
                record.setClockOutType(item.getClockOutType());
                record.setLocation(item.getLocation());
                record.setRemark(item.getRemark());
                attendanceRecordService.updateImportedRecord(record);
            }
            successCount++;
        }
        StringBuilder msg = new StringBuilder("成功导入 " + successCount + " æ¡");
        if (!missingNames.isEmpty()) {
            msg.append(";以下员工不存在,未能导入:").append(String.join("、", missingNames));
        }
        if (!duplicateNames.isEmpty()) {
            msg.append(";以下员工存在多名同名员工,需补充手机号区分,未能导入:").append(String.join("、", duplicateNames));
        }
        if (!invalidRows.isEmpty()) {
            msg.append(";以下记录数据不完整,未能导入:").append(String.join("、", invalidRows));
        }
        return success(msg.toString());
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出考勤记录 Excel")
    @PreAuthorize("@ss.hasPermission('hrm:attendance:export')")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * è€ƒå‹¤è®°å½• Excel å¯¼å…¥æ¨¡æ¿ VO
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class HrmAttendanceRecordImportExcelVO {
    @ExcelProperty("员工姓名")
    private String userName;
    @ExcelProperty("手机号")
    private String phone;
    @ExcelProperty("考勤日期")
    private LocalDate date;
    @ExcelProperty("上班打卡时间")
    private LocalDateTime clockInTime;
    @ExcelProperty("下班打卡时间")
    private LocalDateTime clockOutTime;
    @ExcelProperty(value = "上班打卡类型", converter = DictConvert.class)
    @DictFormat("hrm_clock_type")
    private Integer clockInType;
    @ExcelProperty(value = "下班打卡类型", converter = DictConvert.class)
    @DictFormat("hrm_clock_type")
    private Integer clockOutType;
    @ExcelProperty("打卡地点")
    private String location;
    @ExcelProperty("备注")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordImportReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Schema(description = "管理后台 - è€ƒå‹¤è®°å½•导入 Request VO")
@Data
public class HrmAttendanceRecordImportReqVO {
    @Schema(description = "Excel æ–‡ä»¶", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "Excel æ–‡ä»¶ä¸èƒ½ä¸ºç©º")
    private MultipartFile file;
    @Schema(description = "是否覆盖已存在的数据")
    private Boolean updateSupport = false;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordRespVO.java
@@ -34,6 +34,10 @@
    @ExcelProperty("部门名称")
    private String deptName;
    @Schema(description = "手机号")
    @ExcelProperty("手机号")
    private String phone;
    @Schema(description = "考勤日期")
    @ExcelProperty("考勤日期")
    private LocalDate date;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmLeaveTypeConfigController.java
@@ -2,6 +2,7 @@
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.salary.vo.HrmLeaveTypeConfigCreateReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmLeaveTypeConfigRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.salary.vo.HrmLeaveTypeConfigSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmLeaveTypeConfigDO;
@@ -30,7 +31,7 @@
    @GetMapping("/list")
    @Operation(summary = "获取所有请假类型配置列表")
    @PreAuthorize("@ss.hasPermission('hrm:salary:query')")
    @PreAuthorize("@ss.hasPermission('hrm:leave-type-config:query')")
    public CommonResult<List<HrmLeaveTypeConfigRespVO>> getLeaveTypeConfigList() {
        List<HrmLeaveTypeConfigDO> list = leaveTypeConfigService.getLeaveTypeConfigList();
        return success(BeanUtils.toBean(list, HrmLeaveTypeConfigRespVO.class));
@@ -39,19 +40,37 @@
    @GetMapping("/get")
    @Operation(summary = "获取请假类型配置")
    @Parameter(name = "leaveType", description = "请假类型", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('hrm:salary:query')")
    @PreAuthorize("@ss.hasPermission('hrm:leave-type-config:query')")
    public CommonResult<HrmLeaveTypeConfigRespVO> getLeaveTypeConfig(@RequestParam("leaveType") Integer leaveType) {
        HrmLeaveTypeConfigDO config = leaveTypeConfigService.getLeaveTypeConfig(leaveType);
        return success(BeanUtils.toBean(config, HrmLeaveTypeConfigRespVO.class));
    }
    @PostMapping("/create")
    @Operation(summary = "新增请假类型配置")
    @PreAuthorize("@ss.hasPermission('hrm:leave-type-config:create')")
    public CommonResult<Long> createLeaveTypeConfig(@Valid @RequestBody HrmLeaveTypeConfigCreateReqVO createReqVO) {
        HrmLeaveTypeConfigDO config = BeanUtils.toBean(createReqVO, HrmLeaveTypeConfigDO.class);
        leaveTypeConfigService.createLeaveTypeConfig(config);
        return success(config.getId());
    }
    @PutMapping("/update")
    @Operation(summary = "更新请假类型配置")
    @PreAuthorize("@ss.hasPermission('hrm:salary:update')")
    @PreAuthorize("@ss.hasPermission('hrm:leave-type-config:update')")
    public CommonResult<Boolean> updateLeaveTypeConfig(@Valid @RequestBody HrmLeaveTypeConfigSaveReqVO updateReqVO) {
        HrmLeaveTypeConfigDO config = BeanUtils.toBean(updateReqVO, HrmLeaveTypeConfigDO.class);
        leaveTypeConfigService.updateLeaveTypeConfig(config);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除请假类型配置")
    @Parameter(name = "id", description = "配置ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:leave-type-config:delete')")
    public CommonResult<Boolean> deleteLeaveTypeConfig(@RequestParam("id") Long id) {
        leaveTypeConfigService.deleteLeaveTypeConfig(id);
        return success(true);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryStructureController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary;
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.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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
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 salaryStructureService;
    @PostMapping("/create")
    @Operation(summary = "创建薪酬结构")
    @PreAuthorize("@ss.hasPermission('hrm:salary-structure:create')")
    public CommonResult<Long> createSalaryStructure(@Valid @RequestBody HrmSalaryStructureSaveReqVO reqVO) {
        return success(salaryStructureService.createSalaryStructure(
                BeanUtils.toBean(reqVO, HrmSalaryStructureDO.class)));
    }
    @PutMapping("/update")
    @Operation(summary = "更新薪酬结构")
    @PreAuthorize("@ss.hasPermission('hrm:salary-structure:update')")
    public CommonResult<Boolean> updateSalaryStructure(@Valid @RequestBody HrmSalaryStructureSaveReqVO reqVO) {
        salaryStructureService.updateSalaryStructure(BeanUtils.toBean(reqVO, HrmSalaryStructureDO.class));
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除薪酬结构")
    @Parameter(name = "id", description = "结构编号", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:salary-structure:delete')")
    public CommonResult<Boolean> deleteSalaryStructure(@RequestParam("id") Long id) {
        salaryStructureService.deleteSalaryStructure(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得薪酬结构")
    @PreAuthorize("@ss.hasPermission('hrm:salary-structure:query')")
    public CommonResult<HrmSalaryStructureRespVO> getSalaryStructure(@RequestParam("id") Long id) {
        return success(BeanUtils.toBean(salaryStructureService.getSalaryStructure(id),
                HrmSalaryStructureRespVO.class));
    }
    @GetMapping("/list")
    @Operation(summary = "获得薪酬结构列表")
    @PreAuthorize("@ss.hasPermission('hrm:salary-structure:query')")
    public CommonResult<List<HrmSalaryStructureRespVO>> getSalaryStructureList(
            @RequestParam(value = "name", required = false) String name,
            @RequestParam(value = "status", required = false) Integer status) {
        return success(BeanUtils.toBean(salaryStructureService.getSalaryStructureList(name, status),
                HrmSalaryStructureRespVO.class));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmLeaveTypeConfigCreateReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - è¯·å‡ç±»åž‹æ‰£æ¬¾é…ç½®æ–°å¢ž Request VO")
@Data
public class HrmLeaveTypeConfigCreateReqVO {
    @Schema(description = "请假类型:1-事假, 2-病假, 3-年假, 4-婚假, 5-产假, 6-丧假", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "请假类型不能为空")
    private Integer leaveType;
    @Schema(description = "类型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "事假")
    @NotBlank(message = "类型名称不能为空")
    private String name;
    @Schema(description = "扣款比例(0-1之间,如0.5表示扣50%)", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.5")
    @NotNull(message = "扣款比例不能为空")
    private BigDecimal deductionRatio;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "状态:0-启用, 1-禁用")
    private Integer status;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryStructureRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
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 = "结构编号")
    private Long id;
    @Schema(description = "结构名称")
    private String name;
    @Schema(description = "工作日加班工资倍率")
    private BigDecimal overtimePayRatio;
    @Schema(description = "休息日加班工资倍率")
    private BigDecimal restDayOvertimePayRatio;
    @Schema(description = "法定节假日加班工资倍率")
    private BigDecimal legalHolidayOvertimePayRatio;
    @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,51 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - è–ªé…¬ç»“构新增/修改 Request VO")
@Data
public class HrmSalaryStructureSaveReqVO {
    @Schema(description = "结构编号", example = "1")
    private Long id;
    @Schema(description = "结构名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "标准员工薪酬")
    @NotBlank(message = "结构名称不能为空")
    @Size(max = 100, message = "结构名称不能超过 100 ä¸ªå­—符")
    private String name;
    @Schema(description = "工作日加班工资倍率,0 è¡¨ç¤ºä¸è®¡åŠ ç­å·¥èµ„", example = "1.50")
    @NotNull(message = "工作日加班倍率不能为空")
    @DecimalMin(value = "0", message = "工作日加班倍率不能小于 0")
    @DecimalMax(value = "10", message = "工作日加班倍率不能大于 10")
    private BigDecimal overtimePayRatio;
    @Schema(description = "休息日加班工资倍率,0 è¡¨ç¤ºä¸è®¡è¯¥ç±»åž‹åŠ ç­å·¥èµ„", example = "2.00")
    @NotNull(message = "休息日加班倍率不能为空")
    @DecimalMin(value = "0", message = "休息日加班倍率不能小于 0")
    @DecimalMax(value = "10", message = "休息日加班倍率不能大于 10")
    private BigDecimal restDayOvertimePayRatio;
    @Schema(description = "法定节假日加班工资倍率,0 è¡¨ç¤ºä¸è®¡è¯¥ç±»åž‹åŠ ç­å·¥èµ„", example = "3.00")
    @NotNull(message = "法定节假日加班倍率不能为空")
    @DecimalMin(value = "0", message = "法定节假日加班倍率不能小于 0")
    @DecimalMax(value = "10", message = "法定节假日加班倍率不能大于 10")
    private BigDecimal legalHolidayOvertimePayRatio;
    @Schema(description = "状态:0-启用,1-禁用", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @NotNull(message = "状态不能为空")
    private Integer status;
    @Schema(description = "备注")
    @Size(max = 500, message = "备注不能超过 500 ä¸ªå­—符")
    private String remark;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmOvertimeApplicationDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
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.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@TableName("hrm_overtime_application")
@KeySequence("hrm_overtime_application_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HrmOvertimeApplicationDO extends BaseDO {
    @TableId
    private Long id;
    private String no;
    /** ç³»ç»Ÿç”¨æˆ· ID */
    private Long userId;
    private Long deptId;
    private Long employeeId;
    private LocalDate overtimeDate;
    private LocalTime startTime;
    private LocalTime endTime;
    private BigDecimal duration;
    private Integer overtimeType;
    private Boolean crossDay;
    private Boolean mealDeduction;
    private BigDecimal mealDeductionHours;
    private String reason;
    /** 1-项目,2-工单 */
    private Integer relatedType;
    private String relatedCode;
    private String workContent;
    private Integer status;
    private String processInstanceId;
    private LocalDateTime approveTime;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryStructureDO.java
@@ -45,9 +45,19 @@
    private BigDecimal performanceRatio;
    /**
     * åŠ ç­å·¥èµ„å€çŽ‡
     * å·¥ä½œæ—¥åŠ ç­å·¥èµ„å€çŽ‡
     */
    private BigDecimal overtimePayRatio;
    /**
     * ä¼‘息日加班工资倍率
     */
    private BigDecimal restDayOvertimePayRatio;
    /**
     * æ³•定节假日加班工资倍率
     */
    private BigDecimal legalHolidayOvertimePayRatio;
    /**
     * ç¤¾ä¿åŸºæ•°
@@ -69,4 +79,4 @@
     */
    private String remark;
}
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/approval/HrmOvertimeApplicationMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
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.HrmOvertimeApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmOvertimeApplicationDO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDate;
import java.util.List;
@Mapper
public interface HrmOvertimeApplicationMapper extends BaseMapperX<HrmOvertimeApplicationDO> {
    default PageResult<HrmOvertimeApplicationDO> selectPage(HrmOvertimeApplicationPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<HrmOvertimeApplicationDO>()
                .likeIfPresent(HrmOvertimeApplicationDO::getNo, reqVO.getNo())
                .eqIfPresent(HrmOvertimeApplicationDO::getUserId, reqVO.getUserId())
                .eqIfPresent(HrmOvertimeApplicationDO::getDeptId, reqVO.getDeptId())
                .eqIfPresent(HrmOvertimeApplicationDO::getOvertimeDate, reqVO.getOvertimeDate())
                .eqIfPresent(HrmOvertimeApplicationDO::getOvertimeType, reqVO.getOvertimeType())
                .eqIfPresent(HrmOvertimeApplicationDO::getStatus, reqVO.getStatus())
                .orderByDesc(HrmOvertimeApplicationDO::getId));
    }
    default HrmOvertimeApplicationDO selectByProcessInstanceId(String processInstanceId) {
        return selectOne(HrmOvertimeApplicationDO::getProcessInstanceId, processInstanceId);
    }
    default List<HrmOvertimeApplicationDO> selectApprovedByUserAndDateRange(Long userId, LocalDate start, LocalDate end) {
        return selectList(new LambdaQueryWrapperX<HrmOvertimeApplicationDO>()
                .eq(HrmOvertimeApplicationDO::getUserId, userId)
                .eq(HrmOvertimeApplicationDO::getStatus, HrmAuditStatusEnum.APPROVE.getStatus())
                .between(HrmOvertimeApplicationDO::getOvertimeDate, start, end));
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java
@@ -21,6 +21,14 @@
        return selectOne(HrmEmployeeDO::getPhone, phone);
    }
    default HrmEmployeeDO selectByName(String name) {
        return selectOne(HrmEmployeeDO::getName, name);
    }
    default List<HrmEmployeeDO> selectListByName(String name) {
        return selectList(HrmEmployeeDO::getName, name);
    }
    default HrmEmployeeDO selectByEmployeeNo(String employeeNo) {
        return selectOne(HrmEmployeeDO::getEmployeeNo, employeeNo);
    }
@@ -52,4 +60,8 @@
                .orderByAsc(HrmEmployeeDO::getName));
    }
}
    default Long selectCountBySalaryStructureId(Long salaryStructureId) {
        return selectCount(HrmEmployeeDO::getSalaryStructureId, salaryStructureId);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmEmployeeSalaryMapper.java
@@ -36,4 +36,8 @@
                .orderByDesc(HrmEmployeeSalaryDO::getEffectiveDate));
    }
}
    default Long selectCountBySalaryStructureId(Long salaryStructureId) {
        return selectCount(HrmEmployeeSalaryDO::getSalaryStructureId, salaryStructureId);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/salary/HrmSalaryStructureMapper.java
@@ -15,10 +15,19 @@
@Mapper
public interface HrmSalaryStructureMapper extends BaseMapperX<HrmSalaryStructureDO> {
    default List<HrmSalaryStructureDO> selectListByStatus(Integer status) {
    default List<HrmSalaryStructureDO> selectListByCondition(String name, Integer status) {
        return selectList(new LambdaQueryWrapperX<HrmSalaryStructureDO>()
                .likeIfPresent(HrmSalaryStructureDO::getName, name)
                .eqIfPresent(HrmSalaryStructureDO::getStatus, status)
                .orderByAsc(HrmSalaryStructureDO::getName));
    }
}
    default List<HrmSalaryStructureDO> selectListByStatus(Integer status) {
        return selectListByCondition(null, status);
    }
    default HrmSalaryStructureDO selectByName(String name) {
        return selectOne(HrmSalaryStructureDO::getName, name);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/redis/HrmNoRedisDAO.java
@@ -52,6 +52,9 @@
     */
    public static final String RESIGNATION_APPLICATION_NO_PREFIX = "RA";
    /** åŠ ç­ç”³è¯·ç¼–å·å‰ç¼€ */
    public static final String OVERTIME_APPLICATION_NO_PREFIX = "OT";
    /**
     * å‘˜å·¥åˆåŒç¼–号前缀
     */
@@ -100,6 +103,10 @@
        return generate(RESIGNATION_APPLICATION_NO_PREFIX, 4);
    }
    public String generateOvertimeApplicationNo() {
        return generate(OVERTIME_APPLICATION_NO_PREFIX, 4);
    }
    /**
     * ç”Ÿæˆå‘˜å·¥åˆåŒç¼–号,格式:HT + yyyyMMdd + 4位自增
     * ä¾‹å¦‚:HT202401010001
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
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.HrmOvertimeApplicationPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmOvertimeApplicationRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.HrmOvertimeApplicationSaveReqVO;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
public interface HrmOvertimeApplicationService {
    String BPM_PROCESS_DEFINITION_KEY = "hrm_overtime";
    String APPROVE_CATEGORY_CODE = "overtime_approve";
    Long create(@Valid HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId);
    void update(@Valid HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId);
    void delete(Long id, Long loginUserId);
    HrmOvertimeApplicationRespVO getDetail(Long id, Long loginUserId, boolean canManage);
    HrmOvertimeApplicationRespVO getCurrentApplicant(Long loginUserId);
    PageResult<HrmOvertimeApplicationRespVO> getPage(HrmOvertimeApplicationPageReqVO reqVO);
    void submit(Long id, String processDefinitionKey, Long loginUserId);
    void cancel(Long id, Long loginUserId);
    List<Map<String, Object>> getApproveProcessDefinitionList();
    void updateAuditStatus(Long id, Integer status);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmOvertimeApplicationServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,263 @@
package cn.iocoder.yudao.module.hrm.service.approval;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil;
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.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
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.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import cn.iocoder.yudao.module.hrm.controller.admin.approval.vo.*;
import cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmOvertimeApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmOvertimeApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum;
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 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 java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
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
@Slf4j
public class HrmOvertimeApplicationServiceImpl implements HrmOvertimeApplicationService {
    @Resource private HrmOvertimeApplicationMapper mapper;
    @Resource private HrmEmployeeMapper employeeMapper;
    @Resource private HrmNoRedisDAO noRedisDAO;
    @Resource private DeptApi deptApi;
    @Resource private PostApi postApi;
    @Resource private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Resource private BpmProcessInstanceService bpmProcessInstanceService;
    @Resource private BpmTaskService bpmTaskService;
    @Resource private BpmCategoryService bpmCategoryService;
    @Resource private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long create(HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId) {
        HrmEmployeeDO employee = requireEmployee(loginUserId);
        HrmOvertimeApplicationDO application = BeanUtils.toBean(reqVO, HrmOvertimeApplicationDO.class);
        applyApplicantAndCalculatedTime(application, employee);
        application.setNo(noRedisDAO.generateOvertimeApplicationNo());
        application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
        mapper.insert(application);
        return application.getId();
    }
    @Override
    public void update(HrmOvertimeApplicationSaveReqVO reqVO, Long loginUserId) {
        HrmOvertimeApplicationDO existing = requireOwnedDraft(reqVO.getId(), loginUserId);
        HrmOvertimeApplicationDO update = BeanUtils.toBean(reqVO, HrmOvertimeApplicationDO.class);
        applyApplicantAndCalculatedTime(update, requireEmployee(loginUserId));
        update.setId(existing.getId());
        mapper.updateById(update);
    }
    @Override
    public void delete(Long id, Long loginUserId) {
        HrmOvertimeApplicationDO application = require(id);
        boolean deletable = Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())
                || Objects.equals(application.getStatus(), HrmAuditStatusEnum.CANCEL.getStatus());
        if (!Objects.equals(application.getUserId(), loginUserId) || !deletable) {
            throw exception(OVERTIME_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        mapper.deleteById(id);
    }
    @Override
    public HrmOvertimeApplicationRespVO getDetail(Long id, Long loginUserId, boolean canManage) {
        HrmOvertimeApplicationDO application = mapper.selectById(id);
        if (application != null && !canManage && !Objects.equals(application.getUserId(), loginUserId)
                && !isProcessParticipant(application.getProcessInstanceId(), loginUserId)) {
            return null;
        }
        return application == null ? null : buildResp(application);
    }
    @Override
    public HrmOvertimeApplicationRespVO getCurrentApplicant(Long loginUserId) {
        HrmEmployeeDO employee = requireEmployee(loginUserId);
        HrmOvertimeApplicationRespVO resp = new HrmOvertimeApplicationRespVO();
        fillEmployee(resp, employee);
        return resp;
    }
    @Override
    public PageResult<HrmOvertimeApplicationRespVO> getPage(HrmOvertimeApplicationPageReqVO reqVO) {
        PageResult<HrmOvertimeApplicationDO> page = mapper.selectPage(reqVO);
        return new PageResult<>(page.getList().stream().map(this::buildResp).toList(), page.getTotal());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submit(Long id, String processDefinitionKey, Long loginUserId) {
        HrmOvertimeApplicationDO application = requireOwnedDraft(id, loginUserId);
        validateProcessDefinitions();
        ProcessDefinition definition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
        if (definition == null || !BPM_PROCESS_DEFINITION_KEY.equals(definition.getKey())) {
            throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(loginUserId,
                new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
        mapper.updateById(HrmOvertimeApplicationDO.builder().id(id).processInstanceId(processInstanceId)
                .status(HrmAuditStatusEnum.PROCESS.getStatus()).build());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancel(Long id, Long loginUserId) {
        HrmOvertimeApplicationDO application = require(id);
        if (!Objects.equals(application.getUserId(), loginUserId)
                || !Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) {
            throw exception(OVERTIME_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
        }
        bpmProcessInstanceService.cancelProcessInstanceByStartUser(loginUserId,
                new BpmProcessInstanceCancelReqVO().setId(application.getProcessInstanceId()).setReason("申请人撤销加班申请"));
    }
    @Override
    public List<Map<String, Object>> getApproveProcessDefinitionList() {
        validateProcessDefinitions();
        List<BpmProcessDefinitionInfoDO> infos = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
        Set<String> ids = convertSet(infos, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
        Map<String, ProcessDefinition> latest = new HashMap<>();
        for (ProcessDefinition definition : bpmProcessDefinitionService.getProcessDefinitionList(ids)) {
            if (definition.isSuspended()) continue;
            latest.compute(definition.getKey(), (key, old) -> old == null || definition.getVersion() > old.getVersion() ? definition : old);
        }
        return latest.values().stream().map(item -> {
            Map<String, Object> value = new HashMap<>();
            value.put("id", item.getId()); value.put("key", item.getKey()); value.put("name", item.getName());
            value.put("version", item.getVersion()); return value;
        }).collect(Collectors.toList());
    }
    @Override
    public void updateAuditStatus(Long id, Integer status) {
        HrmOvertimeApplicationDO application = require(id);
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.PROCESS.getStatus())) {
            throw exception(OVERTIME_APPLICATION_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
        }
        mapper.updateById(HrmOvertimeApplicationDO.builder().id(id).status(status)
                .approveTime(LocalDateTime.now()).build());
    }
    private void validateProcessDefinitions() {
        if (!bpmCategoryService.getCategoryMap(Collections.singleton(APPROVE_CATEGORY_CODE)).containsKey(APPROVE_CATEGORY_CODE)) {
            throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        List<BpmProcessDefinitionInfoDO> infos = bpmProcessDefinitionService.getProcessDefinitionInfoListByCategory(APPROVE_CATEGORY_CODE);
        if (CollUtil.isEmpty(infos)) throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        Set<String> ids = convertSet(infos, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
        if (bpmProcessDefinitionService.getProcessDefinitionList(ids).stream().noneMatch(item -> !item.isSuspended())) {
            throw exception(OVERTIME_APPLICATION_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
    }
    private void applyApplicantAndCalculatedTime(HrmOvertimeApplicationDO application, HrmEmployeeDO employee) {
        if (HrmOvertimeTypeEnum.valueOfType(application.getOvertimeType()) == null) {
            throw exception(OVERTIME_APPLICATION_TIME_INVALID);
        }
        application.setUserId(employee.getUserId());
        application.setEmployeeId(employee.getId());
        application.setDeptId(employee.getDeptId());
        long minutes = Duration.between(application.getStartTime(), application.getEndTime()).toMinutes();
        boolean crossDay = minutes <= 0;
        if (crossDay) minutes += 24 * 60;
        BigDecimal mealHours = Boolean.TRUE.equals(application.getMealDeduction())
                ? Optional.ofNullable(application.getMealDeductionHours()).orElse(BigDecimal.ONE) : BigDecimal.ZERO;
        BigDecimal rawHours = BigDecimal.valueOf(minutes).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
        if (Boolean.TRUE.equals(application.getMealDeduction())
                && (mealHours.compareTo(new BigDecimal("0.5")) < 0
                || mealHours.compareTo(new BigDecimal("4")) > 0
                || mealHours.compareTo(rawHours) >= 0)) {
            throw exception(OVERTIME_APPLICATION_TIME_INVALID);
        }
        BigDecimal hours = rawHours
                .subtract(mealHours).multiply(BigDecimal.valueOf(2)).setScale(0, RoundingMode.HALF_UP)
                .divide(BigDecimal.valueOf(2), 1, RoundingMode.UNNECESSARY);
        if (hours.compareTo(BigDecimal.ZERO) <= 0) throw exception(OVERTIME_APPLICATION_TIME_INVALID);
        application.setCrossDay(crossDay);
        application.setMealDeductionHours(mealHours);
        application.setDuration(hours);
    }
    private HrmOvertimeApplicationDO require(Long id) {
        HrmOvertimeApplicationDO value = mapper.selectById(id);
        if (value == null) throw exception(OVERTIME_APPLICATION_NOT_EXISTS);
        return value;
    }
    private HrmOvertimeApplicationDO requireOwnedDraft(Long id, Long userId) {
        HrmOvertimeApplicationDO value = require(id);
        if (!Objects.equals(value.getUserId(), userId)
                || !Objects.equals(value.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(OVERTIME_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        return value;
    }
    private HrmEmployeeDO requireEmployee(Long userId) {
        HrmEmployeeDO employee = employeeMapper.selectByUserId(userId);
        if (employee == null) throw exception(OVERTIME_APPLICATION_EMPLOYEE_NOT_EXISTS);
        return employee;
    }
    /** å½“前待办审批人或曾处理过该流程的审批人可查看业务详情。 */
    private boolean isProcessParticipant(String processInstanceId, Long userId) {
        if (processInstanceId == null || userId == null) {
            return false;
        }
        if (bpmTaskService.getTodoTask(userId, null, processInstanceId) != null) {
            return true;
        }
        return bpmTaskService.getTaskListByProcessInstanceId(processInstanceId, true).stream()
                .anyMatch(task -> Objects.equals(NumberUtil.parseLong(task.getAssignee(), null), userId)
                        || Objects.equals(NumberUtil.parseLong(task.getOwner(), null), userId));
    }
    private HrmOvertimeApplicationRespVO buildResp(HrmOvertimeApplicationDO application) {
        HrmOvertimeApplicationRespVO resp = BeanUtils.toBean(application, HrmOvertimeApplicationRespVO.class);
        HrmEmployeeDO employee = employeeMapper.selectById(application.getEmployeeId());
        if (employee != null) fillEmployee(resp, employee);
        resp.setOvertimeTypeName(HrmOvertimeTypeEnum.getName(application.getOvertimeType()));
        resp.setStatusName(HrmAuditStatusEnum.getNameByStatus(application.getStatus()));
        return resp;
    }
    private void fillEmployee(HrmOvertimeApplicationRespVO resp, HrmEmployeeDO employee) {
        resp.setUserId(employee.getUserId()); resp.setEmployeeId(employee.getId());
        resp.setEmployeeName(employee.getName()); resp.setEmployeeNo(employee.getEmployeeNo());
        resp.setDeptId(employee.getDeptId());
        DeptRespDTO dept = employee.getDeptId() == null ? null : deptApi.getDept(employee.getDeptId());
        PostRespDTO post = employee.getPostId() == null ? null
                : postApi.getPostMap(Collections.singleton(employee.getPostId())).get(employee.getPostId());
        resp.setDeptName(dept == null ? null : dept.getName());
        resp.setPostName(post == null ? null : post.getName());
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/listener/HrmOvertimeApplicationStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.hrm.service.approval.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import cn.iocoder.yudao.module.hrm.service.approval.HrmOvertimeApplicationService;
import cn.iocoder.yudao.module.hrm.util.HrmAuditStatusUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
@Component
public class HrmOvertimeApplicationStatusListener extends BpmProcessInstanceStatusEventListener {
    @Resource private HrmOvertimeApplicationService service;
    @Override protected String getProcessDefinitionKey() { return HrmOvertimeApplicationService.BPM_PROCESS_DEFINITION_KEY; }
    @Override protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Integer status = HrmAuditStatusUtils.convertBpmResultToAuditStatus(event.getStatus());
        if (status == null) status = HrmAuditStatusEnum.PROCESS.getStatus();
        service.updateAuditStatus(Long.valueOf(event.getBusinessKey()), status);
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java
@@ -37,6 +37,12 @@
     */
    HrmAttendanceRecordDO getAttendanceRecordByUserIdAndDate(Long userId, LocalDate date);
    /** åˆ›å»ºå¯¼å…¥çš„考勤记录 */
    void createImportedRecord(HrmAttendanceRecordDO record);
    /** æ›´æ–°å¯¼å…¥çš„考勤记录 */
    void updateImportedRecord(HrmAttendanceRecordDO record);
    /**
     * æ‰“卡(上班打卡)
     *
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
@@ -67,6 +67,86 @@
    }
    @Override
    public void createImportedRecord(HrmAttendanceRecordDO record) {
        // æŒ‰è€ƒå‹¤è§„则重算打卡类型与时长,不信任 Excel å¡«å†™çš„值
        fillClockTypes(record);
        fillCalculatedHours(record);
        attendanceRecordMapper.insert(record);
    }
    @Override
    public void updateImportedRecord(HrmAttendanceRecordDO record) {
        fillClockTypes(record);
        fillCalculatedHours(record);
        attendanceRecordMapper.updateById(record);
    }
    /**
     * æŒ‰è€ƒå‹¤è§„则重算上下班打卡类型(正常/迟到/早退),与打卡逻辑保持一致
     */
    private void fillClockTypes(HrmAttendanceRecordDO record) {
        if (record.getDeptId() == null) {
            return;
        }
        HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId());
        if (rule == null) {
            return;
        }
        // ä¸Šç­æ‰“卡类型:晚于 ä¸Šç­æ—¶é—´+迟到允许 è®°ä¸ºè¿Ÿåˆ°
        if (record.getClockInTime() != null && rule.getWorkStartTime() != null) {
            int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
            LocalTime lateThreshold = rule.getWorkStartTime().plusMinutes(lateAllowMinutes);
            LocalTime clockInOnly = record.getClockInTime().toLocalTime();
            record.setClockInType(clockInOnly.isAfter(lateThreshold)
                    ? HrmAttendanceClockTypeEnum.LATE.getType()
                    : HrmAttendanceClockTypeEnum.NORMAL.getType());
        }
        // ä¸‹ç­æ‰“卡类型:早于 ä¸‹ç­æ—¶é—´-早退允许 è®°ä¸ºæ—©é€€
        if (record.getClockOutTime() != null && rule.getWorkEndTime() != null) {
            int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0;
            LocalTime earlyThreshold = rule.getWorkEndTime().minusMinutes(earlyAllowMinutes);
            LocalTime clockOutOnly = record.getClockOutTime().toLocalTime();
            record.setClockOutType(clockOutOnly.isBefore(earlyThreshold)
                    ? HrmAttendanceClockTypeEnum.EARLY.getType()
                    : HrmAttendanceClockTypeEnum.NORMAL.getType());
        }
    }
    /**
     * æ ¹æ®ä¸Šä¸‹ç­æ‰“卡时间和考勤规则,计算工作时长与加班时长
     */
    private void fillCalculatedHours(HrmAttendanceRecordDO record) {
        // å·¥ä½œæ—¶é•¿ = ä¸‹ç­æ‰“卡时间 - ä¸Šç­æ‰“卡时间(与打卡逻辑一致)
        if (record.getClockInTime() != null && record.getClockOutTime() != null) {
            Duration duration = Duration.between(record.getClockInTime(), record.getClockOutTime());
            BigDecimal workHours = BigDecimal.valueOf(duration.toMinutes())
                    .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
            record.setWorkHours(workHours);
        }
        // åŠ ç­æ—¶é•¿ï¼šæŒ‰è€ƒå‹¤è§„åˆ™çš„æ ‡å‡†ä¸‹ç­æ—¶é—´è®¡ç®—è¶…é¢éƒ¨åˆ†
        if (record.getClockOutTime() != null && record.getDeptId() != null) {
            HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId());
            if (rule != null && rule.getWorkEndTime() != null) {
                LocalTime workEnd = rule.getWorkEndTime();
                LocalDate outDate = record.getClockOutTime().toLocalDate();
                LocalDateTime workEndDateTime = LocalDateTime.of(outDate, workEnd);
                if (record.getClockOutTime().isAfter(workEndDateTime)) {
                    Duration overtimeDuration = Duration.between(workEndDateTime, record.getClockOutTime());
                    BigDecimal overtimeThreshold = rule.getOvertimeThreshold() != null
                            ? rule.getOvertimeThreshold() : BigDecimal.ZERO;
                    BigDecimal overtimeHours = BigDecimal.valueOf(overtimeDuration.toMinutes())
                            .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
                    record.setOvertimeHours(overtimeHours.compareTo(overtimeThreshold) > 0 ? overtimeHours : BigDecimal.ZERO);
                } else {
                    record.setOvertimeHours(BigDecimal.ZERO);
                }
            } else {
                record.setOvertimeHours(BigDecimal.ZERO);
            }
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public HrmAttendanceRecordDO clockIn(Long userId, LocalDateTime clockTime, String location) {
        LocalDate clockDate = clockTime.toLocalDate();
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java
@@ -99,6 +99,12 @@
     */
    HrmEmployeeDO getEmployeeByUserId(Long userId);
    /** æ ¹æ®å‘˜å·¥å§“名获取员工 */
    HrmEmployeeDO getEmployeeByName(String name);
    /** æ ¹æ®å‘˜å·¥å§“名获取所有同名员工 */
    List<HrmEmployeeDO> getEmployeeListByName(String name);
    /**
     * å¯¼å…¥å‘˜å·¥åˆ—表
     *
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -335,6 +335,16 @@
    }
    @Override
    public HrmEmployeeDO getEmployeeByName(String name) {
        return employeeMapper.selectByName(name);
    }
    @Override
    public List<HrmEmployeeDO> getEmployeeListByName(String name) {
        return employeeMapper.selectListByName(name);
    }
    @Override
    public HrmEmployeeImportRespVO importEmployeeList(List<HrmEmployeeImportExcelVO> importEmployees,
                                                      HrmEmployeeImportReqVO importReqVO) {
        // 1. æ ¡éªŒéžç©º
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmLeaveTypeConfigService.java
@@ -22,8 +22,18 @@
    HrmLeaveTypeConfigDO getLeaveTypeConfig(Integer leaveType);
    /**
     * æ–°å¢žè¯·å‡ç±»åž‹é…ç½®
     */
    void createLeaveTypeConfig(HrmLeaveTypeConfigDO config);
    /**
     * æ›´æ–°è¯·å‡ç±»åž‹é…ç½®
     */
    void updateLeaveTypeConfig(HrmLeaveTypeConfigDO config);
    /**
     * åˆ é™¤è¯·å‡ç±»åž‹é…ç½®
     */
    void deleteLeaveTypeConfig(Long id);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmLeaveTypeConfigServiceImpl.java
@@ -10,6 +10,7 @@
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.LEAVE_TYPE_CONFIG_NOT_EXISTS;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.LEAVE_TYPE_CONFIG_TYPE_EXISTS;
/**
 * è¯·å‡ç±»åž‹æ‰£æ¬¾é…ç½® Service å®žçŽ°ç±»
@@ -34,11 +35,26 @@
    }
    @Override
    public void createLeaveTypeConfig(HrmLeaveTypeConfigDO config) {
        // æ ¡éªŒåŒä¸€è¯·å‡ç±»åž‹åªå…è®¸å­˜åœ¨ä¸€æ¡é…ç½®
        if (leaveTypeConfigMapper.selectCount(HrmLeaveTypeConfigDO::getLeaveType, config.getLeaveType()) > 0) {
            throw exception(LEAVE_TYPE_CONFIG_TYPE_EXISTS);
        }
        leaveTypeConfigMapper.insert(config);
    }
    @Override
    public void updateLeaveTypeConfig(HrmLeaveTypeConfigDO config) {
        validateLeaveTypeConfigExists(config.getId());
        leaveTypeConfigMapper.updateById(config);
    }
    @Override
    public void deleteLeaveTypeConfig(Long id) {
        validateLeaveTypeConfigExists(id);
        leaveTypeConfigMapper.deleteById(id);
    }
    private void validateLeaveTypeConfigExists(Long id) {
        if (leaveTypeConfigMapper.selectById(id) == null) {
            throw exception(LEAVE_TYPE_CONFIG_NOT_EXISTS);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryCalculationServiceImpl.java
@@ -5,6 +5,7 @@
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.approval.HrmOvertimeApplicationDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSalaryDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmEmployeeSocialSecurityDO;
@@ -13,7 +14,9 @@
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.HrmSocialSecuritySchemeDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmLeaveApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.approval.HrmOvertimeApplicationMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSocialSecurityMapper;
@@ -22,8 +25,10 @@
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.HrmSocialSecuritySchemeMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryStructureMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.hrm.enums.HrmOvertimeTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -82,6 +87,10 @@
    @Resource
    private HrmLeaveApplicationMapper leaveApplicationMapper;
    @Resource
    private HrmOvertimeApplicationMapper overtimeApplicationMapper;
    @Resource
    private HrmSalaryStructureMapper salaryStructureMapper;
    @Resource
    private HrmLeaveTypeConfigMapper leaveTypeConfigMapper;
    @Resource
    private HrmSalaryPaymentMapper salaryPaymentMapper;
@@ -130,7 +139,10 @@
                if (existCalculations.size() > 1) {
                    throw exception(SALARY_CALCULATION_DUPLICATE, employee.getName(), period);
                }
                continue; // å·²å­˜åœ¨åˆ™è·³è¿‡
                // å·²ç¡®è®¤æˆ–已发放的记录不可自动改写;待确认记录允许重算,以吸收后来审批通过的加班。
                if (existCalculations.get(0).getStatus() != 0) {
                    continue;
                }
            }
            // 2.2 èŽ·å–å‘˜å·¥è–ªé…¬æ¡£æ¡ˆï¼ˆæŒ‰å‘˜å·¥ID查询)
@@ -151,15 +163,20 @@
            // 2.5 æž„建核算记录(包含扣除计算,通过方案获取比例)
            HrmSalaryCalculationDO calculation = buildSalaryCalculation(period, employeeId, employee, employeeSalary, socialSecurity, scheme, effectiveDate);
            // 2.6 ç”Ÿæˆæ ¸ç®—单号
            String no = noRedisDAO.generateSalaryCalculationNo();
            while (salaryCalculationMapper.selectByNo(no) != null) {
                no = noRedisDAO.generateSalaryCalculationNo();
            // 2.6 ä¿å­˜ï¼šé¦–次生成单号;已有待确认记录则保留单号并覆盖重算结果
            if (CollUtil.isNotEmpty(existCalculations)) {
                HrmSalaryCalculationDO existing = existCalculations.get(0);
                calculation.setId(existing.getId());
                calculation.setNo(existing.getNo());
                salaryCalculationMapper.updateById(calculation);
            } else {
                String no = noRedisDAO.generateSalaryCalculationNo();
                while (salaryCalculationMapper.selectByNo(no) != null) {
                    no = noRedisDAO.generateSalaryCalculationNo();
                }
                calculation.setNo(no);
                salaryCalculationMapper.insert(calculation);
            }
            calculation.setNo(no);
            // 2.7 ä¿å­˜æ ¸ç®—记录
            salaryCalculationMapper.insert(calculation);
        }
    }
@@ -174,13 +191,14 @@
    }
    /**
     * æž„建默认的员工薪酬档案(基于员工表的基本工资)
     * æž„建默认的员工薪酬档案。基本工资统一取员工档案,绩效工资默认不自动生成。
     */
    private HrmEmployeeSalaryDO buildDefaultEmployeeSalary(Long employeeId, HrmEmployeeDO employee) {
        BigDecimal baseSalary = employee.getBaseSalary() != null ? employee.getBaseSalary() : BigDecimal.ZERO;
        return HrmEmployeeSalaryDO.builder()
                .userId(employeeId) // ä½¿ç”¨å‘˜å·¥ID
                .salaryStructureId(employee.getSalaryStructureId())
                .baseSalary(employee.getBaseSalary() != null ? employee.getBaseSalary() : BigDecimal.ZERO)
                .baseSalary(baseSalary)
                .performanceSalary(BigDecimal.ZERO)
                .mealAllowance(BigDecimal.ZERO)
                .transportAllowance(BigDecimal.ZERO)
@@ -204,17 +222,18 @@
        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;
        BigDecimal overtimePay = BigDecimal.ZERO;
        OvertimeSalary overtimeSalary = calculateOvertimeSalary(employee, employeeSalary, period, baseSalary);
        BigDecimal overtimePay = overtimeSalary.pay();
        // åº”发工资总额
        BigDecimal totalIncome = baseSalary.add(performanceSalary).add(mealAllowance)
                .add(transportAllowance).add(otherAllowance).add(overtimePay);
        // èŽ·å–ç¤¾ä¿åŸºæ•°ï¼ˆå‘˜å·¥æ¡£æ¡ˆä¸­çš„ä¸ªæ€§åŒ–åŸºæ•°ï¼‰
        // èŽ·å–ç¤¾ä¿åŸºæ•°ï¼šå‘˜å·¥ç¤¾ä¿æ¡£æ¡ˆ > å‘˜å·¥åŸºæœ¬å·¥èµ„
        BigDecimal socialSecurityBase = (socialSecurity != null && socialSecurity.getSocialSecurityBase() != null)
                ? socialSecurity.getSocialSecurityBase() : baseSalary;
        // èŽ·å–å…¬ç§¯é‡‘åŸºæ•°ï¼ˆå‘˜å·¥æ¡£æ¡ˆä¸­çš„ä¸ªæ€§åŒ–åŸºæ•°ï¼‰
        // èŽ·å–å…¬ç§¯é‡‘åŸºæ•°ï¼šå‘˜å·¥ç¤¾ä¿æ¡£æ¡ˆ > å‘˜å·¥åŸºæœ¬å·¥èµ„
        BigDecimal housingFundBase = (socialSecurity != null && socialSecurity.getHousingFundBase() != null)
                ? socialSecurity.getHousingFundBase() : baseSalary;
@@ -277,7 +296,7 @@
                .leaveDeduction(leaveDeduction)
                .actualSalary(actualSalary)
                .workDays(22) // é»˜è®¤å·¥ä½œæ—¥
                .overtimeHours(BigDecimal.ZERO)
                .overtimeHours(overtimeSalary.hours())
                .status(0) // å¾…确认
                .build();
    }
@@ -333,6 +352,54 @@
        return totalLeaveDeduction;
    }
    /**
     * æ±‡æ€»å½“月审批通过的加班申请。小时工资按基本工资 / 21.75 / 8 è®¡ç®—ï¼›
     * å·¥ä½œæ—¥é»˜è®¤ 1.5 å€ã€ä¼‘息日默认 2 å€ã€æ³•定节假日默认 3 å€ï¼Œå‡å¯ç”±è–ªé…¬ç»“构覆盖。
     */
    private OvertimeSalary calculateOvertimeSalary(HrmEmployeeDO employee, HrmEmployeeSalaryDO employeeSalary,
                                                    String period, BigDecimal baseSalary) {
        if (employee.getUserId() == null) {
            return new OvertimeSalary(BigDecimal.ZERO, BigDecimal.ZERO);
        }
        YearMonth month = YearMonth.parse(period);
        List<HrmOvertimeApplicationDO> applications = overtimeApplicationMapper.selectApprovedByUserAndDateRange(
                employee.getUserId(), month.atDay(1), month.atEndOfMonth());
        if (CollUtil.isEmpty(applications)) {
            return new OvertimeSalary(BigDecimal.ZERO, BigDecimal.ZERO);
        }
        Long structureId = employeeSalary.getSalaryStructureId() != null
                ? employeeSalary.getSalaryStructureId() : employee.getSalaryStructureId();
        HrmSalaryStructureDO structure = structureId == null ? null : salaryStructureMapper.selectById(structureId);
        BigDecimal workdayMultiplier = structure != null && structure.getOvertimePayRatio() != null
                ? structure.getOvertimePayRatio() : HrmOvertimeTypeEnum.WORKDAY.getSalaryMultiplier();
        BigDecimal restDayMultiplier = structure != null && structure.getRestDayOvertimePayRatio() != null
                ? structure.getRestDayOvertimePayRatio() : HrmOvertimeTypeEnum.REST_DAY.getSalaryMultiplier();
        BigDecimal legalHolidayMultiplier = structure != null && structure.getLegalHolidayOvertimePayRatio() != null
                ? structure.getLegalHolidayOvertimePayRatio() : HrmOvertimeTypeEnum.LEGAL_HOLIDAY.getSalaryMultiplier();
        BigDecimal hourlySalary = baseSalary.divide(new BigDecimal("21.75"), 8, RoundingMode.HALF_UP)
                .divide(new BigDecimal("8"), 8, RoundingMode.HALF_UP);
        BigDecimal totalHours = BigDecimal.ZERO;
        BigDecimal totalPay = BigDecimal.ZERO;
        for (HrmOvertimeApplicationDO application : applications) {
            BigDecimal hours = application.getDuration() == null ? BigDecimal.ZERO : application.getDuration();
            HrmOvertimeTypeEnum type = HrmOvertimeTypeEnum.valueOfType(application.getOvertimeType());
            if (type == null || hours.signum() <= 0) {
                continue;
            }
            BigDecimal multiplier = switch (type) {
                case WORKDAY -> workdayMultiplier;
                case REST_DAY -> restDayMultiplier;
                case LEGAL_HOLIDAY -> legalHolidayMultiplier;
            };
            totalHours = totalHours.add(hours);
            totalPay = totalPay.add(hourlySalary.multiply(hours).multiply(multiplier));
        }
        return new OvertimeSalary(totalHours.setScale(1, RoundingMode.HALF_UP),
                totalPay.setScale(2, RoundingMode.HALF_UP));
    }
    private record OvertimeSalary(BigDecimal hours, BigDecimal pay) {}
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -577,4 +644,4 @@
        salaryCalculationMapper.deleteById(id);
    }
}
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.hrm.service.salary;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
import java.util.List;
public interface HrmSalaryStructureService {
    Long createSalaryStructure(HrmSalaryStructureDO structure);
    void updateSalaryStructure(HrmSalaryStructureDO structure);
    void deleteSalaryStructure(Long id);
    HrmSalaryStructureDO getSalaryStructure(Long id);
    List<HrmSalaryStructureDO> getSalaryStructureList(String name, Integer status);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryStructureServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
package cn.iocoder.yudao.module.hrm.service.salary;
import cn.iocoder.yudao.module.hrm.dal.dataobject.salary.HrmSalaryStructureDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmEmployeeSalaryMapper;
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.math.BigDecimal;
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_IN_USE;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.SALARY_STRUCTURE_NAME_EXISTS;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.SALARY_STRUCTURE_NOT_EXISTS;
@Service
@Validated
public class HrmSalaryStructureServiceImpl implements HrmSalaryStructureService {
    @Resource
    private HrmSalaryStructureMapper salaryStructureMapper;
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Resource
    private HrmEmployeeSalaryMapper employeeSalaryMapper;
    @Override
    public Long createSalaryStructure(HrmSalaryStructureDO structure) {
        validateNameUnique(null, structure.getName());
        // åŸºæœ¬å·¥èµ„由员工档案维护。数据库历史字段非空,新建结构统一写入占位值 0。
        structure.setBaseSalary(BigDecimal.ZERO);
        salaryStructureMapper.insert(structure);
        return structure.getId();
    }
    @Override
    public void updateSalaryStructure(HrmSalaryStructureDO structure) {
        validateExists(structure.getId());
        validateNameUnique(structure.getId(), structure.getName());
        salaryStructureMapper.updateById(structure);
    }
    @Override
    public void deleteSalaryStructure(Long id) {
        validateExists(id);
        if (employeeMapper.selectCountBySalaryStructureId(id) > 0
                || employeeSalaryMapper.selectCountBySalaryStructureId(id) > 0) {
            throw exception(SALARY_STRUCTURE_IN_USE);
        }
        salaryStructureMapper.deleteById(id);
    }
    @Override
    public HrmSalaryStructureDO getSalaryStructure(Long id) {
        return salaryStructureMapper.selectById(id);
    }
    @Override
    public List<HrmSalaryStructureDO> getSalaryStructureList(String name, Integer status) {
        return salaryStructureMapper.selectListByCondition(name, status);
    }
    private void validateExists(Long id) {
        if (id == null || salaryStructureMapper.selectById(id) == null) {
            throw exception(SALARY_STRUCTURE_NOT_EXISTS);
        }
    }
    private void validateNameUnique(Long id, String name) {
        HrmSalaryStructureDO structure = salaryStructureMapper.selectByName(name);
        if (structure != null && !structure.getId().equals(id)) {
            throw exception(SALARY_STRUCTURE_NAME_EXISTS);
        }
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java
@@ -3,6 +3,9 @@
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.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportExcelVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportRespVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemRespVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
@@ -22,13 +25,17 @@
import cn.iocoder.yudao.module.mes.api.wm.warehouse.dto.MesWmWarehouseRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
@@ -268,4 +275,26 @@
        }
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "获得导入物料模板")
    public void importTemplate(HttpServletResponse response) throws IOException {
        List<MdmItemImportExcelVO> list = Arrays.asList(
                MdmItemImportExcelVO.builder().code("ITEM001").name("示例物料").specification("规格").status(0).isBatchManaged(false).build()
        );
        ExcelUtils.write(response, "物料导入模板.xls", "物料", MdmItemImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入物料")
    @Parameters({
            @Parameter(name = "file", description = "Excel æ–‡ä»¶", required = true),
            @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
    })
    @PreAuthorize("@ss.hasPermission('mdm:item:import')")
    public CommonResult<MdmItemImportRespVO> importExcel(@RequestParam("file") MultipartFile file,
                                                         @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception {
        List<MdmItemImportExcelVO> list = ExcelUtils.read(file, MdmItemImportExcelVO.class);
        return success(itemService.importItemList(list, updateSupport));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.mdm.controller.admin.item.vo;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
 * MDM ç‰©æ–™å¯¼å…¥ Excel VO
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MdmItemImportExcelVO {
    @ExcelProperty("物料编码")
    private String code;
    @ExcelProperty("物料名称")
    private String name;
    @ExcelProperty("条码")
    private String barCode;
    @ExcelProperty("规格型号")
    private String specification;
    @ExcelProperty("物料分类编码")
    private String categoryCode;
    @ExcelProperty("计量单位编码")
    private String unitMeasureCode;
    @ExcelProperty("辅单位1编码")
    private String unitMeasureCode2;
    @ExcelProperty("辅单位1换算比率")
    private BigDecimal unitMeasureRate1;
    @ExcelProperty("辅单位2编码")
    private String unitMeasureCode3;
    @ExcelProperty("辅单位2换算比率")
    private BigDecimal unitMeasureRate2;
    @ExcelProperty("默认仓库编码")
    private String warehouseCode;
    @ExcelProperty("采购价")
    private BigDecimal purchasePrice;
    @ExcelProperty("销售价")
    private BigDecimal salesPrice;
    @ExcelProperty("成本价")
    private BigDecimal costPrice;
    @ExcelProperty("安全库存")
    private BigDecimal safetyStock;
    @ExcelProperty("最低库存")
    private BigDecimal minStock;
    @ExcelProperty("最高库存")
    private BigDecimal maxStock;
    @ExcelProperty("是否批次管理")
    private Boolean isBatchManaged;
    @ExcelProperty("保质期(天)")
    private Integer expiryDay;
    @ExcelProperty("状态")
    private Integer status;
    @ExcelProperty("是否同步到MES")
    private Boolean syncMes;
    @ExcelProperty("备注")
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mdm.controller.admin.item.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - MDM ç‰©æ–™å¯¼å…¥ Response VO")
@Data
@Builder
public class MdmItemImportRespVO {
    @Schema(description = "创建成功的物料数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> createList;
    @Schema(description = "更新成功的物料数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> updateList;
    @Schema(description = "导入失败的物料集合 key ä¸ºç‰©æ–™åï¼Œvalue ä¸ºå¤±è´¥åŽŸå› ", requiredMode = Schema.RequiredMode.REQUIRED)
    private Map<String, String> failureList;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/MdmUnitMeasureController.java
@@ -3,19 +3,27 @@
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.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureImportExcelVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureImportRespVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasurePageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -93,4 +101,27 @@
        return success(unitMeasureService.getUnitMeasureList(ids));
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "获得导入计量单位模板")
    public void importTemplate(HttpServletResponse response) throws IOException {
        List<MdmUnitMeasureImportExcelVO> list = Arrays.asList(
                MdmUnitMeasureImportExcelVO.builder().code("PC").name("片").primaryFlag(true).changeRate(new java.math.BigDecimal("1")).status(0).remark("示例:主单位").build(),
                MdmUnitMeasureImportExcelVO.builder().code("XIA").name("ç®±").primaryFlag(false).primaryCode("PC").changeRate(new java.math.BigDecimal("100")).status(0).remark("示例:1ç®±=100片").build()
        );
        ExcelUtils.write(response, "计量单位导入模板.xls", "计量单位", MdmUnitMeasureImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入计量单位")
    @Parameters({
            @Parameter(name = "file", description = "Excel æ–‡ä»¶", required = true),
            @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
    })
    @PreAuthorize("@ss.hasPermission('mdm:unit-measure:import')")
    public CommonResult<MdmUnitMeasureImportRespVO> importExcel(@RequestParam("file") MultipartFile file,
                                                                @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception {
        List<MdmUnitMeasureImportExcelVO> list = ExcelUtils.read(file, MdmUnitMeasureImportExcelVO.class);
        return success(unitMeasureService.importUnitMeasureList(list, updateSupport));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mdm.controller.admin.unit.vo;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
 * MDM è®¡é‡å•位导入 Excel VO
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MdmUnitMeasureImportExcelVO {
    @ExcelProperty("单位编码")
    private String code;
    @ExcelProperty("单位名称")
    private String name;
    @ExcelProperty("是否主单位")
    private Boolean primaryFlag;
    @ExcelProperty("主单位编码")
    private String primaryCode;
    @ExcelProperty("与主单位换算比例")
    private BigDecimal changeRate;
    @ExcelProperty(value = "状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.COMMON_STATUS)
    private Integer status;
    @ExcelProperty("备注")
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mdm.controller.admin.unit.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - MDM è®¡é‡å•位导入 Response VO")
@Data
@Builder
public class MdmUnitMeasureImportRespVO {
    @Schema(description = "创建成功的单位数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> createList;
    @Schema(description = "更新成功的单位数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> updateList;
    @Schema(description = "导入失败的单位集合 key ä¸ºå•位名,value ä¸ºå¤±è´¥åŽŸå› ", requiredMode = Schema.RequiredMode.REQUIRED)
    private Map<String, String> failureList;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/enums/ErrorCodeConstants.java
@@ -17,6 +17,7 @@
    ErrorCode MDM_ITEM_SKU_IS_DISABLE = new ErrorCode(1_050_005, "物料SKU已禁用");
    ErrorCode MDM_ITEM_SKU_CODE_DUPLICATE = new ErrorCode(1_050_006, "SKU编码已存在");
    ErrorCode MDM_ITEM_EXISTS_REFERENCE = new ErrorCode(1_050_007, "物料已被业务引用,无法删除");
    ErrorCode MDM_ITEM_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_008, "导入的物料数据不能为空");
    // ========== ç‰©æ–™åˆ†ç±»ç›¸å…³ 1-050-051 ~ 1-050-100 ==========
    ErrorCode MDM_ITEM_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_051, "物料分类不存在");
@@ -31,6 +32,7 @@
    ErrorCode MDM_UNIT_MEASURE_CODE_DUPLICATE = new ErrorCode(1_050_102, "计量单位编码已存在");
    ErrorCode MDM_UNIT_MEASURE_NAME_DUPLICATE = new ErrorCode(1_050_103, "计量单位名称已存在");
    ErrorCode MDM_UNIT_MEASURE_EXISTS_ITEM = new ErrorCode(1_050_104, "计量单位被物料使用,无法删除");
    ErrorCode MDM_UNIT_MEASURE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_105, "导入的计量单位数据不能为空");
    // ========== å“ç‰Œç›¸å…³ 1-050-151 ~ 1-050-200 ==========
    ErrorCode MDM_BRAND_NOT_EXISTS = new ErrorCode(1_050_151, "品牌不存在");
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemService.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.mdm.service.item;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportExcelVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportRespVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
@@ -153,4 +155,13 @@
     */
    List<MdmItemDO> getItemSimpleList();
    /**
     * å¯¼å…¥ç‰©æ–™
     *
     * @param importList      å¯¼å…¥åˆ—表
     * @param isUpdateSupport æ˜¯å¦æ”¯æŒæ›´æ–°
     * @return å¯¼å…¥ç»“æžœ
     */
    MdmItemImportRespVO importItemList(List<MdmItemImportExcelVO> importList, Boolean isUpdateSupport);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java
@@ -2,18 +2,23 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
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.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportExcelVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportRespVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.category.MdmItemCategoryMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemBatchConfigMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.unit.MdmUnitMeasureMapper;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
@@ -60,6 +65,12 @@
    private MesMdItemApi mesMdItemApi;
    @Resource
    private MdmItemBatchConfigMapper itemBatchConfigMapper;
    @Resource
    private MdmItemCategoryMapper itemCategoryMapper;
    @Resource
    private MdmUnitMeasureMapper unitMeasureMapper;
    @Resource
    private MesMdItemTypeApi mesMdItemTypeApi;
@@ -422,4 +433,121 @@
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MdmItemImportRespVO importItemList(List<MdmItemImportExcelVO> importList, Boolean isUpdateSupport) {
        if (CollUtil.isEmpty(importList)) {
            throw exception(MDM_ITEM_IMPORT_LIST_IS_EMPTY);
        }
        MdmItemImportRespVO respVO = MdmItemImportRespVO.builder()
                .createList(new ArrayList<>()).updateList(new ArrayList<>())
                .failureList(new LinkedHashMap<>()).build();
        // 1. é¢„热:分类编码 -> ID、单位编码 -> ID
        Map<String, Long> categoryCodeIdMap = new HashMap<>();
        for (MdmItemCategoryDO category : itemCategoryMapper.selectList()) {
            if (category.getCode() != null) {
                categoryCodeIdMap.put(category.getCode(), category.getId());
            }
        }
        Map<String, Long> unitCodeIdMap = new HashMap<>();
        for (MdmUnitMeasureDO unit : unitMeasureMapper.selectList()) {
            unitCodeIdMap.put(unit.getCode(), unit.getId());
        }
        // 2. éåކ坼入
        for (MdmItemImportExcelVO importVO : importList) {
            // 2.1 åŸºç¡€æ ¡éªŒ
            if (StrUtil.isBlank(importVO.getCode()) || StrUtil.isBlank(importVO.getName())) {
                respVO.getFailureList().put(StrUtil.blankToDefault(importVO.getName(), importVO.getCode()), "物料编码和物料名称不能为空");
                continue;
            }
            // 2.2 è§£æžåˆ†ç±»
            Long categoryId = null;
            if (StrUtil.isNotBlank(importVO.getCategoryCode())) {
                categoryId = categoryCodeIdMap.get(importVO.getCategoryCode());
                if (categoryId == null) {
                    respVO.getFailureList().put(importVO.getName(), "物料分类编码不存在:" + importVO.getCategoryCode());
                    continue;
                }
            }
            // 2.3 è§£æžä¸»å•位
            Long unitMeasureId = null;
            if (StrUtil.isNotBlank(importVO.getUnitMeasureCode())) {
                unitMeasureId = unitCodeIdMap.get(importVO.getUnitMeasureCode());
                if (unitMeasureId == null) {
                    respVO.getFailureList().put(importVO.getName(), "计量单位编码不存在:" + importVO.getUnitMeasureCode());
                    continue;
                }
            }
            // 2.4 è§£æžè¾…单位1
            Long unitMeasureId2 = null;
            if (StrUtil.isNotBlank(importVO.getUnitMeasureCode2())) {
                unitMeasureId2 = unitCodeIdMap.get(importVO.getUnitMeasureCode2());
                if (unitMeasureId2 == null) {
                    respVO.getFailureList().put(importVO.getName(), "辅单位1编码不存在:" + importVO.getUnitMeasureCode2());
                    continue;
                }
            }
            // 2.5 è§£æžè¾…单位2
            Long unitMeasureId3 = null;
            if (StrUtil.isNotBlank(importVO.getUnitMeasureCode3())) {
                unitMeasureId3 = unitCodeIdMap.get(importVO.getUnitMeasureCode3());
                if (unitMeasureId3 == null) {
                    respVO.getFailureList().put(importVO.getName(), "辅单位2编码不存在:" + importVO.getUnitMeasureCode3());
                    continue;
                }
            }
            // 2.6 ç»„装 SaveReqVO(复用 create/update é€»è¾‘)
            MdmItemSaveReqVO saveReqVO = new MdmItemSaveReqVO();
            saveReqVO.setCode(importVO.getCode());
            saveReqVO.setName(importVO.getName());
            saveReqVO.setBarCode(importVO.getBarCode());
            saveReqVO.setSpecification(importVO.getSpecification());
            saveReqVO.setCategoryId(categoryId);
            saveReqVO.setUnitMeasureId(unitMeasureId);
            saveReqVO.setUnitMeasureId2(unitMeasureId2);
            saveReqVO.setUnitMeasureRate1(importVO.getUnitMeasureRate1());
            saveReqVO.setUnitMeasureId3(unitMeasureId3);
            saveReqVO.setUnitMeasureRate2(importVO.getUnitMeasureRate2());
            saveReqVO.setPurchasePrice(importVO.getPurchasePrice());
            saveReqVO.setSalesPrice(importVO.getSalesPrice());
            saveReqVO.setCostPrice(importVO.getCostPrice());
            saveReqVO.setSafetyStock(importVO.getSafetyStock());
            saveReqVO.setMinStock(importVO.getMinStock());
            saveReqVO.setMaxStock(importVO.getMaxStock());
            saveReqVO.setIsBatchManaged(importVO.getIsBatchManaged());
            saveReqVO.setExpiryDay(importVO.getExpiryDay());
            saveReqVO.setStatus(importVO.getStatus() != null ? importVO.getStatus() : cn.iocoder.yudao.framework.common.enums.CommonStatusEnum.ENABLE.getStatus());
            saveReqVO.setSyncMes(importVO.getSyncMes() != null ? importVO.getSyncMes() : false);
            saveReqVO.setRemark(importVO.getRemark());
            saveReqVO.setItemType(1); // é»˜è®¤ç‰©æ–™ç±»åž‹ï¼šç‰©æ–™
            // 2.7 å·²å­˜åœ¨ï¼ˆæŒ‰ç¼–码判断)
            MdmItemDO existByCode = itemMapper.selectByCode(importVO.getCode());
            try {
                if (existByCode != null) {
                    if (!Boolean.TRUE.equals(isUpdateSupport)) {
                        respVO.getFailureList().put(importVO.getName(), "物料编码已存在");
                        continue;
                    }
                    saveReqVO.setId(existByCode.getId());
                    updateItem(saveReqVO);
                    respVO.getUpdateList().add(importVO.getName());
                } else {
                    createItem(saveReqVO);
                    respVO.getCreateList().add(importVO.getName());
                }
            } catch (Exception ex) {
                // å•条失败,记录并继续
                String reason = ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException
                        ? ((cn.iocoder.yudao.framework.common.exception.ServiceException) ex).getMessage()
                        : ex.getMessage();
                respVO.getFailureList().put(importVO.getName(), reason == null ? "导入失败" : reason);
            }
        }
        return respVO;
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/unit/MdmUnitMeasureService.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.mdm.service.unit;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureImportExcelVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureImportRespVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasurePageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
@@ -99,4 +101,13 @@
     */
    MdmUnitMeasureDO getUnitMeasureByCode(String code);
    /**
     * å¯¼å…¥è®¡é‡å•位
     *
     * @param importList      å¯¼å…¥åˆ—表
     * @param isUpdateSupport æ˜¯å¦æ”¯æŒæ›´æ–°
     * @return å¯¼å…¥ç»“æžœ
     */
    MdmUnitMeasureImportRespVO importUnitMeasureList(List<MdmUnitMeasureImportExcelVO> importList, Boolean isUpdateSupport);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/unit/MdmUnitMeasureServiceImpl.java
@@ -2,8 +2,11 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureImportExcelVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureImportRespVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasurePageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
@@ -125,4 +128,73 @@
        return unitMeasureMapper.selectByCode(code);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MdmUnitMeasureImportRespVO importUnitMeasureList(List<MdmUnitMeasureImportExcelVO> importList, Boolean isUpdateSupport) {
        if (CollUtil.isEmpty(importList)) {
            throw exception(MDM_UNIT_MEASURE_IMPORT_LIST_IS_EMPTY);
        }
        MdmUnitMeasureImportRespVO respVO = MdmUnitMeasureImportRespVO.builder()
                .createList(new ArrayList<>()).updateList(new ArrayList<>())
                .failureList(new LinkedHashMap<>()).build();
        // 1. å»ºç«‹ç¼–码 -> ID æ˜ å°„(先加载现有单位,再叠加本次新建的单位),用于解析"主单位编码"列
        Map<String, Long> codeIdMap = new HashMap<>();
        for (MdmUnitMeasureDO unit : unitMeasureMapper.selectList()) {
            codeIdMap.put(unit.getCode(), unit.getId());
        }
        // è®°å½•本次导入已处理过的编码,避免同一文件内后续行找不到刚导入的主单位
        Map<String, Long> importedCodeIdMap = new HashMap<>();
        // 2. éåކ坼入
        for (MdmUnitMeasureImportExcelVO importVO : importList) {
            // 2.1 åŸºç¡€æ ¡éªŒ
            if (StrUtil.isBlank(importVO.getCode()) || StrUtil.isBlank(importVO.getName())) {
                respVO.getFailureList().put(StrUtil.blankToDefault(importVO.getName(), importVO.getCode()), "单位编码和单位名称不能为空");
                continue;
            }
            // 2.2 è§£æžä¸»å•位 ID(非主单位时必填主单位编码)
            Long primaryId = null;
            if (StrUtil.isNotBlank(importVO.getPrimaryCode())) {
                primaryId = importedCodeIdMap.get(importVO.getPrimaryCode()) != null
                        ? importedCodeIdMap.get(importVO.getPrimaryCode())
                        : codeIdMap.get(importVO.getPrimaryCode());
                if (primaryId == null) {
                    respVO.getFailureList().put(importVO.getName(), "主单位编码不存在:" + importVO.getPrimaryCode());
                    continue;
                }
            }
            // 2.3 å·²å­˜åœ¨ï¼ˆæŒ‰ç¼–码判断)
            MdmUnitMeasureDO existByCode = unitMeasureMapper.selectByCode(importVO.getCode());
            if (existByCode != null) {
                if (!Boolean.TRUE.equals(isUpdateSupport)) {
                    respVO.getFailureList().put(importVO.getName(), "单位编码已存在");
                    continue;
                }
                // æ”¯æŒæ›´æ–°ï¼šä»¥ç¼–码为准更新
                MdmUnitMeasureDO updateObj = BeanUtils.toBean(importVO, MdmUnitMeasureDO.class);
                updateObj.setId(existByCode.getId());
                updateObj.setPrimaryId(primaryId);
                unitMeasureMapper.updateById(updateObj);
                respVO.getUpdateList().add(importVO.getName());
            } else {
                // 2.4 æ ¡éªŒåç§°å”¯ä¸€ï¼ˆæ–°å¢žåœºæ™¯ï¼‰
                MdmUnitMeasureDO existByName = unitMeasureMapper.selectByName(importVO.getName());
                if (existByName != null) {
                    respVO.getFailureList().put(importVO.getName(), "单位名称已存在");
                    continue;
                }
                // 2.5 æ–°å¢ž
                MdmUnitMeasureDO newObj = BeanUtils.toBean(importVO, MdmUnitMeasureDO.class);
                newObj.setPrimaryId(primaryId);
                unitMeasureMapper.insert(newObj);
                respVO.getCreateList().add(importVO.getName());
                // è®°å½•本次新建的编码,供同文件后续行引用
                importedCodeIdMap.put(importVO.getCode(), newObj.getId());
            }
        }
        return respVO;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/MesMdItemTypeController.java
@@ -3,6 +3,9 @@
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
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.md.item.vo.type.MesMdItemTypeImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeSaveReqVO;
@@ -10,13 +13,18 @@
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@@ -83,4 +91,27 @@
                .setItemOrProduct(itemType.getItemOrProduct())));
    }
    @GetMapping("/get-import-template")
    @Operation(summary = "获得导入物料产品分类模板")
    public void importTemplate(HttpServletResponse response) throws IOException {
        List<MesMdItemTypeImportExcelVO> list = Arrays.asList(
                MesMdItemTypeImportExcelVO.builder().code("RAW").name("原材料").itemOrProduct("ITEM").sort(1).status(0).remark("示例:顶级分类").build(),
                MesMdItemTypeImportExcelVO.builder().code("METAL").name("金属材料").parentCode("RAW").itemOrProduct("ITEM").sort(1).status(0).remark("示例:父分类编码为 RAW").build()
        );
        ExcelUtils.write(response, "物料分类导入模板.xls", "物料分类", MesMdItemTypeImportExcelVO.class, list);
    }
    @PostMapping("/import")
    @Operation(summary = "导入物料产品分类")
    @Parameters({
            @Parameter(name = "file", description = "Excel æ–‡ä»¶", required = true),
            @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
    })
    @PreAuthorize("@ss.hasPermission('mes:md-item-type:import')")
    public CommonResult<MesMdItemTypeImportRespVO> importExcel(@RequestParam("file") MultipartFile file,
                                                               @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception {
        List<MesMdItemTypeImportExcelVO> list = ExcelUtils.read(file, MesMdItemTypeImportExcelVO.class);
        return success(itemTypeService.importItemTypeList(list, updateSupport));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/type/MesMdItemTypeImportExcelVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * MES ç‰©æ–™äº§å“åˆ†ç±»å¯¼å…¥ Excel VO
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MesMdItemTypeImportExcelVO {
    @ExcelProperty("分类编码")
    private String code;
    @ExcelProperty("分类名称")
    private String name;
    @ExcelProperty("父分类编码")
    private String parentCode;
    @ExcelProperty("物料/产品标识")
    private String itemOrProduct;
    @ExcelProperty("显示排序")
    private Integer sort;
    @ExcelProperty("状态")
    private Integer status;
    @ExcelProperty("备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/type/MesMdItemTypeImportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - MES ç‰©æ–™äº§å“åˆ†ç±»å¯¼å…¥ Response VO")
@Data
@Builder
public class MesMdItemTypeImportRespVO {
    @Schema(description = "创建成功的分类数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> createList;
    @Schema(description = "更新成功的分类数组", requiredMode = Schema.RequiredMode.REQUIRED)
    private List<String> updateList;
    @Schema(description = "导入失败的分类集合 key ä¸ºåˆ†ç±»åï¼Œvalue ä¸ºå¤±è´¥åŽŸå› ", requiredMode = Schema.RequiredMode.REQUIRED)
    private Map<String, String> failureList;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/MesPdDocumentController.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.controller.admin.pd.document;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
@@ -14,6 +15,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.document.MesPdDocumentDO;
import cn.iocoder.yudao.module.mes.service.pd.document.MesPdDocumentService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
@@ -117,10 +119,22 @@
        // 1.1 æ‰¹é‡èŽ·å–å…³è”æ•°æ®ï¼ˆä¸Šä¼ äººï¼‰
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(list, MesPdDocumentDO::getUploadUserId));
        // 2. æ‹¼æŽ¥ VO
        return BeanUtils.toBean(list, MesPdDocumentRespVO.class, vo -> {
        List<MesPdDocumentRespVO> voList = BeanUtils.toBean(list, MesPdDocumentRespVO.class, vo -> {
            MapUtils.findAndThen(userMap, vo.getUploadUserId(),
                    user -> vo.setUploadUserNickname(user.getNickname()));
        });
        // 3. æ‹¼æŽ¥æ–‡ä»¶ä¿¡æ¯ï¼šä¼˜å…ˆå–第一个附件的地址作为 fileUrl,并在未填文件名时自动补全
        for (MesPdDocumentRespVO vo : voList) {
            List<StorageBlobRespDTO> attachments = storageAttachmentApi.listAttachments("mes_pd_document", vo.getId());
            if (CollUtil.isNotEmpty(attachments)) {
                StorageBlobRespDTO first = attachments.get(0);
                vo.setFileUrl(first.getUrl());
                if (StrUtil.isEmpty(vo.getFileName())) {
                    vo.setFileName(first.getName());
                }
            }
        }
        return voList;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/vo/MesPdDocumentRespVO.java
@@ -41,6 +41,9 @@
    @ExcelProperty("文件名")
    private String fileName;
    @Schema(description = "文件地址", example = "https://www.iocoder.cn/design_v1.pdf")
    private String fileUrl;
    @Schema(description = "上传人", example = "1")
    private Long uploadUserId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/MesWmReturnVendorController.java
@@ -146,6 +146,14 @@
        return success(returnVendorService.checkReturnVendorQuantity(id));
    }
    @PostMapping("/import-purchase-order-items")
    @Operation(summary = "根据关联采购订单带入物料明细")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:wm-return-vendor:update')")
    public CommonResult<Integer> importPurchaseOrderItems(@RequestParam("id") Long id) {
        return success(returnVendorService.importPurchaseOrderItems(id));
    }
    // ==================== æ‹¼æŽ¥ VO ====================
    private List<MesWmReturnVendorRespVO> buildRespVOList(List<MesWmReturnVendorDO> list) {
@@ -158,8 +166,10 @@
                .collect(java.util.stream.Collectors.toMap(SrmSupplierRespDTO::getId, s -> s));
        // 2. æž„建结果
        return BeanUtils.toBean(list, MesWmReturnVendorRespVO.class, vo ->
                MapUtils.findAndThen(vendorMap, vo.getVendorId(), vendor ->
                        vo.setVendorName(vendor.getName())));
                MapUtils.findAndThen(vendorMap, vo.getVendorId(), vendor -> {
                    vo.setVendorName(vendor.getName());
                    vo.setVendorCode(vendor.getCode());
                }));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorRespVO.java
@@ -18,6 +18,9 @@
    @Schema(description = "退货单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "供应商退货")
    private String name;
    @Schema(description = "采购订单编号", example = "1")
    private Long purchaseOrderId;
    @Schema(description = "采购订单编号", example = "PO20250226001")
    private String purchaseOrderCode;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnvendor/vo/MesWmReturnVendorSaveReqVO.java
@@ -22,6 +22,9 @@
    @NotBlank(message = "退货单名称不能为空")
    private String name;
    @Schema(description = "采购订单编号", example = "1")
    private Long purchaseOrderId;
    @Schema(description = "采购订单编号", example = "PO20250226001")
    private String purchaseOrderCode;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnvendor/MesWmReturnVendorDO.java
@@ -38,6 +38,12 @@
    private String name;
    /**
     * é‡‡è´­è®¢å•编号
     *
     * å…³è” ERP é‡‡è´­è®¢å• {@link cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO#getId()}
     */
    private Long purchaseOrderId;
    /**
     * é‡‡è´­è®¢å•编码(冗余字段,用于显示)
     */
    private String purchaseOrderCode;
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java
@@ -42,4 +42,10 @@
                .le(MesDvMeteringDO::getNextCheckDate, end));
    }
    default List<MesDvMeteringDO> selectListByNextCheckDateBefore(LocalDate date) {
        return selectList(new LambdaQueryWrapperX<MesDvMeteringDO>()
                .isNotNull(MesDvMeteringDO::getNextCheckDate)
                .lt(MesDvMeteringDO::getNextCheckDate, date));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java
@@ -31,4 +31,14 @@
     */
    String METERING_EXPIRE_REMIND_SCAN = "mes:dv:metering:remind:scan:";
    /**
     * è®¡é‡å™¨å…·å·²è¿‡æœŸæé†’去重
     *
     * KEY æ ¼å¼ï¼šmes:dv:metering:overdue:remind:{meteringId}:{nextCheckDate}
     * VALUE æ•°æ®æ ¼å¼ï¼š1
     *
     * ä¸Ž {@link #METERING_EXPIRE_REMIND} ç›¸äº’独立,保证器具过期后仍能单独提醒一次
     */
    String METERING_EXPIRE_REMIND_OVERDUE = "mes:dv:metering:overdue:remind:";
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/dv/metering/MesDvMeteringRemindRedisDAO.java
@@ -41,6 +41,20 @@
    }
    /**
     * å°è¯•标记已提醒(已过期场景,60 å¤©å†…只提醒一次)
     *
     * ä½¿ç”¨ç‹¬ç«‹çš„ KEY,与 {@link #tryMarkReminded} ç›¸äº’隔离,保证器具过期后仍能单独提醒一次
     *
     * @param meteringId è®¡é‡å™¨å…·ç¼–号
     * @param nextCheckDate åˆ°æœŸæ—¥
     * @return true è¡¨ç¤ºé¦–次标记成功,需要发送提醒
     */
    public boolean tryMarkOverdueReminded(Long meteringId, LocalDate nextCheckDate) {
        String key = RedisKeyConstants.METERING_EXPIRE_REMIND_OVERDUE + meteringId + ":" + nextCheckDate;
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "1", REMIND_TIMEOUT));
    }
    /**
     * å°è¯•获取当天的扫描门闩,避免多实例并发重复扫描
     *
     * @param today å½“天日期
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -18,6 +18,7 @@
    ErrorCode MD_ITEM_TYPE_CODE_DUPLICATE = new ErrorCode(1_040_100_005, "同一父分类下已存在该编码的分类");
    ErrorCode MD_ITEM_TYPE_PARENT_IS_CHILD = new ErrorCode(1_040_100_006, "不能设置自己的子分类为父分类");
    ErrorCode MD_ITEM_TYPE_EXITS_ITEM = new ErrorCode(1_040_100_007, "该分类下存在物料,无法删除");
    ErrorCode MD_ITEM_TYPE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_040_100_008, "导入的物料分类数据不能为空");
    // ========== MES åŸºç¡€æ•°æ®-计量单位(1-040-101-000) ==========
    ErrorCode MD_UNIT_MEASURE_NOT_EXISTS = new ErrorCode(1_040_101_000, "计量单位不存在");
@@ -616,6 +617,8 @@
    ErrorCode WM_RETURN_VENDOR_DETAIL_NOT_EXISTS = new ErrorCode(1_040_711_200, "供应商退货单明细不存在");
    ErrorCode WM_RETURN_VENDOR_DETAIL_QUANTITY_INVALID = new ErrorCode(1_040_711_201, "退货明细数量必须大于 0");
    ErrorCode WM_RETURN_VENDOR_DETAIL_LINE_NOT_MATCH = new ErrorCode(1_040_711_202, "拣货明细不属于指定的供应商退货单");
    ErrorCode WM_RETURN_VENDOR_PURCHASE_ORDER_REQUIRED = new ErrorCode(1_040_711_008, "请先选择采购订单");
    ErrorCode WM_RETURN_VENDOR_PURCHASE_ORDER_NO_ITEM = new ErrorCode(1_040_711_009, "采购订单无明细数据,无法带入物料");
    // ========== MES ä»“库管理-产品收货单(1-040-712-000) ==========
    ErrorCode WM_PRODUCT_RECPT_NOT_EXISTS = new ErrorCode(1_040_712_000, "产品收货单不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindServiceImpl.java
@@ -34,9 +34,13 @@
     */
    private static final int DEFAULT_REMIND_DAYS = 30;
    /**
     * åˆ°æœŸæé†’站内信模板编号
     * å³å°†åˆ°æœŸæé†’站内信模板编号
     */
    private static final String REMIND_TEMPLATE_CODE = "mes_metering_expire_remind";
    /**
     * å·²è¿‡æœŸæé†’站内信模板编号
     */
    private static final String OVERDUE_TEMPLATE_CODE = "mes_metering_expire_overdue";
    @Resource
    private MesDvMeteringMapper meteringMapper;
@@ -57,18 +61,28 @@
            }
            // 2. è¯»å–提醒天数(默认 30)
            int remindDays = getRemindDays();
            // 3. æŸ¥è¯¢ [today, today + remindDays] åˆ°æœŸçš„器具
            List<MesDvMeteringDO> list = meteringMapper.selectListByNextCheckDateBetween(
            // 3. æŸ¥è¯¢å·²è¿‡æœŸï¼ˆåˆ°æœŸæ—¥ < ä»Šå¤©ï¼‰ä¸Žå³å°†åˆ°æœŸï¼ˆåˆ°æœŸæ—¥åœ¨ [today, today + remindDays])的器具
            List<MesDvMeteringDO> overdueList = meteringMapper.selectListByNextCheckDateBefore(today);
            List<MesDvMeteringDO> dueList = meteringMapper.selectListByNextCheckDateBetween(
                    today, today.plusDays(remindDays));
            // 4. é€å°å‘送站内信(按器具 + åˆ°æœŸæ—¥åŽ»é‡ï¼‰
            for (MesDvMeteringDO metering : list) {
            // 4. é€å°å‘送站内信(按器具 + åˆ°æœŸæ—¥åŽ»é‡ï¼Œä¸¤ç±»ç›¸äº’ç‹¬ç«‹ï¼‰
            for (MesDvMeteringDO metering : overdueList) {
                if (metering.getChargeUserId() == null || metering.getNextCheckDate() == null) {
                    continue;
                }
                if (!meteringRemindRedisDAO.tryMarkOverdueReminded(metering.getId(), metering.getNextCheckDate())) {
                    continue;
                }
                sendRemindMessage(metering, OVERDUE_TEMPLATE_CODE);
            }
            for (MesDvMeteringDO metering : dueList) {
                if (metering.getChargeUserId() == null || metering.getNextCheckDate() == null) {
                    continue;
                }
                if (!meteringRemindRedisDAO.tryMarkReminded(metering.getId(), metering.getNextCheckDate())) {
                    continue;
                }
                sendRemindMessage(metering);
                sendRemindMessage(metering, REMIND_TEMPLATE_CODE);
            }
        } catch (Exception ex) {
            // best-effort:提醒失败不影响列表查询
@@ -89,14 +103,14 @@
        }
    }
    private void sendRemindMessage(MesDvMeteringDO metering) {
    private void sendRemindMessage(MesDvMeteringDO metering, String templateCode) {
        Map<String, Object> params = new HashMap<>();
        params.put("meteringCode", metering.getCode());
        params.put("meteringName", metering.getName());
        params.put("nextCheckDate", metering.getNextCheckDate().toString());
        NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO()
                .setUserId(metering.getChargeUserId())
                .setTemplateCode(REMIND_TEMPLATE_CODE)
                .setTemplateCode(templateCode)
                .setTemplateParams(params);
        notifyMessageSendApi.sendSingleMessageToAdmin(reqDTO);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemTypeService.java
@@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.mes.service.md.item;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
@@ -90,4 +92,13 @@
     */
    List<MesMdItemTypeDO> getItemTypeChildrenList(Long parentId);
    /**
     * å¯¼å…¥ç‰©æ–™äº§å“åˆ†ç±»
     *
     * @param importList      å¯¼å…¥åˆ—表
     * @param isUpdateSupport æ˜¯å¦æ”¯æŒæ›´æ–°
     * @return å¯¼å…¥ç»“æžœ
     */
    MesMdItemTypeImportRespVO importItemTypeList(List<MesMdItemTypeImportExcelVO> importList, Boolean isUpdateSupport);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemTypeServiceImpl.java
@@ -2,10 +2,13 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.category.MdmItemCategoryApi;
import cn.iocoder.yudao.module.mdm.api.category.dto.MdmItemCategoryRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.type.MesMdItemTypeSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
@@ -230,4 +233,77 @@
        return mesDO;
    }
    @Override
    @org.springframework.transaction.annotation.Transactional(rollbackFor = Exception.class)
    public MesMdItemTypeImportRespVO importItemTypeList(List<MesMdItemTypeImportExcelVO> importList, Boolean isUpdateSupport) {
        if (CollUtil.isEmpty(importList)) {
            throw exception(MD_ITEM_TYPE_IMPORT_LIST_IS_EMPTY);
        }
        MesMdItemTypeImportRespVO respVO = MesMdItemTypeImportRespVO.builder()
                .createList(new ArrayList<>()).updateList(new ArrayList<>())
                .failureList(new LinkedHashMap<>()).build();
        // 1. å»ºç«‹ç¼–码 -> ID æ˜ å°„(现有分类 + æœ¬æ¬¡æ–°å»ºåˆ†ç±»ï¼‰ï¼Œç”¨äºŽè§£æž"父分类编码"列
        Map<String, Long> codeIdMap = new HashMap<>();
        for (MesMdItemTypeDO itemType : itemTypeMapper.selectList()) {
            codeIdMap.put(itemType.getCode(), itemType.getId());
        }
        Map<String, Long> importedCodeIdMap = new HashMap<>();
        // 2. éåކ坼入
        for (MesMdItemTypeImportExcelVO importVO : importList) {
            // 2.1 åŸºç¡€æ ¡éªŒ
            if (StrUtil.isBlank(importVO.getCode()) || StrUtil.isBlank(importVO.getName())) {
                respVO.getFailureList().put(StrUtil.blankToDefault(importVO.getName(), importVO.getCode()), "分类编码和分类名称不能为空");
                continue;
            }
            // 2.2 è§£æžçˆ¶åˆ†ç±» ID
            Long parentId = MesMdItemTypeDO.PARENT_ID_ROOT;
            if (StrUtil.isNotBlank(importVO.getParentCode())) {
                Long pid = importedCodeIdMap.get(importVO.getParentCode()) != null
                        ? importedCodeIdMap.get(importVO.getParentCode())
                        : codeIdMap.get(importVO.getParentCode());
                if (pid == null) {
                    respVO.getFailureList().put(importVO.getName(), "父分类编码不存在:" + importVO.getParentCode());
                    continue;
                }
                parentId = pid;
            }
            // 2.3 å·²å­˜åœ¨ï¼ˆæŒ‰ç¼–码判断)
            MesMdItemTypeDO existByCode = itemTypeMapper.selectByCode(importVO.getCode());
            if (existByCode != null) {
                if (!Boolean.TRUE.equals(isUpdateSupport)) {
                    respVO.getFailureList().put(importVO.getName(), "分类编码已存在");
                    continue;
                }
                MesMdItemTypeDO updateObj = BeanUtils.toBean(importVO, MesMdItemTypeDO.class);
                updateObj.setId(existByCode.getId());
                updateObj.setParentId(parentId);
                itemTypeMapper.updateById(updateObj);
                respVO.getUpdateList().add(importVO.getName());
            } else {
                // 2.4 æ ¡éªŒåŒä¸€çˆ¶åˆ†ç±»ä¸‹åç§°å”¯ä¸€
                MesMdItemTypeDO existByName = itemTypeMapper.selectByParentIdAndName(parentId, importVO.getName());
                if (existByName != null) {
                    respVO.getFailureList().put(importVO.getName(), "同一父分类下已存在该名称的分类");
                    continue;
                }
                // 2.5 æ ¡éªŒåŒä¸€çˆ¶åˆ†ç±»ä¸‹ç¼–码唯一
                MesMdItemTypeDO existByParentCode = itemTypeMapper.selectByParentIdAndCode(parentId, importVO.getCode());
                if (existByParentCode != null) {
                    respVO.getFailureList().put(importVO.getName(), "同一父分类下已存在该编码的分类");
                    continue;
                }
                // 2.6 æ–°å¢ž
                MesMdItemTypeDO newObj = BeanUtils.toBean(importVO, MesMdItemTypeDO.class);
                newObj.setParentId(parentId);
                itemTypeMapper.insert(newObj);
                respVO.getCreateList().add(importVO.getName());
                importedCodeIdMap.put(importVO.getCode(), newObj.getId());
            }
        }
        return respVO;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/document/MesPdDocumentServiceImpl.java
@@ -1,18 +1,23 @@
package cn.iocoder.yudao.module.mes.service.pd.document;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
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.mes.controller.admin.pd.document.vo.MesPdDocumentPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pd.document.vo.MesPdDocumentSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.document.MesPdDocumentDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pd.document.MesPdDocumentMapper;
import cn.iocoder.yudao.module.mes.service.pd.project.MesPdProjectService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -42,20 +47,40 @@
        projectService.validateProjectExists(createReqVO.getProjectId());
        // æ’å…¥
        MesPdDocumentDO document = BeanUtils.toBean(createReqVO, MesPdDocumentDO.class);
        // è¡¥å……上传人、上传时间
        if (document.getUploadUserId() == null) {
            document.setUploadUserId(SecurityFrameworkUtils.getLoginUserId());
        }
        if (document.getUploadTime() == null) {
            document.setUploadTime(LocalDateTime.now());
        }
        documentMapper.insert(document);
        storageAttachmentApi.bindAttachments("file", "mes_pd_document",
                document.getId(), createReqVO.getBlobIds());
        // æœªå¡«å†™æ–‡ä»¶åæ—¶ï¼Œä»Žä¸Šä¼ é™„件自动补全
        fillFileNameIfAbsent(document);
        return document.getId();
    }
    @Override
    public void updateDocument(MesPdDocumentSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateDocumentExists(updateReqVO.getId());
        MesPdDocumentDO existing = validateDocumentExists(updateReqVO.getId());
        // æ›´æ–°
        MesPdDocumentDO updateObj = BeanUtils.toBean(updateReqVO, MesPdDocumentDO.class);
        // ä¿ç•™åŽŸä¸Šä¼ äººã€ä¸Šä¼ æ—¶é—´ï¼›åŽ†å²æ•°æ®ä¸ºç©ºæ—¶è¡¥é½
        if (updateObj.getUploadUserId() == null) {
            updateObj.setUploadUserId(existing.getUploadUserId() != null
                    ? existing.getUploadUserId() : SecurityFrameworkUtils.getLoginUserId());
        }
        if (updateObj.getUploadTime() == null) {
            updateObj.setUploadTime(existing.getUploadTime() != null
                    ? existing.getUploadTime() : LocalDateTime.now());
        }
        documentMapper.updateById(updateObj);
        storageAttachmentApi.updateAttachments("file", "mes_pd_document", updateReqVO.getId(), updateReqVO.getBlobIds());
        // æœªå¡«å†™æ–‡ä»¶åæ—¶ï¼Œä»Žä¸Šä¼ é™„件自动补全
        fillFileNameIfAbsent(updateObj);
    }
    @Override
@@ -100,4 +125,19 @@
        return document;
    }
    /**
     * æœªå¡«å†™æ–‡ä»¶åæ—¶ï¼Œä»Žä¸Šä¼ é™„件自动补全文件名
     *
     * @param document è®¾è®¡èµ„æ–™
     */
    private void fillFileNameIfAbsent(MesPdDocumentDO document) {
        if (StrUtil.isNotEmpty(document.getFileName())) {
            return;
        }
        List<StorageBlobRespDTO> attachments = storageAttachmentApi.listAttachments("mes_pd_document", document.getId());
        if (CollUtil.isNotEmpty(attachments)) {
            document.setFileName(attachments.get(0).getName());
            documentMapper.updateById(document);
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -22,6 +22,7 @@
import cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderSourceTypeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
@@ -282,14 +283,26 @@
            throw exception(PRO_MPS_NOT_ISSUED);
        }
        // çº§è”取消关联的生产工单
        // çº§è”取消关联的生产工单(跟随工单当前状态,且不误伤合并下发的共享工单)
        if (mps.getWorkOrderId() != null) {
            try {
                workOrderService.cancelWorkOrder(mps.getWorkOrderId());
                log.info("[cancelMps] çº§è”取消生产工单,workOrderId={}", mps.getWorkOrderId());
            } catch (Exception e) {
                // å·¥å•可能已完成或已取消,记录日志但不阻止 MPS å–消
                log.warn("[cancelMps] å–消生产工单失败,workOrderId={},原因:{}", mps.getWorkOrderId(), e.getMessage());
            // 1. ç»Ÿè®¡è¯¥å·¥å•被多少条 MPS å¼•用:合并下发的多条 MPS å…±äº«åŒä¸€ä¸ªå·¥å•
            Long refCount = mpsMapper.selectCount(MesProMpsDO::getWorkOrderId, mps.getWorkOrderId());
            if (refCount != null && refCount > 1) {
                // å…±äº«å·¥å•:只取消当前 MPS,不动工单
                log.info("[cancelMps] ç”Ÿäº§å·¥å•({}) è¢« {} æ¡ MPS å…±ç”¨ï¼Œè·³è¿‡çº§è”取消工单",
                        mps.getWorkOrderId(), refCount);
            } else {
                MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(mps.getWorkOrderId());
                if (workOrder != null
                        && MesProWorkOrderStatusEnum.CONFIRMED.getStatus().equals(workOrder.getStatus())) {
                    // åªæœ‰"已确认"状态的工单才需要级联取消
                    workOrderService.cancelWorkOrder(mps.getWorkOrderId());
                    log.info("[cancelMps] çº§è”取消生产工单,workOrderId={}", mps.getWorkOrderId());
                } else {
                    // å·¥å•已是草稿/已完成/已取消,无需级联取消,直接取消 MPS
                    log.info("[cancelMps] ç”Ÿäº§å·¥å•({}) çŠ¶æ€ä¸º {},无需级联取消",
                            mps.getWorkOrderId(), workOrder != null ? workOrder.getStatus() : "null");
                }
            }
        }
@@ -424,6 +437,10 @@
     * åˆ¤æ–­å•个 MPS æ˜¯å¦å®Œæˆ
     */
    private boolean isMpsFinished(MesProMpsDO mps) {
        // å·²å–消的 MPS è§†ä¸ºç»ˆæ€ï¼Œä¸å†é˜»å¡žç”Ÿäº§å®Œæˆåˆ¤æ–­ï¼ˆä¸Ž checkCanCreateSalesNotice çš„口径保持一致)
        if (MesProMpsStatusEnum.CANCELED.getStatus().equals(mps.getStatus())) {
            return true;
        }
        // 1. å·²ç”Ÿäº§æ•°é‡ >= è®¡åˆ’数量
        BigDecimal quantityProduced = ObjUtil.defaultIfNull(mps.getQuantityProduced(), BigDecimal.ZERO);
        if (quantityProduced.compareTo(mps.getQuantity()) < 0) {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorService.java
@@ -102,6 +102,14 @@
    Long getReturnVendorCountByVendorId(Long vendorId);
    /**
     * æ ¹æ®å…³è”的采购订单,带入物料明细到退货单行
     *
     * @param id é€€è´§å•编号
     * @return å¸¦å…¥çš„物料行数
     */
    int importPurchaseOrderItems(Long id);
    /**
     * æ ¡éªŒä¾›åº”商退货单存在且为草稿状态
     *
     * @param id ç¼–号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorServiceImpl.java
@@ -8,6 +8,7 @@
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnvendor.vo.MesWmReturnVendorPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnvendor.vo.MesWmReturnVendorSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnvendor.vo.line.MesWmReturnVendorLineSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnvendor.MesWmReturnVendorDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnvendor.MesWmReturnVendorDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnvendor.MesWmReturnVendorLineDO;
@@ -15,6 +16,9 @@
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnVendorStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.erp.api.purchase.ErpPurchaseOrderApi;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderItemRespDTO;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderRespDTO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
@@ -24,7 +28,9 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@@ -48,12 +54,16 @@
    private SrmSupplierApi srmSupplierApi;
    @Resource
    private MesWmTransactionService wmTransactionService;
    @Resource
    private ErpPurchaseOrderApi purchaseOrderApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createReturnVendor(MesWmReturnVendorSaveReqVO createReqVO) {
        // 1. æ ¡éªŒæ•°æ®
        validateReturnVendorSaveData(createReqVO);
        // 1.1. è‹¥é‡‡è´­è®¢å•号为空但关联了采购订单,则从 ERP é‡‡è´­è®¢å•补齐订单号
        fillPurchaseOrderCode(createReqVO);
        // 2. æ’入主表
        MesWmReturnVendorDO returnVendor = BeanUtils.toBean(createReqVO, MesWmReturnVendorDO.class);
@@ -67,10 +77,29 @@
    public void updateReturnVendor(MesWmReturnVendorSaveReqVO updateReqVO) {
        // 1. æ ¡éªŒæ•°æ®
        validateReturnVendorSaveData(updateReqVO);
        // 1.1. è‹¥é‡‡è´­è®¢å•号为空但关联了采购订单,则从 ERP é‡‡è´­è®¢å•补齐订单号
        fillPurchaseOrderCode(updateReqVO);
        // 2. æ›´æ–°ä¸»è¡¨
        MesWmReturnVendorDO updateObj = BeanUtils.toBean(updateReqVO, MesWmReturnVendorDO.class);
        returnVendorMapper.updateById(updateObj);
    }
    /**
     * å½“采购订单号为空、但关联了采购订单时,从 ERP é‡‡è´­è®¢å•补齐采购订单号
     */
    private void fillPurchaseOrderCode(MesWmReturnVendorSaveReqVO reqVO) {
        if (reqVO == null || reqVO.getPurchaseOrderId() == null) {
            return;
        }
        if (cn.hutool.core.util.StrUtil.isNotBlank(reqVO.getPurchaseOrderCode())) {
            return;
        }
        ErpPurchaseOrderRespDTO purchaseOrder =
                purchaseOrderApi.getPurchaseOrder(reqVO.getPurchaseOrderId()).getCheckedData();
        if (purchaseOrder != null) {
            reqVO.setPurchaseOrderCode(purchaseOrder.getNo());
        }
    }
    @Override
@@ -240,4 +269,53 @@
        return returnVendorMapper.selectCountByVendorId(vendorId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int importPurchaseOrderItems(Long id) {
        // 1. æ ¡éªŒé€€è´§å•存在且为草稿状态
        MesWmReturnVendorDO returnVendor = validateReturnVendorExistsAndPrepare(id);
        if (returnVendor.getPurchaseOrderId() == null) {
            throw exception(WM_RETURN_VENDOR_PURCHASE_ORDER_REQUIRED);
        }
        // 2. èŽ·å–é‡‡è´­è®¢å•åŠæ˜Žç»†
        ErpPurchaseOrderRespDTO purchaseOrder = purchaseOrderApi.getPurchaseOrder(returnVendor.getPurchaseOrderId()).getCheckedData();
        if (purchaseOrder == null) {
            throw exception(WM_PURCHASE_ORDER_NOT_EXISTS);
        }
        List<ErpPurchaseOrderItemRespDTO> items = purchaseOrderApi.getPurchaseOrderItemList(returnVendor.getPurchaseOrderId()).getCheckedData();
        if (CollUtil.isEmpty(items)) {
            throw exception(WM_RETURN_VENDOR_PURCHASE_ORDER_NO_ITEM);
        }
        // 3. å·²å­˜åœ¨çš„行物料集合(按物料去重,避免重复带入)
        Set<Long> existsItemIds = new HashSet<>();
        List<MesWmReturnVendorLineDO> existingLines = returnVendorLineService.getReturnVendorLineListByReturnId(id);
        if (CollUtil.isNotEmpty(existingLines)) {
            existsItemIds.addAll(convertList(existingLines, MesWmReturnVendorLineDO::getItemId));
        }
        // 4. éåŽ†é‡‡è´­è®¢å•æ˜Žç»†ï¼Œç”Ÿæˆé€€è´§è¡Œï¼ˆå¯é€€æ•°é‡ = å·²å…¥åº“ - å·²é€€è´§ï¼‰
        int importedCount = 0;
        for (ErpPurchaseOrderItemRespDTO item : items) {
            if (item.getProductId() == null || existsItemIds.contains(item.getProductId())) {
                continue;
            }
            BigDecimal inCount = item.getInCount() != null ? item.getInCount() : BigDecimal.ZERO;
            BigDecimal returnCount = item.getReturnCount() != null ? item.getReturnCount() : BigDecimal.ZERO;
            BigDecimal quantity = inCount.subtract(returnCount);
            if (quantity.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            MesWmReturnVendorLineSaveReqVO lineReqVO = new MesWmReturnVendorLineSaveReqVO();
            lineReqVO.setReturnId(id);
            lineReqVO.setItemId(item.getProductId());
            lineReqVO.setQuantity(quantity);
            lineReqVO.setRemark(item.getRemark());
            returnVendorLineService.createReturnVendorLine(lineReqVO);
            importedCount++;
        }
        return importedCount;
    }
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/vo/SrmSupplierCertificatePageReqVO.java
@@ -12,6 +12,9 @@
@ToString(callSuper = true)
public class SrmSupplierCertificatePageReqVO extends PageParam {
    @Schema(description = "资质证书名称", example = "营业执照")
    private String certificateName;
    @Schema(description = "供应商ID", example = "1")
    private Long supplierId;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/mysql/SrmSupplierCertificateMapper.java
@@ -15,6 +15,7 @@
    default PageResult<SrmSupplierCertificateDO> selectPage(SrmSupplierCertificatePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<SrmSupplierCertificateDO>()
                .likeIfPresent(SrmSupplierCertificateDO::getCertificateName, reqVO.getCertificateName())
                .eqIfPresent(SrmSupplierCertificateDO::getSupplierId, reqVO.getSupplierId())
                .eqIfPresent(SrmSupplierCertificateDO::getCertificateType, reqVO.getCertificateType())
                .orderByDesc(SrmSupplierCertificateDO::getId));
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/ErrorCodeConstants.java
@@ -13,6 +13,7 @@
    ErrorCode SUPPLIER_CODE_EXISTS = new ErrorCode(1_040_100_001, "供应商编码已存在");
    ErrorCode SUPPLIER_EXISTS_REFERENCE = new ErrorCode(1_040_100_002, "供应商被业务引用,无法删除");
    ErrorCode SUPPLIER_CONTACT_NOT_EXISTS = new ErrorCode(1_040_100_003, "供应商联系人不存在");
    ErrorCode SUPPLIER_NOT_ENABLE = new ErrorCode(1_040_100_004, "供应商已停用,无法参与投标");
    // ========== ä¾›åº”商准入 1_040_200_000 ==========
    ErrorCode APPLY_NOT_EXISTS = new ErrorCode(1_040_200_000, "准入申请不存在");
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierServiceImpl.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.srm.service.supplier;
import cn.hutool.core.collection.CollUtil;
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;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
@@ -110,6 +111,10 @@
    @Override
    public List<SrmSupplierDO> getSupplierAllList() {
        return supplierMapper.selectList(new LambdaQueryWrapperX<SrmSupplierDO>()
                // è¿‡æ»¤åœç”¨ä¾›åº”商,保留未设置状态与启用状态的档案供应商(仅用于招投标场景)
                .and(w -> w.isNull(SrmSupplierDO::getStatus)
                        .or()
                        .ne(SrmSupplierDO::getStatus, CommonStatusEnum.DISABLE.getStatus()))
                .orderByAsc(SrmSupplierDO::getId));
    }
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderBidServiceImpl.java
@@ -1,12 +1,14 @@
package cn.iocoder.yudao.module.srm.service.tender;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmSupplierQuoteSaveReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderBidSaveReqVO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierDO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierQuoteDO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderBidDO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmTenderProjectDO;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierApplyMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierQuoteMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderBidMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderProjectMapper;
@@ -43,7 +45,7 @@
    private SrmTenderProjectMapper tenderProjectMapper;
    @Resource
    private SrmSupplierApplyMapper supplierApplyMapper;
    private SrmSupplierMapper supplierMapper;
    // ========== æŠ•标管理 ==========
@@ -58,9 +60,13 @@
        if (project.getEndTime() != null && LocalDateTime.now().isAfter(project.getEndTime())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_END_TIME_PASSED);
        }
        // æ ¡éªŒä¾›åº”商是否已通过准入审批
        if (!supplierApplyMapper.existsApprovedBySupplierId(createReqVO.getSupplierId())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.BID_SUPPLIER_NOT_APPROVED);
        // æ ¡éªŒæŠ•标供应商存在且未停用(准入状态不限制投标,中标生成采购订单时自动准入)
        SrmSupplierDO supplier = supplierMapper.selectById(createReqVO.getSupplierId());
        if (supplier == null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.SUPPLIER_NOT_EXISTS);
        }
        if (CommonStatusEnum.isDisable(supplier.getStatus())) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.SUPPLIER_NOT_ENABLE, supplier.getName());
        }
        // æ ¡éªŒæŠ•标编号是否重复
        if (tenderBidMapper.existsByBidNo(createReqVO.getBidNo())) {
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/SystemCompanyConfigController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.system.controller.admin.companyconfig;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo.SystemCompanyConfigRespVO;
import cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo.SystemCompanyConfigSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.companyconfig.SystemCompanyConfigDO;
import cn.iocoder.yudao.module.system.service.companyconfig.SystemCompanyConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - å…¬å¸å“ç‰Œä¿¡æ¯")
@RestController
@RequestMapping("/system/company-config")
@Validated
public class SystemCompanyConfigController {
    @Resource
    private SystemCompanyConfigService companyConfigService;
    /**
     * èŽ·å¾—å…¬å¸å“ç‰Œä¿¡æ¯
     *
     * å…ç™»å½•公开接口,供前端登录页 / åº”用启动时读取公司名称、logo、登录页标题。
     */
    @GetMapping("/get")
    @Operation(summary = "获得公司品牌信息(免登录公开)")
    @PermitAll
    public CommonResult<SystemCompanyConfigRespVO> getCompanyConfig() {
        SystemCompanyConfigDO config = companyConfigService.getCompanyConfig();
        return success(BeanUtils.toBean(config, SystemCompanyConfigRespVO.class));
    }
    /**
     * ä¿å­˜å…¬å¸å“ç‰Œä¿¡æ¯
     *
     * å•行 upsert,需登录且拥有 system:company-config:update æƒé™ã€‚
     * æƒé™é€šè¿‡èœå•「公司信息配置 - ä¿®æ”¹ã€æŽˆæƒï¼ˆè§ sql/mysql/system_company_config_menu.sql)。
     */
    @PostMapping("/save")
    @Operation(summary = "保存公司品牌信息")
    @PreAuthorize("@ss.hasPermission('system:company-config:update')")
    public CommonResult<Long> saveCompanyConfig(@Valid @RequestBody SystemCompanyConfigSaveReqVO reqVO) {
        return success(companyConfigService.saveCompanyConfig(reqVO));
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/vo/SystemCompanyConfigRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - å…¬å¸å“ç‰Œä¿¡æ¯ Response VO")
@Data
public class SystemCompanyConfigRespVO {
    @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "公司名称", example = "宁夏九泓化工")
    private String companyName;
    @Schema(description = "logo æ°¸ä¹…公开链接(previewURL)", example = "http://domain/admin-api/system/storage-blob/preview/xxx?publicKey=xxx")
    private String logoUrl;
    @Schema(description = "logo å¯¹åº”çš„ blob è®°å½• ID", example = "86")
    private Long logoBlobId;
    @Schema(description = "登录页标题", example = "制造运营管理平台")
    private String loginTitle;
    @Schema(description = "登录页描述", example = "工程化、高性能、跨组件库的前端模版")
    private String loginDescription;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/companyconfig/vo/SystemCompanyConfigSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Schema(description = "管理后台 - å…¬å¸å“ç‰Œä¿¡æ¯ä¿å­˜ Request VO")
@Data
public class SystemCompanyConfigSaveReqVO {
    @Schema(description = "主键(单行配置,可不传)", example = "1")
    private Long id;
    @Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "宁夏九泓化工")
    @NotBlank(message = "公司名称不能为空")
    @Size(max = 128, message = "公司名称不能超过128个字符")
    private String companyName;
    @Schema(description = "logo æ°¸ä¹…公开链接(previewURL)", example = "http://domain/admin-api/system/storage-blob/preview/xxx?publicKey=xxx")
    @Size(max = 512, message = "logo åœ°å€ä¸èƒ½è¶…过512个字符")
    private String logoUrl;
    @Schema(description = "logo å¯¹åº”çš„ blob è®°å½• ID", example = "86")
    private Long logoBlobId;
    @Schema(description = "登录页标题", example = "制造运营管理平台")
    @Size(max = 128, message = "登录页标题不能超过128个字符")
    private String loginTitle;
    @Schema(description = "登录页描述", example = "工程化、高性能、跨组件库的前端模版")
    @Size(max = 128, message = "登录页描述不能超过128个字符")
    private String loginDescription;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/companyconfig/SystemCompanyConfigDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.system.dal.dataobject.companyconfig;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * å…¬å¸å“ç‰Œä¿¡æ¯é…ç½® DO
 *
 * å•行配置:保存公司名称/logo/登录页标题,前端启动时读取并联动登录页、菜单栏、网页标题。
 *
 * @author è¶…级管理员
 */
@TableName("system_company_config")
@KeySequence("system_company_config_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
public class SystemCompanyConfigDO extends BaseDO {
    /**
     * ä¸»é”®
     */
    private Long id;
    /**
     * å…¬å¸åç§°
     */
    private String companyName;
    /**
     * logo æ°¸ä¹…公开链接(public-upload è¿”回的 previewURL)
     */
    private String logoUrl;
    /**
     * logo å¯¹åº”çš„ blob è®°å½• ID
     */
    private Long logoBlobId;
    /**
     * ç™»å½•页标题
     */
    private String loginTitle;
    /**
     * ç™»å½•页描述
     */
    private String loginDescription;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/companyconfig/SystemCompanyConfigMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.system.dal.mysql.companyconfig;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.system.dal.dataobject.companyconfig.SystemCompanyConfigDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * å…¬å¸å“ç‰Œä¿¡æ¯é…ç½® Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface SystemCompanyConfigMapper extends BaseMapperX<SystemCompanyConfigDO> {
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/companyconfig/SystemCompanyConfigService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.system.service.companyconfig;
import cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo.SystemCompanyConfigSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.companyconfig.SystemCompanyConfigDO;
/**
 * å…¬å¸å“ç‰Œä¿¡æ¯é…ç½® Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface SystemCompanyConfigService {
    /**
     * èŽ·å¾—å…¬å¸å“ç‰Œä¿¡æ¯ï¼ˆå•è¡Œé…ç½®ï¼‰
     *
     * @return å…¬å¸å“ç‰Œä¿¡æ¯ï¼Œæœªé…ç½®æ—¶è¿”回 null
     */
    SystemCompanyConfigDO getCompanyConfig();
    /**
     * ä¿å­˜å…¬å¸å“ç‰Œä¿¡æ¯ï¼ˆå•行 upsert)
     *
     * @param reqVO ä¿å­˜è¯·æ±‚
     * @return ä¸»é”®
     */
    Long saveCompanyConfig(SystemCompanyConfigSaveReqVO reqVO);
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/companyconfig/SystemCompanyConfigServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.system.service.companyconfig;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.system.controller.admin.companyconfig.vo.SystemCompanyConfigSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.companyconfig.SystemCompanyConfigDO;
import cn.iocoder.yudao.module.system.dal.mysql.companyconfig.SystemCompanyConfigMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * å…¬å¸å“ç‰Œä¿¡æ¯é…ç½® Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
public class SystemCompanyConfigServiceImpl implements SystemCompanyConfigService {
    @Resource
    private SystemCompanyConfigMapper companyConfigMapper;
    @Override
    public SystemCompanyConfigDO getCompanyConfig() {
        // å•行配置:按主键升序取第一条
        List<SystemCompanyConfigDO> list = companyConfigMapper.selectList(new LambdaQueryWrapperX<SystemCompanyConfigDO>()
                .orderByAsc(SystemCompanyConfigDO::getId)
                .last("LIMIT 1"));
        return CollUtil.getFirst(list);
    }
    @Override
    public Long saveCompanyConfig(SystemCompanyConfigSaveReqVO reqVO) {
        SystemCompanyConfigDO config = getCompanyConfig();
        if (config == null) {
            // é¦–次保存:插入
            SystemCompanyConfigDO insertObj = BeanUtils.toBean(reqVO, SystemCompanyConfigDO.class);
            companyConfigMapper.insert(insertObj);
            return insertObj.getId();
        }
        // å·²å­˜åœ¨ï¼šæ›´æ–°å•行
        SystemCompanyConfigDO updateObj = BeanUtils.toBean(reqVO, SystemCompanyConfigDO.class);
        updateObj.setId(config.getId());
        companyConfigMapper.updateById(updateObj);
        return config.getId();
    }
}
yudao-server/src/main/resources/application-wtxc.yaml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,222 @@
server:
  port: 48080
--- #################### æ•°æ®åº“相关配置 ####################
spring:
  autoconfigure:
    # noinspection SpringBootApplicationYaml
    exclude:
      - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # é»˜è®¤ local çŽ¯å¢ƒï¼Œä¸å¼€å¯ Quartz çš„自动配置
      - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Qdrant,手动创建
      - org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Redis,手动创建
      - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Milvus,手动创建
      - org.dromara.trans.config.TransServiceConfig # TODO èŠ‹è‰¿ï¼šeasy-trans å…¼å®¹ Spring Boot 4 åŽç§»é™¤ï¼ˆTransServiceConfig å¼•用了已移除的 RestTemplateBuilder)
      # ç¦ç”¨æœªé…ç½® API Key çš„æ¨¡åž‹è‡ªåŠ¨é…ç½®ï¼ˆåªä¿ç•™é€šä¹‰åƒé—® Embedding çš„æ‰‹åŠ¨é…ç½®ï¼‰
      - org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiAudioTranscriptionAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiModerationAutoConfiguration
      - org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration
      - org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.chat.GoogleGenAiChatAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiEmbeddingConnectionAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiTextEmbeddingAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaApiAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaEmbeddingAutoConfiguration
      - org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration
  # æ•°æ®æºé…ç½®é¡¹
  datasource:
    druid: # Druid ã€ç›‘控】相关的全局配置
      web-stat-filter:
        enabled: true
      stat-view-servlet:
        enabled: true
        allow: # è®¾ç½®ç™½åå•,不填则允许所有访问
        url-pattern: /druid/*
        login-username: # æŽ§åˆ¶å°ç®¡ç†ç”¨æˆ·åå’Œå¯†ç 
        login-password:
      filter:
        stat:
          enabled: true
          log-slow-sql: true # æ…¢ SQL è®°å½•
          slow-sql-millis: 100
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
    dynamic: # å¤šæ•°æ®æºé…ç½®
      druid: # Druid ã€è¿žæŽ¥æ± ã€‘相关的全局配置
        initial-size: 1 # åˆå§‹è¿žæŽ¥æ•°
        min-idle: 1 # æœ€å°è¿žæŽ¥æ± æ•°é‡
        max-active: 20 # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
        max-wait: 60000 # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´ï¼Œå•ä½ï¼šæ¯«ç§’ï¼ˆ1 åˆ†é’Ÿï¼‰
        time-between-eviction-runs-millis: 60000 # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 åˆ†é’Ÿï¼‰
        min-evictable-idle-time-millis: 600000 # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位:毫秒(10 åˆ†é’Ÿï¼‰
        max-evictable-idle-time-millis: 1800000 # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位:毫秒(30 åˆ†é’Ÿï¼‰
        validation-query: SELECT 1 FROM DUAL # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        pool-prepared-statements: true # æ˜¯å¦å¼€å¯ PreparedStatement ç¼“å­˜
        max-pool-prepared-statement-per-connection-size: 20 # æ¯ä¸ªè¿žæŽ¥ç¼“存的 PreparedStatement æ•°é‡
      primary: master
      datasource:
        master:
          url: jdbc:mysql://172.17.0.1:9002/mom-wtxc?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          #          url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL è¿žæŽ¥çš„示例
          #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle è¿žæŽ¥çš„示例
          #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer è¿žæŽ¥çš„示例
          #          url: jdbc:dm://127.0.0.1:5236?schema=RUOYI_VUE_PRO # DM è¿žæŽ¥çš„示例
          #          url: jdbc:kingbase8://127.0.0.1:54321/test  # äººå¤§é‡‘仓 KingbaseES è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5866/ruoyi-vue-pro # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
          username: root
          password: wtxc@123456..
          #          username: sa # SQL Server è¿žæŽ¥çš„示例
          #          password: Yudao@2024 # SQL Server è¿žæŽ¥çš„示例
          #          username: SYSDBA # DM è¿žæŽ¥çš„示例
          #          password: SYSDBA001 # DM è¿žæŽ¥çš„示例
          #          username: root # OpenGauss è¿žæŽ¥çš„示例
          #          password: Yudao@2024 # OpenGauss è¿žæŽ¥çš„示例
          #          username: root # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
          #          password: 123456 # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
#        tdengine: # IoT æ•°æ®åº“(需要 IoT ç‰©è”网再开启噢!)
#          lazy: true # å¼€å¯æ‡’加载,保证启动速度
#          url: jdbc:TAOS-WS://127.0.0.1:6041/ruoyi_vue_pro?varcharAsString=true&enableAutoReconnect=true # å‚见:https://t.zsxq.com/qaX9c,开启 TDengine WebSocket è‡ªåŠ¨é‡è¿ž
#          driver-class-name: com.taosdata.jdbc.ws.WebSocketDriver
#          username: root
#          password: taosdata
#          druid:
#            validation-query: SELECT SERVER_STATUS() # TDengine æ•°æ®æºçš„æœ‰æ•ˆæ€§æ£€æŸ¥ SQL
  # Redis é…ç½®ã€‚Redisson é»˜è®¤çš„配置足够使用,一般不需要进行调优
  data:
    redis:
      host: 172.17.0.1 # åœ°å€
      port: 6379 # ç«¯å£
      database: 1 # æ•°æ®åº“索引
#      password: dev # å¯†ç ï¼Œå»ºè®®ç”Ÿäº§çŽ¯å¢ƒå¼€å¯
--- #################### å®šæ—¶ä»»åŠ¡ç›¸å…³é…ç½® ####################
# Quartz é…ç½®é¡¹ï¼Œå¯¹åº” QuartzProperties é…ç½®ç±»
spring:
  quartz:
    auto-startup: true # æœ¬åœ°å¼€å‘环境,尽量不要开启 Job
    scheduler-name: schedulerName # Scheduler åå­—。默认为 schedulerName
    job-store-type: jdbc # Job å­˜å‚¨å™¨ç±»åž‹ã€‚默认为 memory è¡¨ç¤ºå†…存,可选 jdbc ä½¿ç”¨æ•°æ®åº“。
    wait-for-jobs-to-complete-on-shutdown: true # åº”用关闭时,是否等待定时任务执行完成。默认为 false ï¼Œå»ºè®®è®¾ç½®ä¸º true
    properties: # æ·»åŠ  Quartz Scheduler é™„加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html æ–‡æ¡£
      org:
        quartz:
          # Scheduler ç›¸å…³é…ç½®
          scheduler:
            instanceName: schedulerName
            instanceId: AUTO # è‡ªåŠ¨ç”Ÿæˆ instance ID
          # JobStore ç›¸å…³é…ç½®
          jobStore:
            # JobStore å®žçŽ°ç±»ã€‚å¯è§åšå®¢ï¼šhttps://blog.csdn.net/weixin_42458219/article/details/122247162
            class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
            isClustered: true # æ˜¯é›†ç¾¤æ¨¡å¼
            clusterCheckinInterval: 15000 # é›†ç¾¤æ£€æŸ¥é¢‘率,单位:毫秒。默认为 15000,即 15 ç§’
            misfireThreshold: 60000 # misfire é˜€å€¼ï¼Œå•位:毫秒。
          # çº¿ç¨‹æ± ç›¸å…³é…ç½®
          threadPool:
            threadCount: 25 # çº¿ç¨‹æ± å¤§å°ã€‚默认为 10 ã€‚
            threadPriority: 5 # çº¿ç¨‹ä¼˜å…ˆçº§
            class: org.quartz.simpl.SimpleThreadPool # çº¿ç¨‹æ± ç±»åž‹
    jdbc: # ä½¿ç”¨ JDBC çš„ JobStore çš„æ—¶å€™ï¼ŒJDBC çš„配置
      initialize-schema: NEVER # æ˜¯å¦è‡ªåŠ¨ä½¿ç”¨ SQL åˆå§‹åŒ– Quartz è¡¨ç»“构。这里设置成 never ï¼Œæˆ‘们手动创建表结构。
--- #################### æœåŠ¡ä¿éšœç›¸å…³é…ç½® ####################
# Lock4j é…ç½®é¡¹
lock4j:
  acquire-timeout: 3000 # èŽ·å–åˆ†å¸ƒå¼é”è¶…æ—¶æ—¶é—´ï¼Œé»˜è®¤ä¸º 3000 æ¯«ç§’
  expire: 30000 # åˆ†å¸ƒå¼é”çš„超时时间,默认为 30000 æ¯«ç§’
--- #################### ç›‘控相关配置 ####################
# Actuator ç›‘控端点的配置项
management:
  endpoints:
    web:
      base-path: /actuator # Actuator æä¾›çš„ API æŽ¥å£çš„æ ¹ç›®å½•。默认为 /actuator
      exposure:
        include: '*' # éœ€è¦å¼€æ”¾çš„端点。默认值只打开 health å’Œ info ä¸¤ä¸ªç«¯ç‚¹ã€‚通过设置 * ï¼Œå¯ä»¥å¼€æ”¾æ‰€æœ‰ç«¯ç‚¹ã€‚
# Spring Boot Admin é…ç½®é¡¹
spring:
  boot:
    admin:
      # Spring Boot Admin Client å®¢æˆ·ç«¯çš„相关配置
      client:
        url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # è®¾ç½® Spring Boot Admin Server åœ°å€
        instance:
          service-host-type: IP # æ³¨å†Œå®žä¾‹æ—¶ï¼Œä¼˜å…ˆä½¿ç”¨ IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
        username: admin
        password: admin
      # Spring Boot Admin Server æœåŠ¡ç«¯çš„ç›¸å…³é…ç½®
      context-path: /admin # é…ç½® Spring
      # å…è®¸åµŒå…¥ iframe çš„域名(支持通配符),实际部署时,可以改为 "'self' [你的公网域名]"
      frame-ancestors: "'self' localhost localhost:48080 127.0.0.1 127.0.0.1:48080"
# æ—¥å¿—文件配置
logging:
  file:
    name: logs/${spring.application.name}.log # æ—¥å¿—文件名,相对于项目根目录
  level:
    # é…ç½®è‡ªå·±å†™çš„ MyBatis Mapper æ‰“印日志
    cn.iocoder.yudao.module.bpm.dal.mysql: debug
    cn.iocoder.yudao.module.infra.dal.mysql: debug
    cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiErrorLogMapper: INFO # é…ç½® ApiErrorLogMapper çš„æ—¥å¿—级别为 info,避免和 GlobalExceptionHandler é‡å¤æ‰“印
    cn.iocoder.yudao.module.infra.dal.mysql.job.JobLogMapper: INFO # é…ç½® JobLogMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper: INFO # é…ç½® FileConfigMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.system.dal.mysql: debug
    cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper: INFO # é…ç½® SmsChannelMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.member.dal.mysql: debug
    cn.iocoder.yudao.module.crm.dal.mysql: debug
    cn.iocoder.yudao.module.erp.dal.mysql: debug
    cn.iocoder.yudao.module.mes.dal.mysql: debug
    cn.iocoder.yudao.module.ai.dal.mysql: debug
    cn.iocoder.yudao.module.im.dal.mysql: debug
    org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO èŠ‹è‰¿ï¼šå…ˆç¦ç”¨ï¼ŒSpring Boot 3.X å­˜åœ¨éƒ¨åˆ†é”™è¯¯çš„ WARN æç¤º
debug: false
yudao:
  ai:
    vector-store:
      type: milvus # å‘量库类型:milvus
      milvus:
        initialize-schema: true
        database-name: test
        collection-name: knowledge_segment_wtxc
        host: 36.213.90.123
        port: 19530
--- #################### è¶…级管理员相关配置 ####################
# è¶…级管理员配置项,设置当前项目所有自定义的配置
yudao:
  captcha:
    enable: false # æœ¬åœ°çŽ¯å¢ƒï¼Œæš‚æ—¶å…³é—­å›¾ç‰‡éªŒè¯ç ï¼Œæ–¹ä¾¿ç™»å½•ç­‰æŽ¥å£çš„æµ‹è¯•ï¼›
  security:
    mock-enable: true
  access-log: # è®¿é—®æ—¥å¿—的配置项
    enable: false
  demo: false # å…³é—­æ¼”示模式
  storage: # æ–‡ä»¶ä¸Šä¼ é…ç½®
    path: /data/javaWork/mom/file # æ–‡ä»¶å­˜å‚¨æ ¹ç›®å½•
    url-prefix: /admin-api/system/storage-blob # URL路径前缀
    domain: http://42.63.71.140:9001 # åŸŸåå‰ç¼€ï¼ˆæŒ‰å®žé™…域名配置),前端配置好像不太对现在是直连后端,所有要后端端口
    expired: 120 # token过期时间(分钟)
    use-limit: 10 # token使用次数上限
    jwt-secret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG # JWT密钥
    compress: true # æ˜¯å¦åŽ‹ç¼©å›¾ç‰‡
    need-compress-size: 10MB # è§¦å‘压缩的文件大小阈值
    compress-quality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)