gongchunyi
3 天以前 10b88a7ff17caf92f3d4e8a550c1085a70c2517a
Merge dev_New_pro into dev_山西_晋和园_pro

Co-authored-by: Cursor <cursoragent@cursor.com>
已添加105个文件
已修改115个文件
12295 ■■■■■ 文件已修改
src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/aftersalesservice/pojo/AfterSalesService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/aftersalesservice/service/impl/AfterSalesServiceServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/FinancialIntentExecutor.java 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/FinancialAiController.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/schedule/AiSessionCleanupTask.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java 1012 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java 2311 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalInstanceDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeApproverDto.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/FinReimbursementDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalInstanceVo.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeApproverVo.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeVo.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateVo.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/FinReimbursementVo.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceController.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceNodeController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalRecordController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTaskController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateController.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeApproverController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementController.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementDetailController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementTravelController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceNodeMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalRecordMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTaskMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeApproverMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementDetailMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementTravelMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalInstance.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalInstanceNode.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalRecord.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTask.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplate.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNode.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNodeApprover.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursement.java 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursementDetail.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursementTravel.java 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalInstanceNodeService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalInstanceService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalRecordService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTaskService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeApproverService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeService.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTemplateService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementDetailService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementService.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementTravelService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceNodeServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java 846 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalRecordServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTaskServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeApproverServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeServiceImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateServiceImpl.java 250 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveBusinessStatusService.java 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java 131 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementDetailServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java 544 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementTravelServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/utils/ApproveProcessConfigNodeUtils.java 361 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsController.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeDeptController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeUserController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/dto/EnterpriseNewsDto.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeDeptMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeUserMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNews.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeDept.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeUser.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeDeptService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeUserService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsService.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeDeptServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeUserServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsServiceImpl.java 409 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/vo/EnterpriseNewsVo.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/ApprovalStatusEnum.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/EnterpriseNewsStatusEnum.java 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/SalesQuotationStatusEnum.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/ShippingStatusEnum.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/TypeEnums.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/utils/OrderUtils.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/customervisits/service/impl/CustomerVisitsServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskServiceImpl.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/pojo/CustomStorage.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/pojo/ReturnManagement.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnManagementServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrder.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/mapper/SysUserDeptMapper.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/SysNoticeServiceImpl.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/controller/RolesController.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/pojo/Roles.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/PurchaseLedgerController.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/PurchaseReturnOrdersController.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseReturnOrders.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/IPurchaseLedgerService.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java 453 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityTestStandardParamController.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/IQualityInspectService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/utils/QualityInspectHelper.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/PaymentShippingController.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesQuotationDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/PaymentShipping.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesQuotation.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/ShippingInfo.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ShippingInfoService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java 90 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/PersonalAttendanceLocationConfigController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/StaffOnJobController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/dto/StaffOnJobDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/mapper/StaffOnJobMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/IStaffOnJobService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/StaffLeaveServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/pojo/StockInRecord.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/pojo/StockOutRecord.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyOperationController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyOperationParamController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/pojo/TechnologyParam.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/pojo/TechnologyRouting.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-ckgm.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-hqjc.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/financial-agent-prompt.txt 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPaymentApplicationMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPurchaseInvoiceMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPurchasePaymentMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountInvoiceApplicationMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountSalesCollectionMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountSalesInvoiceMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/aftersalesservice/AfterSalesNearExpiryMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/aftersalesservice/AfterSalesServiceMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalInstanceMapper.xml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalInstanceNodeMapper.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalRecordMapper.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTaskMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTemplateMapper.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTemplateNodeApproverMapper.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTemplateNodeMapper.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApproveProcessMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementDetailMapper.xml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementMapper.xml 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementTravelMapper.xml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/KnowledgeBaseMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/SupplierManageMapper.xml 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsMapper.xml 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeDeptMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeUserMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/NoticeMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/RulesRegulationsManagementMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/SealApplicationManagementMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceMaintenanceMapper.xml 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceRepairMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerRecordMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/SparePartsMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/SparePartsRequisitionRecordMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityInspectMapper.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityTestStandardMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerMapper.xml 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesQuotationMapper.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/PersonalAttendanceRecordsMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/PersonalShiftMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/StaffLeaveMapper.xml 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/StaffOnJobMapper.xml 80 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysDeptMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysDictTypeMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysNoticeMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysPostMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysRoleMapper.xml 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysUserMapper.xml 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/technology/TechnologyOperationMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/technology/TechnologyOperationParamMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/DocumentationBorrowManagementMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/DocumentationMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/DocumentationReturnManagementMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/WarehouseGoodsShelvesRowcolMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java
@@ -174,7 +174,7 @@
        if (accountSubjectDto != null && accountSubjectDto.getStatus() != null) {
            queryWrapper.eq(AccountSubject::getStatus, accountSubjectDto.getStatus());
        }
        queryWrapper.orderByAsc(AccountSubject::getSubjectCode).orderByAsc(AccountSubject::getId);
        queryWrapper.orderByDesc(AccountSubject::getSubjectCode).orderByDesc(AccountSubject::getId);
        return queryWrapper;
    }
@@ -296,8 +296,8 @@
        }
        List<AccountSubject> sortedSubjects = new ArrayList<>(subjects);
        sortedSubjects.sort(Comparator
                .comparing(AccountSubject::getSubjectCode, Comparator.nullsLast(String::compareTo))
                .thenComparing(AccountSubject::getId, Comparator.nullsLast(Long::compareTo)));
                .comparing(AccountSubject::getSubjectCode, Comparator.nullsFirst(String::compareTo)).reversed()
                .thenComparing(AccountSubject::getId, Comparator.nullsFirst(Long::compareTo)).reversed());
        Map<Long, AccountSubjectVo> subjectVoMap = new LinkedHashMap<>();
        for (AccountSubject subject : sortedSubjects) {
src/main/java/com/ruoyi/aftersalesservice/pojo/AfterSalesService.java
@@ -115,7 +115,6 @@
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
src/main/java/com/ruoyi/aftersalesservice/service/impl/AfterSalesServiceServiceImpl.java
@@ -11,6 +11,8 @@
import com.ruoyi.aftersalesservice.service.AfterSalesServiceService;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import java.time.LocalDateTime;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
@@ -70,7 +72,7 @@
        if(sysUser == null) throw new RuntimeException("审核人不存在");
        afterSalesServiceNewDto.setCheckNickName(sysUser.getNickName());
        if (StringUtils.isEmpty(afterSalesServiceNewDto.getAfterSalesServiceNo())) {
            String string = OrderUtils.countAfterServiceTodayByCreateTime(afterSalesServiceMapper, "SH_");
            String string = OrderUtils.countAfterServiceTodayByCreateTime(afterSalesServiceMapper, "SH_", afterSalesServiceNewDto.getCreateTime() != null ? afterSalesServiceNewDto.getCreateTime() : LocalDateTime.now());
            afterSalesServiceNewDto.setAfterSalesServiceNo(string);
        }
        return this.save(afterSalesServiceNewDto);
src/main/java/com/ruoyi/ai/assistant/FinancialIntentExecutor.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,284 @@
package com.ruoyi.ai.assistant;
import com.ruoyi.ai.tools.FinancialAgentTools;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class FinancialIntentExecutor {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final Pattern LIMIT_PATTERN = Pattern.compile("(?:前|最近|展示|返回)?\\s*(\\d{1,2})\\s*(?:条|个|名)");
    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
    private static final Pattern RELATIVE_DAY_PATTERN = Pattern.compile("(?:近|最近)\\s*(\\d{1,3})\\s*天");
    private static final Pattern FUTURE_MONTH_PATTERN = Pattern.compile("(?:未来|后续|接下来)\\s*(\\d{1,2})\\s*(?:个)?月");
    private final FinancialAgentTools financialAgentTools;
    public FinancialIntentExecutor(FinancialAgentTools financialAgentTools) {
        this.financialAgentTools = financialAgentTools;
    }
    public String tryExecute(String memoryId, String message) {
        if (!StringUtils.hasText(message)) {
            return null;
        }
        String text = message.trim();
        String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text);
        if (StringUtils.hasText(quickPromptResponse)) {
            return quickPromptResponse;
        }
        DateRange dateRange = extractDateRange(text);
        Integer limit = extractLimit(text);
        String keyword = extractKeyword(text);
        String startDate = dateRange.startDate();
        String endDate = dateRange.endDate();
        String timeRange = dateRange.label();
        if (containsAny(text, "成本核算", "产品成本", "工序成本", "人工成本", "折旧", "材料损耗", "成本最高")) {
            return financialAgentTools.calculateIntelligentCost(memoryId, startDate, endDate, timeRange, keyword, limit);
        }
        if (containsAny(text, "利润分析", "订单利润", "亏损订单", "低利润", "最赚钱客户", "哪个客户最赚钱",
                "客户最赚钱", "利润最高客户", "利润贡献最高", "利润下降")) {
            return financialAgentTools.analyzeOrderProfit(memoryId, startDate, endDate, timeRange, keyword, limit);
        }
        if (containsAny(text, "库存资金", "库存积压", "呆滞库存", "资金占用", "周转率", "库存周转")) {
            return financialAgentTools.analyzeInventoryCapital(memoryId, startDate, endDate, timeRange, keyword, limit);
        }
        if (containsAny(text, "现金流", "回款风险", "付款压力", "资金缺口", "应收", "应付", "回款预测")) {
            return financialAgentTools.forecastCashFlow(memoryId, startDate, endDate, timeRange, extractForecastMonths(text));
        }
        if (containsAny(text, "异常预警", "经营异常", "风险预警", "成本异常", "利润异常", "回款异常", "订单风险")) {
            return financialAgentTools.detectBusinessAnomalies(memoryId, startDate, endDate, timeRange, limit);
        }
        if (containsAny(text, "驾驶舱", "经营看板", "经营总览", "经营仪表盘", "经营大盘")) {
            return financialAgentTools.getBusinessCockpit(memoryId, startDate, endDate, timeRange);
        }
        if (containsAny(text, "日报", "周报", "经营报告", "分析报告")) {
            return financialAgentTools.generateOperationReport(memoryId, startDate, endDate, timeRange,
                    containsAny(text, "周报") ? "weekly" : "daily");
        }
        if (containsAny(text, "业财融合", "业财联动", "口径", "指标解释", "为什么")) {
            return financialAgentTools.retrieveFinancialKnowledge(memoryId, text);
        }
        return null;
    }
    private String tryExecuteQuickPrompt(String memoryId, String text) {
        String normalized = normalizeForMatch(text);
        if ("生成本周经营周报利润与现金流".equals(normalized) || "生成本周经营周报".equals(normalized) || "生成周报".equals(normalized)) {
            DateRange range = weekRange();
            return financialAgentTools.generateOperationReport(memoryId, range.startDate(), range.endDate(), range.label(), "weekly");
        }
        if ("分析本月利润下降原因".equals(normalized)) {
            DateRange range = monthRange();
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("近30天哪个客户利润贡献最高".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("查看本月经营驾驶舱".equals(normalized) || "查看经营驾驶舱".equals(normalized)) {
            DateRange range = monthRange();
            return financialAgentTools.getBusinessCockpit(memoryId, range.startDate(), range.endDate(), range.label());
        }
        if ("查询近30天亏损订单".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("分析近30天库存资金占用".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return financialAgentTools.analyzeInventoryCapital(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("预测未来3个月现金流".equals(normalized)) {
            return financialAgentTools.forecastCashFlow(memoryId, null, null, null, 3);
        }
        if ("哪个工序成本最高".equals(normalized)) {
            return financialAgentTools.calculateIntelligentCost(memoryId, null, null, null, null, null);
        }
        return null;
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.contains(keyword)) {
                return true;
            }
        }
        return false;
    }
    private Integer extractLimit(String text) {
        Matcher matcher = LIMIT_PATTERN.matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(1)) : null;
    }
    private Integer extractForecastMonths(String text) {
        Matcher matcher = FUTURE_MONTH_PATTERN.matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(1)) : null;
    }
    private DateRange extractDateRange(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        if (matcher.find()) {
            String first = matcher.group(1);
            String second = matcher.find() ? matcher.group(1) : first;
            return buildDateRange(first, second, first + "至" + second);
        }
        if (text.contains("本月")) {
            return monthRange();
        }
        if (text.contains("上月")) {
            return lastMonthRange();
        }
        if (text.contains("今年") || text.contains("本年")) {
            return yearRange();
        }
        if (text.contains("本周")) {
            return weekRange();
        }
        Matcher relativeDayMatcher = RELATIVE_DAY_PATTERN.matcher(text);
        if (relativeDayMatcher.find()) {
            int days = Integer.parseInt(relativeDayMatcher.group(1));
            return recentDaysRange(days);
        }
        return new DateRange(null, null, null);
    }
    private DateRange buildDateRange(String start, String end, String label) {
        LocalDate startDate = parseDate(start);
        LocalDate endDate = parseDate(end);
        if (startDate == null || endDate == null) {
            return new DateRange(null, null, null);
        }
        if (startDate.isAfter(endDate)) {
            LocalDate temp = startDate;
            startDate = endDate;
            endDate = temp;
        }
        return new DateRange(formatDate(startDate), formatDate(endDate), label);
    }
    private DateRange recentDaysRange(int days) {
        LocalDate end = LocalDate.now();
        int safeDays = Math.max(days, 1);
        LocalDate start = end.minusDays(safeDays - 1L);
        return new DateRange(formatDate(start), formatDate(end), "近" + safeDays + "天");
    }
    private DateRange monthRange() {
        LocalDate today = LocalDate.now();
        return new DateRange(formatDate(today.withDayOfMonth(1)), formatDate(today), "本月");
    }
    private DateRange weekRange() {
        LocalDate today = LocalDate.now();
        LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
        return new DateRange(formatDate(start), formatDate(today), "本周");
    }
    private DateRange lastMonthRange() {
        YearMonth lastMonth = YearMonth.now().minusMonths(1);
        return new DateRange(formatDate(lastMonth.atDay(1)), formatDate(lastMonth.atEndOfMonth()), "上月");
    }
    private DateRange yearRange() {
        LocalDate today = LocalDate.now();
        return new DateRange(formatDate(today.withDayOfYear(1)), formatDate(today), "今年");
    }
    private LocalDate parseDate(String text) {
        try {
            return LocalDate.parse(text, DATE_FMT);
        } catch (Exception ignored) {
            return null;
        }
    }
    private String formatDate(LocalDate date) {
        return date == null ? null : date.format(DATE_FMT);
    }
    private String normalizeForMatch(String text) {
        if (!StringUtils.hasText(text)) {
            return "";
        }
        return text.replace("(", "")
                .replace(")", "")
                .replace("(", "")
                .replace(")", "")
                .replace(",", "")
                .replace(",", "")
                .replace("。", "")
                .replace(".", "")
                .replace("!", "")
                .replace("!", "")
                .replace("?", "")
                .replace("?", "")
                .replace(":", "")
                .replace(":", "")
                .replace(";", "")
                .replace(";", "")
                .replace(" ", "")
                .trim();
    }
    private String extractKeyword(String text) {
        String cleaned = text
                .replaceAll("\\d{4}-\\d{2}-\\d{2}", "")
                .replaceAll("(?:近|最近)\\s*\\d{1,3}\\s*天", "")
                .replaceAll("(?:前|最近|展示|返回)?\\s*\\d{1,2}\\s*(?:条|个|名)", "")
                .replace("查询", "")
                .replace("查看", "")
                .replace("看下", "")
                .replace("看看", "")
                .replace("帮我", "")
                .replace("请", "")
                .replace("一个", "")
                .replace("为什么", "")
                .replace("哪个客户最赚钱", "")
                .replace("最近哪个客户最赚钱", "")
                .replace("本月哪个客户最赚钱", "")
                .replace("近30天哪个客户最赚钱", "")
                .replace("最赚钱客户", "")
                .replace("客户最赚钱", "")
                .replace("哪个客户利润最高", "")
                .replace("利润最高客户", "")
                .replace("哪个客户利润贡献最高", "")
                .replace("利润贡献最高客户", "")
                .replace("本月", "")
                .replace("本周", "")
                .replace("本年", "")
                .replace("今年", "")
                .replace("上月", "")
                .replace("近30天", "")
                .replace("近7天", "")
                .replace("近90天", "")
                .replace("前10条", "")
                .replace("最近10条", "")
                .replace("前20条", "")
                .replace("最近20条", "")
                .replace("订单利润分析", "")
                .replace("利润分析", "")
                .replace("库存资金分析", "")
                .replace("现金流预测", "")
                .replace("经营驾驶舱", "")
                .replace("日报", "")
                .replace("周报", "")
                .replace("异常预警", "")
                .replace("条", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
    private record DateRange(String startDate, String endDate, String label) {
    }
}
src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
package com.ruoyi.ai.context;
import com.ruoyi.framework.security.LoginUser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class AiSessionUserContext {
    private final Map<String, LoginUser> loginUserByMemoryId = new ConcurrentHashMap<>();
    private final Map<String, Instant> lastAccessTimeByMemoryId = new ConcurrentHashMap<>();
    private static final Duration SESSION_TIMEOUT = Duration.ofHours(24);
    public void bind(String memoryId, LoginUser loginUser) {
        if (!StringUtils.hasText(memoryId) || loginUser == null) {
            return;
        }
        loginUserByMemoryId.put(memoryId, loginUser);
        lastAccessTimeByMemoryId.put(memoryId, Instant.now());
    }
    public LoginUser get(String memoryId) {
        if (!StringUtils.hasText(memoryId)) {
            return null;
        }
        if (isExpired(memoryId)) {
            remove(memoryId);
            return null;
        }
        lastAccessTimeByMemoryId.put(memoryId, Instant.now());
        return loginUserByMemoryId.get(memoryId);
    }
    public void remove(String memoryId) {
        if (!StringUtils.hasText(memoryId)) {
            return;
        }
        loginUserByMemoryId.remove(memoryId);
        lastAccessTimeByMemoryId.remove(memoryId);
    }
    public void cleanExpiredSessions() {
        Instant now = Instant.now();
        lastAccessTimeByMemoryId.entrySet().removeIf(entry -> {
            boolean expired = Duration.between(entry.getValue(), now).compareTo(SESSION_TIMEOUT) > 0;
            if (expired) {
                loginUserByMemoryId.remove(entry.getKey());
            }
            return expired;
        });
    }
    private boolean isExpired(String memoryId) {
        Instant lastAccess = lastAccessTimeByMemoryId.get(memoryId);
        if (lastAccess == null) {
            return true;
        }
        return Duration.between(lastAccess, Instant.now()).compareTo(SESSION_TIMEOUT) > 0;
    }
}
src/main/java/com/ruoyi/ai/controller/FinancialAiController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
package com.ruoyi.ai.controller;
import com.ruoyi.ai.assistant.FinancialAgent;
import com.ruoyi.ai.assistant.FinancialIntentExecutor;
import com.ruoyi.ai.bean.ChatForm;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.ai.service.AiChatSessionService;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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 reactor.core.publisher.Flux;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Tag(name = "财务智能体")
@RestController
@RequestMapping("/financial-ai")
public class FinancialAiController extends BaseController {
    private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private final FinancialAgent financialAgent;
    private final FinancialIntentExecutor financialIntentExecutor;
    private final AiSessionUserContext aiSessionUserContext;
    private final MongoChatMemoryStore mongoChatMemoryStore;
    private final AiChatSessionService aiChatSessionService;
    public FinancialAiController(FinancialAgent financialAgent,
                                 FinancialIntentExecutor financialIntentExecutor,
                                 AiSessionUserContext aiSessionUserContext,
                                 MongoChatMemoryStore mongoChatMemoryStore,
                                 AiChatSessionService aiChatSessionService) {
        this.financialAgent = financialAgent;
        this.financialIntentExecutor = financialIntentExecutor;
        this.aiSessionUserContext = aiSessionUserContext;
        this.mongoChatMemoryStore = mongoChatMemoryStore;
        this.aiChatSessionService = aiChatSessionService;
    }
    @Operation(summary = "财务智能体对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (!StringUtils.hasText(chatForm.getMemoryId())) {
            return Flux.just("memoryId不能为空");
        }
        if (!StringUtils.hasText(chatForm.getMessage())) {
            return Flux.just("message不能为空");
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String memoryId = chatForm.getMemoryId();
        String userMessage = chatForm.getMessage();
        aiSessionUserContext.bind(memoryId, loginUser);
        aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
        String directResponse = financialIntentExecutor.tryExecute(memoryId, userMessage);
        if (StringUtils.isNotEmpty(directResponse)) {
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(directResponse);
        }
        if (isBusinessDataIntent(userMessage)) {
            String noGuessResponse = "未识别到可执行的数据查询条件。为保证结果准确,当前不会推测或编造数据,请补充明确时间范围、客户、供应商或单号后再查询。";
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(noGuessResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(noGuessResponse);
        }
        return financialAgent.chat(memoryId, userMessage, currentDateForPrompt())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
    @Operation(summary = "财务智能体会话列表")
    @GetMapping("/history/sessions")
    public AjaxResult listSessions() {
        return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "财务智能体会话消息")
    @GetMapping("/history/messages/{memoryId}")
    public AjaxResult listMessages(@PathVariable String memoryId) {
        return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "删除财务智能体会话")
    @DeleteMapping("/history/{memoryId}")
    public AjaxResult deleteSession(@PathVariable String memoryId) {
        aiSessionUserContext.remove(memoryId);
        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
    }
    private String currentDateForPrompt() {
        return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
    }
    private boolean isBusinessDataIntent(String message) {
        if (!StringUtils.hasText(message)) {
            return false;
        }
        String text = message.trim();
        return containsAny(text,
                "查询", "查看", "统计", "分析", "建议", "成本核算", "产品成本", "工序成本",
                "订单利润", "亏损订单", "低利润", "库存资金", "库存积压", "呆滞库存",
                "现金流", "回款风险", "付款压力", "资金缺口", "应收", "应付",
                "异常预警", "经营异常", "风险预警", "驾驶舱", "经营看板", "经营总览",
                "日报", "周报", "经营报告", "分析报告", "业财融合", "口径", "指标解释");
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.contains(keyword)) {
                return true;
            }
        }
        return false;
    }
}
src/main/java/com/ruoyi/ai/schedule/AiSessionCleanupTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.ai.schedule;
import com.ruoyi.ai.context.AiSessionUserContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class AiSessionCleanupTask {
    private final AiSessionUserContext aiSessionUserContext;
    public AiSessionCleanupTask(AiSessionUserContext aiSessionUserContext) {
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Scheduled(cron = "0 0 2 * * ?")
    public void cleanupExpiredSessions() {
        try {
            aiSessionUserContext.cleanExpiredSessions();
        } catch (Exception e) {
            System.err.println("清理过期AI会话失败: " + e.getMessage());
        }
    }
}
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1012 @@
package com.ruoyi.ai.tools;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.approve.mapper.ApproveLogMapper;
import com.ruoyi.approve.mapper.ApproveNodeMapper;
import com.ruoyi.approve.mapper.ApproveProcessMapper;
import com.ruoyi.approve.pojo.ApproveLog;
import com.ruoyi.approve.pojo.ApproveNode;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.approve.service.IApproveNodeService;
import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.security.LoginUser;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
@Component
public class ApproveTodoTools {
    private static final int DEFAULT_LIMIT = 10;
    private static final int MAX_LIMIT = 20;
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private final ApproveProcessMapper approveProcessMapper;
    private final ApproveNodeMapper approveNodeMapper;
    private final ApproveLogMapper approveLogMapper;
    private final IApproveNodeService approveNodeService;
    private final ApproveProcessServiceImpl approveProcessService;
    private final AiSessionUserContext aiSessionUserContext;
    public ApproveTodoTools(ApproveProcessMapper approveProcessMapper,
                            ApproveNodeMapper approveNodeMapper,
                            ApproveLogMapper approveLogMapper,
                            IApproveNodeService approveNodeService,
                            ApproveProcessServiceImpl approveProcessService,
                            AiSessionUserContext aiSessionUserContext) {
        this.approveProcessMapper = approveProcessMapper;
        this.approveNodeMapper = approveNodeMapper;
        this.approveLogMapper = approveLogMapper;
        this.approveNodeService = approveNodeService;
        this.approveProcessService = approveProcessService;
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Tool(name = "查询审批待办列表", value = "查询当前登录人相关的审批待办,优先返回自己待处理的审批,支持按状态、类型、关键字和范围过滤。")
    public String listTodos(@ToolMemoryId String memoryId,
                            @P(value = "审批状态,可选值:all、pending、processing、approved、rejected、resubmitted", required = false) String status,
                            @P(value = "审批类型编号,可不传", required = false) Integer approveType,
                            @P(value = "关键字,可匹配流程编号、标题、申请人、当前审批人", required = false) String keyword,
                            @P(value = "返回条数,默认10,最大20", required = false) Integer limit,
                            @P(value = "查询范围,可选值:related、applicant、approver;related è¡¨ç¤ºå½“前用户相关,applicant è¡¨ç¤ºæˆ‘发起的,approver è¡¨ç¤ºå¾…我处理的", required = false) String scope,
                            @P(value = "开始日期 yyyy-MM-dd,可不传", required = false) String startDate,
                            @P(value = "结束日期 yyyy-MM-dd,可不传", required = false) String endDate,
                            @P(value = "时间范围描述,例如 ä»Šå¤©ã€æœ¬æœˆã€è¿‘30天,可不传", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        Long userId = loginUser.getUserId();
        Integer statusCode = parseStatus(status);
        String normalizedScope = normalizeScope(scope);
        boolean hasDateFilter = StringUtils.hasText(startDate) || StringUtils.hasText(endDate) || StringUtils.hasText(timeRange);
        DateRange dateRange = hasDateFilter ? resolveDateRange(startDate, endDate, timeRange) : null;
        LambdaQueryWrapper<ApproveProcess> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ApproveProcess::getApproveDelete, 0);
        if (statusCode == null) {
            wrapper.ne(ApproveProcess::getApproveStatus, 2);
        }
        if (approveType != null) {
            wrapper.eq(ApproveProcess::getApproveType, approveType);
        }
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword)
                    .or().like(ApproveProcess::getApproveReason, keyword)
                    .or().like(ApproveProcess::getApproveUserName, keyword)
                    .or().like(ApproveProcess::getApproveUserCurrentName, keyword));
        }
        if ("applicant".equals(normalizedScope)) {
            wrapper.eq(ApproveProcess::getApproveUser, userId);
            if (statusCode != null) {
                wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
            }
        } else if ("approver".equals(normalizedScope)) {
            wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId);
            if (statusCode != null) {
                wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
            }
        } else if (statusCode != null && (statusCode == 0 || statusCode == 1)) {
            wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId);
            wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
        } else {
            wrapper.and(w -> w.eq(ApproveProcess::getApproveUser, userId)
                    .or().eq(ApproveProcess::getApproveUserCurrentId, userId)
                    .or().apply("FIND_IN_SET({0}, approve_user_ids)", userId));
            if (statusCode != null) {
                wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
            }
        }
        if (dateRange != null) {
            wrapper.ge(ApproveProcess::getCreateTime, dateRange.start().atStartOfDay())
                    .lt(ApproveProcess::getCreateTime, dateRange.end().plusDays(1).atStartOfDay());
        }
        wrapper.orderByDesc(ApproveProcess::getCreateTime)
                .last("limit " + normalizeLimit(limit));
        List<ApproveProcess> processes = defaultList(approveProcessMapper.selectList(wrapper));
        if (processes.isEmpty()) {
            return jsonResponse(true, "todo_list", "未查询到当前用户符合条件的审批待办。",
                    Map.of("count", 0),
                    Map.of("columns", todoColumns(), "items", List.of()),
                    Map.of());
        }
        List<Map<String, Object>> items = processes.stream()
                .filter(process -> canView(process, userId))
                .sorted(Comparator
                        .comparing((ApproveProcess process) -> !Objects.equals(process.getApproveUserCurrentId(), userId))
                        .thenComparing(ApproveProcess::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())))
                .map(process -> {
                    Map<String, Object> item = new LinkedHashMap<>();
                    item.put("approveId", process.getApproveId());
                    item.put("approveType", approveTypeName(process.getApproveType()));
                    item.put("approveUserName", safe(process.getApproveUserName()));
                    item.put("approveUserCurrentName", safe(process.getApproveUserCurrentName()));
                    item.put("approveReason", safe(process.getApproveReason()));
                    item.put("approveStatus", approveStatusName(process.getApproveStatus()));
                    item.put("createTime", formatDateTime(process.getCreateTime()));
                    item.put("relation", relationName(process, userId));
                    return item;
                })
                .collect(Collectors.toList());
        return jsonResponse(true, "todo_list", "已返回当前用户相关审批列表。",
                Map.of(
                        "count", items.size(),
                        "statusFilter", StringUtils.hasText(status) ? status : "all",
                        "approveType", approveType == null ? "" : approveType,
                        "keyword", keyword == null ? "" : keyword,
                        "scope", normalizedScope,
                        "timeRange", dateRange == null ? "all" : dateRange.label(),
                        "startDate", dateRange == null ? "" : dateRange.start().toString(),
                        "endDate", dateRange == null ? "" : dateRange.end().toString()
                ),
                Map.of("columns", todoColumns(), "items", items),
                Map.of());
    }
    @Tool(name = "查询审批待办详情", value = "根据流程编号查询当前登录人可见的审批详情。")
    public String getTodoDetail(@ToolMemoryId String memoryId,
                                @P("流程编号 approveId") String approveId) {
        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
        if (process == null) {
            return "未找到对应审批,或当前用户无权查看该流程。";
        }
        StringJoiner detail = new StringJoiner("\n");
        detail.add("审批详情");
        detail.add("流程编号: " + safe(process.getApproveId()));
        detail.add("审批类型: " + approveTypeName(process.getApproveType()));
        detail.add("申请人: " + safe(process.getApproveUserName()));
        detail.add("申请部门: " + safe(process.getApproveDeptName()));
        detail.add("当前审批人: " + safe(process.getApproveUserCurrentName()));
        detail.add("标题: " + safe(process.getApproveReason()));
        detail.add("状态: " + approveStatusName(process.getApproveStatus()));
        detail.add("申请日期: " + formatDate(process.getApproveTime()));
        detail.add("开始日期: " + formatDate(process.getStartDate()));
        detail.add("结束日期: " + formatDate(process.getEndDate()));
        detail.add("地点: " + safe(process.getLocation()));
        detail.add("金额: " + (process.getPrice() == null ? "" : process.getPrice().toPlainString()));
        detail.add("备注: " + safe(process.getApproveRemark()));
        detail.add("创建时间: " + formatDateTime(process.getCreateTime()));
        detail.add("与当前用户关系: " + relationName(process, currentUserId(memoryId)));
        return detail.toString();
    }
    @Tool(name = "查询审批流转记录", value = "根据流程编号查询审批节点和审批日志,用于回答进度、当前卡点和历史处理记录。")
    public String getTodoProgress(@ToolMemoryId String memoryId,
                                  @P("流程编号 approveId") String approveId) {
        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
        if (process == null) {
            return jsonResponse(false, "todo_progress", "未找到对应审批,或当前用户无权查看该流程。",
                    Map.of("approveId", safe(approveId)),
                    Map.of(),
                    Map.of());
        }
        List<ApproveNode> nodes = listNodes(process);
        List<ApproveLog> logs = listLogs(process.getId());
        ApproveNode currentNode = findCurrentNode(nodes);
        List<Map<String, Object>> nodeItems = nodes.stream().map(node -> {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("approveNodeOrder", node.getApproveNodeOrder());
            item.put("approveNodeUser", safe(node.getApproveNodeUser()));
            item.put("approveNodeUserId", node.getApproveNodeUserId());
            item.put("approveNodeStatus", approveNodeStatusName(node.getApproveNodeStatus()));
            item.put("approveNodeTime", formatDate(node.getApproveNodeTime()));
            item.put("approveNodeReason", safe(node.getApproveNodeReason()));
            item.put("approveNodeRemark", safe(node.getApproveNodeRemark()));
            item.put("isCurrent", currentNode != null && Objects.equals(currentNode.getId(), node.getId()));
            return item;
        }).collect(Collectors.toList());
        List<Map<String, Object>> logItems = logs.stream().map(log -> {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("approveNodeOrder", log.getApproveNodeOrder());
            item.put("approveUser", log.getApproveUser());
            item.put("approveStatus", approveStatusName(log.getApproveStatus()));
            item.put("approveTime", formatDate(log.getApproveTime()));
            item.put("approveRemark", safe(log.getApproveRemark()));
            return item;
        }).collect(Collectors.toList());
        return jsonResponse(true, "todo_progress", "已返回审批流转记录。",
                Map.of(
                        "approveId", safe(process.getApproveId()),
                        "currentStatus", approveStatusName(process.getApproveStatus()),
                        "currentApprover", safe(process.getApproveUserCurrentName()),
                        "currentNodeOrder", currentNode == null ? "" : currentNode.getApproveNodeOrder(),
                        "nodeCount", nodeItems.size(),
                        "logCount", logItems.size()
                ),
                Map.of("nodes", nodeItems, "logs", logItems),
                Map.of());
    }
    @Tool(name = "统计审批待办数据", value = "按用户指定的时间范围统计当前登录人相关审批的状态分布、类型分布和趋势;未指定时默认近7天。")
    public String getTodoStats(@ToolMemoryId String memoryId,
                               @P(value = "开始日期 yyyy-MM-dd,可不传", required = false) String startDate,
                               @P(value = "结束日期 yyyy-MM-dd,可不传", required = false) String endDate,
                               @P(value = "时间范围描述,例如 ä»Šå¤©ã€æœ¬æœˆã€è¿‘30天、2026-04-01到2026-04-27", required = false) String timeRange) {
        Long userId = currentUserId(memoryId);
        List<ApproveProcess> processes = defaultList(approveProcessMapper.selectList(new LambdaQueryWrapper<ApproveProcess>()
                .eq(ApproveProcess::getApproveDelete, 0)
                .and(w -> w.eq(ApproveProcess::getApproveUser, userId)
                        .or().eq(ApproveProcess::getApproveUserCurrentId, userId)
                        .or().apply("FIND_IN_SET({0}, approve_user_ids)", userId))));
        DateRange dateRange = resolveDateRange(startDate, endDate, timeRange);
        List<ApproveProcess> filteredProcesses = processes.stream()
                .filter(process -> withinDateRange(process.getCreateTime(), dateRange))
                .collect(Collectors.toList());
        if (filteredProcesses.isEmpty()) {
            return jsonResponse(true, "todo_stats", "当前用户没有相关审批数据。",
                    Map.of(
                            "total", 0,
                            "startDate", dateRange.start().toString(),
                            "endDate", dateRange.end().toString(),
                            "timeRange", dateRange.label()
                    ),
                    Map.of(
                            "statusDistribution", Map.of(),
                            "typeDistribution", Map.of(),
                            "trend", List.of()
                    ),
                    Map.of());
        }
        Map<String, Long> statusStats = filteredProcesses.stream()
                .collect(Collectors.groupingBy(p -> approveStatusName(p.getApproveStatus()), LinkedHashMap::new, Collectors.counting()));
        Map<String, Long> typeStats = filteredProcesses.stream()
                .collect(Collectors.groupingBy(p -> approveTypeName(p.getApproveType()), LinkedHashMap::new, Collectors.counting()));
        long pendingCount = countByStatus(filteredProcesses, 0);
        long processingCount = countByStatus(filteredProcesses, 1);
        long approvedCount = countByStatus(filteredProcesses, 2);
        long rejectedCount = countByStatus(filteredProcesses, 3);
        long resubmittedCount = countByStatus(filteredProcesses, 4);
        TrendRange trendRange = buildTrendRange(dateRange.start(), dateRange.end(), filteredProcesses);
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("total", filteredProcesses.size());
        summary.put("pending", pendingCount);
        summary.put("processing", processingCount);
        summary.put("approved", approvedCount);
        summary.put("rejected", rejectedCount);
        summary.put("resubmitted", resubmittedCount);
        summary.put("approvalCompletionRate", calculateRate(approvedCount, filteredProcesses.size()));
        summary.put("rejectionRate", calculateRate(rejectedCount, filteredProcesses.size()));
        summary.put("startDate", dateRange.start().toString());
        summary.put("endDate", dateRange.end().toString());
        summary.put("timeRange", dateRange.label());
        summary.put("trendGranularity", trendRange.granularity());
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("statusBarOption", buildStatusBarOption(statusStats));
        charts.put("typePieOption", buildTypePieOption(typeStats));
        charts.put("trendLineOption", buildTrendLineOption(trendRange.labels(), trendRange.values(), trendRange.label()));
        return jsonResponse(true, "todo_stats", "已返回当前用户相关审批统计。",
                summary,
                Map.of(
                        "statusDistribution", statusStats,
                        "typeDistribution", typeStats,
                        "trend", toTrendItems(trendRange.labels(), trendRange.values())
                ),
                charts);
    }
    @Transactional(rollbackFor = Exception.class)
    @Tool(name = "审批待办", value = "执行审批动作,action ä»…支持 approve æˆ– reject,且只能处理当前登录人自己的待审节点。")
    public String reviewTodo(@ToolMemoryId String memoryId,
                             @P("流程编号 approveId") String approveId,
                             @P("动作,approve=通过,reject=驳回") String action,
                             @P(value = "审批备注,可不传", required = false) String remark) {
        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
        if (process == null) {
            return actionResult(false, "review_action", "未找到对应审批,或当前用户无权访问该流程。", approveId, null);
        }
        if (!canOperate(process, currentUserId(memoryId))) {
            return actionResult(false, "review_action", "当前登录人不是该审批的当前处理人。", approveId, null);
        }
        if (process.getApproveStatus() != null && (process.getApproveStatus() == 2 || process.getApproveStatus() == 3)) {
            return actionResult(false, "review_action", "该审批已结束,不能重复处理。", approveId, null);
        }
        List<ApproveNode> nodes = listNodes(process);
        ApproveNode currentNode = findCurrentNode(nodes);
        if (currentNode == null || !Objects.equals(currentNode.getApproveNodeUserId(), currentUserId(memoryId))) {
            return actionResult(false, "review_action", "未找到当前用户可处理的审批节点。", approveId, null);
        }
        String normalizedAction = action == null ? "" : action.trim().toLowerCase();
        currentNode.setApproveNodeRemark(remark);
        currentNode.setApproveNodeReason("reject".equals(normalizedAction) ? remark : null);
        currentNode.setUpdateUser(currentUserId(memoryId));
        currentNode.setUpdateTime(LocalDateTime.now());
        currentNode.setIsLast(isLastNode(nodes, currentNode));
        try {
            switch (normalizedAction) {
                case "approve" -> currentNode.setApproveNodeStatus(1);
                case "reject" -> currentNode.setApproveNodeStatus(2);
                default -> {
                    return actionResult(false, "review_action", "action åªæ”¯æŒ approve æˆ– reject。", approveId, null);
                }
            }
            approveNodeService.updateApproveNode(currentNode);
        } catch (IOException e) {
            throw new RuntimeException("审批处理失败", e);
        }
        ApproveProcess refreshed = getProcessByApproveId(approveId);
        writeApproveLog(memoryId, refreshed, currentNode, remark);
        ApproveNode nextNode = refreshed == null ? null : findCurrentNode(listNodes(refreshed));
        return actionResult(true, "review_action",
                "approve".equals(normalizedAction) ? "审批已通过。" : "审批已驳回。",
                approveId,
                Map.of(
                        "action", normalizedAction,
                        "currentStatus", refreshed == null ? "" : approveStatusName(refreshed.getApproveStatus()),
                        "nextApprover", nextNode == null ? "" : safe(nextNode.getApproveNodeUser()),
                        "remark", safe(remark)
                ));
    }
    @Transactional(rollbackFor = Exception.class)
    @Tool(name = "取消审批待办审核", value = "撤销最近一次审核结果,仅允许最近一次审核人或申请人操作。")
    public String cancelReviewTodo(@ToolMemoryId String memoryId,
                                   @P("流程编号 approveId") String approveId,
                                   @P(value = "取消原因,可不传", required = false) String reason) {
        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
        if (process == null) {
            return actionResult(false, "cancel_review_action", "未找到对应审批,或当前用户无权访问该流程。", approveId, null);
        }
        List<ApproveNode> nodes = listNodes(process);
        ApproveNode lastReviewedNode = nodes.stream()
                .filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() != 0)
                .max(Comparator.comparing(ApproveNode::getApproveNodeOrder))
                .orElse(null);
        if (lastReviewedNode == null) {
            return actionResult(false, "cancel_review_action", "当前流程没有可撤销的审核记录。", approveId, null);
        }
        Long userId = currentUserId(memoryId);
        if (!isAdmin(userId)
                && !Objects.equals(process.getApproveUser(), userId)
                && !Objects.equals(lastReviewedNode.getApproveNodeUserId(), userId)) {
            return actionResult(false, "cancel_review_action", "只有申请人、最近一次审核人或管理员可以撤销。", approveId, null);
        }
        lastReviewedNode.setApproveNodeStatus(0);
        lastReviewedNode.setApproveNodeTime(null);
        lastReviewedNode.setApproveNodeReason(null);
        lastReviewedNode.setApproveNodeRemark(reason);
        lastReviewedNode.setUpdateUser(userId);
        lastReviewedNode.setUpdateTime(LocalDateTime.now());
        approveNodeMapper.updateById(lastReviewedNode);
        ApproveLog latestLog = listLogs(process.getId()).stream()
                .max(Comparator.comparing(ApproveLog::getApproveNodeOrder)
                        .thenComparing(ApproveLog::getApproveTime, Comparator.nullsLast(Date::compareTo)))
                .orElse(null);
        if (latestLog != null) {
            approveLogMapper.deleteById(latestLog.getId());
        }
        process.setApproveOverTime(null);
        process.setApproveRemark(reason);
        process.setApproveStatus(lastReviewedNode.getApproveNodeOrder() == null || lastReviewedNode.getApproveNodeOrder() <= 1 ? 0 : 1);
        process.setApproveUserCurrentId(lastReviewedNode.getApproveNodeUserId());
        process.setApproveUserCurrentName(lastReviewedNode.getApproveNodeUser());
        approveProcessMapper.updateById(process);
        return actionResult(true, "cancel_review_action", "最近一次审核已撤销。", approveId, Map.of(
                "rollbackNodeOrder", lastReviewedNode.getApproveNodeOrder(),
                "currentStatus", approveStatusName(process.getApproveStatus()),
                "currentApprover", safe(process.getApproveUserCurrentName()),
                "reason", safe(reason)
        ));
    }
    @Transactional(rollbackFor = Exception.class)
    @Tool(name = "修改审批待办", value = "修改审批单基础信息,仅允许申请人修改;不支持通过 AI å˜æ›´å®¡æ‰¹ç±»åž‹ã€‚")
    public String updateTodo(@ToolMemoryId String memoryId,
                             @P("流程编号 approveId") String approveId,
                             @P(value = "新的标题,可不传", required = false) String approveReason,
                             @P(value = "新的开始日期 yyyy-MM-dd,可不传", required = false) String startDate,
                             @P(value = "新的结束日期 yyyy-MM-dd,可不传", required = false) String endDate,
                             @P(value = "新的金额,可不传", required = false) BigDecimal price,
                             @P(value = "新的地点,可不传", required = false) String location,
                             @P(value = "新的审批类型,可不传", required = false) Integer approveType,
                             @P(value = "新的备注,可不传", required = false) String approveRemark) {
        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
        if (process == null) {
            return actionResult(false, "update_action", "未找到对应审批,或当前用户无权访问该流程。", approveId, null);
        }
        if (!Objects.equals(process.getApproveUser(), currentUserId(memoryId)) && !isAdmin(currentUserId(memoryId))) {
            return actionResult(false, "update_action", "只有申请人或管理员可以修改审批单。", approveId, null);
        }
        if (process.getApproveStatus() != null && (process.getApproveStatus() == 1 || process.getApproveStatus() == 2)) {
            return actionResult(false, "update_action", "审批处理中或已完成时,不允许通过 AI ä¿®æ”¹ã€‚", approveId, null);
        }
        if (approveType != null && !Objects.equals(approveType, process.getApproveType())) {
            return actionResult(false, "update_action", "AI åŠ©æ‰‹æš‚ä¸æ”¯æŒç›´æŽ¥å˜æ›´å®¡æ‰¹ç±»åž‹ï¼Œé¿å…èŠ‚ç‚¹é…ç½®å¤±çœŸã€‚", approveId, null);
        }
        if (!StringUtils.hasText(approveReason)
                && !StringUtils.hasText(startDate)
                && !StringUtils.hasText(endDate)
                && price == null
                && !StringUtils.hasText(location)
                && !StringUtils.hasText(approveRemark)) {
            return actionResult(false, "update_action", "没有检测到可更新的字段。", approveId, null);
        }
        if (StringUtils.hasText(approveReason)) {
            process.setApproveReason(approveReason);
        }
        if (StringUtils.hasText(startDate)) {
            process.setStartDate(parseDate(startDate));
        }
        if (StringUtils.hasText(endDate)) {
            process.setEndDate(parseDate(endDate));
        }
        if (price != null) {
            process.setPrice(price);
        }
        if (StringUtils.hasText(location)) {
            process.setLocation(location);
        }
        if (StringUtils.hasText(approveRemark)) {
            process.setApproveRemark(approveRemark);
        }
        approveProcessMapper.updateById(process);
        return actionResult(true, "update_action", "审批单已更新。", approveId, Map.of(
                "approveReason", safe(process.getApproveReason()),
                "startDate", formatDate(process.getStartDate()),
                "endDate", formatDate(process.getEndDate()),
                "price", process.getPrice() == null ? "" : process.getPrice(),
                "location", safe(process.getLocation()),
                "approveType", approveTypeName(process.getApproveType()),
                "approveRemark", safe(process.getApproveRemark())
        ));
    }
    @Transactional(rollbackFor = Exception.class)
    @Tool(name = "删除审批待办", value = "删除审批流程,仅允许申请人删除未完成的流程。")
    public String deleteTodo(@ToolMemoryId String memoryId,
                             @P("流程编号 approveId") String approveId) {
        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
        if (process == null) {
            return actionResult(false, "delete_action", "未找到对应审批,或当前用户无权访问该流程。", approveId, null);
        }
        if (!Objects.equals(process.getApproveUser(), currentUserId(memoryId)) && !isAdmin(currentUserId(memoryId))) {
            return actionResult(false, "delete_action", "只有申请人或管理员可以删除审批单。", approveId, null);
        }
        if (process.getApproveStatus() != null && (process.getApproveStatus() == 1 || process.getApproveStatus() == 2)) {
            return actionResult(false, "delete_action", "审批处理中或已完成的流程不允许通过 AI åˆ é™¤ã€‚", approveId, null);
        }
        approveProcessService.delByIds(Collections.singletonList(process.getId()));
        return actionResult(true, "delete_action", "审批流程已删除。", approveId, Map.of(
                "deletedProcessId", process.getId(),
                "approveStatus", approveStatusName(process.getApproveStatus())
        ));
    }
    private ApproveProcess getAccessibleProcess(String memoryId, String approveId) {
        ApproveProcess process = getProcessByApproveId(approveId);
        if (process == null) {
            return null;
        }
        return canView(process, currentUserId(memoryId)) ? process : null;
    }
    private ApproveProcess getProcessByApproveId(String approveId) {
        if (!StringUtils.hasText(approveId)) {
            return null;
        }
        return approveProcessMapper.selectOne(new LambdaQueryWrapper<ApproveProcess>()
                .eq(ApproveProcess::getApproveId, approveId)
                .eq(ApproveProcess::getApproveDelete, 0)
                .last("limit 1"));
    }
    private List<ApproveNode> listNodes(ApproveProcess process) {
        if (process == null) {
            return List.of();
        }
        List<ApproveNode> nodes = defaultList(approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
                .eq(ApproveNode::getDeleteFlag, 0)
                .eq(ApproveNode::getApproveProcessId, process.getApproveId())
                .orderByAsc(ApproveNode::getApproveNodeOrder)));
        if (!nodes.isEmpty()) {
            return nodes;
        }
        return defaultList(approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
                .eq(ApproveNode::getDeleteFlag, 0)
                .eq(ApproveNode::getApproveProcessId, String.valueOf(process.getId()))
                .orderByAsc(ApproveNode::getApproveNodeOrder)));
    }
    private List<ApproveLog> listLogs(Long processId) {
        return defaultList(approveLogMapper.selectList(new LambdaQueryWrapper<ApproveLog>()
                .eq(ApproveLog::getApproveId, processId)
                .orderByAsc(ApproveLog::getApproveNodeOrder, ApproveLog::getApproveTime)));
    }
    private ApproveNode findCurrentNode(List<ApproveNode> nodes) {
        return nodes.stream()
                .filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() == 0)
                .min(Comparator.comparing(ApproveNode::getApproveNodeOrder))
                .orElse(null);
    }
    private boolean isLastNode(List<ApproveNode> nodes, ApproveNode currentNode) {
        Integer maxOrder = nodes.stream()
                .map(ApproveNode::getApproveNodeOrder)
                .filter(Objects::nonNull)
                .max(Integer::compareTo)
                .orElse(null);
        return maxOrder != null && Objects.equals(maxOrder, currentNode.getApproveNodeOrder());
    }
    private void writeApproveLog(String memoryId, ApproveProcess process, ApproveNode currentNode, String remark) {
        if (process == null || currentNode == null) {
            return;
        }
        ApproveLog log = new ApproveLog();
        log.setApproveId(process.getId());
        log.setApproveNodeOrder(currentNode.getApproveNodeOrder());
        log.setApproveUser(currentUserId(memoryId));
        log.setApproveTime(new Date());
        log.setApproveStatus(process.getApproveStatus());
        log.setApproveRemark(remark);
        approveLogMapper.insert(log);
    }
    private boolean canView(ApproveProcess process, Long userId) {
        if (process == null || userId == null) {
            return false;
        }
        return isAdmin(userId)
                || Objects.equals(process.getApproveUser(), userId)
                || Objects.equals(process.getApproveUserCurrentId(), userId)
                || containsUserId(process.getApproveUserIds(), userId);
    }
    private boolean canOperate(ApproveProcess process, Long userId) {
        return process != null && userId != null && Objects.equals(process.getApproveUserCurrentId(), userId);
    }
    private boolean containsUserId(String csv, Long userId) {
        if (!StringUtils.hasText(csv) || userId == null) {
            return false;
        }
        String target = String.valueOf(userId);
        for (String item : csv.split(",")) {
            if (target.equals(item.trim())) {
                return true;
            }
        }
        return false;
    }
    private String relationName(ApproveProcess process, Long userId) {
        if (Objects.equals(process.getApproveUserCurrentId(), userId)) {
            return "当前审批人";
        }
        if (Objects.equals(process.getApproveUser(), userId)) {
            return "申请人";
        }
        if (containsUserId(process.getApproveUserIds(), userId)) {
            return "审批链成员";
        }
        return "可见";
    }
    private List<String> todoColumns() {
        return List.of("approveId", "approveType", "approveUserName", "approveUserCurrentName",
                "approveReason", "approveStatus", "createTime", "relation");
    }
    private int normalizeLimit(Integer limit) {
        if (limit == null || limit <= 0) {
            return DEFAULT_LIMIT;
        }
        return Math.min(limit, MAX_LIMIT);
    }
    private Integer parseStatus(String status) {
        if (!StringUtils.hasText(status) || "all".equalsIgnoreCase(status)) {
            return null;
        }
        return switch (status.trim().toLowerCase()) {
            case "pending" -> 0;
            case "processing" -> 1;
            case "approved" -> 2;
            case "rejected" -> 3;
            case "resubmitted" -> 4;
            default -> null;
        };
    }
    private String normalizeScope(String scope) {
        if (!StringUtils.hasText(scope)) {
            return "related";
        }
        return switch (scope.trim().toLowerCase()) {
            case "applicant", "mine", "created", "initiated" -> "applicant";
            case "approver", "handler", "todo", "pending" -> "approver";
            default -> "related";
        };
    }
    private String approveStatusName(Integer status) {
        if (status == null) {
            return "未知";
        }
        return switch (status) {
            case 0 -> "待审核";
            case 1 -> "审核中";
            case 2 -> "审核完成";
            case 3 -> "审核未通过";
            case 4 -> "已重新提交";
            default -> "未知";
        };
    }
    private String approveNodeStatusName(Integer status) {
        if (status == null) {
            return "未知";
        }
        return switch (status) {
            case 0 -> "未审核";
            case 1 -> "同意";
            case 2 -> "拒绝";
            default -> "未知";
        };
    }
    private String approveTypeName(Integer type) {
        if (type == null) {
            return "未知";
        }
        return switch (type) {
            case 1 -> "公出管理";
            case 2 -> "请假管理";
            case 3 -> "出差管理";
            case 4 -> "报销管理";
            case 5 -> "采购审批";
            case 6 -> "报价审批";
            case 7 -> "发货审批";
            case 8 -> "危险作业审批";
            case 9 -> "办公用品审批";
            default -> "类型" + type;
        };
    }
    private String safe(Object value) {
        return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ');
    }
    private String formatDateTime(Object value) {
        if (value == null) {
            return "";
        }
        if (value instanceof LocalDateTime localDateTime) {
            return localDateTime.format(DATE_TIME_FORMATTER);
        }
        return safe(value);
    }
    private String formatDate(Date value) {
        if (value == null) {
            return "";
        }
        return value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().format(DATE_FORMATTER);
    }
    private long countByStatus(List<ApproveProcess> processes, int status) {
        return processes.stream()
                .filter(process -> process.getApproveStatus() != null)
                .filter(process -> process.getApproveStatus() == status)
                .count();
    }
    private String calculateRate(long part, int total) {
        if (total <= 0) {
            return "0.00%";
        }
        return String.format("%.2f%%", part * 100.0 / total);
    }
    private List<Map<String, Object>> toTrendItems(List<String> dates, List<Long> values) {
        List<Map<String, Object>> items = new ArrayList<>();
        for (int i = 0; i < dates.size(); i++) {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("date", dates.get(i));
            item.put("count", values.get(i));
            items.add(item);
        }
        return items;
    }
    private Map<String, Object> buildStatusBarOption(Map<String, Long> statusStats) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "审批状态分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", new ArrayList<>(statusStats.keySet())));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of(
                "name", "数量",
                "type", "bar",
                "data", new ArrayList<>(statusStats.values()),
                "barWidth", "40%"
        )));
        return option;
    }
    private Map<String, Object> buildTypePieOption(Map<String, Long> typeStats) {
        List<Map<String, Object>> data = typeStats.entrySet().stream()
                .map(entry -> {
                    Map<String, Object> item = new LinkedHashMap<>();
                    item.put("name", entry.getKey());
                    item.put("value", entry.getValue());
                    return item;
                })
                .collect(Collectors.toList());
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "审批类型占比", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("legend", Map.of("orient", "vertical", "left", "left"));
        option.put("series", List.of(Map.of(
                "name", "审批类型",
                "type", "pie",
                "radius", List.of("35%", "65%"),
                "data", data
        )));
        return option;
    }
    private Map<String, Object> buildTrendLineOption(List<String> dates, List<Long> values, String label) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", label + "审批新增趋势", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", dates));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of(
                "name", "新增审批",
                "type", "line",
                "smooth", true,
                "data", values,
                "areaStyle", Map.of()
        )));
        return option;
    }
    private Date parseDate(String dateText) {
        try {
            LocalDate localDate = LocalDate.parse(dateText, DATE_FORMATTER);
            return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
        } catch (Exception e) {
            throw new IllegalArgumentException("日期格式必须是 yyyy-MM-dd");
        }
    }
    private DateRange resolveDateRange(String startDateText, String endDateText, String timeRange) {
        LocalDate today = LocalDate.now();
        LocalDate explicitStart = parseLocalDate(startDateText);
        LocalDate explicitEnd = parseLocalDate(endDateText);
        if (explicitStart != null || explicitEnd != null) {
            LocalDate start = explicitStart != null ? explicitStart : explicitEnd;
            LocalDate end = explicitEnd != null ? explicitEnd : explicitStart;
            if (start.isAfter(end)) {
                LocalDate temp = start;
                start = end;
                end = temp;
            }
            return new DateRange(start, end, start + "至" + end);
        }
        if (!StringUtils.hasText(timeRange)) {
            return new DateRange(today.minusDays(6), today, "近7天");
        }
        String text = timeRange.trim();
        if (text.contains("今天")) {
            return new DateRange(today, today, "今天");
        }
        if (text.contains("昨天") || text.contains("昨日")) {
            LocalDate day = today.minusDays(1);
            return new DateRange(day, day, "昨天");
        }
        if (text.contains("本周")) {
            LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            return new DateRange(start, today, "本周");
        }
        if (text.contains("上周")) {
            LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            LocalDate start = thisWeekStart.minusWeeks(1);
            LocalDate end = thisWeekStart.minusDays(1);
            return new DateRange(start, end, "上周");
        }
        if (text.contains("本月")) {
            LocalDate start = today.withDayOfMonth(1);
            return new DateRange(start, today, "本月");
        }
        if (text.contains("上月")) {
            YearMonth lastMonth = YearMonth.from(today).minusMonths(1);
            return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth(), "上月");
        }
        if (text.contains("本年") || text.contains("今年")) {
            LocalDate start = today.withDayOfYear(1);
            return new DateRange(start, today, "本年");
        }
        if (text.contains("去年")) {
            LocalDate start = today.minusYears(1).withDayOfYear(1);
            LocalDate end = today.minusYears(1).withMonth(12).withDayOfMonth(31);
            return new DateRange(start, end, "去年");
        }
        Matcher relativeMatcher = java.util.regex.Pattern.compile("(近|最近)(\\d+)(天|周|个月|月|å¹´)").matcher(text);
        if (relativeMatcher.find()) {
            int amount = Integer.parseInt(relativeMatcher.group(2));
            String unit = relativeMatcher.group(3);
            LocalDate start = switch (unit) {
                case "天" -> today.minusDays(Math.max(amount - 1L, 0));
                case "周" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
                case "个月", "月" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
                case "å¹´" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
                default -> today.minusDays(6);
            };
            return new DateRange(start, today, "近" + amount + unit);
        }
        Matcher dateMatcher = java.util.regex.Pattern.compile("(\\d{4}-\\d{2}-\\d{2})").matcher(text);
        if (dateMatcher.find()) {
            LocalDate start = LocalDate.parse(dateMatcher.group(1));
            LocalDate end = dateMatcher.find() ? LocalDate.parse(dateMatcher.group(1)) : start;
            if (start.isAfter(end)) {
                LocalDate temp = start;
                start = end;
                end = temp;
            }
            return new DateRange(start, end, start + "至" + end);
        }
        return new DateRange(today.minusDays(6), today, "近7天");
    }
    private boolean withinDateRange(LocalDateTime createTime, DateRange dateRange) {
        if (createTime == null) {
            return false;
        }
        LocalDate date = createTime.toLocalDate();
        return !date.isBefore(dateRange.start()) && !date.isAfter(dateRange.end());
    }
    private TrendRange buildTrendRange(LocalDate start, LocalDate end, List<ApproveProcess> processes) {
        long days = ChronoUnit.DAYS.between(start, end) + 1;
        if (days <= 31) {
            List<String> labels = new ArrayList<>();
            List<Long> values = new ArrayList<>();
            for (LocalDate cursor = start; !cursor.isAfter(end); cursor = cursor.plusDays(1)) {
                LocalDate current = cursor;
                labels.add(current.toString());
                values.add(processes.stream()
                        .filter(process -> process.getCreateTime() != null)
                        .filter(process -> process.getCreateTime().toLocalDate().equals(current))
                        .count());
            }
            return new TrendRange(labels, values, "day", start + "至" + end);
        }
        List<String> labels = new ArrayList<>();
        List<Long> values = new ArrayList<>();
        YearMonth startMonth = YearMonth.from(start);
        YearMonth endMonth = YearMonth.from(end);
        for (YearMonth cursor = startMonth; !cursor.isAfter(endMonth); cursor = cursor.plusMonths(1)) {
            YearMonth current = cursor;
            labels.add(current.toString());
            values.add(processes.stream()
                    .filter(process -> process.getCreateTime() != null)
                    .filter(process -> YearMonth.from(process.getCreateTime()).equals(current))
                    .count());
        }
        return new TrendRange(labels, values, "month", start + "至" + end);
    }
    private LocalDate parseLocalDate(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        return LocalDate.parse(text.trim());
    }
    private String actionResult(boolean success, String type, String description, String approveId, Map<String, Object> data) {
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("approveId", safe(approveId));
        return jsonResponse(success, type, description, summary, data == null ? Map.of() : data, Map.of());
    }
    private String jsonResponse(boolean success,
                                String type,
                                String description,
                                Map<String, Object> summary,
                                Map<String, Object> data,
                                Map<String, Object> charts) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("success", success);
        result.put("type", type);
        result.put("description", description);
        result.put("summary", summary == null ? Map.of() : summary);
        result.put("data", data == null ? Map.of() : data);
        result.put("charts", charts == null ? Map.of() : charts);
        return JSON.toJSONString(result);
    }
    private LoginUser currentLoginUser(String memoryId) {
        LoginUser loginUser = aiSessionUserContext.get(memoryId);
        if (loginUser != null) {
            return loginUser;
        }
        return SecurityUtils.getLoginUser();
    }
    private Long currentUserId(String memoryId) {
        return currentLoginUser(memoryId).getUserId();
    }
    private boolean isAdmin(Long userId) {
        return SecurityUtils.isAdmin(userId);
    }
    private <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    private record DateRange(LocalDate start, LocalDate end, String label) {
    }
    private record TrendRange(List<String> labels, List<Long> values, String granularity, String label) {
    }
}
src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2311 @@
package com.ruoyi.ai.tools;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.ruoyi.account.mapper.AccountStatementMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.account.service.impl.AccountingServiceImpl;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.mapper.SupplierManageMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.pojo.DeviceRepair;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
import com.ruoyi.production.mapper.ProductionAccountMapper;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionPlanMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.mapper.ProductionProductOutputMapper;
import com.ruoyi.production.pojo.ProductionAccount;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionPlan;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.pojo.ProductionProductOutput;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
import com.ruoyi.technology.pojo.TechnologyOperation;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Component
public class FinancialAgentTools {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final Pattern RELATIVE_PATTERN = Pattern.compile("(近|最近)?\\s*(\\d+)\\s*(天|周|个月|月|å¹´)");
    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
    private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
    private static final int DEFAULT_LIMIT = 10;
    private static final int MAX_LIMIT = 50;
    private static final BigDecimal DEFAULT_FALLBACK_MATERIAL_COST_RATE = new BigDecimal("0.60");
    private static final BigDecimal DEFAULT_LABOR_COST_RATE = new BigDecimal("0.15");
    private static final BigDecimal DEFAULT_OVERHEAD_COST_RATE = new BigDecimal("0.10");
    private static final BigDecimal SME_RECEIVABLE_RISK_THRESHOLD = new BigDecimal("500000");
    private static final BigDecimal SME_INVENTORY_RISK_THRESHOLD = new BigDecimal("1000000");
    private static final BigDecimal SME_PROFIT_WARNING_RATE = new BigDecimal("0.08");
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final ProductionAccountMapper productionAccountMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionProductOutputMapper productionProductOutputMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final DeviceRepairMapper deviceRepairMapper;
    private final ProcurementRecordMapper procurementRecordMapper;
    private final ProcurementRecordOutMapper procurementRecordOutMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final AccountStatementMapper accountStatementMapper;
    private final CustomerMapper customerMapper;
    private final SupplierManageMapper supplierManageMapper;
    private final ProductModelMapper productModelMapper;
    private final ProductMapper productMapper;
    private final AiSessionUserContext aiSessionUserContext;
    public FinancialAgentTools(SalesLedgerMapper salesLedgerMapper,
                               SalesLedgerProductMapper salesLedgerProductMapper,
                               ProductionAccountMapper productionAccountMapper,
                               ProductionProductMainMapper productionProductMainMapper,
                               ProductionOperationTaskMapper productionOperationTaskMapper,
                               ProductionOrderMapper productionOrderMapper,
                               ProductionPlanMapper productionPlanMapper,
                               ProductionProductOutputMapper productionProductOutputMapper,
                               TechnologyOperationMapper technologyOperationMapper,
                               DeviceLedgerMapper deviceLedgerMapper,
                               DeviceRepairMapper deviceRepairMapper,
                               ProcurementRecordMapper procurementRecordMapper,
                               ProcurementRecordOutMapper procurementRecordOutMapper,
                               StockInventoryMapper stockInventoryMapper,
                               AccountSalesCollectionMapper accountSalesCollectionMapper,
                               AccountPurchasePaymentMapper accountPurchasePaymentMapper,
                               AccountStatementMapper accountStatementMapper,
                               CustomerMapper customerMapper,
                               SupplierManageMapper supplierManageMapper,
                               ProductModelMapper productModelMapper,
                               ProductMapper productMapper,
                               AiSessionUserContext aiSessionUserContext) {
        this.salesLedgerMapper = salesLedgerMapper;
        this.salesLedgerProductMapper = salesLedgerProductMapper;
        this.productionAccountMapper = productionAccountMapper;
        this.productionProductMainMapper = productionProductMainMapper;
        this.productionOperationTaskMapper = productionOperationTaskMapper;
        this.productionOrderMapper = productionOrderMapper;
        this.productionPlanMapper = productionPlanMapper;
        this.productionProductOutputMapper = productionProductOutputMapper;
        this.technologyOperationMapper = technologyOperationMapper;
        this.deviceLedgerMapper = deviceLedgerMapper;
        this.deviceRepairMapper = deviceRepairMapper;
        this.procurementRecordMapper = procurementRecordMapper;
        this.procurementRecordOutMapper = procurementRecordOutMapper;
        this.stockInventoryMapper = stockInventoryMapper;
        this.accountSalesCollectionMapper = accountSalesCollectionMapper;
        this.accountPurchasePaymentMapper = accountPurchasePaymentMapper;
        this.accountStatementMapper = accountStatementMapper;
        this.customerMapper = customerMapper;
        this.supplierManageMapper = supplierManageMapper;
        this.productModelMapper = productModelMapper;
        this.productMapper = productMapper;
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Tool(name = "财务知识检索", value = "按财务经营问题检索业财融合知识片段与指标口径,作为RAG上下文。")
    public String retrieveFinancialKnowledge(@ToolMemoryId String memoryId,
                                             @P(value = "问题或关键词,例如利润下降、库存周转、资金缺口") String question) {
        List<KnowledgeDoc> knowledgeDocs = financeKnowledgeBase();
        String normalized = normalizeForMatch(question);
        List<KnowledgeDoc> ranked = knowledgeDocs.stream()
                .sorted(Comparator.comparingInt((KnowledgeDoc doc) -> keywordHitCount(doc.keywords(), normalized)).reversed())
                .filter(doc -> keywordHitCount(doc.keywords(), normalized) > 0 || !StringUtils.hasText(normalized))
                .limit(5)
                .toList();
        List<Map<String, Object>> items = ranked.stream().map(doc -> {
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("topic", doc.topic());
            map.put("knowledge", doc.knowledge());
            map.put("relatedTables", doc.relatedTables());
            map.put("suggestedQuestions", doc.suggestedQuestions());
            return map;
        }).toList();
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("question", safe(question));
        summary.put("hitCount", items.size());
        summary.put("retrievalMode", "keyword_rag");
        return jsonResponse(true, "financial_rag_knowledge", "已返回财务知识检索结果", summary, Map.of("items", items), Map.of());
    }
    @Tool(name = "智能成本核算", value = "自动核算产品成本、工序成本、人工成本、设备折旧、材料损耗与订单利润。")
    public String calculateIntelligentCost(@ToolMemoryId String memoryId,
                                           @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                           @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                           @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange,
                                           @P(value = "关键词,可匹配合同号/客户/项目", required = false) String keyword,
                                           @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        if (loginUser == null) {
            return jsonResponse(false, "financial_cost_accounting", "用户信息获取失败", Map.of(), Map.of(), Map.of());
        }
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit);
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("orderCount", bundle.orderMetrics().size());
        summary.put("totalRevenue", bundle.totalRevenue());
        summary.put("totalMaterialCost", bundle.totalMaterialCost());
        summary.put("totalLaborCost", bundle.totalLaborCost());
        summary.put("totalDepreciationCost", bundle.totalDepreciationCost());
        summary.put("totalScrapCost", bundle.totalScrapCost());
        summary.put("totalCost", bundle.totalCost());
        summary.put("totalProfit", bundle.totalProfit());
        summary.put("profitRate", toPercent(rate(bundle.totalProfit(), bundle.totalRevenue())));
        List<Map<String, Object>> orderItems = bundle.orderMetrics().stream()
                .map(this::toOrderCostItem)
                .toList();
        List<Map<String, Object>> processItems = bundle.processCostRanking().entrySet().stream()
                .map(entry -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("processName", entry.getKey());
                    map.put("cost", entry.getValue());
                    return map;
                }).toList();
        List<Map<String, Object>> topCustomerItems = buildCustomerProfitTop(bundle.orderMetrics(), 5);
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("costCompositionPieOption",
                buildCostCompositionPie(bundle.totalMaterialCost(), bundle.totalLaborCost(), bundle.totalDepreciationCost(), bundle.totalScrapCost()));
        charts.put("orderProfitBarOption", buildOrderProfitBar(bundle.orderMetrics()));
        charts.put("processCostBarOption", buildProcessCostBar(bundle.processCostRanking()));
        return jsonResponse(true, "financial_cost_accounting", "已完成智能成本核算", summary,
                Map.of(
                        "orders", orderItems,
                        "processCostRanking", processItems,
                        "topCustomers", topCustomerItems
                ),
                charts
        );
    }
    @Tool(name = "订单利润分析", value = "识别低利润/亏损订单,输出原因分析和优化建议。")
    public String analyzeOrderProfit(@ToolMemoryId String memoryId,
                                     @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                     @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                     @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange,
                                     @P(value = "关键词,可匹配合同号/客户/项目", required = false) String keyword,
                                     @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        if (loginUser == null) {
            return jsonResponse(false, "financial_order_profit_analysis", "用户信息获取失败", Map.of(), Map.of(), Map.of());
        }
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit);
        List<OrderProfitMetric> metrics = bundle.orderMetrics();
        List<OrderProfitMetric> riskyOrders = metrics.stream()
                .filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0 || item.profitRate().compareTo(SME_PROFIT_WARNING_RATE) < 0)
                .sorted(Comparator.comparing(OrderProfitMetric::profitRate)
                        .thenComparing(OrderProfitMetric::profit))
                .toList();
        Map<String, BigDecimal> customerProfitMap = new LinkedHashMap<>();
        for (OrderProfitMetric metric : metrics) {
            customerProfitMap.merge(metric.customerName(), metric.profit(), BigDecimal::add);
        }
        Map.Entry<String, BigDecimal> topCustomer = customerProfitMap.entrySet().stream()
                .max(Map.Entry.comparingByValue())
                .orElse(Map.entry("暂无数据", BigDecimal.ZERO));
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("orderCount", metrics.size());
        summary.put("lossOrderCount", metrics.stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count());
        summary.put("lowProfitOrderCount", riskyOrders.size());
        summary.put("avgProfitRate", toPercent(avgRate(metrics)));
        summary.put("topCustomerByProfit", topCustomer.getKey());
        summary.put("topCustomerProfit", topCustomer.getValue());
        List<Map<String, Object>> riskyItems = riskyOrders.stream()
                .limit(normalizeLimit(limit))
                .map(this::toRiskOrderItem)
                .toList();
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("profitDistributionOption", buildProfitDistributionBar(metrics));
        charts.put("lossOrderTrendOption", buildLossOrderTrendLine(metrics));
        charts.put("customerProfitTopOption", buildCustomerProfitBar(customerProfitMap));
        return jsonResponse(true, "financial_order_profit_analysis", "已完成订单利润分析", summary,
                Map.of(
                        "riskOrders", riskyItems,
                        "allOrders", metrics.stream().map(this::toOrderCostItem).toList(),
                        "customerProfitTop", buildCustomerProfitTop(metrics, 10)
                ),
                charts
        );
    }
    @Tool(name = "库存资金分析", value = "分析库存积压、呆滞库存、资金占用与周转率。")
    public String analyzeInventoryCapital(@ToolMemoryId String memoryId,
                                          @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                          @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                          @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange,
                                          @P(value = "关键词,可匹配产品名称/型号", required = false) String keyword,
                                          @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        int finalLimit = normalizeLimit(limit);
        List<StockInventory> inventoryRows = queryStockInventory(loginUser);
        if (inventoryRows.isEmpty()) {
            return jsonResponse(true, "financial_inventory_capital_analysis", "当前无库存数据",
                    rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
        }
        Set<Long> modelIds = inventoryRows.stream()
                .map(StockInventory::getProductModelId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, ProductModel> productModelMap = queryProductModels(modelIds);
        Map<Long, Product> productMap = queryProducts(productModelMap.values());
        Map<Long, BigDecimal> avgUnitCostByModelId = queryAverageUnitCostByModel(loginUser, modelIds);
        OutboundStats outboundStats = queryOutboundStats(loginUser, modelIds, range);
        List<InventoryMetric> metrics = buildInventoryMetrics(inventoryRows, productModelMap, productMap, avgUnitCostByModelId, outboundStats)
                .stream()
                .filter(metric -> matchInventoryKeyword(metric, keyword))
                .sorted(Comparator.comparing(InventoryMetric::inventoryValue).reversed())
                .toList();
        BigDecimal totalInventoryValue = metrics.stream().map(InventoryMetric::inventoryValue).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal stagnantValue = metrics.stream()
                .filter(metric -> metric.stagnantDays() >= 90)
                .map(InventoryMetric::inventoryValue)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        long stagnantCount = metrics.stream().filter(metric -> metric.stagnantDays() >= 90).count();
        long overstockCount = metrics.stream().filter(InventoryMetric::overstock).count();
        BigDecimal totalOutboundCost = outboundStats.totalOutboundCost();
        BigDecimal turnoverDays = totalOutboundCost.compareTo(BigDecimal.ZERO) > 0
                ? totalInventoryValue.multiply(BigDecimal.valueOf(daysBetween(range.start(), range.end()) + 1L))
                .divide(totalOutboundCost, 2, RoundingMode.HALF_UP)
                : BigDecimal.ZERO;
        List<Map<String, Object>> items = metrics.stream()
                .limit(finalLimit)
                .map(this::toInventoryItem)
                .toList();
        Map<String, Object> summary = rangeSummary(range, metrics.size(), keyword);
        summary.put("totalInventoryValue", totalInventoryValue);
        summary.put("stagnantValue", stagnantValue);
        summary.put("stagnantCount", stagnantCount);
        summary.put("overstockCount", overstockCount);
        summary.put("turnoverDays", turnoverDays);
        summary.put("capitalOccupation", totalInventoryValue);
        summary.put("totalOutboundCost", totalOutboundCost);
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("inventoryValueTopOption", buildInventoryTopBar(metrics));
        charts.put("inventoryAgingPieOption", buildInventoryAgingPie(metrics));
        charts.put("inventoryTurnoverGauge", buildTurnoverGauge(turnoverDays));
        return jsonResponse(true, "financial_inventory_capital_analysis", "已完成库存资金分析", summary, Map.of("items", items), charts);
    }
    @Tool(name = "应收应付与现金流预测", value = "预测未来现金流、回款风险、付款压力与资金缺口。")
    public String forecastCashFlow(@ToolMemoryId String memoryId,
                                   @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                   @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                   @P(value = "时间范围描述,如近90天、本年", required = false) String timeRange,
                                   @P(value = "预测月份数,默认3,最大6", required = false) Integer forecastMonths) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近90天");
        int months = forecastMonths == null || forecastMonths <= 0 ? 3 : Math.min(forecastMonths, 6);
        List<AccountSalesCollection> collections = queryCollections(loginUser, range);
        List<AccountPurchasePayment> payments = queryPayments(loginUser, range);
        List<MonthlyCashFlow> monthlyActual = buildMonthlyCashFlow(range, collections, payments);
        List<MonthlyCashFlow> monthlyForecast = forecastMonthlyCashFlow(monthlyActual, months);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        BigDecimal receivableTotal = snapshot.receivableTotal();
        BigDecimal payableTotal = snapshot.payableTotal();
        BigDecimal forecastNetSum = monthlyForecast.stream().map(MonthlyCashFlow::netFlow).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal coverage = receivableTotal.add(maxZero(forecastNetSum));
        BigDecimal fundGap = maxZero(payableTotal.subtract(coverage));
        Map<String, String> customerNameMap = queryCustomerNameMap(snapshot.receivableTop().stream().map(StatementMetric::entityId).collect(Collectors.toSet()));
        Map<String, String> supplierNameMap = querySupplierNameMap(snapshot.payableTop().stream().map(StatementMetric::entityId).collect(Collectors.toSet()));
        List<Map<String, Object>> receivableRiskItems = snapshot.receivableTop().stream().map(item -> toStatementRiskItem(item, customerNameMap, "customer")).toList();
        List<Map<String, Object>> payablePressureItems = snapshot.payableTop().stream().map(item -> toStatementRiskItem(item, supplierNameMap, "supplier")).toList();
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("actualIncomeTotal", collections.stream().map(AccountSalesCollection::getCollectionAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
        summary.put("actualExpenseTotal", payments.stream().map(AccountPurchasePayment::getPaymentAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
        summary.put("receivableBalance", receivableTotal);
        summary.put("payableBalance", payableTotal);
        summary.put("forecastNetSum", forecastNetSum);
        summary.put("fundGap", fundGap);
        summary.put("forecastMonths", months);
        summary.put("collectionRiskLevel", riskLevelByAmount(receivableTotal));
        summary.put("paymentPressureLevel", riskLevelByAmount(payableTotal));
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("cashFlowTrendOption", buildCashflowTrend(monthlyActual, monthlyForecast));
        charts.put("receivablePayableBarOption", buildReceivablePayableBar(receivableTotal, payableTotal));
        charts.put("fundGapGaugeOption", buildFundGapGauge(fundGap));
        return jsonResponse(true, "financial_cashflow_forecast", "已完成应收应付与现金流预测", summary,
                Map.of(
                        "actualMonthly", monthlyActual.stream().map(this::toMonthlyCashFlowItem).toList(),
                        "forecastMonthly", monthlyForecast.stream().map(this::toMonthlyCashFlowItem).toList(),
                        "receivableRiskTop", receivableRiskItems,
                        "payablePressureTop", payablePressureItems
                ),
                charts
        );
    }
    @Tool(name = "经营异常预警", value = "识别成本异常、利润异常、回款异常、订单风险、库存异常。")
    public String detectBusinessAnomalies(@ToolMemoryId String memoryId,
                                          @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                          @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                          @P(value = "时间范围描述,如近30天", required = false) String timeRange,
                                          @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        int finalLimit = normalizeLimit(limit);
        AnalysisBundle currentBundle = buildOrderProfitBundle(loginUser, range, null, Math.max(finalLimit, 30));
        DateRange prevRange = previousSameLengthRange(range);
        AnalysisBundle prevBundle = buildOrderProfitBundle(loginUser, prevRange, null, 50);
        BigDecimal currentCostRate = rate(currentBundle.totalCost(), currentBundle.totalRevenue());
        BigDecimal prevCostRate = rate(prevBundle.totalCost(), prevBundle.totalRevenue());
        BigDecimal costRateDiff = currentCostRate.subtract(prevCostRate);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        List<InventoryMetric> inventoryMetrics = buildInventoryMetrics(
                queryStockInventory(loginUser),
                queryProductModels(Collections.emptySet()),
                Map.of(),
                queryAverageUnitCostByModel(loginUser, Collections.emptySet()),
                queryOutboundStats(loginUser, Collections.emptySet(), range)
        );
        List<Map<String, Object>> anomalyItems = new ArrayList<>();
        if (costRateDiff.compareTo(new BigDecimal("0.10")) > 0) {
            anomalyItems.add(anomalyItem("high", "成本异常", "单位收入成本率较上周期上升超过10%", Map.of(
                    "currentCostRate", toPercent(currentCostRate),
                    "previousCostRate", toPercent(prevCostRate),
                    "delta", toPercent(costRateDiff)
            )));
        }
        long lossCount = currentBundle.orderMetrics().stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count();
        if (lossCount > 0) {
            anomalyItems.add(anomalyItem("high", "利润异常", "检测到亏损订单", Map.of("lossOrderCount", lossCount)));
        }
        if (snapshot.receivableTotal().compareTo(snapshot.payableTotal().multiply(new BigDecimal("1.2"))) > 0) {
            anomalyItems.add(anomalyItem("medium", "回款异常", "应收余额显著高于应付,回款压力偏大", Map.of(
                    "receivableBalance", snapshot.receivableTotal(),
                    "payableBalance", snapshot.payableTotal()
            )));
        }
        long overdueOrderCount = currentBundle.orderMetrics().stream()
                .filter(item -> item.deliveryDate() != null && item.deliveryDate().isBefore(LocalDate.now()) && item.profitRate().compareTo(new BigDecimal("0.10")) < 0)
                .count();
        if (overdueOrderCount > 0) {
            anomalyItems.add(anomalyItem("medium", "订单风险", "存在低利润且交付已逾期订单", Map.of("overdueRiskOrderCount", overdueOrderCount)));
        }
        long stagnantCount = inventoryMetrics.stream().filter(item -> item.stagnantDays() >= 90).count();
        if (stagnantCount > 0) {
            anomalyItems.add(anomalyItem("medium", "库存异常", "存在超过90天未周转库存", Map.of("stagnantCount", stagnantCount)));
        }
        List<Map<String, Object>> topAnomalies = anomalyItems.stream().limit(finalLimit).toList();
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("anomalyCount", topAnomalies.size());
        summary.put("highRiskCount", topAnomalies.stream().filter(item -> "high".equals(item.get("riskLevel"))).count());
        summary.put("mediumRiskCount", topAnomalies.stream().filter(item -> "medium".equals(item.get("riskLevel"))).count());
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("anomalyLevelPieOption", buildAnomalyLevelPie(topAnomalies));
        charts.put("anomalyTypeBarOption", buildAnomalyTypeBar(topAnomalies));
        return jsonResponse(true, "financial_business_anomaly_warning", "已完成经营异常预警分析", summary,
                Map.of("items", topAnomalies), charts);
    }
    @Tool(name = "AI经营驾驶舱", value = "实时展示产值、利润、库存、回款、设备利用率、订单利润率等核心经营指标。")
    public String getBusinessCockpit(@ToolMemoryId String memoryId,
                                     @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                     @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                     @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "本月");
        AnalysisBundle profitBundle = buildOrderProfitBundle(loginUser, range, null, 30);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        List<StockInventory> inventories = queryStockInventory(loginUser);
        BigDecimal inventoryValue = estimateInventoryValue(loginUser, inventories);
        long deviceTotal = countDevices(loginUser);
        long repairingCount = countRepairingDevices(loginUser);
        BigDecimal deviceUtilization = deviceTotal > 0
                ? new BigDecimal(deviceTotal - repairingCount).multiply(ONE_HUNDRED).divide(new BigDecimal(deviceTotal), 2, RoundingMode.HALF_UP)
                : BigDecimal.ZERO;
        BigDecimal outputValue = profitBundle.totalRevenue();
        BigDecimal profitRate = rate(profitBundle.totalProfit(), profitBundle.totalRevenue());
        BigDecimal collectionRate = snapshot.receivableTotal().compareTo(BigDecimal.ZERO) > 0
                ? ONE_HUNDRED.subtract(rate(snapshot.receivableTotal(), snapshot.receivableTotal().add(snapshot.payableTotal())).multiply(ONE_HUNDRED))
                : BigDecimal.ZERO;
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("outputValue", outputValue);
        summary.put("profit", profitBundle.totalProfit());
        summary.put("profitRate", toPercent(profitRate));
        summary.put("inventoryValue", inventoryValue);
        summary.put("receivableBalance", snapshot.receivableTotal());
        summary.put("payableBalance", snapshot.payableTotal());
        summary.put("collectionRate", toPercent(collectionRate.divide(ONE_HUNDRED, 4, RoundingMode.HALF_UP)));
        summary.put("deviceUtilizationRate", deviceUtilization + "%");
        summary.put("orderProfitRate", toPercent(avgRate(profitBundle.orderMetrics())));
        Map<String, Object> indicators = new LinkedHashMap<>();
        indicators.put("产值", outputValue);
        indicators.put("利润", profitBundle.totalProfit());
        indicators.put("库存资金占用", inventoryValue);
        indicators.put("应收余额", snapshot.receivableTotal());
        indicators.put("设备利用率", deviceUtilization + "%");
        indicators.put("订单平均利润率", toPercent(avgRate(profitBundle.orderMetrics())));
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("kpiCardData", indicators);
        charts.put("profitTrendOption", buildOrderProfitBar(profitBundle.orderMetrics()));
        charts.put("receivablePayableBarOption", buildReceivablePayableBar(snapshot.receivableTotal(), snapshot.payableTotal()));
        charts.put("inventoryProfitGaugeOption", buildInventoryProfitGauge(inventoryValue, profitBundle.totalProfit()));
        return jsonResponse(true, "financial_business_cockpit", "已生成AI经营驾驶舱数据", summary,
                Map.of(
                        "orderProfitTop", profitBundle.orderMetrics().stream()
                                .sorted(Comparator.comparing(OrderProfitMetric::profit).reversed())
                                .limit(10)
                                .map(this::toOrderCostItem)
                                .toList(),
                        "indicators", indicators
                ),
                charts
        );
    }
    @Tool(name = "日报周报自动生成", value = "自动输出经营分析日报/周报与风险建议。")
    public String generateOperationReport(@ToolMemoryId String memoryId,
                                          @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                          @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                          @P(value = "时间范围描述,如今天、本周", required = false) String timeRange,
                                          @P(value = "报告类型 daily/weekly", required = false) String reportType) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange,
                "weekly".equalsIgnoreCase(reportType) ? "本周" : "今天");
        String type = "weekly".equalsIgnoreCase(reportType) ? "weekly" : "daily";
        AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, null, 30);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        BigDecimal inventoryValue = estimateInventoryValue(loginUser, queryStockInventory(loginUser));
        long lossCount = bundle.orderMetrics().stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count();
        List<String> conclusions = new ArrayList<>();
        conclusions.add("营收" + bundle.totalRevenue() + ",利润" + bundle.totalProfit() + ",利润率" + toPercent(rate(bundle.totalProfit(), bundle.totalRevenue())) + "。");
        conclusions.add("应收余额" + snapshot.receivableTotal() + ",应付余额" + snapshot.payableTotal() + ",库存资金占用" + inventoryValue + "。");
        if (lossCount > 0) {
            conclusions.add("发现亏损订单" + lossCount + "个,建议优先复核材料损耗和工序人工效率。");
        } else {
            conclusions.add("当前未发现亏损订单,建议持续跟踪低于8%利润率订单。");
        }
        if (snapshot.receivableTotal().compareTo(snapshot.payableTotal()) > 0) {
            conclusions.add("回款压力偏高,建议针对高应收客户执行分层催收与账期优化。");
        } else {
            conclusions.add("资金压力可控,建议保持付款计划与采购节奏联动。");
        }
        List<Map<String, Object>> riskSuggestions = new ArrayList<>();
        if (lossCount > 0) {
            riskSuggestions.add(riskSuggestion("利润风险", "高", "复核亏损订单BOM和工序工资定额,必要时调整报价与交付节奏。"));
        }
        if (snapshot.receivableTotal().compareTo(SME_RECEIVABLE_RISK_THRESHOLD) > 0) {
            riskSuggestions.add(riskSuggestion("回款风险", "中", "对应收TOP客户建立周度回款计划,并设置预警阈值。"));
        }
        if (inventoryValue.compareTo(SME_INVENTORY_RISK_THRESHOLD) > 0) {
            riskSuggestions.add(riskSuggestion("库存风险", "中", "对高金额呆滞库存执行降价、替代和生产消耗策略。"));
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("reportType", type);
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("orderCount", bundle.orderMetrics().size());
        summary.put("lossOrderCount", lossCount);
        summary.put("riskSuggestionCount", riskSuggestions.size());
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("headline", "weekly".equals(type) ? "经营周报" : "经营日报");
        data.put("conclusions", conclusions);
        data.put("riskSuggestions", riskSuggestions);
        data.put("orderProfitTop", bundle.orderMetrics().stream()
                .sorted(Comparator.comparing(OrderProfitMetric::profitRate))
                .limit(10)
                .map(this::toRiskOrderItem)
                .toList());
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("reportProfitBarOption", buildOrderProfitBar(bundle.orderMetrics()));
        charts.put("reportReceivablePayableOption", buildReceivablePayableBar(snapshot.receivableTotal(), snapshot.payableTotal()));
        return jsonResponse(true, "financial_operation_report", "已自动生成经营分析报告", summary, data, charts);
    }
    private AnalysisBundle buildOrderProfitBundle(LoginUser loginUser, DateRange range, String keyword, Integer limit) {
        List<SalesLedger> ledgers = querySalesLedgers(loginUser, range, keyword, limit);
        if (ledgers.isEmpty()) {
            return AnalysisBundle.empty();
        }
        List<Long> ledgerIds = ledgers.stream().map(SalesLedger::getId).filter(Objects::nonNull).toList();
        List<SalesLedgerProduct> ledgerProducts = queryLedgerProducts(loginUser, ledgerIds);
        Map<Long, List<SalesLedgerProduct>> productsByLedgerId = ledgerProducts.stream()
                .collect(Collectors.groupingBy(SalesLedgerProduct::getSalesLedgerId));
        MaterialCostResult materialCostResult = calculateMaterialCost(loginUser, range, ledgerProducts);
        ProductionCostContext productionCostContext = calculateProductionCost(loginUser, range, ledgers, ledgerProducts, materialCostResult.avgUnitCostByModelId());
        BigDecimal totalDepreciation = calculateTotalDepreciation(loginUser);
        BigDecimal totalRevenue = ledgers.stream()
                .map(SalesLedger::getContractAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        Map<Long, BigDecimal> depreciationCostByLedger = allocateDepreciation(ledgers, totalDepreciation, totalRevenue);
        List<OrderProfitMetric> metrics = new ArrayList<>();
        for (SalesLedger ledger : ledgers) {
            BigDecimal revenue = defaultDecimal(ledger.getContractAmount());
            BigDecimal materialCost = materialCostResult.materialCostByLedgerId().getOrDefault(ledger.getId(), fallbackMaterialCost(productsByLedgerId.get(ledger.getId()), revenue));
            BigDecimal laborCost = productionCostContext.laborCostByLedgerId().getOrDefault(ledger.getId(), BigDecimal.ZERO);
            BigDecimal scrapCost = productionCostContext.scrapCostByLedgerId().getOrDefault(ledger.getId(), BigDecimal.ZERO);
            BigDecimal depreciationCost = depreciationCostByLedger.getOrDefault(ledger.getId(), BigDecimal.ZERO);
            BigDecimal totalCost = materialCost.add(laborCost).add(scrapCost).add(depreciationCost);
            BigDecimal profit = revenue.subtract(totalCost);
            BigDecimal profitRate = rate(profit, revenue);
            String riskLevel = profit.compareTo(BigDecimal.ZERO) < 0
                    ? "high"
                    : (profitRate.compareTo(new BigDecimal("0.08")) < 0 ? "medium" : "low");
            List<String> reasons = buildProfitReasons(revenue, materialCost, laborCost, scrapCost, profit, profitRate);
            String suggestion = buildProfitSuggestion(riskLevel, reasons);
            metrics.add(new OrderProfitMetric(
                    ledger.getId(),
                    safe(ledger.getSalesContractNo()),
                    safe(ledger.getCustomerName()),
                    safe(ledger.getProjectName()),
                    toLocalDate(ledger.getEntryDate()),
                    ledger.getDeliveryDate(),
                    revenue,
                    materialCost,
                    laborCost,
                    depreciationCost,
                    scrapCost,
                    totalCost,
                    profit,
                    profitRate,
                    riskLevel,
                    reasons,
                    suggestion
            ));
        }
        metrics.sort(Comparator.comparing(OrderProfitMetric::entryDate, Comparator.nullsLast(Comparator.reverseOrder()))
                .thenComparing(OrderProfitMetric::ledgerId, Comparator.nullsLast(Comparator.reverseOrder())));
        BigDecimal totalMaterialCost = metrics.stream().map(OrderProfitMetric::materialCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalLaborCost = metrics.stream().map(OrderProfitMetric::laborCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalScrapCost = metrics.stream().map(OrderProfitMetric::scrapCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalDepreciationCost = metrics.stream().map(OrderProfitMetric::depreciationCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalCost = metrics.stream().map(OrderProfitMetric::totalCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalProfit = metrics.stream().map(OrderProfitMetric::profit).reduce(BigDecimal.ZERO, BigDecimal::add);
        return new AnalysisBundle(
                metrics,
                productionCostContext.processCostRanking(),
                totalRevenue,
                totalMaterialCost,
                totalLaborCost,
                totalDepreciationCost,
                totalScrapCost,
                totalCost,
                totalProfit
        );
    }
    private MaterialCostResult calculateMaterialCost(LoginUser loginUser, DateRange range, List<SalesLedgerProduct> ledgerProducts) {
        if (ledgerProducts.isEmpty()) {
            return new MaterialCostResult(Map.of(), Map.of());
        }
        List<Long> ledgerProductIds = ledgerProducts.stream().map(SalesLedgerProduct::getId).filter(Objects::nonNull).toList();
        Set<Long> productModelIds = ledgerProducts.stream().map(SalesLedgerProduct::getProductModelId).filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, Long> productIdToLedgerId = ledgerProducts.stream()
                .filter(item -> item.getId() != null && item.getSalesLedgerId() != null)
                .collect(Collectors.toMap(SalesLedgerProduct::getId, SalesLedgerProduct::getSalesLedgerId, (a, b) -> a));
        Map<Long, BigDecimal> avgUnitCostByModelId = queryAverageUnitCostByModel(loginUser, productModelIds);
        LambdaQueryWrapper<ProcurementRecordOut> outWrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(outWrapper, loginUser.getTenantId(), ProcurementRecordOut::getTenantId);
        applyDeptFilter(outWrapper, loginUser.getCurrentDeptId(), ProcurementRecordOut::getDeptId);
        outWrapper.eq(ProcurementRecordOut::getType, 2)
                .in(ProcurementRecordOut::getSalesLedgerProductId, ledgerProductIds);
        if (range.hasDateFilter()) {
            outWrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
                    .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(outWrapper));
        Set<Integer> storageIds = outList.stream()
                .map(ProcurementRecordOut::getProcurementRecordStorageId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Integer, ProcurementRecordStorage> storageMap = storageIds.isEmpty()
                ? Map.of()
                : defaultList(procurementRecordMapper.selectBatchIds(storageIds)).stream()
                .collect(Collectors.toMap(ProcurementRecordStorage::getId, item -> item, (a, b) -> a));
        Map<Long, BigDecimal> materialCostByLedgerId = new HashMap<>();
        for (ProcurementRecordOut out : outList) {
            Long ledgerId = productIdToLedgerId.get(out.getSalesLedgerProductId());
            if (ledgerId == null) {
                continue;
            }
            ProcurementRecordStorage storage = storageMap.get(out.getProcurementRecordStorageId());
            BigDecimal unitPrice = storage == null ? BigDecimal.ZERO : defaultDecimal(storage.getUnitPrice());
            BigDecimal quantity = defaultDecimal(out.getInboundNum());
            BigDecimal cost = quantity.multiply(unitPrice);
            materialCostByLedgerId.merge(ledgerId, cost, BigDecimal::add);
        }
        return new MaterialCostResult(materialCostByLedgerId, avgUnitCostByModelId);
    }
    private ProductionCostContext calculateProductionCost(LoginUser loginUser,
                                                          DateRange range,
                                                          List<SalesLedger> ledgers,
                                                          List<SalesLedgerProduct> ledgerProducts,
                                                          Map<Long, BigDecimal> avgUnitCostByModelId) {
        if (ledgers.isEmpty()) {
            return ProductionCostContext.empty();
        }
        Set<Long> ledgerIds = ledgers.stream().map(SalesLedger::getId).filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, Set<Long>> productModelToLedgerIds = new HashMap<>();
        for (SalesLedgerProduct product : ledgerProducts) {
            if (product.getProductModelId() == null || product.getSalesLedgerId() == null) {
                continue;
            }
            productModelToLedgerIds.computeIfAbsent(product.getProductModelId(), key -> new HashSet<>()).add(product.getSalesLedgerId());
        }
        LambdaQueryWrapper<ProductionPlan> planWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(planWrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        planWrapper.in(ProductionPlan::getSalesLedgerId, ledgerIds);
        List<ProductionPlan> plans = defaultList(productionPlanMapper.selectList(planWrapper));
        Map<Long, Long> planIdToLedgerId = plans.stream()
                .filter(item -> item.getId() != null && item.getSalesLedgerId() != null)
                .collect(Collectors.toMap(ProductionPlan::getId, ProductionPlan::getSalesLedgerId, (a, b) -> a));
        LambdaQueryWrapper<ProductionOrder> orderWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(orderWrapper, loginUser.getCurrentDeptId(), ProductionOrder::getDeptId);
        if (range.hasDateFilter()) {
            orderWrapper.ge(ProductionOrder::getCreateTime, range.start().atStartOfDay().minusMonths(2))
                    .lt(ProductionOrder::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
        }
        List<ProductionOrder> orders = defaultList(productionOrderMapper.selectList(orderWrapper));
        Map<Long, Set<Long>> orderIdToLedgerIds = new HashMap<>();
        for (ProductionOrder order : orders) {
            Set<Long> orderLedgers = new HashSet<>();
            for (Long planId : parseIdList(order.getProductionPlanIds())) {
                Long ledgerId = planIdToLedgerId.get(planId);
                if (ledgerId != null) {
                    orderLedgers.add(ledgerId);
                }
            }
            if (orderLedgers.isEmpty() && order.getProductModelId() != null) {
                orderLedgers.addAll(productModelToLedgerIds.getOrDefault(order.getProductModelId(), Set.of()));
            }
            if (!orderLedgers.isEmpty()) {
                orderIdToLedgerIds.put(order.getId(), orderLedgers);
            }
        }
        if (orderIdToLedgerIds.isEmpty()) {
            return ProductionCostContext.empty();
        }
        LambdaQueryWrapper<ProductionOperationTask> taskWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(taskWrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        taskWrapper.in(ProductionOperationTask::getProductionOrderId, orderIdToLedgerIds.keySet());
        List<ProductionOperationTask> tasks = defaultList(productionOperationTaskMapper.selectList(taskWrapper));
        Map<Long, Long> taskIdToOrderId = tasks.stream()
                .filter(item -> item.getId() != null && item.getProductionOrderId() != null)
                .collect(Collectors.toMap(ProductionOperationTask::getId, ProductionOperationTask::getProductionOrderId, (a, b) -> a));
        if (taskIdToOrderId.isEmpty()) {
            return ProductionCostContext.empty();
        }
        LambdaQueryWrapper<ProductionProductMain> mainWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(mainWrapper, loginUser.getCurrentDeptId(), ProductionProductMain::getDeptId);
        mainWrapper.in(ProductionProductMain::getProductionOperationTaskId, taskIdToOrderId.keySet());
        if (range.hasDateFilter()) {
            mainWrapper.ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay().minusMonths(2))
                    .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
        }
        List<ProductionProductMain> mainList = defaultList(productionProductMainMapper.selectList(mainWrapper));
        Map<Long, Set<Long>> mainIdToLedgers = new HashMap<>();
        for (ProductionProductMain main : mainList) {
            Long orderId = taskIdToOrderId.get(main.getProductionOperationTaskId());
            if (orderId == null) {
                continue;
            }
            Set<Long> ledgerSet = orderIdToLedgerIds.get(orderId);
            if (ledgerSet == null || ledgerSet.isEmpty()) {
                continue;
            }
            mainIdToLedgers.put(main.getId(), ledgerSet);
        }
        if (mainIdToLedgers.isEmpty()) {
            return ProductionCostContext.empty();
        }
        LambdaQueryWrapper<ProductionAccount> accountWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(accountWrapper, loginUser.getCurrentDeptId(), ProductionAccount::getDeptId);
        accountWrapper.in(ProductionAccount::getProductionProductMainId, mainIdToLedgers.keySet());
        if (range.hasDateFilter()) {
            accountWrapper.ge(ProductionAccount::getSchedulingDate, range.start().atStartOfDay())
                    .lt(ProductionAccount::getSchedulingDate, range.end().plusDays(1).atStartOfDay());
        }
        List<ProductionAccount> accountList = defaultList(productionAccountMapper.selectList(accountWrapper));
        Map<String, BigDecimal> salaryQuotaByOperation = defaultList(technologyOperationMapper.selectList(new LambdaQueryWrapper<TechnologyOperation>()
                        .select(TechnologyOperation::getName, TechnologyOperation::getSalaryQuota)))
                .stream()
                .filter(item -> StringUtils.hasText(item.getName()))
                .collect(Collectors.toMap(TechnologyOperation::getName, item -> defaultDecimal(item.getSalaryQuota()), (a, b) -> a));
        Map<Long, BigDecimal> laborCostByLedger = new HashMap<>();
        Map<String, BigDecimal> processCostMap = new HashMap<>();
        for (ProductionAccount account : accountList) {
            Set<Long> ledgerSet = mainIdToLedgers.get(account.getProductionProductMainId());
            if (ledgerSet == null || ledgerSet.isEmpty()) {
                continue;
            }
            BigDecimal cost = estimateLaborCost(account, salaryQuotaByOperation);
            if (cost.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal split = cost.divide(new BigDecimal(ledgerSet.size()), 4, RoundingMode.HALF_UP);
            for (Long ledgerId : ledgerSet) {
                laborCostByLedger.merge(ledgerId, split, BigDecimal::add);
            }
            processCostMap.merge(safe(account.getTechnologyOperationName()), cost, BigDecimal::add);
        }
        LambdaQueryWrapper<ProductionProductOutput> outputWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(outputWrapper, loginUser.getCurrentDeptId(), ProductionProductOutput::getDeptId);
        outputWrapper.in(ProductionProductOutput::getProductionProductMainId, mainIdToLedgers.keySet());
        if (range.hasDateFilter()) {
            outputWrapper.ge(ProductionProductOutput::getCreateTime, range.start().atStartOfDay())
                    .lt(ProductionProductOutput::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        List<ProductionProductOutput> outputList = defaultList(productionProductOutputMapper.selectList(outputWrapper));
        Map<Long, BigDecimal> scrapCostByLedger = new HashMap<>();
        for (ProductionProductOutput output : outputList) {
            Set<Long> ledgerSet = mainIdToLedgers.get(output.getProductionProductMainId());
            if (ledgerSet == null || ledgerSet.isEmpty()) {
                continue;
            }
            BigDecimal scrapQty = defaultDecimal(output.getScrapQty());
            if (scrapQty.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal unitCost = avgUnitCostByModelId.getOrDefault(output.getProductModelId(), BigDecimal.ZERO);
            BigDecimal scrapCost = scrapQty.multiply(unitCost);
            BigDecimal split = scrapCost.divide(new BigDecimal(ledgerSet.size()), 4, RoundingMode.HALF_UP);
            for (Long ledgerId : ledgerSet) {
                scrapCostByLedger.merge(ledgerId, split, BigDecimal::add);
            }
        }
        Map<String, BigDecimal> processCostRanking = processCostMap.entrySet().stream()
                .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
                .limit(10)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
        return new ProductionCostContext(laborCostByLedger, scrapCostByLedger, processCostRanking);
    }
    private List<SalesLedger> querySalesLedgers(LoginUser loginUser, DateRange range, String keyword, Integer limit) {
        LambdaQueryWrapper<SalesLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), SalesLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedger::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(SalesLedger::getSalesContractNo, keyword)
                    .or().like(SalesLedger::getCustomerContractNo, keyword)
                    .or().like(SalesLedger::getCustomerName, keyword)
                    .or().like(SalesLedger::getProjectName, keyword)
                    .or().like(SalesLedger::getSalesman, keyword));
        }
        if (range.hasDateFilter()) {
            wrapper.ge(SalesLedger::getEntryDate, toDate(range.start()))
                    .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end()));
        }
        wrapper.orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId);
        if (limit != null && limit > 0) {
            wrapper.last("limit " + normalizeLimit(limit));
        }
        return defaultList(salesLedgerMapper.selectList(wrapper));
    }
    private List<SalesLedgerProduct> queryLedgerProducts(LoginUser loginUser, List<Long> ledgerIds) {
        if (ledgerIds == null || ledgerIds.isEmpty()) {
            return List.of();
        }
        LambdaQueryWrapper<SalesLedgerProduct> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedgerProduct::getDeptId);
        wrapper.in(SalesLedgerProduct::getSalesLedgerId, ledgerIds)
                .eq(SalesLedgerProduct::getType, 1);
        return defaultList(salesLedgerProductMapper.selectList(wrapper));
    }
    private List<StockInventory> queryStockInventory(LoginUser loginUser) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        return defaultList(stockInventoryMapper.selectList(wrapper));
    }
    private Map<Long, ProductModel> queryProductModels(Set<Long> modelIds) {
        if (modelIds == null || modelIds.isEmpty()) {
            return defaultList(productModelMapper.selectList(null)).stream()
                    .filter(item -> item.getId() != null)
                    .collect(Collectors.toMap(ProductModel::getId, item -> item, (a, b) -> a));
        }
        LambdaQueryWrapper<ProductModel> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(ProductModel::getId, modelIds);
        return defaultList(productModelMapper.selectList(wrapper)).stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(ProductModel::getId, item -> item, (a, b) -> a));
    }
    private Map<Long, Product> queryProducts(Collection<ProductModel> models) {
        Set<Long> productIds = models == null ? Set.of() : models.stream()
                .map(ProductModel::getProductId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (productIds.isEmpty()) {
            return Map.of();
        }
        LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(Product::getId, productIds);
        return defaultList(productMapper.selectList(wrapper)).stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(Product::getId, item -> item, (a, b) -> a));
    }
    private Map<Long, BigDecimal> queryAverageUnitCostByModel(LoginUser loginUser, Set<Long> productModelIds) {
        LambdaQueryWrapper<ProcurementRecordStorage> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementRecordStorage::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementRecordStorage::getDeptId);
        wrapper.in(ProcurementRecordStorage::getType, List.of(1, 2));
        if (productModelIds != null && !productModelIds.isEmpty()) {
            wrapper.in(ProcurementRecordStorage::getProductModelId, productModelIds);
        }
        List<ProcurementRecordStorage> rows = defaultList(procurementRecordMapper.selectList(wrapper));
        Map<Long, BigDecimal> amountByModel = new HashMap<>();
        Map<Long, BigDecimal> qtyByModel = new HashMap<>();
        for (ProcurementRecordStorage row : rows) {
            if (row.getProductModelId() == null) {
                continue;
            }
            BigDecimal qty = defaultDecimal(row.getInboundNum());
            if (qty.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal amount = defaultDecimal(row.getUnitPrice()).multiply(qty);
            amountByModel.merge(row.getProductModelId(), amount, BigDecimal::add);
            qtyByModel.merge(row.getProductModelId(), qty, BigDecimal::add);
        }
        Map<Long, BigDecimal> result = new HashMap<>();
        for (Map.Entry<Long, BigDecimal> entry : amountByModel.entrySet()) {
            BigDecimal qty = qtyByModel.get(entry.getKey());
            if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            result.put(entry.getKey(), entry.getValue().divide(qty, 6, RoundingMode.HALF_UP));
        }
        return result;
    }
    private OutboundStats queryOutboundStats(LoginUser loginUser, Set<Long> productModelIds, DateRange range) {
        LambdaQueryWrapper<ProcurementRecordOut> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementRecordOut::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementRecordOut::getDeptId);
        if (productModelIds != null && !productModelIds.isEmpty()) {
            wrapper.in(ProcurementRecordOut::getProductModelId, productModelIds);
        }
        if (range.hasDateFilter()) {
            wrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
                    .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(wrapper));
        if (outList.isEmpty()) {
            return OutboundStats.empty();
        }
        Set<Integer> storageIds = outList.stream()
                .map(ProcurementRecordOut::getProcurementRecordStorageId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Integer, ProcurementRecordStorage> storageMap = storageIds.isEmpty()
                ? Map.of()
                : defaultList(procurementRecordMapper.selectBatchIds(storageIds)).stream()
                .collect(Collectors.toMap(ProcurementRecordStorage::getId, item -> item, (a, b) -> a));
        Map<Long, BigDecimal> outboundQtyByModel = new HashMap<>();
        Map<Long, LocalDateTime> lastOutboundTimeByModel = new HashMap<>();
        BigDecimal totalOutboundCost = BigDecimal.ZERO;
        for (ProcurementRecordOut out : outList) {
            Long modelId = out.getProductModelId();
            if (modelId == null) {
                continue;
            }
            BigDecimal qty = defaultDecimal(out.getInboundNum());
            outboundQtyByModel.merge(modelId, qty, BigDecimal::add);
            if (out.getCreateTime() != null) {
                LocalDateTime existing = lastOutboundTimeByModel.get(modelId);
                if (existing == null || out.getCreateTime().isAfter(existing)) {
                    lastOutboundTimeByModel.put(modelId, out.getCreateTime());
                }
            }
            ProcurementRecordStorage storage = storageMap.get(out.getProcurementRecordStorageId());
            BigDecimal unitPrice = storage == null ? BigDecimal.ZERO : defaultDecimal(storage.getUnitPrice());
            totalOutboundCost = totalOutboundCost.add(unitPrice.multiply(qty));
        }
        return new OutboundStats(outboundQtyByModel, lastOutboundTimeByModel, totalOutboundCost);
    }
    private List<InventoryMetric> buildInventoryMetrics(List<StockInventory> inventoryRows,
                                                        Map<Long, ProductModel> productModelMap,
                                                        Map<Long, Product> productMap,
                                                        Map<Long, BigDecimal> avgUnitCostByModelId,
                                                        OutboundStats outboundStats) {
        Map<Long, InventoryMetricBuilder> metricBuilderByModel = new HashMap<>();
        for (StockInventory row : inventoryRows) {
            if (row.getProductModelId() == null) {
                continue;
            }
            InventoryMetricBuilder builder = metricBuilderByModel.computeIfAbsent(row.getProductModelId(), InventoryMetricBuilder::new);
            builder.addQuantity(maxZero(defaultDecimal(row.getQualitity()).subtract(defaultDecimal(row.getLockedQuantity()))));
            builder.addLockedQuantity(defaultDecimal(row.getLockedQuantity()));
            builder.addWarnNum(defaultDecimal(row.getWarnNum()));
            if (row.getCreateTime() != null) {
                builder.updateFirstInTime(row.getCreateTime());
            }
        }
        List<InventoryMetric> result = new ArrayList<>();
        LocalDate today = LocalDate.now();
        for (InventoryMetricBuilder builder : metricBuilderByModel.values()) {
            Long modelId = builder.modelId();
            ProductModel model = productModelMap.get(modelId);
            Product product = model == null ? null : productMap.get(model.getProductId());
            BigDecimal unitCost = avgUnitCostByModelId.getOrDefault(modelId, BigDecimal.ZERO);
            BigDecimal value = builder.quantity().multiply(unitCost);
            LocalDateTime lastOutTime = outboundStats.lastOutboundTimeByModel().get(modelId);
            long stagnantDays;
            if (lastOutTime != null) {
                stagnantDays = daysBetween(lastOutTime.toLocalDate(), today);
            } else if (builder.firstInTime() != null) {
                stagnantDays = daysBetween(builder.firstInTime().toLocalDate(), today);
            } else {
                stagnantDays = 0;
            }
            BigDecimal outQty = outboundStats.outboundQtyByModel().getOrDefault(modelId, BigDecimal.ZERO);
            boolean overstock = builder.warnNum().compareTo(BigDecimal.ZERO) > 0
                    && builder.quantity().compareTo(builder.warnNum().multiply(new BigDecimal("3"))) > 0;
            result.add(new InventoryMetric(
                    modelId,
                    product == null ? "未知产品" : safe(product.getProductName()),
                    model == null ? "未知型号" : safe(model.getModel()),
                    builder.quantity(),
                    builder.lockedQuantity(),
                    unitCost,
                    value,
                    outQty,
                    stagnantDays,
                    overstock
            ));
        }
        return result;
    }
    private List<AccountSalesCollection> queryCollections(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId);
        if (range.hasDateFilter()) {
            wrapper.ge(AccountSalesCollection::getCollectionDate, range.start())
                    .le(AccountSalesCollection::getCollectionDate, range.end());
        }
        wrapper.orderByAsc(AccountSalesCollection::getCollectionDate);
        return defaultList(accountSalesCollectionMapper.selectList(wrapper));
    }
    private List<AccountPurchasePayment> queryPayments(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountPurchasePayment> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchasePayment::getDeptId);
        if (range.hasDateFilter()) {
            wrapper.ge(AccountPurchasePayment::getPaymentDate, range.start())
                    .le(AccountPurchasePayment::getPaymentDate, range.end());
        }
        wrapper.orderByAsc(AccountPurchasePayment::getPaymentDate);
        return defaultList(accountPurchasePaymentMapper.selectList(wrapper));
    }
    private List<MonthlyCashFlow> buildMonthlyCashFlow(DateRange range,
                                                       List<AccountSalesCollection> collections,
                                                       List<AccountPurchasePayment> payments) {
        Map<YearMonth, BigDecimal> incomeByMonth = new LinkedHashMap<>();
        Map<YearMonth, BigDecimal> expenseByMonth = new LinkedHashMap<>();
        DateRange monthlyRange = range.hasDateFilter() ? range : inferCashFlowRange(collections, payments);
        if (!monthlyRange.hasDateFilter()) {
            return List.of();
        }
        YearMonth startMonth = YearMonth.from(monthlyRange.start());
        YearMonth endMonth = YearMonth.from(monthlyRange.end());
        for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) {
            incomeByMonth.put(month, BigDecimal.ZERO);
            expenseByMonth.put(month, BigDecimal.ZERO);
        }
        for (AccountSalesCollection row : collections) {
            if (row.getCollectionDate() == null) {
                continue;
            }
            YearMonth month = YearMonth.from(row.getCollectionDate());
            if (incomeByMonth.containsKey(month)) {
                incomeByMonth.put(month, incomeByMonth.get(month).add(defaultDecimal(row.getCollectionAmount())));
            }
        }
        for (AccountPurchasePayment row : payments) {
            if (row.getPaymentDate() == null) {
                continue;
            }
            YearMonth month = YearMonth.from(row.getPaymentDate());
            if (expenseByMonth.containsKey(month)) {
                expenseByMonth.put(month, expenseByMonth.get(month).add(defaultDecimal(row.getPaymentAmount())));
            }
        }
        List<MonthlyCashFlow> result = new ArrayList<>();
        for (YearMonth month : incomeByMonth.keySet()) {
            BigDecimal income = incomeByMonth.get(month);
            BigDecimal expense = expenseByMonth.getOrDefault(month, BigDecimal.ZERO);
            result.add(new MonthlyCashFlow(month.toString(), income, expense, income.subtract(expense)));
        }
        return result;
    }
    private DateRange inferCashFlowRange(List<AccountSalesCollection> collections,
                                         List<AccountPurchasePayment> payments) {
        LocalDate min = null;
        LocalDate max = null;
        for (AccountSalesCollection row : defaultList(collections)) {
            if (row.getCollectionDate() == null) {
                continue;
            }
            min = min == null || row.getCollectionDate().isBefore(min) ? row.getCollectionDate() : min;
            max = max == null || row.getCollectionDate().isAfter(max) ? row.getCollectionDate() : max;
        }
        for (AccountPurchasePayment row : defaultList(payments)) {
            if (row.getPaymentDate() == null) {
                continue;
            }
            min = min == null || row.getPaymentDate().isBefore(min) ? row.getPaymentDate() : min;
            max = max == null || row.getPaymentDate().isAfter(max) ? row.getPaymentDate() : max;
        }
        return min == null || max == null ? new DateRange(null, null, "全部") : new DateRange(min, max, "全部");
    }
    private List<MonthlyCashFlow> forecastMonthlyCashFlow(List<MonthlyCashFlow> actual, int forecastMonths) {
        if (actual.isEmpty()) {
            List<MonthlyCashFlow> defaults = new ArrayList<>();
            YearMonth now = YearMonth.now();
            for (int i = 1; i <= forecastMonths; i++) {
                YearMonth month = now.plusMonths(i);
                defaults.add(new MonthlyCashFlow(month.toString(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
            }
            return defaults;
        }
        List<BigDecimal> series = actual.stream().map(MonthlyCashFlow::netFlow).toList();
        BigDecimal avg = series.stream().reduce(BigDecimal.ZERO, BigDecimal::add)
                .divide(new BigDecimal(series.size()), 4, RoundingMode.HALF_UP);
        BigDecimal slope = BigDecimal.ZERO;
        if (series.size() > 1) {
            slope = series.get(series.size() - 1).subtract(series.get(0))
                    .divide(new BigDecimal(series.size() - 1), 4, RoundingMode.HALF_UP);
        }
        YearMonth lastMonth = YearMonth.parse(actual.get(actual.size() - 1).month());
        List<MonthlyCashFlow> forecast = new ArrayList<>();
        for (int i = 1; i <= forecastMonths; i++) {
            YearMonth month = lastMonth.plusMonths(i);
            BigDecimal net = avg.add(slope.multiply(new BigDecimal(i))).setScale(2, RoundingMode.HALF_UP);
            BigDecimal income = net.compareTo(BigDecimal.ZERO) >= 0 ? net : BigDecimal.ZERO;
            BigDecimal expense = net.compareTo(BigDecimal.ZERO) >= 0 ? BigDecimal.ZERO : net.abs();
            forecast.add(new MonthlyCashFlow(month.toString(), income, expense, net));
        }
        return forecast;
    }
    private StatementSnapshot buildStatementSnapshot(LoginUser loginUser) {
        LambdaQueryWrapper<AccountStatement> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountStatement::getDeptId);
        wrapper.orderByDesc(AccountStatement::getStatementMonth);
        List<AccountStatement> rows = defaultList(accountStatementMapper.selectList(wrapper));
        if (rows.isEmpty()) {
            return StatementSnapshot.empty();
        }
        Map<String, AccountStatement> latestByEntity = new HashMap<>();
        for (AccountStatement row : rows) {
            if (row.getAccountType() == null || row.getCustomerId() == null || !StringUtils.hasText(row.getStatementMonth())) {
                continue;
            }
            String key = row.getAccountType() + "::" + row.getCustomerId();
            AccountStatement existing = latestByEntity.get(key);
            if (existing == null || row.getStatementMonth().compareTo(existing.getStatementMonth()) > 0) {
                latestByEntity.put(key, row);
            }
        }
        BigDecimal receivableTotal = BigDecimal.ZERO;
        BigDecimal payableTotal = BigDecimal.ZERO;
        List<StatementMetric> receivableMetrics = new ArrayList<>();
        List<StatementMetric> payableMetrics = new ArrayList<>();
        for (AccountStatement row : latestByEntity.values()) {
            BigDecimal closing = defaultDecimal(row.getClosingBalance());
            if (Objects.equals(row.getAccountType(), 1)) {
                receivableTotal = receivableTotal.add(closing);
                receivableMetrics.add(new StatementMetric(String.valueOf(row.getCustomerId()), closing,
                        defaultDecimal(row.getCurrentPlan()), defaultDecimal(row.getCurrentActually()), safe(row.getStatementMonth())));
            } else if (Objects.equals(row.getAccountType(), 2)) {
                payableTotal = payableTotal.add(closing);
                payableMetrics.add(new StatementMetric(String.valueOf(row.getCustomerId()), closing,
                        defaultDecimal(row.getCurrentPlan()), defaultDecimal(row.getCurrentActually()), safe(row.getStatementMonth())));
            }
        }
        receivableMetrics.sort(Comparator.comparing(StatementMetric::closingBalance).reversed());
        payableMetrics.sort(Comparator.comparing(StatementMetric::closingBalance).reversed());
        return new StatementSnapshot(
                receivableTotal,
                payableTotal,
                receivableMetrics.stream().limit(10).toList(),
                payableMetrics.stream().limit(10).toList()
        );
    }
    private BigDecimal calculateTotalDepreciation(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
        wrapper.eq(DeviceLedger::getIsDepr, 1);
        List<DeviceLedger> devices = defaultList(deviceLedgerMapper.selectList(wrapper));
        BigDecimal total = BigDecimal.ZERO;
        for (DeviceLedger device : devices) {
            total = total.add(defaultDecimal(AccountingServiceImpl.calculatePreciseDepreciation(device)));
        }
        return total;
    }
    private Map<Long, BigDecimal> allocateDepreciation(List<SalesLedger> ledgers, BigDecimal totalDepreciation, BigDecimal totalRevenue) {
        if (ledgers.isEmpty() || totalDepreciation.compareTo(BigDecimal.ZERO) <= 0) {
            return Map.of();
        }
        Map<Long, BigDecimal> result = new HashMap<>();
        if (totalRevenue.compareTo(BigDecimal.ZERO) <= 0) {
            BigDecimal avg = totalDepreciation.divide(new BigDecimal(ledgers.size()), 4, RoundingMode.HALF_UP);
            for (SalesLedger ledger : ledgers) {
                result.put(ledger.getId(), avg);
            }
            return result;
        }
        for (SalesLedger ledger : ledgers) {
            BigDecimal revenue = defaultDecimal(ledger.getContractAmount());
            BigDecimal ratio = revenue.divide(totalRevenue, 6, RoundingMode.HALF_UP);
            result.put(ledger.getId(), totalDepreciation.multiply(ratio));
        }
        return result;
    }
    private BigDecimal fallbackMaterialCost(List<SalesLedgerProduct> products, BigDecimal revenue) {
        if (products != null && !products.isEmpty()) {
            BigDecimal productAmount = products.stream()
                    .map(SalesLedgerProduct::getTaxExclusiveTotalPrice)
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            if (productAmount.compareTo(BigDecimal.ZERO) > 0) {
                return productAmount;
            }
        }
        BigDecimal materialCost = revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE);
        BigDecimal laborCost = revenue.multiply(DEFAULT_LABOR_COST_RATE);
        BigDecimal overheadCost = revenue.multiply(DEFAULT_OVERHEAD_COST_RATE);
        return materialCost.add(laborCost).add(overheadCost);
    }
    private BigDecimal estimateTotalCost(BigDecimal revenue, List<SalesLedgerProduct> products) {
        if (revenue == null || revenue.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        BigDecimal materialCost = BigDecimal.ZERO;
        if (products != null && !products.isEmpty()) {
            materialCost = products.stream()
                    .map(SalesLedgerProduct::getTaxExclusiveTotalPrice)
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
        }
        if (materialCost.compareTo(BigDecimal.ZERO) <= 0) {
            materialCost = revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE);
        }
        BigDecimal laborCost = revenue.multiply(DEFAULT_LABOR_COST_RATE);
        BigDecimal overheadCost = revenue.multiply(DEFAULT_OVERHEAD_COST_RATE);
        return materialCost.add(laborCost).add(overheadCost);
    }
    private Map<String, String> queryCustomerNameMap(Set<String> idSet) {
        if (idSet == null || idSet.isEmpty()) {
            return Map.of();
        }
        Set<Long> ids = idSet.stream()
                .map(this::toLongOrNull)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (ids.isEmpty()) {
            return Map.of();
        }
        LambdaQueryWrapper<Customer> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(Customer::getId, ids);
        return defaultList(customerMapper.selectList(wrapper)).stream()
                .collect(Collectors.toMap(item -> String.valueOf(item.getId()), Customer::getCustomerName, (a, b) -> a));
    }
    private Map<String, String> querySupplierNameMap(Set<String> idSet) {
        if (idSet == null || idSet.isEmpty()) {
            return Map.of();
        }
        Set<Long> ids = idSet.stream()
                .map(this::toLongOrNull)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (ids.isEmpty()) {
            return Map.of();
        }
        LambdaQueryWrapper<SupplierManage> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(SupplierManage::getId, ids);
        return defaultList(supplierManageMapper.selectList(wrapper)).stream()
                .collect(Collectors.toMap(item -> String.valueOf(item.getId()), SupplierManage::getSupplierName, (a, b) -> a));
    }
    private String riskLevelByAmount(BigDecimal amount) {
        if (amount.compareTo(new BigDecimal("5000000")) >= 0) {
            return "high";
        }
        if (amount.compareTo(new BigDecimal("1000000")) >= 0) {
            return "medium";
        }
        return "low";
    }
    private long countDevices(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
        return deviceLedgerMapper.selectCount(wrapper);
    }
    private long countRepairingDevices(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
        wrapper.in(DeviceRepair::getStatus, List.of(0, 3));
        return deviceRepairMapper.selectCount(wrapper);
    }
    private BigDecimal estimateInventoryValue(LoginUser loginUser, List<StockInventory> inventories) {
        if (inventories == null || inventories.isEmpty()) {
            return BigDecimal.ZERO;
        }
        Set<Long> modelIds = inventories.stream().map(StockInventory::getProductModelId).filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, BigDecimal> costMap = queryAverageUnitCostByModel(loginUser, modelIds);
        BigDecimal total = BigDecimal.ZERO;
        for (StockInventory inventory : inventories) {
            BigDecimal qty = maxZero(defaultDecimal(inventory.getQualitity()).subtract(defaultDecimal(inventory.getLockedQuantity())));
            BigDecimal unit = costMap.getOrDefault(inventory.getProductModelId(), BigDecimal.ZERO);
            total = total.add(qty.multiply(unit));
        }
        return total;
    }
    private DateRange previousSameLengthRange(DateRange range) {
        if (!range.hasDateFilter()) {
            return new DateRange(null, null, "全部");
        }
        long days = daysBetween(range.start(), range.end()) + 1L;
        LocalDate prevEnd = range.start().minusDays(1);
        LocalDate prevStart = prevEnd.minusDays(days - 1L);
        return new DateRange(prevStart, prevEnd, prevStart + "至" + prevEnd);
    }
    private List<String> buildProfitReasons(BigDecimal revenue,
                                            BigDecimal materialCost,
                                            BigDecimal laborCost,
                                            BigDecimal scrapCost,
                                            BigDecimal profit,
                                            BigDecimal profitRate) {
        List<String> reasons = new ArrayList<>();
        BigDecimal materialRate = rate(materialCost, revenue);
        if (materialRate.compareTo(new BigDecimal("0.70")) >= 0) {
            reasons.add("材料成本占比超过70%");
        } else if (materialRate.compareTo(new BigDecimal("0.55")) >= 0) {
            reasons.add("材料成本占比偏高");
        }
        BigDecimal laborRate = rate(laborCost, revenue);
        if (laborRate.compareTo(new BigDecimal("0.20")) >= 0) {
            reasons.add("人工成本占比超过20%");
        } else if (laborRate.compareTo(new BigDecimal("0.12")) >= 0) {
            reasons.add("人工成本增长偏快");
        }
        BigDecimal scrapRate = rate(scrapCost, revenue);
        if (scrapRate.compareTo(new BigDecimal("0.05")) >= 0) {
            reasons.add("报废损耗占比偏高");
        }
        if (profit.compareTo(BigDecimal.ZERO) < 0) {
            reasons.add("订单处于亏损状态");
        } else if (profitRate.compareTo(new BigDecimal("0.08")) < 0) {
            reasons.add("利润率低于8%");
        }
        if (reasons.isEmpty()) {
            reasons.add("成本结构处于合理区间");
        }
        return reasons;
    }
    private String buildProfitSuggestion(String riskLevel, List<String> reasons) {
        if ("high".equals(riskLevel)) {
            return "优先复核BOM用量与工序定额,必要时调整报价和付款条款,并限制超账期交付。";
        }
        if ("medium".equals(riskLevel)) {
            return "建议优化采购批次和工序排产,提升一次合格率并同步执行毛利预警。";
        }
        if (reasons.stream().anyMatch(item -> item.contains("材料"))) {
            return "保持材料采购成本看板,按周跟踪主要材料单价波动。";
        }
        return "维持当前经营节奏,继续跟踪订单利润率和回款效率。";
    }
    private Map<String, Object> toOrderCostItem(OrderProfitMetric metric) {
        Map<String, Object> item = new LinkedHashMap<>();
        item.put("ledgerId", metric.ledgerId());
        item.put("salesContractNo", metric.salesContractNo());
        item.put("customerName", metric.customerName());
        item.put("projectName", metric.projectName());
        item.put("entryDate", formatDate(metric.entryDate()));
        item.put("deliveryDate", formatDate(metric.deliveryDate()));
        item.put("revenue", metric.revenue());
        item.put("materialCost", metric.materialCost());
        item.put("laborCost", metric.laborCost());
        item.put("depreciationCost", metric.depreciationCost());
        item.put("scrapCost", metric.scrapCost());
        item.put("totalCost", metric.totalCost());
        item.put("profit", metric.profit());
        item.put("profitRate", toPercent(metric.profitRate()));
        item.put("riskLevel", metric.riskLevel());
        item.put("reasons", metric.reasons());
        item.put("suggestion", metric.suggestion());
        return item;
    }
    private Map<String, Object> toRiskOrderItem(OrderProfitMetric metric) {
        Map<String, Object> map = toOrderCostItem(metric);
        map.put("priority", "high".equals(metric.riskLevel()) ? "high" : ("medium".equals(metric.riskLevel()) ? "medium" : "low"));
        return map;
    }
    private List<Map<String, Object>> buildCustomerProfitTop(List<OrderProfitMetric> metrics, int topN) {
        Map<String, BigDecimal> customerProfitMap = new HashMap<>();
        Map<String, BigDecimal> customerRevenueMap = new HashMap<>();
        for (OrderProfitMetric metric : metrics) {
            customerProfitMap.merge(metric.customerName(), metric.profit(), BigDecimal::add);
            customerRevenueMap.merge(metric.customerName(), metric.revenue(), BigDecimal::add);
        }
        return customerProfitMap.entrySet().stream()
                .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
                .limit(topN)
                .map(entry -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    BigDecimal revenue = customerRevenueMap.getOrDefault(entry.getKey(), BigDecimal.ZERO);
                    map.put("customerName", entry.getKey());
                    map.put("profit", entry.getValue());
                    map.put("revenue", revenue);
                    map.put("profitRate", toPercent(rate(entry.getValue(), revenue)));
                    return map;
                })
                .toList();
    }
    private Map<String, Object> toInventoryItem(InventoryMetric metric) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("productModelId", metric.modelId());
        map.put("productName", metric.productName());
        map.put("model", metric.modelName());
        map.put("quantity", metric.quantity());
        map.put("lockedQuantity", metric.lockedQuantity());
        map.put("avgUnitCost", metric.avgUnitCost());
        map.put("inventoryValue", metric.inventoryValue());
        map.put("outboundQuantity", metric.outboundQuantity());
        map.put("stagnantDays", metric.stagnantDays());
        map.put("overstock", metric.overstock());
        map.put("riskLevel", metric.stagnantDays() >= 90 ? "high" : (metric.stagnantDays() >= 30 ? "medium" : "low"));
        return map;
    }
    private boolean matchInventoryKeyword(InventoryMetric metric, String keyword) {
        if (!StringUtils.hasText(keyword)) {
            return true;
        }
        return metric.productName().contains(keyword.trim()) || metric.modelName().contains(keyword.trim());
    }
    private Map<String, Object> toMonthlyCashFlowItem(MonthlyCashFlow flow) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("month", flow.month());
        map.put("income", flow.income());
        map.put("expense", flow.expense());
        map.put("netFlow", flow.netFlow());
        return map;
    }
    private Map<String, Object> toStatementRiskItem(StatementMetric metric, Map<String, String> nameMap, String type) {
        BigDecimal actualRate = rate(metric.actualAmount(), metric.planAmount());
        Map<String, Object> map = new LinkedHashMap<>();
        map.put(type + "Id", metric.entityId());
        map.put(type + "Name", safe(nameMap.get(metric.entityId())));
        map.put("statementMonth", metric.statementMonth());
        map.put("closingBalance", metric.closingBalance());
        map.put("planAmount", metric.planAmount());
        map.put("actualAmount", metric.actualAmount());
        map.put("actualRate", toPercent(actualRate));
        map.put("riskLevel", metric.closingBalance().compareTo(new BigDecimal("1000000")) > 0 || actualRate.compareTo(new BigDecimal("0.50")) < 0 ? "high" : "medium");
        return map;
    }
    private Map<String, Object> anomalyItem(String level, String type, String message, Map<String, Object> detail) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("riskLevel", level);
        map.put("type", type);
        map.put("message", message);
        map.put("detail", detail);
        return map;
    }
    private Map<String, Object> riskSuggestion(String type, String level, String suggestion) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("type", type);
        map.put("level", level);
        map.put("suggestion", suggestion);
        return map;
    }
    private Map<String, Object> buildCostCompositionPie(BigDecimal material, BigDecimal labor, BigDecimal depreciation, BigDecimal scrap) {
        List<Map<String, Object>> data = List.of(
                Map.of("name", "材料成本", "value", material),
                Map.of("name", "人工成本", "value", labor),
                Map.of("name", "折旧成本", "value", depreciation),
                Map.of("name", "损耗成本", "value", scrap)
        );
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "成本构成", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("series", List.of(Map.of("name", "成本构成", "type", "pie", "radius", "60%", "data", data)));
        return option;
    }
    private Map<String, Object> buildOrderProfitBar(List<OrderProfitMetric> metrics) {
        List<OrderProfitMetric> top = metrics.stream()
                .sorted(Comparator.comparing(OrderProfitMetric::profit))
                .limit(10)
                .toList();
        List<String> xData = top.stream().map(OrderProfitMetric::salesContractNo).toList();
        List<BigDecimal> yData = top.stream().map(OrderProfitMetric::profit).toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "订单利润分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", xData));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "利润", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildProcessCostBar(Map<String, BigDecimal> processCosts) {
        List<String> xData = new ArrayList<>(processCosts.keySet());
        List<BigDecimal> yData = new ArrayList<>(processCosts.values());
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "工序成本排名", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", xData));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "成本", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildProfitDistributionBar(List<OrderProfitMetric> metrics) {
        List<OrderProfitMetric> sorted = metrics.stream()
                .sorted(Comparator.comparing(OrderProfitMetric::profitRate))
                .limit(15)
                .toList();
        List<String> xData = sorted.stream().map(OrderProfitMetric::salesContractNo).toList();
        List<BigDecimal> yData = sorted.stream().map(metric -> metric.profitRate().multiply(ONE_HUNDRED).setScale(2, RoundingMode.HALF_UP)).toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "订单利润率分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", xData));
        option.put("yAxis", Map.of("type", "value", "name", "%"));
        option.put("series", List.of(Map.of("name", "利润率", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildLossOrderTrendLine(List<OrderProfitMetric> metrics) {
        Map<String, Long> lossByDate = new LinkedHashMap<>();
        List<OrderProfitMetric> sorted = metrics.stream()
                .filter(metric -> metric.entryDate() != null)
                .sorted(Comparator.comparing(OrderProfitMetric::entryDate))
                .toList();
        for (OrderProfitMetric metric : sorted) {
            String day = formatDate(metric.entryDate());
            long inc = metric.profit().compareTo(BigDecimal.ZERO) < 0 ? 1L : 0L;
            lossByDate.merge(day, inc, Long::sum);
        }
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "亏损订单趋势", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", new ArrayList<>(lossByDate.keySet())));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "亏损订单数", "type", "line", "smooth", true, "data", new ArrayList<>(lossByDate.values()))));
        return option;
    }
    private Map<String, Object> buildCustomerProfitBar(Map<String, BigDecimal> customerProfitMap) {
        List<Map.Entry<String, BigDecimal>> top = customerProfitMap.entrySet().stream()
                .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
                .limit(10)
                .toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "客户利润贡献TOP10", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", top.stream().map(Map.Entry::getKey).toList()));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "利润", "type", "bar", "data", top.stream().map(Map.Entry::getValue).toList())));
        return option;
    }
    private Map<String, Object> buildInventoryTopBar(List<InventoryMetric> metrics) {
        List<InventoryMetric> top = metrics.stream()
                .sorted(Comparator.comparing(InventoryMetric::inventoryValue).reversed())
                .limit(10)
                .toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "库存资金占用TOP10", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", top.stream().map(item -> item.productName() + "/" + item.modelName()).toList()));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "资金占用", "type", "bar", "data", top.stream().map(InventoryMetric::inventoryValue).toList())));
        return option;
    }
    private Map<String, Object> buildInventoryAgingPie(List<InventoryMetric> metrics) {
        long normal = metrics.stream().filter(item -> item.stagnantDays() < 30).count();
        long slow = metrics.stream().filter(item -> item.stagnantDays() >= 30 && item.stagnantDays() < 90).count();
        long stagnant = metrics.stream().filter(item -> item.stagnantDays() >= 90).count();
        List<Map<String, Object>> data = List.of(
                Map.of("name", "正常", "value", normal),
                Map.of("name", "缓慢", "value", slow),
                Map.of("name", "呆滞", "value", stagnant)
        );
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "库存库龄分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("series", List.of(Map.of("type", "pie", "radius", "60%", "data", data)));
        return option;
    }
    private Map<String, Object> buildTurnoverGauge(BigDecimal turnoverDays) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "库存周转天数", "left", "center"));
        option.put("series", List.of(Map.of(
                "type", "gauge",
                "min", 0,
                "max", 180,
                "detail", Map.of("formatter", "{value}天"),
                "data", List.of(Map.of("value", turnoverDays, "name", "周转天数"))
        )));
        return option;
    }
    private Map<String, Object> buildCashflowTrend(List<MonthlyCashFlow> actual, List<MonthlyCashFlow> forecast) {
        List<String> labels = new ArrayList<>();
        List<BigDecimal> netActual = new ArrayList<>();
        List<BigDecimal> netForecast = new ArrayList<>();
        for (MonthlyCashFlow point : actual) {
            labels.add(point.month());
            netActual.add(point.netFlow());
            netForecast.add(null);
        }
        for (MonthlyCashFlow point : forecast) {
            labels.add(point.month());
            netActual.add(null);
            netForecast.add(point.netFlow());
        }
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "现金流趋势(实际+预测)", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", labels));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(
                Map.of("name", "实际净现金流", "type", "line", "smooth", true, "data", netActual),
                Map.of("name", "预测净现金流", "type", "line", "smooth", true, "data", netForecast)
        ));
        return option;
    }
    private Map<String, Object> buildReceivablePayableBar(BigDecimal receivable, BigDecimal payable) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "应收应付余额对比", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", List.of("应收", "应付")));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "余额", "type", "bar", "data", List.of(receivable, payable))));
        return option;
    }
    private Map<String, Object> buildFundGapGauge(BigDecimal fundGap) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "资金缺口", "left", "center"));
        option.put("series", List.of(Map.of(
                "type", "gauge",
                "min", 0,
                "max", 10000000,
                "detail", Map.of("formatter", "{value}"),
                "data", List.of(Map.of("value", fundGap, "name", "资金缺口"))
        )));
        return option;
    }
    private Map<String, Object> buildAnomalyLevelPie(List<Map<String, Object>> anomalies) {
        long high = anomalies.stream().filter(item -> "high".equals(item.get("riskLevel"))).count();
        long medium = anomalies.stream().filter(item -> "medium".equals(item.get("riskLevel"))).count();
        long low = anomalies.stream().filter(item -> "low".equals(item.get("riskLevel"))).count();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "异常等级分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("series", List.of(Map.of("type", "pie", "radius", "60%", "data", List.of(
                Map.of("name", "高风险", "value", high),
                Map.of("name", "中风险", "value", medium),
                Map.of("name", "低风险", "value", low)
        ))));
        return option;
    }
    private Map<String, Object> buildAnomalyTypeBar(List<Map<String, Object>> anomalies) {
        Map<String, Long> countByType = new LinkedHashMap<>();
        for (Map<String, Object> anomaly : anomalies) {
            countByType.merge(String.valueOf(anomaly.get("type")), 1L, Long::sum);
        }
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "异常类型分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", new ArrayList<>(countByType.keySet())));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "异常数", "type", "bar", "data", new ArrayList<>(countByType.values()))));
        return option;
    }
    private Map<String, Object> buildInventoryProfitGauge(BigDecimal inventoryValue, BigDecimal profit) {
        BigDecimal ratio = inventoryValue.compareTo(BigDecimal.ZERO) <= 0
                ? BigDecimal.ZERO
                : profit.divide(inventoryValue, 4, RoundingMode.HALF_UP).multiply(ONE_HUNDRED);
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "利润/库存资金比", "left", "center"));
        option.put("series", List.of(Map.of(
                "type", "gauge",
                "min", -100,
                "max", 100,
                "detail", Map.of("formatter", "{value}%"),
                "data", List.of(Map.of("value", ratio.setScale(2, RoundingMode.HALF_UP), "name", "利润资金比"))
        )));
        return option;
    }
    private int normalizeLimit(Integer limit) {
        if (limit == null || limit <= 0) {
            return DEFAULT_LIMIT;
        }
        return Math.min(limit, MAX_LIMIT);
    }
    private DateRange resolveDateRange(String startDate, String endDate, String timeRange, String defaultLabel) {
        LocalDate today = LocalDate.now();
        LocalDate explicitStart = parseLocalDate(startDate);
        LocalDate explicitEnd = parseLocalDate(endDate);
        if (explicitStart != null || explicitEnd != null) {
            LocalDate start = explicitStart != null ? explicitStart : explicitEnd;
            LocalDate end = explicitEnd != null ? explicitEnd : explicitStart;
            if (start.isAfter(end)) {
                LocalDate temp = start;
                start = end;
                end = temp;
            }
            return new DateRange(start, end, start + "至" + end);
        }
        if (!StringUtils.hasText(timeRange)) {
            return new DateRange(null, null, "全部");
        }
        String text = timeRange.trim();
        if (text.contains("今天")) {
            return new DateRange(today, today, "今天");
        }
        if (text.contains("昨天") || text.contains("昨日")) {
            LocalDate day = today.minusDays(1);
            return new DateRange(day, day, "昨天");
        }
        if (text.contains("本周")) {
            LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            return new DateRange(start, today, "本周");
        }
        if (text.contains("上周")) {
            LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            LocalDate start = thisWeekStart.minusWeeks(1);
            LocalDate end = thisWeekStart.minusDays(1);
            return new DateRange(start, end, "上周");
        }
        if (text.contains("本月")) {
            return new DateRange(today.withDayOfMonth(1), today, "本月");
        }
        if (text.contains("上月")) {
            YearMonth lastMonth = YearMonth.from(today).minusMonths(1);
            return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth(), "上月");
        }
        if (text.contains("今年") || text.contains("本年")) {
            return new DateRange(today.withDayOfYear(1), today, "今年");
        }
        Matcher relativeMatcher = RELATIVE_PATTERN.matcher(text);
        if (relativeMatcher.find()) {
            int amount = Integer.parseInt(relativeMatcher.group(2));
            String unit = relativeMatcher.group(3);
            LocalDate start = switch (unit) {
                case "天" -> today.minusDays(Math.max(amount - 1L, 0));
                case "周" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
                case "个月", "月" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
                case "å¹´" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
                default -> today.minusDays(29);
            };
            return new DateRange(start, today, "近" + amount + unit);
        }
        Matcher dateMatcher = DATE_PATTERN.matcher(text);
        if (dateMatcher.find()) {
            LocalDate start = parseLocalDate(dateMatcher.group(1));
            LocalDate end = dateMatcher.find() ? parseLocalDate(dateMatcher.group(1)) : start;
            if (start != null && end != null) {
                if (start.isAfter(end)) {
                    LocalDate temp = start;
                    start = end;
                    end = temp;
                }
                return new DateRange(start, end, start + "至" + end);
            }
        }
        return new DateRange(null, null, "全部");
    }
    private LocalDate parseLocalDate(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        try {
            return LocalDate.parse(text.trim(), DATE_FMT);
        } catch (Exception ignored) {
            return null;
        }
    }
    private Date toDate(LocalDate localDate) {
        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private Date toExclusiveEndDate(LocalDate localDate) {
        return Date.from(localDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private LocalDate toLocalDate(Date date) {
        if (date == null) {
            return null;
        }
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
    }
    private String formatDate(LocalDate date) {
        return date == null ? "" : date.format(DATE_FMT);
    }
    private String displayDate(LocalDate date) {
        return date == null ? "" : date.format(DATE_FMT);
    }
    private long daysBetween(LocalDate start, LocalDate end) {
        if (start == null || end == null || start.isAfter(end)) {
            return 0;
        }
        return end.toEpochDay() - start.toEpochDay();
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private BigDecimal maxZero(BigDecimal value) {
        return value == null || value.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : value;
    }
    private BigDecimal rate(BigDecimal numerator, BigDecimal denominator) {
        if (denominator == null || denominator.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        return defaultDecimal(numerator).divide(denominator, 6, RoundingMode.HALF_UP);
    }
    private String toPercent(BigDecimal decimal) {
        if (decimal == null) {
            return "0.00%";
        }
        BigDecimal rate = decimal.multiply(ONE_HUNDRED).setScale(2, RoundingMode.HALF_UP);
        return rate.toPlainString() + "%";
    }
    private BigDecimal avgRate(List<OrderProfitMetric> metrics) {
        if (metrics == null || metrics.isEmpty()) {
            return BigDecimal.ZERO;
        }
        BigDecimal sum = metrics.stream().map(OrderProfitMetric::profitRate).reduce(BigDecimal.ZERO, BigDecimal::add);
        return sum.divide(new BigDecimal(metrics.size()), 6, RoundingMode.HALF_UP);
    }
    private BigDecimal estimateLaborCost(ProductionAccount account, Map<String, BigDecimal> salaryQuotaByOperation) {
        BigDecimal salaryQuota = salaryQuotaByOperation.getOrDefault(safe(account.getTechnologyOperationName()), BigDecimal.ZERO);
        BigDecimal finishedNum = defaultDecimal(account.getFinishedNum());
        BigDecimal workHours = defaultDecimal(account.getWorkHours());
        if (salaryQuota.compareTo(BigDecimal.ZERO) > 0 && finishedNum.compareTo(BigDecimal.ZERO) > 0) {
            return finishedNum.multiply(salaryQuota);
        }
        if (salaryQuota.compareTo(BigDecimal.ZERO) > 0 && workHours.compareTo(BigDecimal.ZERO) > 0) {
            return workHours.multiply(salaryQuota);
        }
        if (workHours.compareTo(BigDecimal.ZERO) > 0) {
            return workHours;
        }
        return finishedNum;
    }
    private List<Long> parseIdList(String raw) {
        if (!StringUtils.hasText(raw)) {
            return List.of();
        }
        String text = raw.replace("[", "").replace("]", "").replace(" ", "");
        if (!StringUtils.hasText(text)) {
            return List.of();
        }
        List<Long> result = new ArrayList<>();
        for (String part : text.split(",")) {
            if (!StringUtils.hasText(part)) {
                continue;
            }
            try {
                result.add(Long.parseLong(part.trim()));
            } catch (Exception ignored) {
            }
        }
        return result;
    }
    private int keywordHitCount(List<String> keywords, String question) {
        if (!StringUtils.hasText(question) || keywords == null) {
            return 0;
        }
        int count = 0;
        for (String keyword : keywords) {
            if (question.contains(keyword)) {
                count++;
            }
        }
        return count;
    }
    private String normalizeForMatch(String text) {
        if (!StringUtils.hasText(text)) {
            return "";
        }
        return text.replace(",", "")
                .replace(",", "")
                .replace("。", "")
                .replace(".", "")
                .replace("!", "")
                .replace("!", "")
                .replace("?", "")
                .replace("?", "")
                .replace(":", "")
                .replace(":", "")
                .replace(";", "")
                .replace(";", "")
                .replace(" ", "")
                .trim();
    }
    private String safe(Object value) {
        return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ').trim();
    }
    private LoginUser currentLoginUser(String memoryId) {
        LoginUser loginUser = aiSessionUserContext.get(memoryId);
        if (loginUser != null) {
            return loginUser;
        }
        return SecurityUtils.getLoginUser();
    }
    private Map<String, Object> rangeSummary(DateRange range, int count, String keyword) {
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("count", count);
        summary.put("keyword", safe(keyword));
        return summary;
    }
    private Long toLongOrNull(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            return Long.valueOf(value.trim());
        } catch (Exception ignored) {
            return null;
        }
    }
    private <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    private <T> void applyTenantFilter(LambdaQueryWrapper<T> wrapper, Long tenantId, SFunction<T, Long> field) {
        if (tenantId != null) {
            wrapper.eq(field, tenantId);
        }
    }
    private <T> void applyDeptFilter(LambdaQueryWrapper<T> wrapper, Long deptId, SFunction<T, Long> field) {
        if (deptId != null) {
            wrapper.eq(field, deptId);
        }
    }
    private List<KnowledgeDoc> financeKnowledgeBase() {
        return List.of(
                new KnowledgeDoc(
                        "利润下降分析框架",
                        List.of("利润下降", "亏损订单", "毛利率", "净利率"),
                        "先看收入端(订单结构、单价、交付延迟),再看成本端(材料、人工、折旧、损耗),最后看现金端(回款、账期、坏账风险)。",
                        List.of("sales_ledger", "sales_ledger_product", "production_account", "device_ledger", "account_statement"),
                        List.of("为什么本月利润下降?", "哪些订单亏损最严重?", "成本上升来自哪个工序?")
                ),
                new KnowledgeDoc(
                        "库存资金占用诊断",
                        List.of("库存积压", "呆滞库存", "周转率", "资金占用"),
                        "库存资金诊断重点看:库存价值、近30天出库成本、呆滞天数、超储比例,形成去库存与采购节奏联动策略。",
                        List.of("stock_inventory", "procurement_record_storage", "procurement_record_out"),
                        List.of("哪些物料资金占用最高?", "哪些库存超过90天未周转?", "库存周转天数是否异常?")
                ),
                new KnowledgeDoc(
                        "现金流与账款风险",
                        List.of("现金流", "应收", "应付", "回款", "资金缺口"),
                        "现金流判断要结合收款、付款、应收应付余额与预测净流量,重点关注高余额客户和高集中付款供应商。",
                        List.of("account_sales_collection", "account_purchase_payment", "account_statement"),
                        List.of("未来三个月是否有资金缺口?", "哪个客户回款风险最高?", "付款压力最大的是哪些供应商?")
                ),
                new KnowledgeDoc(
                        "业财一体化口径",
                        List.of("业财融合", "业财联动", "口径", "驾驶舱"),
                        "订单利润口径=销售收入-材料成本-人工成本-设备折旧-损耗成本;经营驾驶舱联动订单、生产、库存、设备、账款数据。",
                        List.of("sales_ledger", "production_operation_task", "production_product_main", "device_ledger", "stock_inventory", "account_statement"),
                        List.of("订单利润率如何计算?", "经营驾驶舱核心指标有哪些?")
                )
        );
    }
    private String jsonResponse(boolean success,
                                String type,
                                String description,
                                Map<String, Object> summary,
                                Map<String, Object> data,
                                Map<String, Object> charts) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("success", success);
        result.put("type", type);
        result.put("description", description);
        result.put("summary", summary == null ? Map.of() : summary);
        result.put("data", data == null ? Map.of() : data);
        result.put("charts", charts == null ? Map.of() : charts);
        return JSON.toJSONString(result);
    }
    private record DateRange(LocalDate start, LocalDate end, String label) {
        private boolean hasDateFilter() {
            return start != null && end != null;
        }
    }
    private record OrderProfitMetric(Long ledgerId,
                                     String salesContractNo,
                                     String customerName,
                                     String projectName,
                                     LocalDate entryDate,
                                     LocalDate deliveryDate,
                                     BigDecimal revenue,
                                     BigDecimal materialCost,
                                     BigDecimal laborCost,
                                     BigDecimal depreciationCost,
                                     BigDecimal scrapCost,
                                     BigDecimal totalCost,
                                     BigDecimal profit,
                                     BigDecimal profitRate,
                                     String riskLevel,
                                     List<String> reasons,
                                     String suggestion) {
    }
    private record AnalysisBundle(List<OrderProfitMetric> orderMetrics,
                                  Map<String, BigDecimal> processCostRanking,
                                  BigDecimal totalRevenue,
                                  BigDecimal totalMaterialCost,
                                  BigDecimal totalLaborCost,
                                  BigDecimal totalDepreciationCost,
                                  BigDecimal totalScrapCost,
                                  BigDecimal totalCost,
                                  BigDecimal totalProfit) {
        private static AnalysisBundle empty() {
            return new AnalysisBundle(List.of(), Map.of(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO,
                    BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
        }
    }
    private record MaterialCostResult(Map<Long, BigDecimal> materialCostByLedgerId,
                                      Map<Long, BigDecimal> avgUnitCostByModelId) {
    }
    private record ProductionCostContext(Map<Long, BigDecimal> laborCostByLedgerId,
                                         Map<Long, BigDecimal> scrapCostByLedgerId,
                                         Map<String, BigDecimal> processCostRanking) {
        private static ProductionCostContext empty() {
            return new ProductionCostContext(Map.of(), Map.of(), Map.of());
        }
    }
    private record InventoryMetric(Long modelId,
                                   String productName,
                                   String modelName,
                                   BigDecimal quantity,
                                   BigDecimal lockedQuantity,
                                   BigDecimal avgUnitCost,
                                   BigDecimal inventoryValue,
                                   BigDecimal outboundQuantity,
                                   long stagnantDays,
                                   boolean overstock) {
    }
    private static class InventoryMetricBuilder {
        private final Long modelId;
        private BigDecimal quantity = BigDecimal.ZERO;
        private BigDecimal lockedQuantity = BigDecimal.ZERO;
        private BigDecimal warnNum = BigDecimal.ZERO;
        private LocalDateTime firstInTime;
        private InventoryMetricBuilder(Long modelId) {
            this.modelId = modelId;
        }
        private void addQuantity(BigDecimal quantity) {
            this.quantity = this.quantity.add(quantity);
        }
        private void addLockedQuantity(BigDecimal lockedQuantity) {
            this.lockedQuantity = this.lockedQuantity.add(lockedQuantity);
        }
        private void addWarnNum(BigDecimal warnNum) {
            this.warnNum = this.warnNum.max(warnNum);
        }
        private void updateFirstInTime(LocalDateTime createTime) {
            if (this.firstInTime == null || createTime.isBefore(this.firstInTime)) {
                this.firstInTime = createTime;
            }
        }
        private Long modelId() {
            return modelId;
        }
        private BigDecimal quantity() {
            return quantity;
        }
        private BigDecimal lockedQuantity() {
            return lockedQuantity;
        }
        private BigDecimal warnNum() {
            return warnNum;
        }
        private LocalDateTime firstInTime() {
            return firstInTime;
        }
    }
    private record OutboundStats(Map<Long, BigDecimal> outboundQtyByModel,
                                 Map<Long, LocalDateTime> lastOutboundTimeByModel,
                                 BigDecimal totalOutboundCost) {
        private static OutboundStats empty() {
            return new OutboundStats(Map.of(), Map.of(), BigDecimal.ZERO);
        }
    }
    private record MonthlyCashFlow(String month, BigDecimal income, BigDecimal expense, BigDecimal netFlow) {
    }
    private record StatementMetric(String entityId,
                                   BigDecimal closingBalance,
                                   BigDecimal planAmount,
                                   BigDecimal actualAmount,
                                   String statementMonth) {
    }
    private record StatementSnapshot(BigDecimal receivableTotal,
                                     BigDecimal payableTotal,
                                     List<StatementMetric> receivableTop,
                                     List<StatementMetric> payableTop) {
        private static StatementSnapshot empty() {
            return new StatementSnapshot(BigDecimal.ZERO, BigDecimal.ZERO, List.of(), List.of());
        }
    }
    private record KnowledgeDoc(String topic,
                                List<String> keywords,
                                String knowledge,
                                List<String> relatedTables,
                                List<String> suggestedQuestions) {
    }
}
src/main/java/com/ruoyi/approve/bean/dto/ApprovalInstanceDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.basic.dto.StorageBlobDTO;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalInstanceDto extends ApprovalInstance {
    private String approveAction;
    private String approveComment;
    private String createTimeEnd;
    private String createTimeStart;
    private List<StorageBlobDTO> storageBlobDTOs;
}
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalTemplateDto  extends ApprovalTemplate {
    private List<ApprovalTemplateNodeDto> nodes;
}
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeApproverDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import lombok.Data;
@Data
public class ApprovalTemplateNodeApproverDto extends ApprovalTemplateNodeApprover {
}
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalTemplateNodeDto extends ApprovalTemplateNode {
    private List<ApprovalTemplateNodeApproverDto> approvers;
}
src/main/java/com/ruoyi/approve/bean/dto/FinReimbursementDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.FinReimbursement;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import com.ruoyi.basic.dto.StorageBlobDTO;
import lombok.Data;
import java.util.List;
@Data
public class FinReimbursementDto extends FinReimbursement {
    private String createTimeStart;
    private String createTimeEnd;
    private FinReimbursementTravel  travel;
    private List<FinReimbursementDetail> details;
    private List<ApprovalTemplateNodeDto> nodes;
    private List<StorageBlobDTO> storageBlobDTOs;
}
src/main/java/com/ruoyi/approve/bean/vo/ApprovalInstanceVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.approve.bean.vo;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalRecord;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.basic.dto.StorageBlobVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalInstanceVo extends ApprovalInstance {
    //当前用户是否可以审批
    @Schema(description = "当前用户是否可以审批")
    private Boolean isApprove;
    //审批流程
    private List<ApprovalTask> tasks;
    //审批记录
    private List<ApprovalRecord>  records;
    @Schema(description = "业务名称")
    private String businessName;
    private List<StorageBlobVO> storageBlobVOList;
}
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeApproverVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.approve.bean.vo;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import lombok.Data;
@Data
public class ApprovalTemplateNodeApproverVo extends ApprovalTemplateNodeApprover {
}
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package com.ruoyi.approve.bean.vo;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalTemplateNodeVo extends ApprovalTemplateNode {
    private List<ApprovalTemplateNodeApproverVo> approvers;
}
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.approve.bean.vo;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalTemplateVo extends ApprovalTemplate {
    private List<ApprovalTemplateNodeVo> nodes;
    private String createdUserName;
}
src/main/java/com/ruoyi/approve/bean/vo/FinReimbursementVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.approve.bean.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.approve.pojo.*;
import com.ruoyi.basic.dto.StorageBlobVO;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class FinReimbursementVo extends FinReimbursement {
    private String createTimeStart;
    private String createTimeEnd;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;
    private FinReimbursementTravel travel;
    private List<FinReimbursementDetail> details;
    //审批流程
    private List<ApprovalTask> tasks;
    //审批记录
    private List<ApprovalRecord>  records;
    private List<StorageBlobVO> storageBlobVOList;
}
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
package com.ruoyi.approve.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.vo.ApprovalInstanceVo;
import com.ruoyi.approve.service.ApprovalInstanceService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹å®žä¾‹è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:46
 */
@RestController
@RequestMapping("/approvalInstance")
@Tag(name = "审批实例表")
@AllArgsConstructor
public class ApprovalInstanceController extends BaseController {
    private final ApprovalInstanceService approvalInstanceService;
    @GetMapping("/listPage")
    @Operation(summary = "分页查询")
    @Log(title = "审批列表分页查询", businessType = BusinessType.OTHER)
    public R listPage(Page<ApprovalInstanceVo> page, ApprovalInstanceDto approvalInstanceDto) {
        return approvalInstanceService.listPage(page, approvalInstanceDto);
    }
    @PostMapping("/save")
    @Operation(summary = "保存")
    @Log(title = "审批列表保存", businessType = BusinessType.INSERT)
    public R save(@RequestBody ApprovalInstanceDto approvalInstanceDto) {
        return approvalInstanceService.add(approvalInstanceDto) ? R.ok() : R.fail();
    }
    @PutMapping("/update")
    @Operation(summary = "更新")
    @Log(title = "审批列表更新", businessType = BusinessType.UPDATE)
    public R update(@RequestBody ApprovalInstanceDto approvalInstanceDto) {
        return approvalInstanceService.update(approvalInstanceDto) ? R.ok() : R.fail();
    }
    @DeleteMapping("/delete")
    @Log(title = "审批列表删除", businessType = BusinessType.DELETE)
    @Operation(summary = "删除")
    public R delete(@RequestBody List<Long> ids) {
        return approvalInstanceService.delete(ids) ? R.ok() : R.fail();
    }
    @Operation(summary = "审批")
    @PostMapping("/approve")
    @Log(title = "审批列表审批", businessType = BusinessType.UPDATE)
    public R approve(@RequestBody ApprovalInstanceDto approvalInstanceDto) {
        return approvalInstanceService.approve(approvalInstanceDto);
    }
}
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceNodeController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.approve.controller;
import com.ruoyi.framework.web.controller.BaseController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å®¡æ‰¹èŠ‚ç‚¹å®žä¾‹è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:54
 */
@RestController
@RequestMapping("/approvalInstanceNode")
public class ApprovalInstanceNodeController extends BaseController {
}
src/main/java/com/ruoyi/approve/controller/ApprovalRecordController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å®¡æ‰¹è®°å½•表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:28:21
 */
@RestController
@RequestMapping("/approvalRecord")
public class ApprovalRecordController {
}
src/main/java/com/ruoyi/approve/controller/ApprovalTaskController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å®¡æ‰¹ä»»åŠ¡è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:32:37
 */
@RestController
@RequestMapping("/approvalTask")
public class ApprovalTaskController {
}
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
package com.ruoyi.approve.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalTemplateDto;
import com.ruoyi.approve.bean.vo.ApprovalTemplateVo;
import com.ruoyi.approve.service.ApprovalTemplateService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ‹é“è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:08
 */
@RestController
@RequestMapping("/approvalTemplate")
@Tag(name = "审批模板表")
@AllArgsConstructor
public class ApprovalTemplateController  extends BaseController {
    private final ApprovalTemplateService approvalTemplateService;
    @GetMapping("/listPage")
    @Operation(summary = "分页查询")
    @Log(title = "审批模板分页查询", businessType = BusinessType.OTHER)
    public R listPage(Page<ApprovalTemplateVo> page, ApprovalTemplateDto approvalTemplateDto) {
        return R.ok(approvalTemplateService.listPage(page, approvalTemplateDto));
    }
    @PostMapping("/add")
    @Operation(summary = "添加")
    @Log(title = "添加审批模板", businessType = BusinessType.INSERT)
    public R add(@RequestBody ApprovalTemplateDto approvalTemplateDto) {
        return R.ok(approvalTemplateService.saveApprovalTemplateDto(approvalTemplateDto));
    }
    @PutMapping("/update")
    @Operation(summary = "修改")
    @Log(title = "修改审批模板", businessType = BusinessType.UPDATE)
    public R update(@RequestBody ApprovalTemplateDto approvalTemplateDto) {
        return R.ok(approvalTemplateService.updateApprovalTemplateDto(approvalTemplateDto));
    }
    @PostMapping("/delete")
    @Operation(summary = "删除")
    @Log(title = "删除审批模板", businessType = BusinessType.DELETE)
    public R delete(@RequestBody List<Long> ids) {
        return R.ok(approvalTemplateService.delete(ids));
    }
    @GetMapping("/list/{type}")
    @Operation(summary = "查询所有审批模板")
    public R list(@PathVariable("type") Integer type) {
        return R.ok(approvalTemplateService.listApprovalTemplateVo(type));
    }
    @GetMapping("/detail/{id}")
    @Operation(summary = "查询审批模板详情")
    @Log(title = "查询审批模板详情", businessType = BusinessType.OTHER)
    public R detail(@PathVariable("id") Long id) {
        return R.ok(approvalTemplateService.getApprovalTemplateVoById(id));
    }
}
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeApproverController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹å®¡æ‰¹äººè¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:30
 */
@RestController
@RequestMapping("/approvalTemplateNodeApprover")
public class ApprovalTemplateNodeApproverController {
}
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:19
 */
@RestController
@RequestMapping("/approvalTemplateNode")
public class ApprovalTemplateNodeController {
}
src/main/java/com/ruoyi/approve/controller/FinReimbursementController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
package com.ruoyi.approve.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.service.FinReimbursementService;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * <p>
 * æŠ¥é”€å•主表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@RestController
@RequestMapping("/finReimbursement")
@Tag(name = "报销单主表", description = "报销单主表")
@AllArgsConstructor
public class FinReimbursementController {
    private final FinReimbursementService finReimbursementService;
    @GetMapping("/listPage")
    @Operation(summary = "分页查询")
    public R listPage(Page<FinReimbursementVo> page, FinReimbursementDto finReimbursementDto) {
        return R.ok(finReimbursementService.listPage(finReimbursementDto, page));
    }
    @PostMapping("/save")
    @Operation(summary = "保存")
    public R save(@RequestBody FinReimbursementDto finReimbursementDto) {
        return R.ok(finReimbursementService.add(finReimbursementDto));
    }
    @GetMapping("/detail")
    @Operation(summary = "详情")
    public R detail(Long id) {
        return R.ok(finReimbursementService.detail(id));
    }
    @PostMapping("/update")
    @Operation(summary = "修改")
    public R update(@RequestBody FinReimbursementDto finReimbursementDto) {
        return R.ok(finReimbursementService.update(finReimbursementDto));
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除")
    public R delete(@RequestBody List<Long> ids) {
        return R.ok(finReimbursementService.delete(ids));
    }
}
src/main/java/com/ruoyi/approve/controller/FinReimbursementDetailController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * æŠ¥é”€å•明细表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@RestController
@RequestMapping("/finReimbursementDetail")
public class FinReimbursementDetailController {
}
src/main/java/com/ruoyi/approve/controller/FinReimbursementTravelController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å·®æ—…报销扩展表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@RestController
@RequestMapping("/finReimbursementTravel")
public class FinReimbursementTravelController {
}
src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.vo.ApprovalInstanceVo;
import com.ruoyi.approve.pojo.ApprovalInstance;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 * å®¡æ‰¹å®žä¾‹è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:46
 */
@Mapper
public interface ApprovalInstanceMapper extends BaseMapper<ApprovalInstance> {
    IPage<ApprovalInstanceVo> listPage(Page<ApprovalInstanceVo> page,@Param("ew") ApprovalInstanceDto approvalInstanceDto);
}
src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceNodeMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.ApprovalInstanceNode;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å®¡æ‰¹èŠ‚ç‚¹å®žä¾‹è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:54
 */
@Mapper
public interface ApprovalInstanceNodeMapper extends BaseMapper<ApprovalInstanceNode> {
}
src/main/java/com/ruoyi/approve/mapper/ApprovalRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.ApprovalRecord;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å®¡æ‰¹è®°å½•表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:28:21
 */
@Mapper
public interface ApprovalRecordMapper extends BaseMapper<ApprovalRecord> {
}
src/main/java/com/ruoyi/approve/mapper/ApprovalTaskMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.ApprovalTask;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å®¡æ‰¹ä»»åŠ¡è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:32:37
 */
@Mapper
public interface ApprovalTaskMapper extends BaseMapper<ApprovalTask> {
}
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalTemplateDto;
import com.ruoyi.approve.bean.vo.ApprovalTemplateVo;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:08
 */
@Mapper
public interface ApprovalTemplateMapper extends BaseMapper<ApprovalTemplate> {
    IPage<ApprovalTemplateVo> listPage(Page<ApprovalTemplateVo> page,@Param("ew") ApprovalTemplateDto approvalTemplateDto);
}
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeApproverMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹å®¡æ‰¹äººè¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:30
 */
@Mapper
public interface ApprovalTemplateNodeApproverMapper extends BaseMapper<ApprovalTemplateNodeApprover> {
}
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:19
 */
@Mapper
public interface ApprovalTemplateNodeMapper extends BaseMapper<ApprovalTemplateNode> {
}
src/main/java/com/ruoyi/approve/mapper/FinReimbursementDetailMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * æŠ¥é”€å•明细表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@Mapper
public interface FinReimbursementDetailMapper extends BaseMapper<FinReimbursementDetail> {
}
src/main/java/com/ruoyi/approve/mapper/FinReimbursementMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.pojo.FinReimbursement;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 * æŠ¥é”€å•主表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@Mapper
public interface FinReimbursementMapper extends BaseMapper<FinReimbursement> {
    IPage<FinReimbursementVo> listPage(@Param("ew") FinReimbursementDto finReimbursementDto, Page<FinReimbursementVo> page);
}
src/main/java/com/ruoyi/approve/mapper/FinReimbursementTravelMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å·®æ—…报销扩展表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@Mapper
public interface FinReimbursementTravelMapper extends BaseMapper<FinReimbursementTravel> {
}
src/main/java/com/ruoyi/approve/pojo/ApprovalInstance.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,151 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹å®žä¾‹è¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:46
 */
@Getter
@Setter
@ToString
@TableName("approval_instance")
@ApiModel(value = "ApprovalInstance对象", description = "审批实例表")
public class ApprovalInstance implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * å®¡æ‰¹å®žä¾‹ID
     */
    @Schema(description ="审批实例ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹ç¼–号
     */
    @Schema(description ="审批编号")
    private String instanceNo;
    /**
     * æ¨¡æ¿ID
     */
    @Schema(description ="模板ID")
    private Long templateId;
    /**
     * æ¨¡æ¿åç§°
     */
    @Schema(description ="模板名称")
    private String templateName;
    /**
     * ä¸šåŠ¡ID
     */
    @Schema(description ="业务ID")
    private Long businessId;
    /**
     * ä¸šåŠ¡ç±»åž‹
     */
    @Schema(description ="业务类型")
    private Long businessType;
    /**
     * å®¡æ‰¹æ ‡é¢˜
     */
    @Schema(description ="审批标题")
    private String title;
    /**
     * å®¡æ‰¹çŠ¶æ€
     */
    @Schema(description ="审批状态 PENDING - å¾…审批/进行中  APPROVED - å·²é€šè¿‡/已完成  REJECTED - å·²é©³å›ž")
    private String status;
    /**
     * å½“前审批级别
     */
    @Schema(description ="当前审批级别")
    private Integer currentLevel;
    /**
     * ç”³è¯·äººID
     */
    @Schema(description ="申请人ID")
    private Long applicantId;
    /**
     * ç”³è¯·äººåç§°
     */
    @Schema(description ="申请人名称")
    private String applicantName;
    /**
     * ç”³è¯·æ—¶é—´
     */
    @Schema(description ="申请时间")
    private LocalDateTime applyTime;
    /**
     * å®Œæˆæ—¶é—´
     */
    @Schema(description ="完成时间")
    private LocalDateTime finishTime;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description ="创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description ="创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description ="更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description ="更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * é€»è¾‘删除
     */
    @Schema(description ="逻辑删除")
    private Byte deleted;
    @Schema(description = "表单数据")
    private String formConfig;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalInstanceNode.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹èŠ‚ç‚¹å®žä¾‹è¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:54
 */
@Getter
@Setter
@ToString
@TableName("approval_instance_node")
@ApiModel(value = "ApprovalInstanceNode对象", description = "审批节点实例表")
public class ApprovalInstanceNode implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * èŠ‚ç‚¹å®žä¾‹ID
     */
    @Schema(description ="节点实例ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹å®žä¾‹ID
     */
    @Schema(description ="审批实例ID")
    private Long instanceId;
    /**
     * å®¡æ‰¹çº§åˆ«
     */
    @Schema(description ="审批级别")
    private Integer levelNo;
    /**
     * å®¡æ‰¹ç±»åž‹
     */
    @Schema(description ="审批类型")
    private String approveType;
    /**
     * èŠ‚ç‚¹çŠ¶æ€
     */
    @Schema(description ="节点状态 PENDING - å¾…处理 APPROVED - å·²é€šè¿‡ REJECTED - å·²é©³å›ž")
    private String status;
    /**
     * å¼€å§‹æ—¶é—´
     */
    @Schema(description ="开始时间")
    private LocalDateTime startTime;
    /**
     * å®Œæˆæ—¶é—´
     */
    @Schema(description ="完成时间")
    private LocalDateTime finishTime;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description ="创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description ="创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description ="更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description ="更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * é€»è¾‘删除
     */
    @Schema(description ="逻辑删除")
    private Byte deleted;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹è®°å½•表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:28:21
 */
@Getter
@Setter
@ToString
@TableName("approval_record")
@ApiModel(value = "ApprovalRecord对象", description = "审批记录表")
public class ApprovalRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * å®¡æ‰¹è®°å½•ID
     */
    @Schema(description ="审批记录ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹å®žä¾‹ID
     */
    @Schema(description ="审批实例ID")
    private Long instanceId;
    /**
     * èŠ‚ç‚¹å®žä¾‹ID
     */
    @Schema(description ="节点实例ID")
    private Long nodeId;
    /**
     * å®¡æ‰¹ä»»åŠ¡ID
     */
    @Schema(description ="审批任务ID")
    private Long taskId;
    /**
     * æ“ä½œäººID
     */
    @Schema(description ="操作人ID")
    private Long operatorId;
    /**
     * æ“ä½œäººåç§°
     */
    @Schema(description ="操作人名称")
    private String operatorName;
    /**
     * æ“ä½œç±»åž‹
     */
    @Schema(description ="操作类型")
    private String action;
    /**
     * å®¡æ‰¹æ„è§
     */
    @Schema(description ="审批意见")
    private String comment;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description ="创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description ="创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * é€»è¾‘删除
     */
    @Schema(description ="逻辑删除")
    private Byte deleted;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,128 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹ä»»åŠ¡è¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:32:37
 */
@Getter
@Setter
@ToString
@TableName("approval_task")
@ApiModel(value = "ApprovalTask对象", description = "审批任务表")
public class ApprovalTask implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * å®¡æ‰¹ä»»åŠ¡ID
     */
    @Schema(description ="审批任务ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹å®žä¾‹ID
     */
    @Schema(description ="审批实例ID")
    private Long instanceId;
    /**
     * èŠ‚ç‚¹å®žä¾‹ID
     */
    @Schema(description ="节点实例ID")
    private Long nodeId;
    /**
     * å®¡æ‰¹çº§åˆ«
     */
    @Schema(description ="审批级别")
    private Integer levelNo;
    /**
     * å®¡æ‰¹äººID
     */
    @Schema(description ="审批人ID")
    private Long approverId;
    /**
     * å®¡æ‰¹äººåç§°
     */
    @Schema(description ="审批人名称")
    private String approverName;
    /**
     * ä»»åŠ¡çŠ¶æ€
     */
    @Schema(description ="任务状态 PENDING - å¾…审批 APPROVED - å·²åŒæ„  REJECTED - å·²æ‹’绝")
    private String taskStatus;
    /**
     * å®¡æ‰¹æ—¶é—´
     */
    @Schema(description ="审批时间")
    private LocalDateTime approveTime;
    /**
     * å®¡æ‰¹æ„è§
     */
    @Schema(description ="审批意见")
    private String comment;
    /**
     * æ˜¯å¦å·²è¯»
     */
    @Schema(description ="是否已读")
    private Byte isRead;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description ="创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description ="创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description ="更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description ="更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * é€»è¾‘删除
     */
    @Schema(description ="逻辑删除")
    private Byte deleted;
    @Schema(description ="部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplate.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿è¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:08
 */
@Getter
@Setter
@ToString
@TableName("approval_template")
@ApiModel(value = "ApprovalTemplate对象", description = "审批模板表")
public class ApprovalTemplate implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * æ¨¡æ¿ID
     */
    @Schema(description ="模板ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * æ¨¡æ¿åç§°
     */
    @Schema(description ="模板名称")
    private String templateName;
    /**
     * å¯ç”¨çŠ¶æ€ï¼š1启用,0停用
     */
    @Schema(description ="启用状态:1启用,0停用")
    private Byte enabled;
    /**
     * æ¨¡æ¿è¯´æ˜Ž
     */
    @Schema(description ="模板说明")
    private String description;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description ="创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description ="创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description ="更新人")
    @TableField(fill = FieldFill.UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description ="更新时间")
    @TableField(fill = FieldFill.UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    /**
     * é€»è¾‘删除:0否,1是
     */
    @Schema(description ="逻辑删除:0否,1是")
    private Integer deleted;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @Schema(description = "表单配置")
    private String formConfig;
    @Schema(description = "模板类型:0系统内置,1自定义")
    private Integer templateType;
    @Schema(description = "业务类型")
    private Long businessType;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNode.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹è¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:19
 */
@Getter
@Setter
@ToString
@TableName("approval_template_node")
@ApiModel(value = "ApprovalTemplateNode对象", description = "审批模板节点表")
public class ApprovalTemplateNode implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * èŠ‚ç‚¹ID
     */
    @Schema(description ="节点ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹æ¨¡æ¿ID
     */
    @Schema(description ="审批模板ID")
    private Long templateId;
    /**
     * å®¡æ‰¹çº§åˆ«ï¼Œä»Ž1开始
     */
    @Schema(description ="审批级别,从1开始")
    private Integer levelNo;
    /**
     * å®¡æ‰¹æ–¹å¼ï¼šAND会签,OR或签
     */
    @Schema(description ="审批方式:AND会签,OR或签")
    private String approveType;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNodeApprover.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹å®¡æ‰¹äººè¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:30
 */
@Getter
@Setter
@ToString
@TableName("approval_template_node_approver")
@ApiModel(value = "ApprovalTemplateNodeApprover对象", description = "审批模板节点审批人表")
public class ApprovalTemplateNodeApprover implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @Schema(description = "主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹èŠ‚ç‚¹ID
     */
    @Schema(description ="审批节点ID")
    private Long nodeId;
    /**
     * å®¡æ‰¹æ¨¡æ¿ID
     */
    @Schema(description ="审批模板ID")
    private Long templateId;
    /**
     * å®¡æ‰¹äººID
     */
    @Schema(description ="审批人ID")
    private Long approverId;
    /**
     * å®¡æ‰¹äººåç§°å†—ä½™
     */
    @Schema(description ="审批人名称冗余")
    private String approverName;
    /**
     * å®¡æ‰¹äººæŽ’序
     */
    @Schema(description ="审批人排序")
    private Integer sortNo;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT)
    private Long deleted ;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/approve/pojo/FinReimbursement.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,209 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
 * æŠ¥é”€å•主表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@Getter
@Setter
@ToString
@TableName("fin_reimbursement")
@ApiModel(value = "FinReimbursement对象", description = "报销单主表")
public class FinReimbursement implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @Schema(description = "主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * æŠ¥é”€å•号
     */
    @Schema(description = "报销单号")
    private String billNo;
    /**
     * æŠ¥é”€ç±»åž‹ï¼š1-差旅报销,2-费用报销
     */
    @Schema(description = "报销类型:1-差旅报销,2-费用报销")
    private Byte reimbursementType;
    /**
     * è´¹ç”¨ç±»åž‹ï¼šå·®æ—…è´¹/办公采购/业务招待/交通费/通讯费/其他
     */
    @Schema(description = "费用类型:差旅费/办公采购/业务招待/交通费/通讯费/其他")
    private String expenseType;
    /**
     * ç”³è¯·äººID
     */
    @Schema(description = "申请人ID")
    private Long applicantId;
    /**
     * å‘˜å·¥ç¼–号
     */
    @Schema(description = "员工编号")
    private String applicantCode;
    /**
     * å‘˜å·¥å§“名
     */
    @Schema(description = "员工姓名")
    private String applicantName;
    /**
     * ç”³è¯·éƒ¨é—¨ID
     */
    @Schema(description = "申请部门ID")
    private Long applicantDeptId;
    /**
     * ç”³è¯·éƒ¨é—¨åç§°
     */
    @Schema(description = "申请部门名称")
    private String applicantDeptName;
    /**
     * æŠ¥é”€åŽŸå› 
     */
    @Schema(description = "报销原因")
    private String reason;
    /**
     * ç”³è¯·é‡‘额
     */
    @Schema(description = "申请金额")
    private BigDecimal applyAmount;
    /**
     * æ˜Žç»†æ±‡æ€»é‡‘额
     */
    @Schema(description = "明细汇总金额")
    private BigDecimal detailTotalAmount;
    /**
     * æ”¶æ¬¾äºº
     */
    @Schema(description = "收款人")
    private String payeeName;
    /**
     * æ”¶æ¬¾è´¦å·
     */
    @Schema(description = "收款账号")
    private String payeeAccount;
    /**
     * å¼€æˆ·æ”¯è¡Œ
     */
    @Schema(description = "开户支行")
    private String payeeBank;
    /**
     * å®¡æ‰¹å®žä¾‹ID,对应 approval_instance.id
     */
    @Schema(description = "审批实例ID,对应 approval_instance.id")
    private Long approvalInstanceId;
    /**
     * å®¡æ‰¹æµç¨‹ID,对应 approve_process.id
     */
    @Schema(description = "审批流程ID,对应 approve_process.id")
    private Long approveProcessId;
    /**
     * å•据状态:DRAFT-草稿,IN_APPROVAL-审批中,APPROVED-审批通过,REJECTED-审批驳回,WITHDRAWN-已撤回,PAID-已付款
     */
    @Schema(description = "单据状态:DRAFT-草稿,IN_APPROVAL-审批中,APPROVED-审批通过,REJECTED-审批驳回,WITHDRAWN-已撤回,PAID-已付款")
    private String billStatus;
    /**
     * å®¡æ‰¹é€šè¿‡æ—¶é—´
     */
    @Schema(description = "审批通过时间")
    private LocalDateTime approvedTime;
    /**
     * ä»˜æ¬¾æ—¶é—´
     */
    @Schema(description = "付款时间")
    private LocalDateTime paidTime;
    /**
     * ç”Ÿæˆçš„财务支出记录ID,对应 account_expense.id
     */
    @Schema(description = "生成的财务支出记录ID,对应 account_expense.id")
    private Long accountExpenseId;
    /**
     * å¤‡æ³¨
     */
    @Schema(description = "备注")
    private String remark;
    /**
     * ç§Ÿæˆ·ID
     */
    @Schema(description = "租户ID")
    private Long tenantId;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description = "更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * å½’属部门ID
     */
    @Schema(description = "归属部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * é€»è¾‘删除:0-否,1-是
     */
    @Schema(description = "逻辑删除:0-否,1-是")
    private Byte deleted;
}
src/main/java/com/ruoyi/approve/pojo/FinReimbursementDetail.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,157 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * <p>
 * æŠ¥é”€å•明细表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@Getter
@Setter
@ToString
@TableName("fin_reimbursement_detail")
@ApiModel(value = "FinReimbursementDetail对象", description = "报销单明细表")
public class FinReimbursementDetail implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @Schema(description = "主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * æŠ¥é”€å•ID,对应 fin_reimbursement.id
     */
    @Schema(description = "报销单ID,对应 fin_reimbursement.id")
    private Long reimbursementId;
    /**
     * æ˜Žç»†è¡Œå·
     */
    @Schema(description = "明细行号")
    private Integer rowNo;
    /**
     * å‘票日期
     */
    @Schema(description = "发票日期")
    private LocalDate invoiceDate;
    /**
     * è´¹ç”¨ç§‘ç›®
     */
    @Schema(description = "费用科目")
    private String expenseCategory;
    /**
     * é‡‘额
     */
    @Schema(description = "金额")
    private BigDecimal amount;
    /**
     * æè¿°
     */
    @Schema(description = "描述")
    private String description;
    /**
     * å‘票号码
     */
    @Schema(description = "发票号码")
    private String invoiceNo;
    /**
     * å‘票类型
     */
    @Schema(description = "发票类型")
    private String invoiceType;
    /**
     * ç¥¨é¢é‡‘额
     */
    @Schema(description = "票面金额")
    private BigDecimal invoiceAmount;
    /**
     * ç¨Žçއ
     */
    @Schema(description = "税率")
    private BigDecimal taxRate;
    /**
     * ç¨Žé¢
     */
    @Schema(description = "税额")
    private BigDecimal taxAmount;
    /**
     * å¤‡æ³¨
     */
    @Schema(description = "备注")
    private String remark;
    /**
     * ç§Ÿæˆ·ID
     */
    @Schema(description = "租户ID")
    private Long tenantId;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description = "更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * å½’属部门ID
     */
    @Schema(description = "归属部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * é€»è¾‘删除:0-否,1-是
     */
    @Schema(description = "逻辑删除:0-否,1-是")
    private Byte deleted;
}
src/main/java/com/ruoyi/approve/pojo/FinReimbursementTravel.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,162 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
 * å·®æ—…报销扩展表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@Getter
@Setter
@ToString
@TableName("fin_reimbursement_travel")
@ApiModel(value = "FinReimbursementTravel对象", description = "差旅报销扩展表")
public class FinReimbursementTravel implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @Schema(description = "主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * æŠ¥é”€å•ID,对应 fin_reimbursement.id
     */
    @Schema(description = "报销单ID,对应 fin_reimbursement.id")
    private Long reimbursementId;
    /**
     * å‡ºå·®å¼€å§‹æ—¶é—´
     */
    @Schema(description = "出差开始时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;
    /**
     * å‡ºå·®ç»“束时间
     */
    @Schema(description = "出差结束时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;
    /**
     * å‡ºå·®å¤©æ•°
     */
    @Schema(description = "出差天数")
    private BigDecimal travelDays;
    /**
     * å‡ºå·®åœ°/出发城市
     */
    @Schema(description = "出差地/出发城市")
    private String departureCity;
    /**
     * ç›®çš„地/目的城市
     */
    @Schema(description = "目的地/目的城市")
    private String destinationCity;
    /**
     * é…’店标准
     */
    @Schema(description = "酒店标准")
    private BigDecimal hotelStandard;
    /**
     * ä½å®¿å¤©æ•°
     */
    @Schema(description = "住宿天数")
    private BigDecimal lodgingDays;
    /**
     * ç”Ÿæ´»è¡¥è´´
     */
    @Schema(description = "生活补贴")
    private BigDecimal mealAllowance;
    /**
     * äº¤é€šè¡¥è´´
     */
    @Schema(description = "交通补贴")
    private BigDecimal transportAllowance;
    /**
     * ä½å®¿é™é¢
     */
    @Schema(description = "住宿限额")
    private BigDecimal lodgingLimit;
    /**
     * ç‰¹æ‰¹æ ‡è®°æ–‡æœ¬ï¼Œå¦‚在标准范围内/超标特批
     */
    @Schema(description = "特批标记文本,如在标准范围内/超标特批")
    private String standardTag;
    /**
     * æ˜¯å¦åœ¨æ ‡å‡†å†…:1-是,0-否
     */
    @Schema(description = "是否在标准内:1-是,0-否")
    private Byte withinStandard;
    /**
     * ç§Ÿæˆ·ID
     */
    @Schema(description = "租户ID")
    private Long tenantId;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description = "更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * å½’属部门ID
     */
    @Schema(description = "归属部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/approve/service/ApprovalInstanceNodeService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.ApprovalInstanceNode;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * å®¡æ‰¹èŠ‚ç‚¹å®žä¾‹è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:54
 */
public interface ApprovalInstanceNodeService extends IService<ApprovalInstanceNode> {
}
src/main/java/com/ruoyi/approve/service/ApprovalInstanceService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.approve.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.vo.ApprovalInstanceVo;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.framework.web.domain.R;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹å®žä¾‹è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:46
 */
public interface ApprovalInstanceService extends IService<ApprovalInstance> {
    R listPage(Page<ApprovalInstanceVo> page, ApprovalInstanceDto approvalInstanceDto);
    Boolean add(ApprovalInstanceDto approvalInstanceDto);
    Boolean update(ApprovalInstanceDto approvalInstanceDto);
    Boolean delete(List<Long> ids);
    R approve(ApprovalInstanceDto approvalInstanceDto);
    R autoApprove(Long instanceId);
}
src/main/java/com/ruoyi/approve/service/ApprovalRecordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.ApprovalRecord;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * å®¡æ‰¹è®°å½•表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:28:21
 */
public interface ApprovalRecordService extends IService<ApprovalRecord> {
}
src/main/java/com/ruoyi/approve/service/ApprovalTaskService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * å®¡æ‰¹ä»»åŠ¡è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:32:37
 */
public interface ApprovalTaskService extends IService<ApprovalTask> {
}
src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeApproverService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹å®¡æ‰¹äººè¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:30
 */
public interface ApprovalTemplateNodeApproverService extends IService<ApprovalTemplateNodeApprover> {
}
src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeDto;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:19
 */
public interface ApprovalTemplateNodeService extends IService<ApprovalTemplateNode> {
    Boolean saveApprovalTemplateNode(Long id, List<ApprovalTemplateNodeDto> nodes);
}
src/main/java/com/ruoyi/approve/service/ApprovalTemplateService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.approve.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalTemplateDto;
import com.ruoyi.approve.bean.vo.ApprovalTemplateVo;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:08
 */
public interface ApprovalTemplateService extends IService<ApprovalTemplate> {
    IPage<ApprovalTemplateVo> listPage(Page<ApprovalTemplateVo> page, ApprovalTemplateDto approvalTemplateDto);
    Boolean saveApprovalTemplateDto(ApprovalTemplateDto approvalTemplateDto);
    Boolean updateApprovalTemplateDto(ApprovalTemplateDto approvalTemplateDto);
    Boolean delete(List<Long> ids);
    List<ApprovalTemplateVo> listApprovalTemplateVo(Integer type);
    ApprovalTemplateVo getApprovalTemplateVoById(Long id);
}
src/main/java/com/ruoyi/approve/service/FinReimbursementDetailService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * æŠ¥é”€å•明细表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
public interface FinReimbursementDetailService extends IService<FinReimbursementDetail> {
}
src/main/java/com/ruoyi/approve/service/FinReimbursementService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.approve.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.pojo.FinReimbursement;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
 * <p>
 * æŠ¥é”€å•主表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
public interface FinReimbursementService extends IService<FinReimbursement> {
    IPage<FinReimbursementVo> listPage(FinReimbursementDto finReimbursementDto, Page<FinReimbursementVo> page);
    Boolean add(FinReimbursementDto finReimbursementDto);
    FinReimbursementVo detail(Long id);
    Boolean update(FinReimbursementDto finReimbursementDto);
    Boolean delete(List<Long> ids);
}
src/main/java/com/ruoyi/approve/service/FinReimbursementTravelService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * å·®æ—…报销扩展表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
public interface FinReimbursementTravelService extends IService<FinReimbursementTravel> {
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceNodeServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.ApprovalInstanceNode;
import com.ruoyi.approve.mapper.ApprovalInstanceNodeMapper;
import com.ruoyi.approve.service.ApprovalInstanceNodeService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * å®¡æ‰¹èŠ‚ç‚¹å®žä¾‹è¡¨ æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:54
 */
@Service
public class ApprovalInstanceNodeServiceImpl extends ServiceImpl<ApprovalInstanceNodeMapper, ApprovalInstanceNode> implements ApprovalInstanceNodeService {
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,846 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.vo.ApprovalInstanceVo;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.mapper.ApprovalTemplateNodeApproverMapper;
import com.ruoyi.approve.mapper.FinReimbursementMapper;
import com.ruoyi.approve.pojo.*;
import com.ruoyi.approve.service.*;
import com.ruoyi.approve.utils.ApproveProcessConfigNodeUtils;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsMapper;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeDeptMapper;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeUserMapper;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser;
import com.ruoyi.common.enums.*;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.mapper.SysUserDeptMapper;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.project.system.service.ISysNoticeService;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.quality.utils.QualityInspectHelper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.mapper.SalesQuotationMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.SalesQuotation;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.staff.mapper.HolidayApplicationMapper;
import com.ruoyi.staff.pojo.HolidayApplication;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
 * <p>
 * å®¡æ‰¹å®žä¾‹æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @since 2026-05-18 03:27:46
 */
@Service
@RequiredArgsConstructor
public class ApprovalInstanceServiceImpl extends ServiceImpl<ApprovalInstanceMapper, ApprovalInstance> implements ApprovalInstanceService {
    private static final String ENTERPRISE_NEWS_STATUS_PUBLISHED = "PUBLISHED";
    private static final String ENTERPRISE_NEWS_STATUS_REJECTED = "REJECTED";
    private final ApprovalInstanceMapper approvalInstanceMapper;
    private final ApproveProcessConfigNodeUtils approveProcessConfigNodeUtils;
    private final ApprovalInstanceNodeService approvalInstanceNodeService;
    private final ApprovalTaskService approvalTaskService;
    private final ApprovalRecordService approvalRecordService;
    private final ApprovalTemplateNodeService approvalTemplateNodeService;
    private final FinReimbursementMapper finReimbursementMapper;
    private final FileUtil fileUtil;
    private final ISysNoticeService sysNoticeService;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final StockUtils stockUtils;
    private final SalesQuotationMapper salesQuotationMapper;
    private final ShippingInfoMapper shippingInfoMapper;
    private final QualityInspectHelper qualityInspectHelper;
    private final EnterpriseNewsScopeUserMapper enterpriseNewsScopeUserMapper;
    private final SysUserMapper sysUserMapper;
    private final SysUserDeptMapper sysUserDeptMapper;
    private final SysDeptMapper sysDeptMapper;
    private final HolidayApplicationMapper holidayApplicationMapper;
    private final EnterpriseNewsMapper enterpriseNewsMapper;
    private final EnterpriseNewsScopeDeptMapper enterpriseNewsScopeDeptMapper;
    private final ApprovalTemplateNodeApproverMapper approvalTemplateNodeApproverMapper;
    @Override
    public R listPage(Page<ApprovalInstanceVo> page, ApprovalInstanceDto approvalInstanceDto) {
        IPage<ApprovalInstanceVo> approvalInstanceVoIPage = approvalInstanceMapper.listPage(page, approvalInstanceDto);
        List<ApprovalInstanceVo> records = approvalInstanceVoIPage.getRecords();
        if (records == null || records.isEmpty()) {
            return R.ok(approvalInstanceVoIPage);
        }
        records.forEach(vo -> {
            vo.setBusinessName(TypeEnums.getLabelByValue(vo.getBusinessType()));
        });
        Long currentUserId = SecurityUtils.getUserId();
        List<Long> instanceIds = records.stream()
                .map(ApprovalInstanceVo::getId)
                .filter(id -> id != null)
                .distinct()
                .collect(Collectors.toList());
        if (!instanceIds.isEmpty()) {
            Map<Long, List<ApprovalRecord>> recordMap = approvalRecordService.list(
                    Wrappers.<ApprovalRecord>lambdaQuery()
                            .in(ApprovalRecord::getInstanceId, instanceIds)
                            .eq(ApprovalRecord::getDeleted, 0)
            ).stream().collect(Collectors.groupingBy(ApprovalRecord::getInstanceId));
            Map<Long, List<ApprovalTask>> taskMap = approvalTaskService.list(
                    Wrappers.<ApprovalTask>lambdaQuery()
                            .in(ApprovalTask::getInstanceId, instanceIds)
                            .eq(ApprovalTask::getDeleted, 0)
            ).stream().collect(Collectors.groupingBy(ApprovalTask::getInstanceId));
            for (ApprovalInstanceVo vo : records) {
                vo.setIsApprove(approveProcessConfigNodeUtils.isCurrentApprover(vo.getId(), currentUserId));
                vo.setRecords(recordMap.getOrDefault(vo.getId(), new ArrayList<>()));
                vo.setTasks(taskMap.getOrDefault(vo.getId(), new ArrayList<>()));
                vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.APPROVAL_INSTANCE, vo.getId()));
            }
        }
        return R.ok(approvalInstanceVoIPage);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean add(ApprovalInstanceDto approvalInstanceDto) {
        String instanceNo = OrderUtils.countTodayByCreateTime(approvalInstanceMapper, "SP", "instance_no", approvalInstanceDto.getCreateTime() != null ? approvalInstanceDto.getCreateTime() : LocalDateTime.now());
        approvalInstanceDto.setInstanceNo(instanceNo);
        approvalInstanceDto.setStatus("PENDING");
        approvalInstanceDto.setCurrentLevel(1);
        boolean saved = this.save(approvalInstanceDto);
        if (!saved) {
            return false;
        }
        approveProcessConfigNodeUtils.createCurrentNodeAndTasks(approvalInstanceDto);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVAL_INSTANCE, approvalInstanceDto.getId(), approvalInstanceDto.getStorageBlobDTOs());
        sendApproveNotice(approvalInstanceDto, approveProcessConfigNodeUtils.getCurrentPendingTasks(approvalInstanceDto.getId()));
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean update(ApprovalInstanceDto approvalInstanceDto) {
        if (approvalInstanceDto == null || approvalInstanceDto.getId() == null) {
            return false;
        }
        // åˆ¤æ–­æ˜¯å¦æœ‰æ­£åœ¨è¿›è¡Œçš„审批任务,有则不允许修改
        long pendingTaskCount = approvalTaskService.count(
                Wrappers.<ApprovalTask>lambdaQuery()
                        .eq(ApprovalTask::getInstanceId, approvalInstanceDto.getId())
                        .eq(ApprovalTask::getTaskStatus, "PENDING")
                        .eq(ApprovalTask::getDeleted, 0)
        );
        if (pendingTaskCount > 0) {
            throw new ServiceException("该审批单有正在进行的审批任务,不允许修改");
        }
        boolean updated = this.updateById(approvalInstanceDto);
        if (!updated) {
            return false;
        }
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVAL_INSTANCE, approvalInstanceDto.getId(), approvalInstanceDto.getStorageBlobDTOs());
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean delete(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return false;
        }
        fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVAL_INSTANCE, ids);
        int instanceRows = approvalInstanceMapper.update(
                null,
                Wrappers.<ApprovalInstance>lambdaUpdate()
                        .in(ApprovalInstance::getId, ids)
                        .eq(ApprovalInstance::getDeleted, 0)
                        .set(ApprovalInstance::getDeleted, (byte) 1)
        );
        LambdaUpdateWrapper<ApprovalInstanceNode> nodeUpdateWrapper = Wrappers.lambdaUpdate();
        nodeUpdateWrapper.in(ApprovalInstanceNode::getInstanceId, ids)
                .eq(ApprovalInstanceNode::getDeleted, 0)
                .set(ApprovalInstanceNode::getDeleted, (byte) 1);
        approvalInstanceNodeService.update(nodeUpdateWrapper);
        LambdaUpdateWrapper<ApprovalTask> taskUpdateWrapper = Wrappers.lambdaUpdate();
        taskUpdateWrapper.in(ApprovalTask::getInstanceId, ids)
                .eq(ApprovalTask::getDeleted, 0)
                .set(ApprovalTask::getDeleted, (byte) 1);
        approvalTaskService.update(taskUpdateWrapper);
        LambdaUpdateWrapper<ApprovalRecord> recordUpdateWrapper = Wrappers.lambdaUpdate();
        recordUpdateWrapper.in(ApprovalRecord::getInstanceId, ids)
                .eq(ApprovalRecord::getDeleted, 0)
                .set(ApprovalRecord::getDeleted, (byte) 1);
        approvalRecordService.update(recordUpdateWrapper);
        return instanceRows > 0;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R approve(ApprovalInstanceDto approvalInstanceDto) {
        if (approvalInstanceDto == null || approvalInstanceDto.getId() == null) {
            return R.fail("审批实例 ID ä¸èƒ½ä¸ºç©º");
        }
        String approveAction = normalizeApproveAction(approvalInstanceDto.getApproveAction());
        if (approveAction == null) {
            return R.fail("审批动作只支持 APPROVED æˆ– REJECTED");
        }
        ApprovalInstance instance = getPendingApprovalInstance(approvalInstanceDto.getId());
        if (instance == null) {
            return R.fail("审批实例不存在");
        }
        ApprovalInstanceNode currentNode = approveProcessConfigNodeUtils.getCurrentNode(instance.getId());
        if (currentNode == null) {
            return R.fail("当前没有待处理的审批节点");
        }
        Long currentUserId = SecurityUtils.getUserId();
        ApprovalTask currentTask = approveProcessConfigNodeUtils.getCurrentUserTask(instance.getId(), currentUserId);
        if (currentTask == null) {
            return R.fail("当前用户没有可审批任务");
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String operatorName = loginUser.getUser() != null ? loginUser.getUser().getNickName() : SecurityUtils.getUsername();
        LocalDateTime now = LocalDateTime.now();
        if (!updateCurrentTask(approvalInstanceDto, approveAction, currentTask, now)) {
            return R.fail("当前任务已被处理,请刷新后重试");
        }
        saveApprovalRecord(
                instance.getId(),
                currentNode.getId(),
                currentTask.getId(),
                currentUserId,
                operatorName,
                approveAction,
                approvalInstanceDto.getApproveComment()
        );
        //审批拒绝的处理
        if ("REJECTED".equals(approveAction)) {
            return rejectCurrentNode(instance, currentNode, now);
        }
        if (!approveProcessConfigNodeUtils.canProceedToNextLevel(instance.getId(), currentNode.getApproveType())) {
            return R.ok("审批成功,等待其他审批人处理");
        }
        return approveAndMoveNext(instance, currentNode, approvalInstanceDto, now);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R autoApprove(Long instanceId) {
        if (instanceId == null) {
            return R.fail("审批实例 ID ä¸èƒ½ä¸ºç©º");
        }
        ApprovalInstance instance = getPendingApprovalInstance(instanceId);
        if (instance == null) {
            return R.fail("审批实例不存在");
        }
        if ("REJECTED".equals(instance.getStatus())) {
            return R.fail("审批已驳回,无法自动通过");
        }
        if ("APPROVED".equals(instance.getStatus())) {
            return R.ok("审批已完成");
        }
        ApprovalInstanceDto autoApproveDto = new ApprovalInstanceDto();
        autoApproveDto.setId(instanceId);
        autoApproveDto.setApproveComment("系统自动审批");
        int loopCount = 0;
        while (loopCount++ < 20) {
            ApprovalInstance currentInstance = getPendingApprovalInstance(instanceId);
            if (currentInstance == null) {
                return R.fail("审批实例不存在");
            }
            if ("APPROVED".equals(currentInstance.getStatus())) {
                return R.ok("审批已完成");
            }
            if ("REJECTED".equals(currentInstance.getStatus())) {
                return R.fail("审批已驳回,无法自动通过");
            }
            ApprovalInstanceNode currentNode = approveProcessConfigNodeUtils.getCurrentNode(currentInstance.getId());
            if (currentNode == null) {
                currentInstance.setStatus("APPROVED");
                currentInstance.setFinishTime(LocalDateTime.now());
                this.updateById(currentInstance);
                handleBusinessAfterApprovalFinished(currentInstance);
                return R.ok("审批已完成");
            }
            List<ApprovalTask> pendingTasks = approvalTaskService.list(
                    Wrappers.<ApprovalTask>lambdaQuery()
                            .eq(ApprovalTask::getInstanceId, currentInstance.getId())
                            .eq(ApprovalTask::getNodeId, currentNode.getId())
                            .eq(ApprovalTask::getTaskStatus, "PENDING")
                            .eq(ApprovalTask::getDeleted, 0)
            );
            LocalDateTime now = LocalDateTime.now();
            for (ApprovalTask currentTask : pendingTasks) {
                if (!updateCurrentTask(autoApproveDto, "APPROVED", currentTask, now)) {
                    return R.fail("当前任务已被处理,请刷新后重试");
                }
                saveApprovalRecord(
                        currentInstance.getId(),
                        currentNode.getId(),
                        currentTask.getId(),
                        0L,
                        "系统自动审批",
                        "APPROVED",
                        autoApproveDto.getApproveComment()
                );
            }
            if (!approveProcessConfigNodeUtils.canProceedToNextLevel(currentInstance.getId(), currentNode.getApproveType())) {
                return R.ok("审批成功,等待其他审批人处理");
            }
            R moveResult = moveToNextLevel(currentInstance, currentNode, autoApproveDto, now, false);
            if (!R.isSuccess(moveResult)) {
                return moveResult;
            }
        }
        return R.fail("自动审批循环次数超限");
    }
    private String normalizeApproveAction(String approveAction) {
        if (!StringUtils.hasText(approveAction)) {
            return null;
        }
        String normalizedAction = approveAction.trim().toUpperCase(Locale.ROOT);
        return "APPROVED".equals(normalizedAction) || "REJECTED".equals(normalizedAction)
                ? normalizedAction
                : null;
    }
    private ApprovalInstance getPendingApprovalInstance(Long instanceId) {
        return this.getOne(
                new LambdaQueryWrapper<ApprovalInstance>()
                        .eq(ApprovalInstance::getId, instanceId)
                        .eq(ApprovalInstance::getDeleted, 0)
                        .last("LIMIT 1")
        );
    }
    private boolean updateCurrentTask(ApprovalInstanceDto approvalInstanceDto,
                                      String approveAction,
                                      ApprovalTask currentTask,
                                      LocalDateTime now) {
        // ä»…允许待审批任务被成功处理一次,避免并发下重复审批成功。
        return approvalTaskService.update(
                Wrappers.<ApprovalTask>lambdaUpdate()
                        .eq(ApprovalTask::getId, currentTask.getId())
                        .eq(ApprovalTask::getTaskStatus, "PENDING")
                        .eq(ApprovalTask::getDeleted, 0)
                        .set(ApprovalTask::getTaskStatus, approveAction)
                        .set(ApprovalTask::getComment, approvalInstanceDto.getApproveComment())
                        .set(ApprovalTask::getApproveTime, now)
                        .set(ApprovalTask::getIsRead, (byte) 1)
        );
    }
    private R rejectCurrentNode(ApprovalInstance instance, ApprovalInstanceNode currentNode, LocalDateTime now) {
        if (!updateCurrentNodeStatus(currentNode.getId(), "REJECTED", now)) {
            return R.ok("当前节点已处理完成");
        }
        closePendingTasks(instance.getId(), currentNode.getId());
        instance.setStatus("REJECTED");
        instance.setFinishTime(now);
        this.updateById(instance);
        // é©³å›žå¯¹åº”的企业新闻, å·®æ—…报销
        if (instance.getBusinessType().equals(TypeEnums.ENTERPRISE_NEWS_APPROVAL.getCode())) {
            enterpriseNewsMapper.update(
                    new LambdaUpdateWrapper<EnterpriseNews>()
                            .eq(EnterpriseNews::getId, instance.getBusinessId())
                            .set(EnterpriseNews::getStatus, "REJECTED")
            );
        }else if (instance.getBusinessType().equals(TypeEnums.TRAVEL_REIMBURSEMENT_APPROVAL.getCode())||instance.getBusinessType().equals(TypeEnums.EXPENSE_APPROVAL.getCode())) {
            finReimbursementMapper.update(
                    new LambdaUpdateWrapper<FinReimbursement>()
                            .eq(FinReimbursement::getId, instance.getBusinessId())
                            .set(FinReimbursement::getBillStatus, "REJECTED")
            );
        }
        return R.ok("审批已驳回");
    }
    private R approveAndMoveNext(ApprovalInstance instance,
                                 ApprovalInstanceNode currentNode,
                                 ApprovalInstanceDto approvalInstanceDto,
                                 LocalDateTime now) {
        return moveToNextLevel(instance, currentNode, approvalInstanceDto, now, true);
    }
    private R moveToNextLevel(ApprovalInstance instance,
                              ApprovalInstanceNode currentNode,
                              ApprovalInstanceDto approvalInstanceDto,
                              LocalDateTime now,
                              boolean notifyNextNode) {
        if (!updateCurrentNodeStatus(currentNode.getId(), "APPROVED", now)) {
            return R.ok("当前节点已处理完成");
        }
        closePendingTasks(instance.getId(), currentNode.getId());
        int nextLevel = currentNode.getLevelNo() + 1;
        ApprovalInstanceNode nextInstanceNode = approvalInstanceNodeService.getOne(
                new LambdaQueryWrapper<ApprovalInstanceNode>()
                        .eq(ApprovalInstanceNode::getInstanceId, instance.getId())
                        .eq(ApprovalInstanceNode::getLevelNo, nextLevel)
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .orderByAsc(ApprovalInstanceNode::getId)
                        .last("LIMIT 1")
        );
        if (nextInstanceNode != null) {
            if (!activateNextInstanceNode(nextInstanceNode.getId(), now)) {
                return R.ok("下一审批节点已被激活,请刷新后重试");
            }
            instance.setCurrentLevel(nextLevel);
            instance.setStatus("PENDING");
            this.updateById(instance);
            if (notifyNextNode) {
                List<ApprovalTask> nextTasks = approvalTaskService.list(
                        Wrappers.<ApprovalTask>lambdaQuery()
                                .eq(ApprovalTask::getInstanceId, instance.getId())
                                .eq(ApprovalTask::getNodeId, nextInstanceNode.getId())
                                .eq(ApprovalTask::getTaskStatus, "PENDING")
                                .eq(ApprovalTask::getDeleted, 0)
                );
                sendApproveNotice(instance, nextTasks);
            }
            return R.ok("审批成功,已流转到下一节点");
        }
        ApprovalTemplateNode nextTemplateNode = approvalTemplateNodeService.getOne(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .eq(ApprovalTemplateNode::getTemplateId, instance.getTemplateId())
                        .eq(ApprovalTemplateNode::getLevelNo, nextLevel)
                        .orderByAsc(ApprovalTemplateNode::getId)
                        .last("LIMIT 1")
        );
        if (nextTemplateNode == null) {
            instance.setStatus("APPROVED");
            instance.setFinishTime(now);
            this.updateById(instance);
            handleBusinessAfterApprovalFinished(instance);
            return R.ok("审批已完成");
        }
        instance.setCurrentLevel(nextLevel);
        instance.setStatus("PENDING");
        this.updateById(instance);
        approveProcessConfigNodeUtils.createCurrentNodeAndTasks(instance, false);
        if (notifyNextNode) {
            sendApproveNotice(instance, approveProcessConfigNodeUtils.getCurrentPendingTasks(approvalInstanceDto.getId()));
        }
        return R.ok("审批成功,已流转到下一节点");
    }
    private boolean activateNextInstanceNode(Long nodeId, LocalDateTime now) {
        return approvalInstanceNodeService.update(
                Wrappers.<ApprovalInstanceNode>lambdaUpdate()
                        .eq(ApprovalInstanceNode::getId, nodeId)
                        .eq(ApprovalInstanceNode::getStatus, "WAITING")
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .set(ApprovalInstanceNode::getStatus, "PENDING")
                        .set(ApprovalInstanceNode::getStartTime, now)
        );
    }
    private boolean updateCurrentNodeStatus(Long nodeId, String targetStatus, LocalDateTime now) {
        // ä»…允许一个请求将当前节点从待处理推进到目标状态,避免重复流转。
        return approvalInstanceNodeService.update(
                Wrappers.<ApprovalInstanceNode>lambdaUpdate()
                        .eq(ApprovalInstanceNode::getId, nodeId)
                        .eq(ApprovalInstanceNode::getStatus, "PENDING")
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .set(ApprovalInstanceNode::getStatus, targetStatus)
                        .set(ApprovalInstanceNode::getFinishTime, now)
        );
    }
    private void handleBusinessAfterApprovalFinished(ApprovalInstance instance) {
        String status = instance.getStatus();
        Long businessType = instance.getBusinessType();
        if (TypeEnums.PURCHASE_APPROVAL.getCode().equals(businessType)) {
            handlePurchaseApprovalFinished(instance, status);
            return;
        }
        if (TypeEnums.QUOTATION_APPROVAL.getCode().equals(businessType)) {
            handleSalesQuotationApprovalFinished(instance, status);
            return;
        }
        if (TypeEnums.SHIPPING_APPROVAL.getCode().equals(businessType)) {
            handleShippingApprovalFinished(instance, status);
            return;
        }
        if (TypeEnums.TRAVEL_REIMBURSEMENT_APPROVAL.getCode().equals(businessType)
                || TypeEnums.EXPENSE_APPROVAL.getCode().equals(businessType)) {
            handleReimbursementApprovalFinished(instance, status);
            return;
        }
        // é©³å›žå¯¹åº”的企业新闻、加班申请、请假申请可以重新再提交
        if (TypeEnums.LEAVE_APPROVAL.getCode().equals(businessType)
                || TypeEnums.OVERTIME_APPROVAL.getCode().equals(businessType)) {
            handleHolidayApplicationApprovalFinished(instance, status);
            return;
        }
        if (TypeEnums.ENTERPRISE_NEWS_APPROVAL.getCode().equals(businessType)) {
            handleNewsApprovalFinished(instance, status);
        }
    }
    private void handleReimbursementApprovalFinished(ApprovalInstance instance, String status) {
        if (instance == null || instance.getBusinessId() == null) {
            return;
        }
        FinReimbursement reimbursement = new FinReimbursement();
        reimbursement.setId(instance.getBusinessId());
        if ("APPROVED".equals(status)) {
            reimbursement.setBillStatus("APPROVED");
            reimbursement.setApprovedTime(instance.getFinishTime());
        } else if ("REJECTED".equals(status)) {
            reimbursement.setBillStatus("REJECTED");
        } else if ("PENDING".equals(status)) {
            reimbursement.setBillStatus("IN_APPROVAL");
        } else {
            return;
        }
        finReimbursementMapper.updateById(reimbursement);
    }
    private void handleNewsApprovalFinished(ApprovalInstance instance, String status) {
        if (instance == null || instance.getBusinessId() == null) {
            return;
        }
        EnterpriseNews enterpriseNews = new EnterpriseNews();
        enterpriseNews.setId(instance.getBusinessId());
        if ("APPROVED".equals(status)) {
            enterpriseNews.setStatus(ENTERPRISE_NEWS_STATUS_PUBLISHED);
            enterpriseNewsMapper.updateById(enterpriseNews);
            sendEnterpriseNewsNotice(instance.getBusinessId());
            return;
        }
        if ("REJECTED".equals(status)) {
            enterpriseNews.setStatus(ENTERPRISE_NEWS_STATUS_REJECTED);
            enterpriseNewsMapper.updateById(enterpriseNews);
        }
    }
    private void handleHolidayApplicationApprovalFinished(ApprovalInstance instance, String status) {
        if (instance == null || instance.getBusinessId() == null) {
            return;
        }
        HolidayApplication holidayApplication = new HolidayApplication();
        holidayApplication.setId(instance.getBusinessId());
        if ("APPROVED".equals(status)) {
            holidayApplication.setStatus("APPROVED");
        } else if ("REJECTED".equals(status)) {
            holidayApplication.setStatus("REJECTED");
        } else if ("PENDING".equals(status)) {
            holidayApplication.setStatus("PENDING");
        } else {
            return;
        }
        holidayApplicationMapper.updateById(holidayApplication);
    }
    private void handlePurchaseApprovalFinished(ApprovalInstance instance, String status) {
        PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectOne(
                new LambdaQueryWrapper<PurchaseLedger>()
                        .eq(PurchaseLedger::getId, instance.getBusinessId())
                        .last("limit 1")
        );
        if (purchaseLedger == null) {
            return;
        }
        if ("APPROVED".equals(status)) {
            purchaseLedger.setApprovalStatus(ApprovalStatusEnum.APPROVED.getCode());
            List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(
                    new QueryWrapper<SalesLedgerProduct>().lambda()
                            .eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedger.getId())
                            .eq(SalesLedgerProduct::getType, 2)
            );
            for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) {
                if (salesLedgerProduct.getIsChecked()) {
                    qualityInspectHelper.addQualityInspect(purchaseLedger, salesLedgerProduct);
                } else {
                    stockUtils.addStockWithBatchNo(
                            salesLedgerProduct.getProductModelId(),
                            salesLedgerProduct.getQuantity(),
                            StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(),
                            purchaseLedger.getId(),
                            purchaseLedger.getPurchaseContractNumber() + "-" + salesLedgerProduct.getId()
                    );
                }
            }
        } else if ("REJECTED".equals(status)) {
            purchaseLedger.setApprovalStatus(ApprovalStatusEnum.REJECTED.getCode());
        } else if ("PENDING".equals(status)) {
            purchaseLedger.setApprovalStatus(ApprovalStatusEnum.IN_PROGRESS.getCode());
        }
        purchaseLedgerMapper.updateById(purchaseLedger);
    }
    private void handleSalesQuotationApprovalFinished(ApprovalInstance instance, String status) {
        SalesQuotation salesQuote = salesQuotationMapper.selectOne(
                new LambdaQueryWrapper<SalesQuotation>()
                        .eq(SalesQuotation::getId, instance.getBusinessId())
                        .last("limit 1")
        );
        if (salesQuote == null) {
            return;
        }
        if ("APPROVED".equals(status)) {
            salesQuote.setStatus(SalesQuotationStatusEnum.APPROVED.getCode());
        } else if ("REJECTED".equals(status)) {
            salesQuote.setStatus(SalesQuotationStatusEnum.REJECTED.getCode());
        } else if ("PENDING".equals(status)) {
            salesQuote.setStatus(SalesQuotationStatusEnum.IN_PROGRESS.getCode());
        }
        salesQuotationMapper.updateById(salesQuote);
    }
    private void handleShippingApprovalFinished(ApprovalInstance instance, String status) {
        ShippingInfo shippingInfo = shippingInfoMapper.selectOne(
                new LambdaQueryWrapper<ShippingInfo>()
                        .eq(ShippingInfo::getId, instance.getTitle())
                        .orderByDesc(ShippingInfo::getCreateTime)
                        .last("limit 1")
        );
        if (shippingInfo == null) {
            return;
        }
        if ("APPROVED".equals(status)) {
            shippingInfo.setStatus(ShippingStatusEnum.APPROVED.getCode());
            shippingInfo.setShippingDate(new Date());
            stockUtils.shipmentStatus(StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), shippingInfo.getId());
        } else if ("REJECTED".equals(status)) {
            stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
            shippingInfo.setStatus(ShippingStatusEnum.REJECTED.getCode());
        } else if ("PENDING".equals(status)) {
            shippingInfo.setStatus(ShippingStatusEnum.IN_PROGRESS.getCode());
        }
        shippingInfoMapper.updateById(shippingInfo);
    }
    private List<ApprovalTask> createNodeAndTasks(ApprovalInstance instance, ApprovalTemplateNode templateNode) {
        List<ApprovalTemplateNodeApprover> approvers = approvalTemplateNodeApproverMapper.selectList(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .eq(ApprovalTemplateNodeApprover::getTemplateId, instance.getTemplateId())
                        .eq(ApprovalTemplateNodeApprover::getNodeId, templateNode.getId())
                        .eq(ApprovalTemplateNodeApprover::getDeleted, 0L)
                        .orderByAsc(ApprovalTemplateNodeApprover::getSortNo)
        );
        if (approvers == null || approvers.isEmpty()) {
            throw new RuntimeException("下一审批节点未配置审批人");
        }
        ApprovalInstanceNode instanceNode = new ApprovalInstanceNode();
        instanceNode.setInstanceId(instance.getId());
        instanceNode.setLevelNo(templateNode.getLevelNo());
        instanceNode.setApproveType(templateNode.getApproveType());
        instanceNode.setStatus("PENDING");
        instanceNode.setStartTime(LocalDateTime.now());
        instanceNode.setDeleted((byte) 0);
        approvalInstanceNodeService.save(instanceNode);
        List<ApprovalTask> taskList = new ArrayList<>(approvers.size());
        for (ApprovalTemplateNodeApprover approver : approvers) {
            ApprovalTask task = new ApprovalTask();
            task.setInstanceId(instance.getId());
            task.setNodeId(instanceNode.getId());
            task.setLevelNo(instanceNode.getLevelNo());
            task.setApproverId(approver.getApproverId());
            task.setApproverName(approver.getApproverName());
            task.setTaskStatus("PENDING");
            task.setIsRead((byte) 0);
            task.setDeleted((byte) 0);
            taskList.add(task);
        }
        approvalTaskService.saveBatch(taskList);
        return taskList;
    }
    private void sendApproveNotice(ApprovalInstance instance, List<ApprovalTask> tasks) {
        if (instance == null || tasks == null || tasks.isEmpty()) {
            return;
        }
        List<Long> approverIds = tasks.stream()
                .map(ApprovalTask::getApproverId)
                .filter(id -> id != null && id > 0)
                .distinct()
                .collect(Collectors.toList());
        if (approverIds.isEmpty()) {
            return;
        }
        String title = StringUtils.hasText(instance.getTemplateName()) ? instance.getTemplateName() : "审批提醒";
        String message = "审批单号 " + instance.getInstanceNo() + " éœ€è¦æ‚¨å®¡æ‰¹";
        String jumpPath = "/officeProcessAutomation/ApproveManage/approve-list?id=" + instance.getId();
        sysNoticeService.simpleNoticeByUser(title, message, approverIds, jumpPath);
    }
    private void sendEnterpriseNewsNotice(Long newsId) {
        EnterpriseNews enterpriseNews = enterpriseNewsMapper.selectById(newsId);
        if (enterpriseNews == null) {
            return;
        }
        List<Long> userIds = getEnterpriseNewsNoticeUserIds(enterpriseNews);
        if (userIds == null || userIds.isEmpty()) {
            return;
        }
        String title = "企业新闻";
        String message = "您有新的企业新闻《" + enterpriseNews.getTitle() + "》请及时查阅";
        String jumpPath = "/officeProcessAutomation/EnterpriseNews?id=" + newsId;
        sysNoticeService.simpleNoticeByUser(title, message, userIds, jumpPath);
    }
    private List<Long> getEnterpriseNewsNoticeUserIds(EnterpriseNews enterpriseNews) {
        if (enterpriseNews == null || !org.springframework.util.StringUtils.hasText(enterpriseNews.getReadScope())) {
            return Collections.emptyList();
        }
        String readScope = enterpriseNews.getReadScope().trim();
        if ("all".equals(readScope)) {
            return sysUserMapper.selectList(new LambdaQueryWrapper<SysUser>()
                            .select(SysUser::getUserId)
                            .eq(SysUser::getDelFlag, "0"))
                    .stream()
                    .map(SysUser::getUserId)
                    .filter(id -> id != null && id > 0)
                    .distinct()
                    .collect(Collectors.toList());
        }
        if ("dept".equals(readScope)) {
            List<Long> deptIds = enterpriseNewsScopeDeptMapper.selectList(
                            new LambdaQueryWrapper<EnterpriseNewsScopeDept>()
                                    .eq(EnterpriseNewsScopeDept::getNewsId, enterpriseNews.getId()))
                    .stream()
                    .map(EnterpriseNewsScopeDept::getDeptId)
                    .filter(id -> id != null && id > 0)
                    .distinct()
                    .collect(Collectors.toList());
            if (deptIds.isEmpty()) {
                return Collections.emptyList();
            }
            return sysUserDeptMapper.selectDistinctUserIdsByDeptIds(collectDeptIdsWithChildren(deptIds));
        }
        if ("custom".equals(readScope)) {
            return enterpriseNewsScopeUserMapper.selectList(
                            new LambdaQueryWrapper<EnterpriseNewsScopeUser>()
                                    .eq(EnterpriseNewsScopeUser::getNewsId, enterpriseNews.getId()))
                    .stream()
                    .map(EnterpriseNewsScopeUser::getUserId)
                    .filter(id -> id != null && id > 0)
                    .distinct()
                    .collect(Collectors.toList());
        }
        return Collections.emptyList();
    }
    private List<Long> collectDeptIdsWithChildren(List<Long> deptIds) {
        Set<Long> allDeptIds = new LinkedHashSet<>();
        for (Long deptId : deptIds) {
            if (deptId == null) {
                continue;
            }
            allDeptIds.add(deptId);
            List<SysDept> children = sysDeptMapper.selectChildrenDeptById(deptId);
            if (children != null && !children.isEmpty()) {
                for (SysDept child : children) {
                    if (child != null && child.getDeptId() != null) {
                        allDeptIds.add(child.getDeptId());
                    }
                }
            }
        }
        return new ArrayList<>(allDeptIds);
    }
    private void closePendingTasks(Long instanceId, Long nodeId) {
        LambdaUpdateWrapper<ApprovalTask> updateWrapper = Wrappers.lambdaUpdate();
        updateWrapper.eq(ApprovalTask::getInstanceId, instanceId)
                .eq(ApprovalTask::getNodeId, nodeId)
                .eq(ApprovalTask::getTaskStatus, "PENDING")
                .eq(ApprovalTask::getDeleted, 0)
                .set(ApprovalTask::getDeleted, (byte) 1);
        approvalTaskService.update(updateWrapper);
    }
    private void saveApprovalRecord(Long instanceId,
                                    Long nodeId,
                                    Long taskId,
                                    Long operatorId,
                                    String operatorName,
                                    String action,
                                    String comment) {
        ApprovalRecord record = new ApprovalRecord();
        record.setInstanceId(instanceId);
        record.setNodeId(nodeId);
        record.setTaskId(taskId);
        record.setOperatorId(operatorId);
        record.setOperatorName(operatorName);
        record.setAction(action);
        record.setComment(comment);
        record.setDeleted((byte) 0);
        approvalRecordService.save(record);
    }
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalRecordServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.ApprovalRecord;
import com.ruoyi.approve.mapper.ApprovalRecordMapper;
import com.ruoyi.approve.service.ApprovalRecordService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * å®¡æ‰¹è®°å½•表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:28:21
 */
@Service
public class ApprovalRecordServiceImpl extends ServiceImpl<ApprovalRecordMapper, ApprovalRecord> implements ApprovalRecordService {
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalTaskServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.approve.mapper.ApprovalTaskMapper;
import com.ruoyi.approve.service.ApprovalTaskService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * å®¡æ‰¹ä»»åŠ¡è¡¨ æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:32:37
 */
@Service
public class ApprovalTaskServiceImpl extends ServiceImpl<ApprovalTaskMapper, ApprovalTask> implements ApprovalTaskService {
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeApproverServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import com.ruoyi.approve.mapper.ApprovalTemplateNodeApproverMapper;
import com.ruoyi.approve.service.ApprovalTemplateNodeApproverService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹å®¡æ‰¹äººè¡¨ æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:30
 */
@Service
public class ApprovalTemplateNodeApproverServiceImpl extends ServiceImpl<ApprovalTemplateNodeApproverMapper, ApprovalTemplateNodeApprover> implements ApprovalTemplateNodeApproverService {
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeApproverDto;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeDto;
import com.ruoyi.approve.mapper.ApprovalTemplateNodeMapper;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import com.ruoyi.approve.service.ApprovalTemplateNodeApproverService;
import com.ruoyi.approve.service.ApprovalTemplateNodeService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹è¡¨ æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:19
 */
@Service
@RequiredArgsConstructor
public class ApprovalTemplateNodeServiceImpl extends ServiceImpl<ApprovalTemplateNodeMapper, ApprovalTemplateNode> implements ApprovalTemplateNodeService {
    private final ApprovalTemplateNodeMapper approvalTemplateNodeMapper;
    private final ApprovalTemplateNodeApproverService approvalTemplateNodeApproverService;
    @Override
    public Boolean saveApprovalTemplateNode(Long templateId, List<ApprovalTemplateNodeDto> nodes) {
        if (nodes == null || nodes.isEmpty()) {
            throw new RuntimeException("节点列表不能为空");
        }
        List<ApprovalTemplateNodeApprover> approverList = new ArrayList<>();
        for (ApprovalTemplateNodeDto nodeDto : nodes) {
            ApprovalTemplateNode node = new ApprovalTemplateNode();
            BeanUtils.copyProperties(nodeDto, node);
            node.setTemplateId(templateId);
            approvalTemplateNodeMapper.insert(node);
            List<ApprovalTemplateNodeApproverDto> approvers = nodeDto.getApprovers();
            if (approvers == null || approvers.isEmpty()) {
                throw new RuntimeException("节点审批人不能为空");
            }
            for (ApprovalTemplateNodeApproverDto approverDto : approvers) {
                ApprovalTemplateNodeApprover approver = new ApprovalTemplateNodeApprover();
                BeanUtils.copyProperties(approverDto, approver);
                approver.setNodeId(node.getId());
                approver.setTemplateId(templateId);
                approver.setDeleted(0L);
                approverList.add(approver);
            }
        }
        approvalTemplateNodeApproverService.saveBatch(approverList);
        return true;
    }
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,250 @@
package com.ruoyi.approve.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.dto.ApprovalTemplateDto;
import com.ruoyi.approve.bean.vo.ApprovalTemplateNodeApproverVo;
import com.ruoyi.approve.bean.vo.ApprovalTemplateNodeVo;
import com.ruoyi.approve.bean.vo.ApprovalTemplateVo;
import com.ruoyi.approve.mapper.ApprovalTemplateMapper;
import com.ruoyi.approve.mapper.ApprovalTemplateNodeApproverMapper;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import com.ruoyi.approve.service.ApprovalTemplateNodeService;
import com.ruoyi.approve.service.ApprovalTemplateService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @since 2026-05-18 11:20:08
 */
@Service
@RequiredArgsConstructor
public class ApprovalTemplateServiceImpl extends ServiceImpl<ApprovalTemplateMapper, ApprovalTemplate> implements ApprovalTemplateService {
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final ApprovalTemplateNodeService approvalTemplateNodeService;
    private final ApprovalTemplateNodeApproverMapper approvalTemplateNodeApproverMapper;
    @Override
    public IPage<ApprovalTemplateVo> listPage(Page<ApprovalTemplateVo> page, ApprovalTemplateDto approvalTemplateDto) {
        IPage<ApprovalTemplateVo> approvalTemplateVoIPage = approvalTemplateMapper.listPage(page, approvalTemplateDto);
        fillTemplateVoNodes(approvalTemplateVoIPage.getRecords());
        return approvalTemplateVoIPage;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean saveApprovalTemplateDto(ApprovalTemplateDto approvalTemplateDto) {
        approvalTemplateMapper.insert(approvalTemplateDto);
        approvalTemplateNodeService.remove(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .eq(ApprovalTemplateNode::getTemplateId, approvalTemplateDto.getId())
        );
        approvalTemplateNodeApproverMapper.delete(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .eq(ApprovalTemplateNodeApprover::getTemplateId, approvalTemplateDto.getId())
        );
        return approvalTemplateNodeService.saveApprovalTemplateNode(
                approvalTemplateDto.getId(),
                approvalTemplateDto.getNodes()
        );
    }
    @Override
    public Boolean updateApprovalTemplateDto(ApprovalTemplateDto approvalTemplateDto) {
        approvalTemplateMapper.updateById(approvalTemplateDto);
        approvalTemplateNodeService.remove(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .eq(ApprovalTemplateNode::getTemplateId, approvalTemplateDto.getId())
        );
        approvalTemplateNodeApproverMapper.delete(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .eq(ApprovalTemplateNodeApprover::getTemplateId, approvalTemplateDto.getId())
        );
        return approvalTemplateNodeService.saveApprovalTemplateNode(
                approvalTemplateDto.getId(),
                approvalTemplateDto.getNodes()
        );
    }
    @Transactional(rollbackFor = Exception.class)
    public Boolean delete(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return false;
        }
        ApprovalTemplate updateEntity = new ApprovalTemplate();
        updateEntity.setDeleted(1);
        LambdaUpdateWrapper<ApprovalTemplate> updateWrapper = Wrappers.lambdaUpdate();
        updateWrapper.in(ApprovalTemplate::getId, ids)
                .eq(ApprovalTemplate::getDeleted, 0);
        int rows = approvalTemplateMapper.update(updateEntity, updateWrapper);
        return rows == ids.size();
    }
    @Override
    public List<ApprovalTemplateVo> listApprovalTemplateVo(Integer type) {
        List<ApprovalTemplate> templateList = this.list(
                new LambdaQueryWrapper<ApprovalTemplate>()
                        .eq(ApprovalTemplate::getDeleted, 0)
                        .eq(ApprovalTemplate::getEnabled, 1)
                        .orderByDesc(ApprovalTemplate::getTemplateType)
                        .orderByDesc(ApprovalTemplate::getId)
        );
        if (CollUtil.isEmpty(templateList)) {
            return Collections.emptyList();
        }
        List<ApprovalTemplateVo> templateVos = templateList.stream()
                .map(template -> {
                    ApprovalTemplateVo templateVo = new ApprovalTemplateVo();
                    BeanUtils.copyProperties(template, templateVo);
                    return templateVo;
                })
                .collect(Collectors.toList());
        fillTemplateVoNodes(templateVos);
        return templateVos;
    }
    @Override
    public ApprovalTemplateVo getApprovalTemplateVoById(Long id) {
        if (id == null) {
            throw new IllegalArgumentException("参数 id ä¸èƒ½ä¸ºç©º");
        }
        ApprovalTemplate template = this.getOne(
                new LambdaQueryWrapper<ApprovalTemplate>()
                        .eq(ApprovalTemplate::getId, id)
                        .eq(ApprovalTemplate::getDeleted, 0)
        );
        if (template == null) {
            throw new IllegalArgumentException("模板不存在");
        }
        List<ApprovalTemplateNode> nodeList = approvalTemplateNodeService.list(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .eq(ApprovalTemplateNode::getTemplateId, id)
                        .orderByAsc(ApprovalTemplateNode::getLevelNo)
        );
        List<ApprovalTemplateNodeApprover> approverList = approvalTemplateNodeApproverMapper.selectList(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .eq(ApprovalTemplateNodeApprover::getTemplateId, id)
                        .eq(ApprovalTemplateNodeApprover::getDeleted, 0L)
        );
        Map<Long, List<ApprovalTemplateNode>> nodeMap = nodeList.stream()
                .collect(Collectors.groupingBy(ApprovalTemplateNode::getTemplateId));
        Map<Long, List<ApprovalTemplateNodeApprover>> approverMap = approverList.stream()
                .collect(Collectors.groupingBy(ApprovalTemplateNodeApprover::getNodeId));
        return buildTemplateVo(template, nodeMap, approverMap);
    }
    /**
     * æ‰¹é‡å¡«å……模板节点及节点审批人,避免循环查库。
     */
    private void fillTemplateVoNodes(List<ApprovalTemplateVo> templateVos) {
        if (CollUtil.isEmpty(templateVos)) {
            return;
        }
        List<Long> templateIds = templateVos.stream()
                .map(ApprovalTemplateVo::getId)
                .collect(Collectors.toList());
        List<ApprovalTemplateNode> nodeList = approvalTemplateNodeService.list(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .in(ApprovalTemplateNode::getTemplateId, templateIds)
                        .orderByAsc(ApprovalTemplateNode::getLevelNo)
        );
        List<ApprovalTemplateNodeApprover> approverList = approvalTemplateNodeApproverMapper.selectList(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .in(ApprovalTemplateNodeApprover::getTemplateId, templateIds)
                        .eq(ApprovalTemplateNodeApprover::getDeleted, 0L)
        );
        Map<Long, List<ApprovalTemplateNode>> nodeMap = nodeList.stream()
                .collect(Collectors.groupingBy(ApprovalTemplateNode::getTemplateId));
        Map<Long, List<ApprovalTemplateNodeApprover>> approverMap = approverList.stream()
                .collect(Collectors.groupingBy(ApprovalTemplateNodeApprover::getNodeId));
        templateVos.forEach(templateVo -> templateVo.setNodes(
                nodeMap.getOrDefault(templateVo.getId(), Collections.emptyList())
                        .stream()
                        .sorted(Comparator.comparing(
                                ApprovalTemplateNode::getLevelNo,
                                Comparator.nullsLast(Integer::compareTo)
                        ))
                        .map(node ->  buildNodeVo(node, approverMap))
                        .collect(Collectors.toList())
        ));
    }
    private ApprovalTemplateVo buildTemplateVo(ApprovalTemplate template,
                                               Map<Long, List<ApprovalTemplateNode>> nodeMap,
                                               Map<Long, List<ApprovalTemplateNodeApprover>> approverMap) {
        ApprovalTemplateVo templateVo = new ApprovalTemplateVo();
        BeanUtils.copyProperties(template, templateVo);
        List<ApprovalTemplateNodeVo> nodeVos = nodeMap
                .getOrDefault(template.getId(), Collections.emptyList())
                .stream()
                .sorted(Comparator.comparing(
                        ApprovalTemplateNode::getLevelNo,
                        Comparator.nullsLast(Integer::compareTo)
                ))
                .map(node -> buildNodeVo(node, approverMap))
                .collect(Collectors.toList());
        templateVo.setNodes(nodeVos);
        return templateVo;
    }
    private ApprovalTemplateNodeVo buildNodeVo(ApprovalTemplateNode node,
                                               Map<Long, List<ApprovalTemplateNodeApprover>> approverMap) {
        ApprovalTemplateNodeVo nodeVo = new ApprovalTemplateNodeVo();
        BeanUtils.copyProperties(node, nodeVo);
        List<ApprovalTemplateNodeApproverVo> approverVos = approverMap
                .getOrDefault(node.getId(), Collections.emptyList())
                .stream()
                .sorted(Comparator.comparing(
                        ApprovalTemplateNodeApprover::getSortNo,
                        Comparator.nullsLast(Integer::compareTo)
                ))
                .map(this::buildApproverVo)
                .collect(Collectors.toList());
        nodeVo.setApprovers(approverVos);
        return nodeVo;
    }
    private ApprovalTemplateNodeApproverVo buildApproverVo(ApprovalTemplateNodeApprover approver) {
        ApprovalTemplateNodeApproverVo approverVo = new ApprovalTemplateNodeApproverVo();
        BeanUtils.copyProperties(approver, approverVo);
        return approverVo;
    }
}
src/main/java/com/ruoyi/approve/service/impl/ApproveBusinessStatusService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,175 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityInspectParamMapper;
import com.ruoyi.quality.mapper.QualityTestStandardMapper;
import com.ruoyi.quality.mapper.QualityTestStandardParamMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.mapper.SalesQuotationMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.SalesQuotation;
import com.ruoyi.sales.pojo.ShippingInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ApproveBusinessStatusService {
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesQuotationMapper salesQuotationMapper;
    private final ShippingInfoMapper shippingInfoMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final StockUtils stockUtils;
    private final QualityInspectMapper qualityInspectMapper;
    private final QualityTestStandardMapper qualityTestStandardMapper;
    private final QualityTestStandardParamMapper qualityTestStandardParamMapper;
    private final QualityInspectParamMapper qualityInspectParamMapper;
    /**
     * ç»Ÿä¸€åŒæ­¥å®¡æ‰¹ç»“果对应的业务单据状态。
     * status:1-审核中,2-审核完成,3-审核未通过。
     */
    public void syncBusinessStatus(Integer approveType, String approveReason, Integer status) {
        if (approveType == null || status == null || !StringUtils.hasText(approveReason)) {
            return;
        }
        switch (approveType) {
            case 5:
                syncPurchaseStatus(approveReason, status);
                break;
            case 6:
                syncSalesQuotationStatus(approveReason, status);
                break;
            case 7:
                syncShippingStatus(approveReason, status);
                break;
            default:
                break;
        }
    }
    // é‡‡è´­å®¡æ‰¹é€šè¿‡æ—¶ï¼ŒæŒ‰äº§å“è´¨æ£€é…ç½®å†³å®šç”Ÿæˆè´¨æ£€å•或直接入库。
    private void syncPurchaseStatus(String approveReason, Integer status) {
        PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectOne(new LambdaQueryWrapper<PurchaseLedger>()
                .eq(PurchaseLedger::getPurchaseContractNumber, approveReason)
                .last("limit 1"));
        if (purchaseLedger == null) {
            return;
        }
        if (status.equals(2)) {
            purchaseLedger.setApprovalStatus(3);
            List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(new QueryWrapper<SalesLedgerProduct>()
                    .lambda().eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedger.getId()).eq(SalesLedgerProduct::getType, 2));
            for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) {
                if (Boolean.TRUE.equals(salesLedgerProduct.getIsChecked())) {
                    addQualityInspect(purchaseLedger, salesLedgerProduct);
                } else {
                    stockUtils.addStockWithBatchNo(
                            salesLedgerProduct.getProductModelId(),
                            salesLedgerProduct.getQuantity(),
                            StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(),
                            purchaseLedger.getId(),
                            purchaseLedger.getPurchaseContractNumber() + "-" + salesLedgerProduct.getId());
                }
            }
        } else if (status.equals(3)) {
            purchaseLedger.setApprovalStatus(4);
        } else if (status.equals(1)) {
            purchaseLedger.setApprovalStatus(2);
        } else {
            return;
        }
        purchaseLedgerMapper.updateById(purchaseLedger);
    }
    // æŠ¥ä»·å®¡æ‰¹çŠ¶æ€å›žå†™åˆ°é”€å”®æŠ¥ä»·å•çŠ¶æ€ã€‚
    private void syncSalesQuotationStatus(String approveReason, Integer status) {
        SalesQuotation salesQuote = salesQuotationMapper.selectOne(new LambdaQueryWrapper<SalesQuotation>()
                .eq(SalesQuotation::getQuotationNo, approveReason)
                .last("limit 1"));
        if (salesQuote == null) {
            return;
        }
        if (status.equals(2)) {
            salesQuote.setStatus("通过");
        } else if (status.equals(3)) {
            salesQuote.setStatus("拒绝");
        } else if (status.equals(1)) {
            salesQuote.setStatus("审核中");
        } else {
            return;
        }
        salesQuotationMapper.updateById(salesQuote);
    }
    // å‘货审批通过时同步发货状态和出库审批状态;拒绝时删除待确认出库记录。
    private void syncShippingStatus(String approveReason, Integer status) {
        ShippingInfo shippingInfo = shippingInfoMapper.selectOne(new LambdaQueryWrapper<ShippingInfo>()
                .eq(ShippingInfo::getShippingNo, approveReason)
                .orderByDesc(ShippingInfo::getCreateTime)
                .last("limit 1"));
        if (shippingInfo == null) {
            return;
        }
        if (status.equals(2)) {
            shippingInfo.setStatus("审核通过");
            shippingInfo.setShippingDate(new Date());
            stockUtils.shipmentStatus(StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), shippingInfo.getId());
        } else if (status.equals(3)) {
            stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
            shippingInfo.setStatus("审核拒绝");
        } else if (status.equals(1)) {
            shippingInfo.setStatus("审核中");
        } else {
            return;
        }
        shippingInfoMapper.updateById(shippingInfo);
    }
    // ç”Ÿæˆé‡‡è´­è´¨æ£€å•,并按产品质检标准初始化质检参数。
    private void addQualityInspect(PurchaseLedger purchaseLedger, SalesLedgerProduct saleProduct) {
        QualityInspect qualityInspect = new QualityInspect();
        qualityInspect.setInspectType(0);
        qualityInspect.setSupplier(purchaseLedger.getSupplierName());
        qualityInspect.setPurchaseLedgerId(purchaseLedger.getId());
        qualityInspect.setProductId(saleProduct.getProductId());
        qualityInspect.setProductName(saleProduct.getProductCategory());
        qualityInspect.setModel(saleProduct.getSpecificationModel());
        qualityInspect.setProductModelId(saleProduct.getProductModelId());
        qualityInspect.setUnit(saleProduct.getUnit());
        qualityInspect.setQuantity(saleProduct.getQuantity());
        qualityInspectMapper.insert(qualityInspect);
        List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(saleProduct.getProductId(), 0, null);
        if (qualityTestStandard.size() > 0) {
            qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
            qualityInspectMapper.updateById(qualityInspect);
            qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
                            .eq(QualityTestStandardParam::getTestStandardId, qualityTestStandard.get(0).getId()))
                    .forEach(qualityTestStandardParam -> {
                        QualityInspectParam param = new QualityInspectParam();
                        com.ruoyi.common.utils.bean.BeanUtils.copyProperties(qualityTestStandardParam, param);
                        param.setId(null);
                        param.setInspectId(qualityInspect.getId());
                        qualityInspectParamMapper.insert(param);
                    });
        }
    }
}
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java
@@ -1,7 +1,6 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -14,26 +13,12 @@
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.project.system.service.ISysNoticeService;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityInspectParamMapper;
import com.ruoyi.quality.mapper.QualityTestStandardMapper;
import com.ruoyi.quality.mapper.QualityTestStandardParamMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
import com.ruoyi.sales.mapper.*;
import com.ruoyi.sales.pojo.*;
import com.ruoyi.sales.mapper.CommonFileMapper;
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.service.impl.CommonFileServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -55,18 +40,8 @@
    private final SysUserMapper sysUserMapper;
    private final ISysNoticeService sysNoticeService;
    private final CommonFileMapper fileMapper;
    private final DeviceRepairMapper deviceRepairMapper;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesQuotationMapper salesQuotationMapper;
    private final ShippingInfoMapper shippingInfoMapper;
    private final ShippingProductDetailMapper shippingProductDetailMapper;
    private final CommonFileServiceImpl commonFileService;
    private final StockUtils stockUtils;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final QualityTestStandardMapper qualityTestStandardMapper;
    private final QualityTestStandardParamMapper qualityTestStandardParamMapper;
    private final QualityInspectParamMapper qualityInspectParamMapper;
    private final ApproveBusinessStatusService approveBusinessStatusService;
    private final FileUtil fileUtil;
@@ -162,74 +137,7 @@
        }
        approveProcessMapper.updateById(approveProcess);
        //采购审核
        if (approveProcess.getApproveType().equals(5)) {
            PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectOne(new LambdaQueryWrapper<PurchaseLedger>()
                    .eq(PurchaseLedger::getPurchaseContractNumber, approveProcess.getApproveReason())
                    .last("limit 1"));
            if (purchaseLedger != null) {
                if (status.equals(2)) {
                    // åŒæ„
                    purchaseLedger.setApprovalStatus(3);
                    List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(new QueryWrapper<SalesLedgerProduct>()
                            .lambda().eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedger.getId()).eq(SalesLedgerProduct::getType, 2));
                    for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) {
                        // è´¨æ£€
                        if (salesLedgerProduct.getIsChecked()) {
                            addQualityInspect(purchaseLedger, salesLedgerProduct);
                        } else {
                            //直接入库
                            stockUtils.addStockWithBatchNo(salesLedgerProduct.getProductModelId(), salesLedgerProduct.getQuantity(), StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(), purchaseLedger.getId(), purchaseLedger.getPurchaseContractNumber() + "-" + salesLedgerProduct.getId());
                        }
                    }
                } else if (status.equals(3)) {
                    // æ‹’绝
                    purchaseLedger.setApprovalStatus(4);
                } else if (status.equals(1)) {
                    // å®¡æ ¸ä¸­
                    purchaseLedger.setApprovalStatus(2);
                }
                purchaseLedgerMapper.updateById(purchaseLedger);
            }
        }
        // é”€å”®æŠ¥ä»·çŠ¶æ€ä¿®æ”¹
        if (approveProcess.getApproveType().equals(6)) {
            SalesQuotation salesQuote = salesQuotationMapper.selectOne(new LambdaQueryWrapper<SalesQuotation>()
                    .eq(SalesQuotation::getQuotationNo, approveProcess.getApproveReason())
                    .last("limit 1"));
            // åŒæ„
            if (status.equals(2) && salesQuote != null) {
                salesQuote.setStatus("通过");
            } else if (status.equals(3) && salesQuote != null) {
                salesQuote.setStatus("拒绝");
            } else if (status.equals(1) && salesQuote != null) {
                salesQuote.setStatus("审核中");
            }
            salesQuotationMapper.updateById(salesQuote);
        }
        // å‡ºåº“审批修改=发货审批
        if (approveProcess.getApproveType().equals(7)) {
            ShippingInfo shippingInfo = shippingInfoMapper.selectOne(new LambdaQueryWrapper<ShippingInfo>()
                    .eq(ShippingInfo::getShippingNo, approveProcess.getApproveReason())
                    .orderByDesc(ShippingInfo::getCreateTime)
                    .last("limit 1"));
            if (shippingInfo != null) {
                if (status.equals(2)) {
                    shippingInfo.setStatus("审核通过");
                    shippingInfo.setShippingDate(new Date());
                    //更改出库审核状态(待确认改成待审核)
                    stockUtils.shipmentStatus(StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), shippingInfo.getId());
                } else if (status.equals(3)) {
                    //删除原本(待确认)的出库审核状态
                    stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
                    shippingInfo.setStatus("审核拒绝");
                } else if (status.equals(1)) {
                    shippingInfo.setStatus("审核中");
                }
                shippingInfoMapper.updateById(shippingInfo);
            }
        }
        approveBusinessStatusService.syncBusinessStatus(approveProcess.getApproveType(), approveProcess.getApproveReason(), status);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVE_NODE, approveNode.getId(), approveNode.getStorageBlobDTOS());
    }
@@ -305,34 +213,6 @@
                return "办公用品审批";
        }
        return null;
    }
    private void addQualityInspect(PurchaseLedger purchaseLedger, SalesLedgerProduct saleProduct) {
        QualityInspect qualityInspect = new QualityInspect();
        qualityInspect.setInspectType(0);
        qualityInspect.setSupplier(purchaseLedger.getSupplierName());
        qualityInspect.setPurchaseLedgerId(purchaseLedger.getId());
        qualityInspect.setProductId(saleProduct.getProductId());
        qualityInspect.setProductName(saleProduct.getProductCategory());
        qualityInspect.setModel(saleProduct.getSpecificationModel());
        qualityInspect.setProductModelId(saleProduct.getProductModelId());
        qualityInspect.setUnit(saleProduct.getUnit());
        qualityInspect.setQuantity(saleProduct.getQuantity());
        qualityInspectMapper.insert(qualityInspect);
        List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(saleProduct.getProductId(), 0, null);
        if (qualityTestStandard.size() > 0) {
            qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
            qualityInspectMapper.updateById(qualityInspect);
            qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
                            .eq(QualityTestStandardParam::getTestStandardId, qualityTestStandard.get(0).getId()))
                    .forEach(qualityTestStandardParam -> {
                        QualityInspectParam param = new QualityInspectParam();
                        com.ruoyi.common.utils.bean.BeanUtils.copyProperties(qualityTestStandardParam, param);
                        param.setId(null);
                        param.setInspectId(qualityInspect.getId());
                        qualityInspectParamMapper.insert(param);
                    });
        }
    }
}
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
@@ -1,8 +1,6 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -25,10 +23,8 @@
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysNotice;
import com.ruoyi.project.system.domain.SysUser;
@@ -38,10 +34,8 @@
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.sales.mapper.CommonFileMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.service.impl.CommonFileServiceImpl;
import lombok.RequiredArgsConstructor;
@@ -53,15 +47,12 @@
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ApproveProcessServiceImpl extends ServiceImpl<ApproveProcessMapper, ApproveProcess> implements IApproveProcessService {
    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
    private final SysDeptMapper sysDeptMapper;
    private final IApproveNodeService approveNodeService;
    private final SysUserMapper sysUserMapper;
@@ -70,11 +61,10 @@
    private final CommonFileServiceImpl commonFileService;
    private final ISysNoticeService sysNoticeService;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final StockUtils stockUtils;
    private final ShippingInfoMapper shippingInfoMapper;
    private final ApproveNodeMapper approveNodeMapper;
    private final ApproveProcessConfigNodeService approveProcessConfigNodeService;
    private final ApproveBusinessStatusService approveBusinessStatusService;
    private final FileUtil fileUtil;
    private final ApproveProcessConfigNodeMapper approveProcessConfigNodeMapper;
@@ -82,63 +72,34 @@
    public void addApprove(ApproveProcessVO approveProcessVO) throws Exception {
        SysUser sysUser = SecurityUtils.getLoginUser().getUser();
        SysDept sysDept = sysDeptMapper.selectDeptById(SecurityUtils.getLoginUser().getCurrentDeptId());
        List<ApproveProcessConfigNodeVo> list = approveProcessConfigNodeService.listNode(approveProcessVO.getApproveType());
        if (sysDept == null) throw new RuntimeException("部门不存在");
        if (sysUser == null) throw new RuntimeException("申请人不存在");
        List<ApproveProcessConfigNodeVo> list = Optional.ofNullable(approveProcessConfigNodeService.listNode(approveProcessVO.getApproveType()))
                .orElse(Collections.emptyList());
        List<Long> nodeIds = list.stream()
                .map(ApproveProcessConfigNodeVo::getApproverId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        // æ— å®¡æ ¸äººé€»è¾‘添加
        // å®¡æ‰¹é…ç½®æ²¡æœ‰æœ‰æ•ˆå®¡æ ¸äººæ—¶ï¼Œä¸æ–°å¢žååŒå®¡æ‰¹æµç¨‹ï¼Œç›´æŽ¥æ‰§è¡Œä¸šåŠ¡å®¡æ ¸é€šè¿‡é€»è¾‘ã€‚
        if (CollectionUtils.isEmpty(nodeIds)) {
            autoPassPurchaseApproveIfNoApprover(approveProcessVO); // é‡‡è´­å•无审核人逻辑
            approveBusinessStatusService.syncBusinessStatus(approveProcessVO.getApproveType(), approveProcessVO.getApproveReason(), 2);
            return;
        }
        List<SysUser> sysUsers = sysUserMapper.selectUserByIds(nodeIds);
        if (CollectionUtils.isEmpty(sysUsers)) throw new RuntimeException("审核用户不存在");
        if (sysDept == null) throw new RuntimeException("部门不存在");
        if (sysUser == null) throw new RuntimeException("申请人不存在");
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        ApproveProcess approveProcess = new ApproveProcess();
        String no = OrderUtils.countTodayByCreateTime(approveProcessMapper, "", "approve_id");
        approveProcess.setApproveId(no);
        approveProcess.setApproveUser(sysUser.getUserId());
        approveProcess.setApproveUserName(sysUser.getNickName());
        approveProcess.setApproveDeptId(sysDept.getDeptId());
        approveProcess.setApproveUserIds(nodeIds.stream().map(String::valueOf).collect(Collectors.joining(",")));
        approveProcess.setApproveDeptName(sysDept.getDeptName());
        approveProcess.setApproveUserNames(sysUsers.stream().map(SysUser::getNickName).collect(Collectors.joining(",")));
        approveProcess.setApproveTime(StringUtils.isEmpty(approveProcessVO.getApproveTime()) ? new Date() : dateFormat.parse(approveProcessVO.getApproveTime()));
        approveProcess.setApproveReason(approveProcessVO.getApproveReason());
        approveProcess.setDeviceRepairId(approveProcessVO.getDeviceRepairId());
        approveProcess.setMaintenancePrice(approveProcessVO.getMaintenancePrice());
        approveProcess.setPrice(approveProcessVO.getPrice());
        approveProcess.setStartDate(approveProcessVO.getStartDate());
        approveProcess.setEndDate(approveProcessVO.getEndDate());
        approveProcess.setStartDateTime(approveProcessVO.getStartDateTime());
        approveProcess.setEndDateTime(approveProcessVO.getEndDateTime());
        approveProcess.setApproveStatus(0);
        approveProcess.setApproveDelete(0);
        approveProcess.setApproveType(approveProcessVO.getApproveType());
        approveProcess.setCreateTime(LocalDateTime.now());
        approveProcess.setTenantId(approveProcessVO.getApproveDeptId());
        approveProcess.setApproveUserCurrentId(nodeIds.get(0));
        approveProcess.setApproveUserCurrentName(sysUsers
                .stream()
                .filter(SysUser -> SysUser.getUserId().equals(nodeIds.get(0)))
                .collect(Collectors.toList())
                .get(0)
                .getNickName());
        // è®¾ç½®çŠ¶æ€ä¸ºé‡æ–°æäº¤
        if (approveProcessVO.getId() != null) {
            ApproveProcess approveProcess1 = approveProcessMapper.selectById(approveProcessVO.getId());
            approveProcess1.setApproveStatus(4);
            approveProcessMapper.updateById(approveProcess1);
        }
        // æœ‰å®¡æ ¸äººæ—¶ï¼ŒæŒ‰æ­£å¸¸ååŒå®¡æ‰¹æµç¨‹åˆ›å»ºå®¡æ‰¹ä¸»è¡¨ã€å®¡æ‰¹èŠ‚ç‚¹å¹¶é€šçŸ¥é¦–ä¸ªå®¡æ ¸äººã€‚
        ApproveProcess approveProcess = buildApproveProcess(approveProcessVO, sysUser, sysDept, nodeIds, sysUsers, 0);
        markResubmitted(approveProcessVO);
        save(approveProcess);
        //初始化审批节点
        String nodeIdStr = nodeIds.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
        approveNodeService.initApproveNodes(nodeIdStr, no, approveProcessVO.getApproveDeptId());
        approveNodeService.initApproveNodes(nodeIdStr, approveProcess.getApproveId(), approveProcessVO.getApproveDeptId());
        // é™„件绑定
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVE_PROCESS, approveProcess.getId(), approveProcessVO.getStorageBlobDTOS());
        /*消息通知*/
@@ -156,24 +117,56 @@
        }
    }
    private void autoPassPurchaseApproveIfNoApprover(ApproveProcessVO approveProcessVO) {
        if (!Objects.equals(approveProcessVO.getApproveType(), 5)
                || !StringUtils.hasText(approveProcessVO.getApproveReason())) {
            throw new RuntimeException("审核用户不存在");
    private ApproveProcess buildApproveProcess(ApproveProcessVO approveProcessVO, SysUser sysUser, SysDept sysDept,
                                               List<Long> nodeIds, List<SysUser> sysUsers, Integer approveStatus) throws Exception {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        ApproveProcess approveProcess = new ApproveProcess();
        String no = OrderUtils.countTodayByCreateTime(approveProcessMapper, "", "approve_id", approveProcess.getCreateTime() != null ? approveProcess.getCreateTime() : LocalDateTime.now());
        approveProcess.setApproveId(no);
        approveProcess.setApproveUser(sysUser.getUserId());
        approveProcess.setApproveUserName(sysUser.getNickName());
        approveProcess.setApproveDeptId(sysDept.getDeptId());
        approveProcess.setApproveDeptName(sysDept.getDeptName());
        approveProcess.setApproveUserIds(nodeIds.stream().map(String::valueOf).collect(Collectors.joining(",")));
        approveProcess.setApproveUserNames(sysUsers.stream().map(SysUser::getNickName).collect(Collectors.joining(",")));
        approveProcess.setApproveTime(StringUtils.isEmpty(approveProcessVO.getApproveTime()) ? new Date() : dateFormat.parse(approveProcessVO.getApproveTime()));
        approveProcess.setApproveReason(approveProcessVO.getApproveReason());
        approveProcess.setDeviceRepairId(approveProcessVO.getDeviceRepairId());
        approveProcess.setMaintenancePrice(approveProcessVO.getMaintenancePrice());
        approveProcess.setPrice(approveProcessVO.getPrice());
        approveProcess.setStartDate(approveProcessVO.getStartDate());
        approveProcess.setEndDate(approveProcessVO.getEndDate());
        approveProcess.setStartDateTime(approveProcessVO.getStartDateTime());
        approveProcess.setEndDateTime(approveProcessVO.getEndDateTime());
        approveProcess.setApproveStatus(approveStatus);
        approveProcess.setApproveDelete(0);
        approveProcess.setApproveType(approveProcessVO.getApproveType());
        approveProcess.setCreateTime(LocalDateTime.now());
        approveProcess.setTenantId(approveProcessVO.getApproveDeptId());
        if (!CollectionUtils.isEmpty(nodeIds)) {
            SysUser currentUser = sysUsers.stream()
                    .filter(user -> user.getUserId().equals(nodeIds.get(0)))
                    .findFirst()
                    .orElseThrow(() -> new RuntimeException("审核用户不存在"));
            approveProcess.setApproveUserCurrentId(currentUser.getUserId());
            approveProcess.setApproveUserCurrentName(currentUser.getNickName());
        }
        if (approveStatus.equals(2) || approveStatus.equals(3) || approveStatus.equals(4)) {
            approveProcess.setApproveOverTime(new Date());
        }
        return approveProcess;
        }
        purchaseLedgerMapper.update(null, new LambdaUpdateWrapper<PurchaseLedger>()
                .eq(PurchaseLedger::getPurchaseContractNumber, approveProcessVO.getApproveReason())
                .set(PurchaseLedger::getApprovalStatus, 3));
        //采购入库
        PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectOne(new LambdaQueryWrapper<PurchaseLedger>()
                .eq(PurchaseLedger::getPurchaseContractNumber, approveProcessVO.getApproveReason())
                .last("limit 1"));
        List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(new QueryWrapper<SalesLedgerProduct>()
                .lambda().eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedger.getId()).eq(SalesLedgerProduct::getType, 2));
        for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) {
            stockUtils.addStockWithBatchNo(salesLedgerProduct.getProductModelId(), salesLedgerProduct.getQuantity(), StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(), purchaseLedger.getId(),purchaseLedger.getPurchaseContractNumber()+"-"+salesLedgerProduct.getId());
    private void markResubmitted(ApproveProcessVO approveProcessVO) {
        if (approveProcessVO.getId() == null) {
            return;
        }
        ApproveProcess approveProcess = approveProcessMapper.selectById(approveProcessVO.getId());
        if (approveProcess == null) {
            return;
        }
        approveProcess.setApproveStatus(4);
        approveProcessMapper.updateById(approveProcess);
    }
    @Override
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementDetailServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import com.ruoyi.approve.mapper.FinReimbursementDetailMapper;
import com.ruoyi.approve.service.FinReimbursementDetailService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * æŠ¥é”€å•明细表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@Service
public class FinReimbursementDetailServiceImpl extends ServiceImpl<FinReimbursementDetailMapper, FinReimbursementDetail> implements FinReimbursementDetailService {
}
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,544 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeApproverDto;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeDto;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.mapper.FinReimbursementDetailMapper;
import com.ruoyi.approve.mapper.FinReimbursementMapper;
import com.ruoyi.approve.mapper.FinReimbursementTravelMapper;
import com.ruoyi.approve.pojo.*;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.approve.service.*;
import com.ruoyi.common.enums.TypeEnums;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.project.system.service.ISysNoticeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
 * <p>
 * æŠ¥é”€å•主表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@Service
@RequiredArgsConstructor
public class FinReimbursementServiceImpl extends ServiceImpl<FinReimbursementMapper, FinReimbursement> implements FinReimbursementService {
    private static final String BILL_STATUS_DRAFT = "DRAFT";
    private static final String BILL_STATUS_IN_APPROVAL = "IN_APPROVAL";
    private static final String NODE_STATUS_WAITING = "WAITING";
    private final ApprovalInstanceMapper approvalInstanceMapper;
    private final ApprovalInstanceService approvalInstanceService;
    private final ApprovalInstanceNodeService approvalInstanceNodeService;
    private final ApprovalTaskService approvalTaskService;
    private final ApprovalRecordService approvalRecordService;
    private final FinReimbursementMapper finReimbursementMapper;
    private final FinReimbursementTravelMapper finReimbursementTravelMapper;
    private final FinReimbursementDetailMapper finReimbursementDetailMapper;
    private final FileUtil fileUtil;
    private final ISysNoticeService sysNoticeService;
    @Override
    public IPage<FinReimbursementVo> listPage(FinReimbursementDto finReimbursementDto, Page<FinReimbursementVo> page) {
        IPage<FinReimbursementVo> finReimbursementVoIPage = finReimbursementMapper.listPage(finReimbursementDto, page);
        finReimbursementVoIPage.getRecords().forEach(vo -> {
            vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.FIN_REIMBURSEMENT,  vo.getId()));
        });
        return finReimbursementVoIPage;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean add(FinReimbursementDto finReimbursementDto) {
        String billStatus = validateAddParam(finReimbursementDto);
        // ç”ŸæˆæŠ¥é”€å•号
        String billNo = OrderUtils.countTodayByCreateTime(finReimbursementMapper, "BXD", "bill_no", finReimbursementDto.getCreateTime() != null ? finReimbursementDto.getCreateTime() : LocalDateTime.now());
        List<FinReimbursementDetail> details = finReimbursementDto.getDetails();
        BigDecimal totalAmount = details.stream()
                .map(FinReimbursementDetail::getAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        FinReimbursement reimbursement = buildReimbursement(finReimbursementDto, billNo, totalAmount, billStatus);
        // ä¿å­˜æŠ¥é”€å•主表
        boolean saved = this.save(reimbursement);
        if (!saved || reimbursement.getId() == null) {
            throw new ServiceException("新增报销单失败");
        }
        Long reimbursementId = reimbursement.getId();
        // ä¿å­˜å·®æ—…报销扩展信息(报销类型为差旅报销时)
        FinReimbursementTravel travel = finReimbursementDto.getTravel();
        if (isTravelReimbursement(finReimbursementDto.getReimbursementType())) {
            travel.setReimbursementId(reimbursementId);
            int travelRows = finReimbursementTravelMapper.insert(travel);
            if (travelRows != 1) {
                throw new ServiceException("新增差旅报销扩展信息失败");
            }
        }
        // ä¿å­˜æŠ¥é”€å•明细
        for (int i = 0; i < details.size(); i++) {
            FinReimbursementDetail detail = details.get(i);
            detail.setId(null);
            detail.setReimbursementId(reimbursementId);
            detail.setRowNo(i + 1);
            int detailRows = finReimbursementDetailMapper.insert(detail);
            if (detailRows != 1) {
                throw new ServiceException("新增报销单明细失败");
            }
        }
        if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
            startApproval(reimbursement, finReimbursementDto);
        }
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, reimbursementId, finReimbursementDto.getStorageBlobDTOs());
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean update(FinReimbursementDto finReimbursementDto) {
        String billStatus = validateUpdateParam(finReimbursementDto);
        Long reimbursementId = finReimbursementDto.getId();
        FinReimbursement existing = finReimbursementMapper.selectById(reimbursementId);
        if (existing == null) {
            throw new ServiceException("报销单不存在");
        }
        // è®¡ç®—明细汇总金额
        List<FinReimbursementDetail> details = finReimbursementDto.getDetails();
        BigDecimal totalAmount = details.stream()
                .map(FinReimbursementDetail::getAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // æ›´æ–°ä¸»è¡¨
        FinReimbursement reimbursement = buildReimbursement(
                finReimbursementDto,
                existing.getBillNo(),
                totalAmount,
                billStatus
        );
        reimbursement.setId(reimbursementId);
        int mainRows = finReimbursementMapper.updateById(reimbursement);
        if (mainRows != 1) {
            throw new ServiceException("更新报销单主表失败");
        }
        // æŸ¥è¯¢æ•°æ®åº“中已有的明细
        List<FinReimbursementDetail> existingDetails = finReimbursementDetailMapper.selectList(
                new LambdaQueryWrapper<FinReimbursementDetail>()
                        .eq(FinReimbursementDetail::getReimbursementId, reimbursementId));
        Set<Long> existingDetailIds = existingDetails.stream()
                .map(FinReimbursementDetail::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        // æ–°æ˜Žç»†ä¸­æœ‰ID的 â†’ æ›´æ–°ï¼›æ— ID的 â†’ æ–°å¢ž
        Set<Long> submittedDetailIds = new HashSet<>();
        for (int i = 0; i < details.size(); i++) {
            FinReimbursementDetail detail = details.get(i);
            detail.setReimbursementId(reimbursementId);
            detail.setRowNo(i + 1);
            if (detail.getId() != null && existingDetailIds.contains(detail.getId())) {
                finReimbursementDetailMapper.updateById(detail);
                submittedDetailIds.add(detail.getId());
            } else {
                detail.setId(null);
                finReimbursementDetailMapper.insert(detail);
            }
        }
        // æ•°æ®åº“中已有但新明细中没有的 â†’ åˆ é™¤
        for (Long existingId : existingDetailIds) {
            if (!submittedDetailIds.contains(existingId)) {
                finReimbursementDetailMapper.deleteById(existingId);
            }
        }
        // å·®æ—…扩展:有则更新,无则新增
        FinReimbursementTravel existingTravel = finReimbursementTravelMapper.selectOne(
                new LambdaQueryWrapper<FinReimbursementTravel>()
                        .eq(FinReimbursementTravel::getReimbursementId, reimbursementId)
                        .last("LIMIT 1"));
        FinReimbursementTravel travel = finReimbursementDto.getTravel();
        if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && travel != null) {
            travel.setReimbursementId(reimbursementId);
            if (existingTravel != null) {
                travel.setId(existingTravel.getId());
                finReimbursementTravelMapper.updateById(travel);
            } else {
                travel.setId(null);
                finReimbursementTravelMapper.insert(travel);
            }
        }
        resetApprovalFlow(existing, reimbursementId);
        if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
            reimbursement.setApprovalInstanceId(null);
            startApproval(reimbursement, finReimbursementDto);
        }
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, reimbursementId, finReimbursementDto.getStorageBlobDTOs());
        return true;
    }
    @Override
    public FinReimbursementVo detail(Long id) {
        if (id == null ) {
            throw new ServiceException("报销单ID不能为空");
        }
        FinReimbursement reimbursement = finReimbursementMapper.selectById(id);
        if (reimbursement == null) {
            throw new ServiceException("报销单不存在");
        }
        FinReimbursementVo vo = new FinReimbursementVo();
        vo.setId(reimbursement.getId());
        vo.setBillNo(reimbursement.getBillNo());
        vo.setReimbursementType(reimbursement.getReimbursementType());
        vo.setExpenseType(reimbursement.getExpenseType());
        vo.setApplicantId(reimbursement.getApplicantId());
        vo.setApplicantCode(reimbursement.getApplicantCode());
        vo.setApplicantName(reimbursement.getApplicantName());
        vo.setApplicantDeptId(reimbursement.getApplicantDeptId());
        vo.setApplicantDeptName(reimbursement.getApplicantDeptName());
        vo.setReason(reimbursement.getReason());
        vo.setApplyAmount(reimbursement.getApplyAmount());
        vo.setDetailTotalAmount(reimbursement.getDetailTotalAmount());
        vo.setPayeeName(reimbursement.getPayeeName());
        vo.setPayeeAccount(reimbursement.getPayeeAccount());
        vo.setPayeeBank(reimbursement.getPayeeBank());
        vo.setApprovalInstanceId(reimbursement.getApprovalInstanceId());
        vo.setApproveProcessId(reimbursement.getApproveProcessId());
        vo.setBillStatus(reimbursement.getBillStatus());
        vo.setApprovedTime(reimbursement.getApprovedTime());
        vo.setPaidTime(reimbursement.getPaidTime());
        vo.setAccountExpenseId(reimbursement.getAccountExpenseId());
        vo.setRemark(reimbursement.getRemark());
        vo.setTenantId(reimbursement.getTenantId());
        vo.setCreateUser(reimbursement.getCreateUser());
        vo.setCreateTime(reimbursement.getCreateTime());
        vo.setUpdateUser(reimbursement.getUpdateUser());
        vo.setUpdateTime(reimbursement.getUpdateTime());
        vo.setDeptId(reimbursement.getDeptId());
        vo.setDeleted(reimbursement.getDeleted());
        vo.setDetails(finReimbursementDetailMapper.selectList(
                new LambdaQueryWrapper<FinReimbursementDetail>()
                        .eq(FinReimbursementDetail::getReimbursementId, reimbursement.getId())
                        .orderByAsc(FinReimbursementDetail::getRowNo)
        ));
        if (isTravelReimbursement(reimbursement.getReimbursementType())) {
            vo.setTravel(finReimbursementTravelMapper.selectOne(
                    new LambdaQueryWrapper<FinReimbursementTravel>()
                            .eq(FinReimbursementTravel::getReimbursementId, reimbursement.getId())
                            .last("LIMIT 1")
            ));
        }
        vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.FIN_REIMBURSEMENT, reimbursement.getId()));
        //审批记录返回
        vo.setTasks(approvalTaskService.list(new LambdaQueryWrapper<ApprovalTask>().eq(ApprovalTask::getInstanceId, reimbursement.getApprovalInstanceId())));
        vo.setRecords(approvalRecordService.list(new LambdaQueryWrapper<ApprovalRecord>().eq(ApprovalRecord::getInstanceId, reimbursement.getApprovalInstanceId())));
        return vo;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean delete(List<Long> ids) {
        fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, ids);
        //先删除明细
        finReimbursementDetailMapper.delete(new LambdaQueryWrapper<FinReimbursementDetail>().in(FinReimbursementDetail::getReimbursementId, ids));
        //删除差旅
        finReimbursementTravelMapper.delete(new LambdaQueryWrapper<FinReimbursementTravel>().in(FinReimbursementTravel::getReimbursementId, ids));
        //删除主表
        int rows = finReimbursementMapper.delete(new LambdaQueryWrapper<FinReimbursement>().in(FinReimbursement::getId, ids));
        return rows == ids.size();
    }
    private String validateUpdateParam(FinReimbursementDto finReimbursementDto) {
        if (finReimbursementDto == null || finReimbursementDto.getId() == null) {
            throw new ServiceException("报销单ID不能为空");
        }
        if (finReimbursementDto.getReimbursementType() == null) {
            throw new ServiceException("报销类型不能为空");
        }
        String billStatus = normalizeBillStatus(finReimbursementDto.getBillStatus());
        if (billStatus == null) {
            throw new ServiceException("单据状态只支持 DRAFT æˆ– IN_APPROVAL");
        }
        if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
            validateApprovalNodes(finReimbursementDto.getNodes());
        }
        List<FinReimbursementDetail> details = finReimbursementDto.getDetails();
        if (details == null || details.isEmpty()) {
            throw new ServiceException("报销单明细不能为空");
        }
        for (FinReimbursementDetail detail : details) {
            if (detail == null) {
                throw new ServiceException("报销单明细不能为空");
            }
            if (detail.getAmount() == null || detail.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
                throw new ServiceException("报销单明细金额必须大于0");
            }
        }
        if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() == null) {
            throw new ServiceException("差旅报销必须填写差旅扩展信息");
        }
        if (!isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() != null) {
            throw new ServiceException("非差旅报销不允许填写差旅扩展信息");
        }
        return billStatus;
    }
    private String validateAddParam(FinReimbursementDto finReimbursementDto) {
        if (finReimbursementDto == null) {
            throw new ServiceException("报销单数据不能为空");
        }
        if (finReimbursementDto.getReimbursementType() == null) {
            throw new ServiceException("报销类型不能为空");
        }
        String billStatus = normalizeBillStatus(finReimbursementDto.getBillStatus());
        if (billStatus == null) {
            throw new ServiceException("单据状态只支持 DRAFT æˆ– IN_APPROVAL");
        }
        if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
            validateApprovalNodes(finReimbursementDto.getNodes());
        }
        List<FinReimbursementDetail> details = finReimbursementDto.getDetails();
        if (details == null || details.isEmpty()) {
            throw new ServiceException("报销单明细不能为空");
        }
        for (FinReimbursementDetail detail : details) {
            if (detail == null) {
                throw new ServiceException("报销单明细不能为空");
            }
            if (detail.getAmount() == null || detail.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
                throw new ServiceException("报销单明细金额必须大于0");
            }
        }
        if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() == null) {
            throw new ServiceException("差旅报销必须填写差旅扩展信息");
        }
        if (!isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() != null) {
            throw new ServiceException("非差旅报销不允许填写差旅扩展信息");
        }
        return billStatus;
    }
    private FinReimbursement buildReimbursement(FinReimbursementDto finReimbursementDto, String billNo, BigDecimal totalAmount, String billStatus) {
        FinReimbursement reimbursement = new FinReimbursement();
        reimbursement.setId(null);
        reimbursement.setBillNo(billNo);
        reimbursement.setReimbursementType(finReimbursementDto.getReimbursementType());
        reimbursement.setExpenseType(finReimbursementDto.getExpenseType());
        reimbursement.setApplicantId(finReimbursementDto.getApplicantId());
        reimbursement.setApplicantCode(finReimbursementDto.getApplicantCode());
        reimbursement.setApplicantName(finReimbursementDto.getApplicantName());
        reimbursement.setApplicantDeptId(finReimbursementDto.getApplicantDeptId());
        reimbursement.setApplicantDeptName(finReimbursementDto.getApplicantDeptName());
        reimbursement.setReason(finReimbursementDto.getReason());
        reimbursement.setApplyAmount(finReimbursementDto.getApplyAmount());
        reimbursement.setDetailTotalAmount(totalAmount);
        reimbursement.setPayeeName(finReimbursementDto.getPayeeName());
        reimbursement.setPayeeAccount(finReimbursementDto.getPayeeAccount());
        reimbursement.setPayeeBank(finReimbursementDto.getPayeeBank());
        reimbursement.setRemark(finReimbursementDto.getRemark());
        reimbursement.setTenantId(finReimbursementDto.getTenantId());
        reimbursement.setApproveProcessId(null);
        reimbursement.setBillStatus(billStatus);
        return reimbursement;
    }
    private void startApproval(FinReimbursement reimbursement, FinReimbursementDto finReimbursementDto) {
        Long businessType = resolveBusinessType(finReimbursementDto.getReimbursementType());
        ApprovalInstanceDto approvalInstanceDto = new ApprovalInstanceDto();
        approvalInstanceDto.setInstanceNo(OrderUtils.countTodayByCreateTime(approvalInstanceMapper, "SP", "instance_no", approvalInstanceDto.getCreateTime() != null ? approvalInstanceDto.getCreateTime() : LocalDateTime.now()));
        approvalInstanceDto.setBusinessId(reimbursement.getId());
        approvalInstanceDto.setTemplateId(null);
        approvalInstanceDto.setTemplateName(TypeEnums.getLabelByValue(businessType) + "审批");
        approvalInstanceDto.setBusinessType(businessType);
        approvalInstanceDto.setTitle("报销单号:" + reimbursement.getBillNo());
        approvalInstanceDto.setApplicantId(reimbursement.getApplicantId() != null ? reimbursement.getApplicantId() : SecurityUtils.getUserId());
        approvalInstanceDto.setApplicantName(reimbursement.getApplicantName() != null ? reimbursement.getApplicantName() : SecurityUtils.getLoginUser().getNickName());
        approvalInstanceDto.setApplyTime(LocalDateTime.now());
        approvalInstanceDto.setStatus("PENDING");
        approvalInstanceDto.setCurrentLevel(1);
        boolean approvalSaved = approvalInstanceService.save(approvalInstanceDto);
        if (!approvalSaved || approvalInstanceDto.getId() == null) {
            throw new ServiceException("发起审批失败");
        }
        List<ApprovalTask> firstTasks = createApprovalNodes(approvalInstanceDto, finReimbursementDto.getNodes());
        sendApproveNotice(approvalInstanceDto, firstTasks);
        FinReimbursement update = new FinReimbursement();
        update.setId(reimbursement.getId());
        update.setApprovalInstanceId(approvalInstanceDto.getId());
        update.setBillStatus(BILL_STATUS_IN_APPROVAL);
        int rows = finReimbursementMapper.updateById(update);
        if (rows != 1) {
            throw new ServiceException("回填审批实例失败");
        }
    }
    private List<ApprovalTask> createApprovalNodes(ApprovalInstanceDto approvalInstanceDto, List<ApprovalTemplateNodeDto> nodes) {
        List<ApprovalTask> firstTasks = Collections.emptyList();
        for (int i = 0; i < nodes.size(); i++) {
            ApprovalTemplateNodeDto nodeDto = nodes.get(i);
            ApprovalInstanceNode instanceNode = new ApprovalInstanceNode();
            instanceNode.setInstanceId(approvalInstanceDto.getId());
            instanceNode.setLevelNo(nodeDto.getLevelNo());
            instanceNode.setApproveType(nodeDto.getApproveType());
            instanceNode.setStatus(i == 0 ? "PENDING" : NODE_STATUS_WAITING);
            instanceNode.setStartTime(i == 0 ? LocalDateTime.now() : null);
            instanceNode.setDeleted((byte) 0);
            approvalInstanceNodeService.save(instanceNode);
            List<ApprovalTask> tasks = nodeDto.getApprovers().stream().map(approver -> {
                ApprovalTask task = new ApprovalTask();
                task.setInstanceId(approvalInstanceDto.getId());
                task.setNodeId(instanceNode.getId());
                task.setLevelNo(instanceNode.getLevelNo());
                task.setApproverId(approver.getApproverId());
                task.setApproverName(approver.getApproverName());
                task.setTaskStatus("PENDING");
                task.setIsRead((byte) 0);
                task.setDeleted((byte) 0);
                return task;
            }).collect(Collectors.toList());
            approvalTaskService.saveBatch(tasks);
            if (i == 0) {
                firstTasks = tasks;
                ApprovalRecord record = new ApprovalRecord();
                record.setInstanceId(approvalInstanceDto.getId());
                record.setNodeId(instanceNode.getId());
                record.setOperatorId(approvalInstanceDto.getApplicantId());
                record.setOperatorName(approvalInstanceDto.getApplicantName());
                record.setAction("SUBMIT");
                record.setComment("发起审批");
                record.setDeleted((byte) 0);
                approvalRecordService.save(record);
            }
        }
        return firstTasks;
    }
    private void validateApprovalNodes(List<ApprovalTemplateNodeDto> nodes) {
        if (nodes == null || nodes.isEmpty()) {
            throw new ServiceException("提交审批时审批节点不能为空");
        }
        for (int i = 0; i < nodes.size(); i++) {
            ApprovalTemplateNodeDto node = nodes.get(i);
            if (node == null) {
                throw new ServiceException("审批节点不能为空");
            }
            if (node.getLevelNo() == null) {
                node.setLevelNo(i + 1);
            }
            if (!StringUtils.hasText(node.getApproveType())) {
                throw new ServiceException("审批节点审批方式不能为空");
            }
            List<ApprovalTemplateNodeApproverDto> approvers = node.getApprovers();
            if (approvers == null || approvers.isEmpty()) {
                throw new ServiceException("审批节点审批人不能为空");
            }
            for (ApprovalTemplateNodeApproverDto approver : approvers) {
                if (approver == null || approver.getApproverId() == null) {
                    throw new ServiceException("审批人不能为空");
                }
            }
        }
    }
    private void sendApproveNotice(ApprovalInstanceDto instance, List<ApprovalTask> tasks) {
        if (instance == null || tasks == null || tasks.isEmpty()) {
            return;
        }
        List<Long> approverIds = tasks.stream()
                .map(ApprovalTask::getApproverId)
                .filter(id -> id != null && id > 0)
                .distinct()
                .collect(Collectors.toList());
        if (approverIds.isEmpty()) {
            return;
        }
        String title = "报销审批";
        String message = "审批单号 " + instance.getInstanceNo() + " éœ€è¦æ‚¨å®¡æ‰¹";
        String jumpPath = "/approvalInstance?id=" + instance.getId();
        sysNoticeService.simpleNoticeByUser(title, message, approverIds, jumpPath);
    }
    private void resetApprovalFlow(FinReimbursement existing, Long reimbursementId) {
        if (existing == null || existing.getApprovalInstanceId() == null) {
            return;
        }
        Long approvalInstanceId = existing.getApprovalInstanceId();
        if (!"REJECTED".equals(existing.getBillStatus())) {
            approvalInstanceService.delete(Collections.singletonList(approvalInstanceId));
        }
        clearApprovalBinding(reimbursementId);
    }
    private void clearApprovalBinding(Long reimbursementId) {
        int rows = finReimbursementMapper.update(
                null,
                Wrappers.<FinReimbursement>lambdaUpdate()
                        .eq(FinReimbursement::getId, reimbursementId)
                        .set(FinReimbursement::getApprovalInstanceId, null)
        );
        if (rows != 1) {
            throw new ServiceException("重置审批流程失败");
        }
    }
    private Long resolveBusinessType(Byte reimbursementType) {
        return isTravelReimbursement(reimbursementType)
                ? TypeEnums.TRAVEL_REIMBURSEMENT_APPROVAL.getCode()
                : TypeEnums.EXPENSE_APPROVAL.getCode();
    }
    private String normalizeBillStatus(String billStatus) {
        if (billStatus == null) {
            return BILL_STATUS_DRAFT;
        }
        String normalized = billStatus.trim().toUpperCase();
        if (BILL_STATUS_DRAFT.equals(normalized) || BILL_STATUS_IN_APPROVAL.equals(normalized)) {
            return normalized;
        }
        return null;
    }
    private boolean isTravelReimbursement(Byte reimbursementType) {
        return Byte.valueOf((byte) 1).equals(reimbursementType);
    }
}
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementTravelServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import com.ruoyi.approve.mapper.FinReimbursementTravelMapper;
import com.ruoyi.approve.service.FinReimbursementTravelService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * å·®æ—…报销扩展表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@Service
public class FinReimbursementTravelServiceImpl extends ServiceImpl<FinReimbursementTravelMapper, FinReimbursementTravel> implements FinReimbursementTravelService {
}
src/main/java/com/ruoyi/approve/utils/ApproveProcessConfigNodeUtils.java
@@ -1,9 +1,370 @@
package com.ruoyi.approve.utils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalInstanceNode;
import com.ruoyi.approve.pojo.ApprovalRecord;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import com.ruoyi.approve.service.ApprovalInstanceNodeService;
import com.ruoyi.approve.service.ApprovalRecordService;
import com.ruoyi.approve.service.ApprovalTaskService;
import com.ruoyi.approve.service.ApprovalTemplateNodeApproverService;
import com.ruoyi.approve.service.ApprovalTemplateNodeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
 * å®¡æ‰¹æµç¨‹èŠ‚ç‚¹å·¥å…·ç±»
 */
@Component
@RequiredArgsConstructor
public class ApproveProcessConfigNodeUtils {
    private final ApprovalInstanceNodeService instanceNodeService;
    private final ApprovalTaskService approvalTaskService;
    private final ApprovalRecordService approvalRecordService;
    private final ApprovalTemplateNodeService approvalTemplateNodeService;
    private final ApprovalTemplateNodeApproverService approvalTemplateNodeApproverService;
    /**
     * æŒ‰å½“前层级创建审批节点和审批任务。
     * è¯¥é‡è½½ä¼šåŒæ—¶å†™å…¥ä¸€æ¡å‘起审批记录。
     */
    @Transactional(rollbackFor = Exception.class)
    public ApprovalInstanceNode createCurrentNodeAndTasks(ApprovalInstance instance) {
        return createCurrentNodeAndTasks(instance, true);
    }
    /**
     * æŒ‰å½“前层级创建审批节点和审批任务。
     *
     * @param instance å®¡æ‰¹å®žä¾‹
     * @param createSubmitRecord æ˜¯å¦åˆ›å»ºå‘起审批记录
     * @return åˆ›å»ºå‡ºçš„当前节点实例
     */
    @Transactional(rollbackFor = Exception.class)
    public ApprovalInstanceNode createCurrentNodeAndTasks(ApprovalInstance instance, boolean createSubmitRecord) {
        if (instance == null || instance.getId() == null) {
            throw new RuntimeException("审批实例不能为空");
        }
        if (instance.getTemplateId() == null) {
            throw new RuntimeException("审批模板不能为空");
        }
        Integer currentLevel = instance.getCurrentLevel() == null ? 1 : instance.getCurrentLevel();
        ApprovalInstanceNode existsNode = instanceNodeService.getOne(
                new LambdaQueryWrapper<ApprovalInstanceNode>()
                        .eq(ApprovalInstanceNode::getInstanceId, instance.getId())
                        .eq(ApprovalInstanceNode::getLevelNo, currentLevel)
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .last("LIMIT 1")
        );
        if (existsNode != null) {
            return existsNode;
        }
        ApprovalTemplateNode templateNode = approvalTemplateNodeService.getOne(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .eq(ApprovalTemplateNode::getTemplateId, instance.getTemplateId())
                        .eq(ApprovalTemplateNode::getLevelNo, currentLevel)
                        .orderByAsc(ApprovalTemplateNode::getId)
                        .last("LIMIT 1")
        );
        if (templateNode == null) {
            throw new RuntimeException("未找到当前层级对应的审批模板节点");
        }
        List<ApprovalTemplateNodeApprover> approvers = approvalTemplateNodeApproverService.list(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .eq(ApprovalTemplateNodeApprover::getTemplateId, instance.getTemplateId())
                        .eq(ApprovalTemplateNodeApprover::getNodeId, templateNode.getId())
                        .eq(ApprovalTemplateNodeApprover::getDeleted, 0L)
                        .orderByAsc(ApprovalTemplateNodeApprover::getSortNo)
        );
        if (approvers == null || approvers.isEmpty()) {
            throw new RuntimeException("当前审批节点未配置审批人");
        }
        ApprovalInstanceNode instanceNode = new ApprovalInstanceNode();
        instanceNode.setInstanceId(instance.getId());
        instanceNode.setLevelNo(templateNode.getLevelNo());
        instanceNode.setApproveType(templateNode.getApproveType());
        instanceNode.setStatus("PENDING");
        instanceNode.setStartTime(LocalDateTime.now());
        instanceNode.setDeleted((byte) 0);
        instanceNodeService.save(instanceNode);
        List<ApprovalTask> taskList = new ArrayList<>(approvers.size());
        for (ApprovalTemplateNodeApprover approver : approvers) {
            ApprovalTask task = new ApprovalTask();
            task.setInstanceId(instance.getId());
            task.setNodeId(instanceNode.getId());
            task.setLevelNo(instanceNode.getLevelNo());
            task.setApproverId(approver.getApproverId());
            task.setApproverName(approver.getApproverName());
            task.setTaskStatus("PENDING");
            task.setIsRead((byte) 0);
            task.setDeleted((byte) 0);
            taskList.add(task);
        }
        approvalTaskService.saveBatch(taskList);
        if (createSubmitRecord) {
            ApprovalRecord record = new ApprovalRecord();
            record.setInstanceId(instance.getId());
            record.setNodeId(instanceNode.getId());
            record.setOperatorId(instance.getApplicantId());
            record.setOperatorName(instance.getApplicantName());
            record.setAction("SUBMIT");
            record.setComment("发起审批");
            record.setDeleted((byte) 0);
            approvalRecordService.save(record);
        }
        return instanceNode;
    }
    /**
     * æŸ¥è¯¢å½“前待处理节点。
     */
    public ApprovalInstanceNode getCurrentNode(Long instanceId) {
        if (instanceId == null) {
            return null;
        }
        return instanceNodeService.getOne(
                new LambdaQueryWrapper<ApprovalInstanceNode>()
                        .eq(ApprovalInstanceNode::getInstanceId, instanceId)
                        .eq(ApprovalInstanceNode::getStatus, "PENDING")
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .orderByAsc(ApprovalInstanceNode::getLevelNo)
                        .last("LIMIT 1")
        );
    }
    /**
     * æŸ¥è¯¢å½“前审批层级。
     */
    public Integer getCurrentLevel(Long instanceId) {
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        return currentNode != null ? currentNode.getLevelNo() : null;
    }
    /**
     * æŸ¥è¯¢å½“前节点下的待审批任务。
     */
    public List<ApprovalTask> getCurrentPendingTasks(Long instanceId) {
        if (instanceId == null) {
            return List.of();
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return List.of();
        }
        return approvalTaskService.list(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getInstanceId, instanceId)
                        .eq(ApprovalTask::getNodeId, currentNode.getId())
                        .eq(ApprovalTask::getTaskStatus, "PENDING")
                        .eq(ApprovalTask::getDeleted, 0)
                        .orderByAsc(ApprovalTask::getLevelNo)
        );
    }
    /**
     * æŸ¥è¯¢å½“前用户在当前节点上的待审批任务。
     */
    public ApprovalTask getCurrentUserTask(Long instanceId, Long userId) {
        if (instanceId == null || userId == null) {
            return null;
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return null;
        }
        return approvalTaskService.getOne(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getInstanceId, instanceId)
                        .eq(ApprovalTask::getNodeId, currentNode.getId())
                        .eq(ApprovalTask::getApproverId, userId)
                        .eq(ApprovalTask::getTaskStatus, "PENDING")
                        .eq(ApprovalTask::getDeleted, 0)
                        .last("LIMIT 1")
        );
    }
    /**
     * åˆ¤æ–­å½“前用户是否是当前审批人。
     */
    public boolean isCurrentApprover(Long instanceId, Long userId) {
        return getCurrentUserTask(instanceId, userId) != null;
    }
    /**
     * æŸ¥è¯¢å½“前节点的审批人 ID åˆ—表。
     */
    public List<Long> getCurrentNodeApproverIds(Long instanceId) {
        return getCurrentPendingTasks(instanceId).stream()
                .map(ApprovalTask::getApproverId)
                .distinct()
                .collect(Collectors.toList());
    }
    /**
     * æŸ¥è¯¢å½“前节点剩余待审批人数。
     */
    public int getRemainingApproverCount(Long instanceId) {
        return getCurrentPendingTasks(instanceId).size();
    }
    /**
     * æŸ¥è¯¢å½“前节点已同意人数。
     */
    public int getApprovedCount(Long instanceId) {
        if (instanceId == null) {
            return 0;
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return 0;
        }
        return Math.toIntExact(approvalTaskService.count(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getInstanceId, instanceId)
                        .eq(ApprovalTask::getNodeId, currentNode.getId())
                        .eq(ApprovalTask::getTaskStatus, "APPROVED")
                        .eq(ApprovalTask::getDeleted, 0)
        ));
    }
    /**
     * æŸ¥è¯¢å½“前节点已拒绝人数。
     */
    public int getRejectedCount(Long instanceId) {
        if (instanceId == null) {
            return 0;
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return 0;
        }
        return Math.toIntExact(approvalTaskService.count(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getInstanceId, instanceId)
                        .eq(ApprovalTask::getNodeId, currentNode.getId())
                        .eq(ApprovalTask::getTaskStatus, "REJECTED")
                        .eq(ApprovalTask::getDeleted, 0)
        ));
    }
    /**
     * åˆ¤æ–­å½“前节点是否可以流转到下一层。
     */
    public boolean canProceedToNextLevel(Long instanceId, String approveType) {
        if (instanceId == null || approveType == null) {
            return false;
        }
        if (getRejectedCount(instanceId) > 0) {
            return false;
        }
        int totalApproverCount = getCurrentPendingTasks(instanceId).size() + getApprovedCount(instanceId);
        int approvedCount = getApprovedCount(instanceId);
        if ("AND".equalsIgnoreCase(approveType)) {
            return approvedCount > 0 && approvedCount == totalApproverCount;
        }
        if ("OR".equalsIgnoreCase(approveType)) {
            return approvedCount > 0;
        }
        return false;
    }
    /**
     * æŸ¥è¯¢å½“前用户在当前节点上的任务状态。
     */
    public String getUserTaskStatus(Long instanceId, Long userId) {
        if (instanceId == null || userId == null) {
            return null;
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return null;
        }
        ApprovalTask task = approvalTaskService.getOne(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getInstanceId, instanceId)
                        .eq(ApprovalTask::getNodeId, currentNode.getId())
                        .eq(ApprovalTask::getApproverId, userId)
                        .eq(ApprovalTask::getDeleted, 0)
                        .last("LIMIT 1")
        );
        return task != null ? task.getTaskStatus() : null;
    }
    /**
     * æŸ¥è¯¢æŒ‡å®šç”¨æˆ·çš„全部待审批任务。
     */
    public List<ApprovalTask> getUserAllPendingTasks(Long userId) {
        if (userId == null) {
            return List.of();
        }
        return approvalTaskService.list(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getApproverId, userId)
                        .eq(ApprovalTask::getTaskStatus, "PENDING")
                        .eq(ApprovalTask::getDeleted, 0)
                        .orderByDesc(ApprovalTask::getCreateTime)
        );
    }
    /**
     * æŸ¥è¯¢å®¡æ‰¹å®žä¾‹çš„进度摘要。
     */
    public String getApprovalProgress(Long instanceId) {
        if (instanceId == null) {
            return "无效的审批实例";
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return "审批已完成或尚未开始";
        }
        int approvedCount = getApprovedCount(instanceId);
        int rejectedCount = getRejectedCount(instanceId);
        int pendingCount = getRemainingApproverCount(instanceId);
        int totalCount = approvedCount + rejectedCount + pendingCount;
        return String.format(
                "第%d级审批:总人数=%d,已同意=%d,已拒绝=%d,待审批=%d",
                currentNode.getLevelNo(),
                totalCount,
                approvedCount,
                rejectedCount,
                pendingCount
        );
    }
}
src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java
@@ -194,7 +194,12 @@
    // Account
    SALES_REFUND_AMOUNT_ORDER("sales_refund_amount_order"),
    SALES_RECEIPT_RETURN("sales_receipt_return"),
    ACCOUNT_EXPENSE("account_expense"),
    FIN_REIMBURSEMENT("fin_reimbursement"),
    FIN_VOUCHER("fin_voucher"),
    ACCOUNT_FILE("account_file"),
    ENTERPRISE_NEWS("enterprise_news"),
    APPROVAL_INSTANCE("approval_instance"),
    ACCOUNT_INVOICE_APPLICATION("account_invoice_application"),
    ACCOUNT_PURCHASE_INVOICE("account_purchase_invoice");
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java
@@ -21,8 +21,12 @@
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.procurementrecord.pojo.ReturnManagement;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesQuotationMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesQuotation;
import com.ruoyi.sales.vo.CustomerTransactionsDetailsVo;
import com.ruoyi.sales.vo.CustomerTransactionsVo;
import lombok.AllArgsConstructor;
@@ -51,6 +55,10 @@
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements ICustomerService {
    @Autowired
    private  SalesLedgerMapper salesLedgerMapper;
    @Autowired
    private SalesQuotationMapper salesQuotationMapper;
    @Autowired
    private ReturnManagementMapper returnManagementMapper;
    @Autowired
    private CustomerMapper customerMapper;
@@ -218,10 +226,24 @@
    @Transactional(rollbackFor = Exception.class)
    public int deleteCustomerByIds(Long[] ids) {
        List<Long> idList = Arrays.asList(ids);
        // æ£€æŸ¥æ˜¯å¦æœ‰é”€å”®å°è´¦å…³è”
        List<SalesLedger> salesLedgers = salesLedgerMapper.selectList(new QueryWrapper<SalesLedger>().lambda().in(SalesLedger::getCustomerId, idList));
        if (!salesLedgers.isEmpty()) {
            throw new RuntimeException("客户档案下有销售合同,请先删除销售合同");
            throw new RuntimeException("客户档案下有销售台账,请先删除销售台账");
        }
        // æ£€æŸ¥æ˜¯å¦æœ‰é”€å”®é€€è´§å…³è”
        List<ReturnManagement> returnManagements = returnManagementMapper.selectList(new QueryWrapper<ReturnManagement>().lambda().in(ReturnManagement::getCustomerId, idList));
        if (!returnManagements.isEmpty()) {
            throw new RuntimeException("客户档案下有销售退货,请先删除销售退货");
        }
        // æ£€æŸ¥æ˜¯å¦æœ‰é”€å”®æŠ¥ä»·å…³è”
        List<SalesQuotation> salesQuotations = salesQuotationMapper.selectList(new QueryWrapper<SalesQuotation>().lambda().in(SalesQuotation::getCustomerId, idList));
        if (!salesQuotations.isEmpty()) {
            throw new RuntimeException("客户档案下有销售报价,请先删除销售报价");
        }
        // æŸ¥è¯¢æ˜¯å¦æœ‰å·²åˆ†é…çš„公海客户
        List<Customer> assignedPools = customerMapper.selectList(
                new QueryWrapper<Customer>().lambda()
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.ruoyi.collaborativeApproval.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.collaborativeApproval.dto.EnterpriseNewsDto;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsService;
import com.ruoyi.collaborativeApproval.vo.EnterpriseNewsVo;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * <p>
 * ä¼ä¸šæ–°é—»è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:50:59
 */
@RestController
@RequestMapping("/enterpriseNews")
@Tag(name = "企业新闻表")
@AllArgsConstructor
public class EnterpriseNewsController {
    private final EnterpriseNewsService enterpriseNewsService;
    @Operation(summary = "分页查询")
    @GetMapping("/listPage")
    @Log(title = "企业新闻分页查询", businessType = BusinessType.OTHER)
    public R listPage(Page<EnterpriseNewsVo>  page , EnterpriseNewsDto enterpriseNewsDto) {
        return R.ok(enterpriseNewsService.listPage(page, enterpriseNewsDto));
    }
    @PostMapping("/save")
    @Operation(summary = "保存")
    @Log(title = "保存企业新闻", businessType = BusinessType.INSERT)
    public R save(@RequestBody EnterpriseNewsDto enterpriseNewsDto) {
        return R.ok(enterpriseNewsService.add(enterpriseNewsDto));
    }
    @PutMapping("/update")
    @Operation(summary = "更新")
    @Log(title = "更新企业新闻", businessType = BusinessType.UPDATE)
    public R update(@RequestBody EnterpriseNewsDto enterpriseNewsDto) {
        return R.ok(enterpriseNewsService.updateEnterpriseNewsDto(enterpriseNewsDto));
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除")
    @Log(title = "删除企业新闻", businessType = BusinessType.DELETE)
    public R delete(@RequestBody List<Long> ids) {
        return R.ok(enterpriseNewsService.delete(ids));
    }
}
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeDeptController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.collaborativeApproval.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围部门表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:12
 */
@RestController
@RequestMapping("/enterpriseNewsScopeDept")
public class EnterpriseNewsScopeDeptController {
}
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeUserController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.collaborativeApproval.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围用户表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:23
 */
@RestController
@RequestMapping("/enterpriseNewsScopeUser")
public class EnterpriseNewsScopeUserController {
}
src/main/java/com/ruoyi/collaborativeApproval/dto/EnterpriseNewsDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.ruoyi.collaborativeApproval.dto;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import lombok.Data;
import java.util.List;
@Data
public class EnterpriseNewsDto extends EnterpriseNews {
    private String createUserName;
    private List<Long> deptIds;
    private List<Long> userIds;
    private Long templateId;
    private String templateName;
    private String createTimeStart;
    private String createTimeEnd;
    private List<StorageBlobDTO> storageBlobDTOs;
}
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.collaborativeApproval.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.collaborativeApproval.dto.EnterpriseNewsDto;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import com.ruoyi.collaborativeApproval.vo.EnterpriseNewsVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 * ä¼ä¸šæ–°é—»è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:50:59
 */
@Mapper
public interface EnterpriseNewsMapper extends BaseMapper<EnterpriseNews> {
    IPage<EnterpriseNewsVo> listPage(Page<EnterpriseNewsVo> page,@Param("enterpriseNewsDto") EnterpriseNewsDto enterpriseNewsDto);
}
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeDeptMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.collaborativeApproval.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围部门表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:12
 */
@Mapper
public interface EnterpriseNewsScopeDeptMapper extends BaseMapper<EnterpriseNewsScopeDept> {
}
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeUserMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.collaborativeApproval.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围用户表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:23
 */
@Mapper
public interface EnterpriseNewsScopeUserMapper extends BaseMapper<EnterpriseNewsScopeUser> {
}
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNews.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
package com.ruoyi.collaborativeApproval.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * ä¼ä¸šæ–°é—»è¡¨
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:50:59
 */
@Getter
@Setter
@ToString
@TableName("enterprise_news")
@ApiModel(value = "EnterpriseNews对象", description = "企业新闻表")
public class EnterpriseNews implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "编号 ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "标题 Title")
    private String title;
    @Schema(description = "摘要 Summary")
    private String summary;
    @Schema(description = "正文 Content")
    private String content;
    @Schema(description = "分类 Category")
    private String category;
    @Schema(description = "阅读范围 Read scope: all å…¨å‘˜, dept éƒ¨é—¨, custom è‡ªå®šä¹‰")
    private String readScope;
    @Schema(description = "是否必读 Required flag: 0 å¦, 1 æ˜¯")
    private Byte isRequired;
    @Schema(description = "状态 Status: DRAFT è‰ç¨¿, PENDING å¾…审批, PUBLISHED å·²å‘布, REJECTED é©³å›ž, OFFLINE å·²ä¸‹çº¿")
    private String status;
    @Schema(description = "应读人数 Required read count")
    private Integer requiredReadCount;
    @Schema(description = "已读人数 Read count")
    private Integer readCount;
    @Schema(description = "创建人 Create user")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @Schema(description = "创建时间 Create time")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss", iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime createTime;
    @Schema(description = "更新人 Update user")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    @Schema(description = "更新时间 Update time")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss", iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime updateTime;
    @Schema(description = "部门ID Dept ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeDept.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package com.ruoyi.collaborativeApproval.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围部门表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:12
 */
@Getter
@Setter
@ToString
@TableName("enterprise_news_scope_dept")
@ApiModel(value = "EnterpriseNewsScopeDept对象", description = "企业新闻阅读范围部门表")
public class EnterpriseNewsScopeDept implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ç¼–号
     */
    @ApiModelProperty("编号")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * ä¼ä¸šæ–°é—»ID
     */
    @ApiModelProperty("企业新闻ID")
    private Long newsId;
    /**
     * éƒ¨é—¨ID
     */
    @ApiModelProperty("部门ID")
    private Long deptId;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeUser.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package com.ruoyi.collaborativeApproval.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围用户表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:23
 */
@Getter
@Setter
@ToString
@TableName("enterprise_news_scope_user")
@ApiModel(value = "EnterpriseNewsScopeUser对象", description = "企业新闻阅读范围用户表")
public class EnterpriseNewsScopeUser implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ç¼–号
     */
    @ApiModelProperty("编号")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * ä¼ä¸šæ–°é—»ID
     */
    @ApiModelProperty("企业新闻ID")
    private Long newsId;
    /**
     * ç”¨æˆ·ID
     */
    @ApiModelProperty("用户ID")
    private Long userId;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeDeptService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.collaborativeApproval.service;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围部门表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:12
 */
public interface EnterpriseNewsScopeDeptService extends IService<EnterpriseNewsScopeDept> {
}
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeUserService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.collaborativeApproval.service;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围用户表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:23
 */
public interface EnterpriseNewsScopeUserService extends IService<EnterpriseNewsScopeUser> {
}
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package com.ruoyi.collaborativeApproval.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.collaborativeApproval.dto.EnterpriseNewsDto;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.collaborativeApproval.vo.EnterpriseNewsVo;
import java.util.List;
/**
 * <p>
 * ä¼ä¸šæ–°é—»è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:50:59
 */
public interface EnterpriseNewsService extends IService<EnterpriseNews> {
    IPage<EnterpriseNewsVo> listPage(Page<EnterpriseNewsVo> page, EnterpriseNewsDto enterpriseNewsDto);
    Boolean add(EnterpriseNewsDto enterpriseNewsDto);
    Boolean updateEnterpriseNewsDto(EnterpriseNewsDto enterpriseNewsDto);
    Boolean delete(List<Long> ids);
}
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeDeptServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.collaborativeApproval.service.impl;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeDeptMapper;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsScopeDeptService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围部门表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:12
 */
@Service
public class EnterpriseNewsScopeDeptServiceImpl extends ServiceImpl<EnterpriseNewsScopeDeptMapper, EnterpriseNewsScopeDept> implements EnterpriseNewsScopeDeptService {
}
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeUserServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.collaborativeApproval.service.impl;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeUserMapper;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsScopeUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围用户表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:23
 */
@Service
public class EnterpriseNewsScopeUserServiceImpl extends ServiceImpl<EnterpriseNewsScopeUserMapper, EnterpriseNewsScopeUser> implements EnterpriseNewsScopeUserService {
}
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,409 @@
package com.ruoyi.collaborativeApproval.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.mapper.ApprovalTemplateMapper;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.ruoyi.approve.service.ApprovalInstanceService;
import com.ruoyi.approve.utils.ApproveProcessConfigNodeUtils;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.collaborativeApproval.dto.EnterpriseNewsDto;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsMapper;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsScopeDeptService;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsScopeUserService;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsService;
import com.ruoyi.collaborativeApproval.vo.EnterpriseNewsVo;
import com.ruoyi.common.enums.TypeEnums;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.mapper.SysUserDeptMapper;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.project.system.service.ISysNoticeService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
 * ä¼ä¸šæ–°é—»è¡¨æœåŠ¡å®žçŽ°ç±»
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:50:59
 */
@Service
@RequiredArgsConstructor
public class EnterpriseNewsServiceImpl extends ServiceImpl<EnterpriseNewsMapper, EnterpriseNews> implements EnterpriseNewsService {
    private static final String READ_SCOPE_ALL = "all";
    private static final String READ_SCOPE_DEPT = "dept";
    private static final String READ_SCOPE_CUSTOM = "custom";
    private static final String STATUS_DRAFT = "DRAFT";
    private static final String STATUS_PENDING = "PENDING";
    private static final String STATUS_PUBLISHED = "PUBLISHED";
    private static final String STATUS_REJECTED = "REJECTED";
    private static final String STATUS_OFFLINE = "OFFLINE";
    private final EnterpriseNewsMapper enterpriseNewsMapper;
    private final EnterpriseNewsScopeDeptService enterpriseNewsScopeDeptService;
    private final EnterpriseNewsScopeUserService enterpriseNewsScopeUserService;
    private final SysUserMapper sysUserMapper;
    private final SysDeptMapper sysDeptMapper;
    private final SysUserDeptMapper sysUserDeptMapper;
    private final ApprovalInstanceMapper approvalInstanceMapper;
    private final ApprovalInstanceService approvalInstanceService;
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final ApproveProcessConfigNodeUtils approveProcessConfigNodeUtils;
    private final ISysNoticeService sysNoticeService;
    private final FileUtil fileUtil;
    @Override
    public IPage<EnterpriseNewsVo> listPage(Page<EnterpriseNewsVo> page, EnterpriseNewsDto enterpriseNewsDto) {
        IPage<EnterpriseNewsVo> enterpriseNewsVoIPage = enterpriseNewsMapper.listPage(page, enterpriseNewsDto);
        enterpriseNewsVoIPage.getRecords().forEach(enterpriseNewsVo -> {
            enterpriseNewsVo.setStorageBlobDTOs(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.ENTERPRISE_NEWS, enterpriseNewsVo.getId()));
        });
        return enterpriseNewsVoIPage;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean add(EnterpriseNewsDto enterpriseNewsDto) {
        validateForSave(enterpriseNewsDto);
        String readScope = normalizeReadScope(enterpriseNewsDto.getReadScope());
        List<Long> deptIds = distinctIds(enterpriseNewsDto.getDeptIds());
        List<Long> userIds = distinctIds(enterpriseNewsDto.getUserIds());
        EnterpriseNews enterpriseNews = new EnterpriseNews();
        BeanUtils.copyProperties(enterpriseNewsDto, enterpriseNews);
        enterpriseNews.setReadScope(readScope);
        enterpriseNews.setIsRequired(enterpriseNewsDto.getIsRequired() == null ? (byte) 0 : enterpriseNewsDto.getIsRequired());
        enterpriseNews.setStatus(normalizeStatus(enterpriseNewsDto.getStatus(), STATUS_DRAFT));
        enterpriseNews.setReadCount(0);
        enterpriseNews.setRequiredReadCount(calculateRequiredReadCount(readScope, deptIds, userIds));
        Long[] loginDeptIds = SecurityUtils.getDeptId();
        if (StringUtils.isNotEmpty(loginDeptIds)) {
            enterpriseNews.setDeptId(loginDeptIds[0]);
        }
        if (!save(enterpriseNews) || enterpriseNews.getId() == null) {
            throw new ServiceException("新增企业新闻失败");
        }
        saveReadScopeRelations(enterpriseNews.getId(), readScope, deptIds, userIds);
        if (STATUS_PENDING.equals(enterpriseNews.getStatus())) {
            startEnterpriseNewsApproval(enterpriseNews, enterpriseNewsDto);
        }
        //添加附件
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.ENTERPRISE_NEWS, enterpriseNews.getId(), enterpriseNewsDto.getStorageBlobDTOs());
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean updateEnterpriseNewsDto(EnterpriseNewsDto enterpriseNewsDto) {
        if (enterpriseNewsDto == null || enterpriseNewsDto.getId() == null) {
            throw new ServiceException("企业新闻ID不能为空");
        }
        EnterpriseNews oldEnterpriseNews = getById(enterpriseNewsDto.getId());
        if (oldEnterpriseNews == null) {
            throw new ServiceException("企业新闻不存在");
        }
        if (!STATUS_DRAFT.equals(oldEnterpriseNews.getStatus())
                && !STATUS_REJECTED.equals(oldEnterpriseNews.getStatus())) {
            throw new ServiceException("待审批或已发布的企业新闻不允许修改");
        }
        validateForSave(enterpriseNewsDto);
        String readScope = normalizeReadScope(enterpriseNewsDto.getReadScope());
        List<Long> deptIds = distinctIds(enterpriseNewsDto.getDeptIds());
        List<Long> userIds = distinctIds(enterpriseNewsDto.getUserIds());
        EnterpriseNews enterpriseNews = new EnterpriseNews();
        BeanUtils.copyProperties(enterpriseNewsDto, enterpriseNews);
        enterpriseNews.setReadScope(readScope);
        enterpriseNews.setIsRequired(enterpriseNewsDto.getIsRequired() == null ? oldEnterpriseNews.getIsRequired() : enterpriseNewsDto.getIsRequired());
        enterpriseNews.setStatus(normalizeStatus(enterpriseNewsDto.getStatus(), oldEnterpriseNews.getStatus()));
        enterpriseNews.setReadCount(oldEnterpriseNews.getReadCount());
        enterpriseNews.setRequiredReadCount(calculateRequiredReadCount(readScope, deptIds, userIds));
        enterpriseNews.setCreateUser(oldEnterpriseNews.getCreateUser());
        enterpriseNews.setCreateTime(oldEnterpriseNews.getCreateTime());
        enterpriseNews.setDeptId(oldEnterpriseNews.getDeptId());
        if (!updateById(enterpriseNews)) {
            throw new ServiceException("修改企业新闻失败");
        }
        clearReadScopeRelations(enterpriseNews.getId());
        saveReadScopeRelations(enterpriseNews.getId(), readScope, deptIds, userIds);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.ENTERPRISE_NEWS, enterpriseNews.getId(), enterpriseNewsDto.getStorageBlobDTOs());
        if (STATUS_PENDING.equals(enterpriseNews.getStatus())) {
            resetEnterpriseNewsApprovalFlow(oldEnterpriseNews);
            startEnterpriseNewsApproval(enterpriseNews, enterpriseNewsDto);
        }
        return true;
    }
    @Override
    public Boolean delete(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return false;
        }
        if (!removeByIds(ids)) {
            throw new ServiceException("删除企业新闻失败");
        }
        ids.forEach(this::clearReadScopeRelations);
        return true;
    }
    private void validateForSave(EnterpriseNewsDto enterpriseNewsDto) {
        if (enterpriseNewsDto == null) {
            throw new ServiceException("企业新闻数据不能为空");
        }
        if (StringUtils.isEmpty(enterpriseNewsDto.getTitle())) {
            throw new ServiceException("标题不能为空");
        }
        if (StringUtils.isEmpty(enterpriseNewsDto.getContent())) {
            throw new ServiceException("正文不能为空");
        }
        normalizeStatus(enterpriseNewsDto.getStatus(), STATUS_DRAFT);
        String readScope = normalizeReadScope(enterpriseNewsDto.getReadScope());
        List<Long> deptIds = distinctIds(enterpriseNewsDto.getDeptIds());
        List<Long> userIds = distinctIds(enterpriseNewsDto.getUserIds());
        if (READ_SCOPE_DEPT.equals(readScope) && StringUtils.isEmpty(deptIds)) {
            throw new ServiceException("请选择阅读范围部门");
        }
        if (READ_SCOPE_CUSTOM.equals(readScope) && StringUtils.isEmpty(userIds)) {
            throw new ServiceException("请选择自定义阅读人员");
        }
        validateDeptIds(deptIds);
        validateUserIds(userIds);
    }
    private String normalizeReadScope(String readScope) {
        String normalized = StringUtils.isEmpty(readScope) ? READ_SCOPE_ALL : readScope.trim();
        if (!READ_SCOPE_ALL.equals(normalized)
                && !READ_SCOPE_DEPT.equals(normalized)
                && !READ_SCOPE_CUSTOM.equals(normalized)) {
            throw new ServiceException("阅读范围不合法");
        }
        return normalized;
    }
    private String normalizeStatus(String status, String defaultStatus) {
        String normalized = StringUtils.isEmpty(status) ? defaultStatus : status.trim().toUpperCase();
        if (!STATUS_DRAFT.equals(normalized)
                && !STATUS_PENDING.equals(normalized)
                && !STATUS_PUBLISHED.equals(normalized)
                && !STATUS_REJECTED.equals(normalized)
                && !STATUS_OFFLINE.equals(normalized)) {
            throw new ServiceException("企业新闻状态不合法");
        }
        return normalized;
    }
    private void validateDeptIds(List<Long> deptIds) {
        if (StringUtils.isEmpty(deptIds)) {
            return;
        }
        for (Long deptId : deptIds) {
            SysDept sysDept = sysDeptMapper.selectDeptById(deptId);
            if (deptId == null || sysDept == null) {
                throw new ServiceException("阅读范围部门不存在");
            }
        }
    }
    private void validateUserIds(List<Long> userIds) {
        if (StringUtils.isEmpty(userIds)) {
            return;
        }
        List<SysUser> users = sysUserMapper.selectUsersByIds(userIds);
        if (users.size() != userIds.size()) {
            throw new ServiceException("自定义阅读人员包含无效用户");
        }
    }
    private Integer calculateRequiredReadCount(String readScope, List<Long> deptIds, List<Long> userIds) {
        if (READ_SCOPE_ALL.equals(readScope)) {
            Long count = sysUserMapper.selectCount(new LambdaQueryWrapper<SysUser>()
                    .eq(SysUser::getDelFlag, "0"));
            return count == null ? 0 : count.intValue();
        }
        if (READ_SCOPE_DEPT.equals(readScope)) {
            List<Long> allDeptIds = collectDeptIdsWithChildren(deptIds);
            if (StringUtils.isEmpty(allDeptIds)) {
                return 0;
            }
            Long count = sysUserDeptMapper.countDistinctUserIdsByDeptIds(allDeptIds);
            return count == null ? 0 : count.intValue();
        }
        return userIds.size();
    }
    private List<Long> collectDeptIdsWithChildren(List<Long> deptIds) {
        Set<Long> allDeptIds = new LinkedHashSet<>();
        for (Long deptId : deptIds) {
            if (deptId == null) {
                continue;
            }
            allDeptIds.add(deptId);
            List<SysDept> children = sysDeptMapper.selectChildrenDeptById(deptId);
            if (StringUtils.isNotEmpty(children)) {
                for (SysDept child : children) {
                    allDeptIds.add(child.getDeptId());
                }
            }
        }
        return new ArrayList<>(allDeptIds);
    }
    private void saveReadScopeRelations(Long newsId, String readScope, List<Long> deptIds, List<Long> userIds) {
        if (READ_SCOPE_DEPT.equals(readScope)) {
            List<EnterpriseNewsScopeDept> scopeDeptList = new ArrayList<>();
            for (Long deptId : deptIds) {
                EnterpriseNewsScopeDept scopeDept = new EnterpriseNewsScopeDept();
                scopeDept.setNewsId(newsId);
                scopeDept.setDeptId(deptId);
                scopeDeptList.add(scopeDept);
            }
            if (StringUtils.isNotEmpty(scopeDeptList)) {
                enterpriseNewsScopeDeptService.saveBatch(scopeDeptList);
            }
            return;
        }
        if (READ_SCOPE_CUSTOM.equals(readScope)) {
            List<EnterpriseNewsScopeUser> scopeUserList = new ArrayList<>();
            for (Long userId : userIds) {
                EnterpriseNewsScopeUser scopeUser = new EnterpriseNewsScopeUser();
                scopeUser.setNewsId(newsId);
                scopeUser.setUserId(userId);
                scopeUserList.add(scopeUser);
            }
            if (StringUtils.isNotEmpty(scopeUserList)) {
                enterpriseNewsScopeUserService.saveBatch(scopeUserList);
            }
        }
    }
    private void clearReadScopeRelations(Long newsId) {
        enterpriseNewsScopeDeptService.remove(new LambdaQueryWrapper<EnterpriseNewsScopeDept>()
                .eq(EnterpriseNewsScopeDept::getNewsId, newsId));
        enterpriseNewsScopeUserService.remove(new LambdaQueryWrapper<EnterpriseNewsScopeUser>()
                .eq(EnterpriseNewsScopeUser::getNewsId, newsId));
    }
    private void resetEnterpriseNewsApprovalFlow(EnterpriseNews oldEnterpriseNews) {
        if (oldEnterpriseNews == null || !STATUS_DRAFT.equals(oldEnterpriseNews.getStatus())) {
            return;
        }
        List<Long> approvalInstanceIds = approvalInstanceMapper.selectList(new LambdaQueryWrapper<ApprovalInstance>()
                        .eq(ApprovalInstance::getBusinessId, oldEnterpriseNews.getId())
                        .eq(ApprovalInstance::getBusinessType, TypeEnums.ENTERPRISE_NEWS_APPROVAL.getCode())
                        .eq(ApprovalInstance::getDeleted, (byte) 0))
                .stream()
                .map(ApprovalInstance::getId)
                .filter(id -> id != null && id > 0)
                .collect(Collectors.toList());
        if (StringUtils.isEmpty(approvalInstanceIds)) {
            return;
        }
        approvalInstanceService.delete(approvalInstanceIds);
    }
    private List<Long> distinctIds(List<Long> ids) {
        if (StringUtils.isEmpty(ids)) {
            return new ArrayList<>();
        }
        Set<Long> distinctSet = new LinkedHashSet<>();
        for (Long id : ids) {
            if (id != null) {
                distinctSet.add(id);
            }
        }
        return new ArrayList<>(distinctSet);
    }
    private void startEnterpriseNewsApproval(EnterpriseNews enterpriseNews, EnterpriseNewsDto enterpriseNewsDto) {
        if (enterpriseNewsDto.getTemplateId() == null) {
            throw new ServiceException("审批模板不能为空");
        }
        String templateName = enterpriseNewsDto.getTemplateName();
        if (StringUtils.isEmpty(templateName)) {
            ApprovalTemplate approvalTemplate = approvalTemplateMapper.selectById(enterpriseNewsDto.getTemplateId());
            if (approvalTemplate == null) {
                throw new ServiceException("审批模板不存在");
            }
            templateName = approvalTemplate.getTemplateName();
        }
        ApprovalInstance approvalInstance = new ApprovalInstance();
        approvalInstance.setInstanceNo(OrderUtils.countTodayByCreateTime(approvalInstanceMapper, "SP", "instance_no", enterpriseNews.getCreateTime() != null ? enterpriseNews.getCreateTime() : LocalDateTime.now()));
        approvalInstance.setTemplateId(enterpriseNewsDto.getTemplateId());
        approvalInstance.setTemplateName(templateName);
        approvalInstance.setBusinessId(enterpriseNews.getId());
        approvalInstance.setBusinessType(TypeEnums.ENTERPRISE_NEWS_APPROVAL.getCode());
        approvalInstance.setTitle(enterpriseNews.getTitle());
        approvalInstance.setStatus("PENDING");
        approvalInstance.setCurrentLevel(1);
        approvalInstance.setApplicantId(SecurityUtils.getUserId());
        approvalInstance.setApplicantName(SecurityUtils.getLoginUser().getNickName());
        approvalInstance.setApplyTime(LocalDateTime.now());
        approvalInstance.setDeleted((byte) 0);
        approvalInstance.setCreateUser(SecurityUtils.getUserId());
        approvalInstance.setUpdateUser(SecurityUtils.getUserId());
        approvalInstance.setDeptId(enterpriseNews.getDeptId());
        approvalInstanceMapper.insert(approvalInstance);
        approveProcessConfigNodeUtils.createCurrentNodeAndTasks(approvalInstance);
        sendApproveNotice(approvalInstance, approveProcessConfigNodeUtils.getCurrentPendingTasks(approvalInstance.getId()));
    }
    private void sendApproveNotice(ApprovalInstance instance, List<ApprovalTask> tasks) {
        if (instance == null || tasks == null || tasks.isEmpty()) {
            return;
        }
        List<Long> approverIds = tasks.stream()
                .map(ApprovalTask::getApproverId)
                .filter(id -> id != null && id > 0)
                .distinct()
                .collect(Collectors.toList());
        if (approverIds.isEmpty()) {
            return;
        }
        String title = StringUtils.isNotEmpty(instance.getTemplateName()) ? instance.getTemplateName() : "审批提醒";
        String message = "审批单号 " + instance.getInstanceNo() + " éœ€è¦æ‚¨å®¡æ‰¹";
        String jumpPath = "/officeProcessAutomation/ApproveManage/approve-list/?id=" + instance.getId();
        sysNoticeService.simpleNoticeByUser(title, message, approverIds, jumpPath);
    }
}
src/main/java/com/ruoyi/collaborativeApproval/vo/EnterpriseNewsVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package com.ruoyi.collaborativeApproval.vo;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import lombok.Data;
import java.util.List;
@Data
public class EnterpriseNewsVo extends EnterpriseNews {
    private String createUserName;
    private List<StorageBlobVO> storageBlobDTOs;
}
src/main/java/com/ruoyi/common/enums/ApprovalStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package com.ruoyi.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®¡æ‰¹çŠ¶æ€æžšä¸¾
 */
@Getter
@AllArgsConstructor
public enum ApprovalStatusEnum implements BaseEnum<Integer> {
    DRAFT(0, "草稿"),
    PENDING(1, "待审批"),
    IN_PROGRESS(2, "审批中"),
    APPROVED(3, "已通过"),
    REJECTED(4, "已驳回");
    private final Integer value;
    private final String label;
    @Override
    public Integer getCode() {
        return value;
    }
    @Override
    public String getValue() {
        return label;
    }
    public static ApprovalStatusEnum fromValue(Integer value) {
        if (value == null) {
            return null;
        }
        for (ApprovalStatusEnum status : values()) {
            if (status.getCode().equals(value)) {
                return status;
            }
        }
        return null;
    }
    public static String getLabelByValue(Integer value) {
        ApprovalStatusEnum statusEnum = fromValue(value);
        return statusEnum != null ? statusEnum.getValue() : "未知状态";
    }
}
src/main/java/com/ruoyi/common/enums/EnterpriseNewsStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,143 @@
package com.ruoyi.common.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
 * ä¼ä¸šæ–°é—»çŠ¶æ€æžšä¸¾ç±»
 *
 * @author ruoyi
 * @date 2026-05-20
 */
@Schema(description = "企业新闻状态枚举")
public enum EnterpriseNewsStatusEnum implements BaseEnum<String> {
    /**
     * è‰ç¨¿
     */
    @Schema(description = "草稿")
    DRAFT("DRAFT", "草稿"),
    /**
     * å¾…审批
     */
    @Schema(description = "待审批")
    PENDING("PENDING", "待审批"),
    /**
     * å·²å‘布
     */
    @Schema(description = "已发布")
    PUBLISHED("PUBLISHED", "已发布"),
    /**
     * é©³å›ž
     */
    @Schema(description = "驳回")
    REJECTED("REJECTED", "驳回"),
    /**
     * å·²ä¸‹çº¿
     */
    @Schema(description = "已下线")
    OFFLINE("OFFLINE", "已下线");
    /**
     * çŠ¶æ€ç 
     */
    private final String code;
    /**
     * çŠ¶æ€æè¿°
     * -- GETTER --
     *  èŽ·å–çŠ¶æ€æè¿°
     *
     * @return çŠ¶æ€æè¿°
     */
    @Getter
    private final String description;
    EnterpriseNewsStatusEnum(String code, String description) {
        this.code = code;
        this.description = description;
    }
    /**
     * èŽ·å–çŠ¶æ€ç 
     *
     * @return çŠ¶æ€ç 
     */
    @JsonValue
    public String getCode() {
        return code;
    }
    @Override
    public String getValue() {
        return "";
    }
    /**
     * æ ¹æ®çŠ¶æ€ç èŽ·å–æžšä¸¾
     *
     * @param code çŠ¶æ€ç 
     * @return æžšä¸¾å€¼
     */
    @JsonCreator
    public static EnterpriseNewsStatusEnum getByCode(String code) {
        for (EnterpriseNewsStatusEnum status : values()) {
            if (status.code.equals(code)) {
                return status;
            }
        }
        throw new IllegalArgumentException("Invalid enterprise news status code: " + code);
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºè‰ç¨¿çŠ¶æ€
     *
     * @return æ˜¯å¦ä¸ºè‰ç¨¿çŠ¶æ€
     */
    public boolean isDraft() {
        return DRAFT.equals(this);
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºå¾…审批状态
     *
     * @return æ˜¯å¦ä¸ºå¾…审批状态
     */
    public boolean isPending() {
        return PENDING.equals(this);
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºå·²å‘布状态
     *
     * @return æ˜¯å¦ä¸ºå·²å‘布状态
     */
    public boolean isPublished() {
        return PUBLISHED.equals(this);
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºé©³å›žçŠ¶æ€
     *
     * @return æ˜¯å¦ä¸ºé©³å›žçŠ¶æ€
     */
    public boolean isRejected() {
        return REJECTED.equals(this);
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºå·²ä¸‹çº¿çŠ¶æ€
     *
     * @return æ˜¯å¦ä¸ºå·²ä¸‹çº¿çŠ¶æ€
     */
    public boolean isOffline() {
        return OFFLINE.equals(this);
    }
}
src/main/java/com/ruoyi/common/enums/SalesQuotationStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package com.ruoyi.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * é”€å”®æŠ¥ä»·çŠ¶æ€æžšä¸¾
 */
@Getter
@AllArgsConstructor
public enum SalesQuotationStatusEnum implements BaseEnum<String> {
    DRAFT("草稿", "草稿"),
    PENDING("待审批", "待审批"),
    IN_PROGRESS("审核中", "审核中"),
    APPROVED("通过", "通过"),
    REJECTED("拒绝", "拒绝");
    private final String value;
    private final String label;
    @Override
    public String getCode() {
        return value;
    }
    @Override
    public String getValue() {
        return label;
    }
    public static SalesQuotationStatusEnum fromValue(String value) {
        if (value == null) {
            return null;
        }
        for (SalesQuotationStatusEnum status : values()) {
            if (status.getCode().equals(value)) {
                return status;
            }
        }
        return null;
    }
}
src/main/java/com/ruoyi/common/enums/ShippingStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package com.ruoyi.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å‘货审批状态枚举
 */
@Getter
@AllArgsConstructor
public enum ShippingStatusEnum implements BaseEnum<String> {
    PENDING("待确认", "待确认"),
    IN_PROGRESS("审核中", "审核中"),
    APPROVED("审核通过", "审核通过"),
    REJECTED("审核拒绝", "审核拒绝");
    private final String value;
    private final String label;
    @Override
    public String getCode() {
        return value;
    }
    @Override
    public String getValue() {
        return label;
    }
    public static ShippingStatusEnum fromValue(String value) {
        if (value == null) {
            return null;
        }
        for (ShippingStatusEnum status : values()) {
            if (status.getCode().equals(value)) {
                return status;
            }
        }
        return null;
    }
}
src/main/java/com/ruoyi/common/enums/TypeEnums.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
package com.ruoyi.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum TypeEnums implements BaseEnum<Long> {
    PUBLIC_OUT(1L, "公出管理"),
    LEAVE(2L, "请假管理"),
    BUSINESS_TRIP(3L, "出差管理"),
    REIMBURSEMENT(4L, "报销管理"),
    PURCHASE_APPROVAL(5L, "采购审批"),
    QUOTATION_APPROVAL(6L, "报价审批"),
    SHIPPING_APPROVAL(7L, "发货审批"),
    DANGEROUS_OPERATION(8L, "危险作业审批"),
    OFFICE_SUPPLIES(9L, "办公用品审批"),
    REGULARIZATION_APPROVAL(10L, "转正审批"),
    TRANSFER_APPROVAL(11L, "调动审批"),
    RESIGNATION_APPROVAL(12L, "离职审批"),
    WORK_HANDOVER_APPROVAL(13L, "工作交接审批"),
    LEAVE_APPROVAL(14L, "请假审批"),
    OVERTIME_APPROVAL(15L, "加班审批"),
    TRAVEL_REIMBURSEMENT_APPROVAL(16L, "出差报销审批"),
    EXPENSE_APPROVAL(17L, "费用审批"),
    ENTERPRISE_NEWS_APPROVAL(18L, "企业新闻审批");
    private final Long value;
    private final String label;
    @Override
    public Long getCode() {
        return value;
    }
    @Override
    public String getValue() {
        return label;
    }
    /**
     * æ ¹æ®å€¼èŽ·å–å¯¹åº”çš„æžšä¸¾
     * @param value ä¸šåŠ¡ç±»åž‹å€¼
     * @return å¯¹åº”的枚举,未匹配返回null
     */
    public static TypeEnums fromValue(Long value) {
        if (value == null) {
            return null;
        }
        for (TypeEnums type : values()) {
            if (type.getCode().equals(value)) {
                return type;
            }
        }
        return null;
    }
    /**
     * æ ¹æ®å€¼èŽ·å–æè¿°
     * @param value ä¸šåŠ¡ç±»åž‹å€¼
     * @return ä¸šåŠ¡ç±»åž‹æè¿°ï¼ŒæœªåŒ¹é…è¿”å›ž"自定义审批"
     */
    public static String getLabelByValue(Long value) {
        TypeEnums typeEnum = fromValue(value);
        return typeEnum != null ? typeEnum.getValue() : "自定义审批";
    }
}
src/main/java/com/ruoyi/common/utils/OrderUtils.java
@@ -61,8 +61,11 @@
     * @param <T> å®žä½“类型
     * @return è®¢å•编号
     */
    public static <T> String countTodayByCreateTime(BaseMapper<T> mapper,String preFix,String code) {
        LocalDate today = LocalDate.now();
    public static <T> String countTodayByCreateTime(BaseMapper<T> mapper,String preFix,String code, LocalDateTime createTime) {
        if (createTime == null) {
            createTime = LocalDateTime.now();
        }
        LocalDate today = createTime.toLocalDate();
        LocalDateTime todayStart = today.atStartOfDay();
        LocalDateTime tomorrowStart = today.plusDays(1).atStartOfDay();
        String dateStr = today.format(DateTimeFormatter.BASIC_ISO_DATE);
@@ -109,13 +112,17 @@
     * @param <T> å®žä½“类泛型
     * @return å½“天记录数量
     */
    public static <T> String countAfterServiceTodayByCreateTime(BaseMapper<T> mapper,String preFix) {
    public static <T> String countAfterServiceTodayByCreateTime(BaseMapper<T> mapper,String preFix, LocalDateTime createTime) {
        if (createTime == null) {
            createTime = LocalDateTime.now();
        }
        LocalDate localDate = createTime.toLocalDate();
        LocalDateTime todayStart = LocalDateTime.of(
                LocalDateTime.now().toLocalDate(),
                localDate,
                LocalTime.MIN
        );
        LocalDateTime todayEnd = LocalDateTime.of(
                LocalDateTime.now().toLocalDate(),
                localDate,
                LocalTime.MAX
        );
@@ -127,6 +134,6 @@
                .lt("create_time", endDate);
        Long aLong = mapper.selectCount(queryWrapper);
        return preFix + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE).replaceAll("-", "") + String.format("%03d", (aLong + 1));
        return preFix + localDate.format(DateTimeFormatter.ISO_LOCAL_DATE).replaceAll("-", "") + String.format("%03d", (aLong + 1));
    }
}
src/main/java/com/ruoyi/customervisits/service/impl/CustomerVisitsServiceImpl.java
@@ -37,7 +37,7 @@
                wrapper.like(CustomerVisits::getVisitingPeople, customerVisits.getVisitingPeople());
            }
        }
        wrapper.orderByDesc(CustomerVisits::getId);
        return customerVisitsMapper.selectPage(page, wrapper);
    }
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskServiceImpl.java
@@ -1,5 +1,6 @@
package com.ruoyi.device.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.bean.BeanUtils;
@@ -34,7 +35,7 @@
    @Override
    public AjaxResult listPage(Page page, MaintenanceTask maintenanceTask) {
        Page<MaintenanceTask> taskPage = maintenanceTaskMapper.selectPage(page, null);
        Page<MaintenanceTask> taskPage = maintenanceTaskMapper.selectPage(page, new QueryWrapper<MaintenanceTask>().orderByDesc("create_time"));
        // 2. å¦‚果没有数据,直接返回空分页
        if (taskPage.getRecords().isEmpty()) {
            return AjaxResult.success(taskPage);
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java
@@ -67,6 +67,7 @@
        if (StringUtils.isNotBlank(inspectionTaskDto.getInspectionProject())) {
            queryWrapper.like(InspectionTask::getInspectionProject, inspectionTaskDto.getInspectionProject());
        }
        queryWrapper.orderByDesc(InspectionTask::getCreateTime);
        IPage<InspectionTask> entityPage = inspectionTaskMapper.selectPage(page, queryWrapper);
        //  æ— æ•°æ®æå‰è¿”回
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
@@ -53,6 +53,7 @@
        if (timingTask.getIsEnabled() != null) {
            queryWrapper.eq(TimingTask::getIsEnabled, timingTask.getIsEnabled());
        }
        queryWrapper.orderByDesc(TimingTask::getCreateTime);
        IPage<TimingTask> taskPage = timingTaskMapper.selectPage(page, queryWrapper);
        // 2. å¦‚果没有数据,直接返回空分页
src/main/java/com/ruoyi/procurementrecord/pojo/CustomStorage.java
@@ -97,7 +97,6 @@
    /**
     * å…¥åº“æ—¶é—´
     */
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDateTime createTime;
src/main/java/com/ruoyi/procurementrecord/pojo/ReturnManagement.java
@@ -63,7 +63,6 @@
    @Schema(description = "创建时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @Schema(description = "更新时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordServiceImpl.java
@@ -535,7 +535,7 @@
            Long aLong = customStorageMapper.selectCount(null);
            item.setInboundBatches(aLong.equals(0L) ? "第1批次(自定义入库)" : "第"+ (aLong + 1) + "批次(自定义入库)" );
            item.setCreateBy(loginUser.getNickName());
            item.setCode(OrderUtils.countTodayByCreateTime(customStorageMapper, "", "code"));
            item.setCode(OrderUtils.countTodayByCreateTime(customStorageMapper, "", "code", item.getCreateTime() != null ? item.getCreateTime() : LocalDateTime.now()));
            customStorageMapper.insert(item);
        });
        return AjaxResult.success("自定义入库成功");
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnManagementServiceImpl.java
@@ -8,6 +8,8 @@
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import java.time.LocalDateTime;
import com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.bean.dto.ReturnSaleProductDto;
import com.ruoyi.procurementrecord.bean.vo.ShippingInfoVo;
@@ -56,7 +58,7 @@
    @Override
    public boolean addReturnManagementDto(ReturnManagementDto returnManagementDto) {
        if (ObjectUtils.isEmpty(returnManagementDto.getReturnNo())){
            String rt = OrderUtils.countTodayByCreateTime(returnManagementMapper, "RT","return_no");
            String rt = OrderUtils.countTodayByCreateTime(returnManagementMapper, "RT","return_no", returnManagementDto.getCreateTime() != null ? returnManagementDto.getCreateTime() : LocalDateTime.now());
            returnManagementDto.setReturnNo(rt);
        }
        save(returnManagementDto);
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java
@@ -18,6 +18,7 @@
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
@Component
@@ -88,11 +89,21 @@
     * @param recordId
     */
    public void addStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId) {
        addStock(productModelId, quantity, recordType, recordId, null);
    }
    /**
     * åˆæ ¼å…¥åº“
     * @param recordType
     * @param recordId
     */
    public void addStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId, LocalDateTime createTime) {
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setRecordId(recordId);
        stockInventoryDto.setRecordType(String.valueOf(recordType));
        stockInventoryDto.setQualitity(quantity);
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setCreateTime(createTime);
        stockInventoryService.addStockInRecordOnly(stockInventoryDto);
    }
@@ -104,12 +115,22 @@
     * @param recordId
     */
    public void addStockWithBatchNo(Long productModelId, BigDecimal quantity, String recordType, Long recordId, String batchNo) {
        addStockWithBatchNo(productModelId, quantity, recordType, recordId, batchNo, null);
    }
    /**
     * åˆæ ¼å…¥åº“带批次号
     * @param recordType
     * @param recordId
     */
    public void addStockWithBatchNo(Long productModelId, BigDecimal quantity, String recordType, Long recordId, String batchNo, LocalDateTime createTime) {
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setRecordId(recordId);
        stockInventoryDto.setRecordType(String.valueOf(recordType));
        stockInventoryDto.setQualitity(quantity);
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setBatchNo(batchNo);
        stockInventoryDto.setCreateTime(createTime);
        stockInventoryService.addStockInRecordOnly(stockInventoryDto);
    }
src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java
@@ -43,7 +43,7 @@
    @PostMapping("/updateRouteItem")
    @Operation(summary = "修改生产订单的工艺路线详情")
    public R updateRouteItem(@RequestBody ProductionOrderRoutingOperation productionOrderRoutingOperation) {
        return R.ok(productionOrderRoutingOperationService.updateRouteItem(productionOrderRoutingOperation));
        return productionOrderRoutingOperationService.updateRouteItem(productionOrderRoutingOperation);
    }
    @Log(title = "生产工序路由", businessType = BusinessType.DELETE)
src/main/java/com/ruoyi/production/pojo/ProductionOrder.java
@@ -41,7 +41,6 @@
    private String npsNo;
    @Schema(description = "录入时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @Schema(description = "更新时间")
src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java
@@ -13,4 +13,5 @@
    R deleteRouteItem(Long id);
    int sortRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation);
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -44,6 +44,7 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@@ -115,7 +116,7 @@
        // ä¸‹å•入口统一补齐来源单据、计划和工艺信息,避免前端分别传多套字段。
        validateAndFillOrder(productionOrder, oldOrder);
        if (productionOrder.getNpsNo() == null || productionOrder.getNpsNo().trim().isEmpty()) {
            productionOrder.setNpsNo(generateNextOrderNo());
            productionOrder.setNpsNo(generateNextOrderNo(productionOrder.getCreateTime() != null ? productionOrder.getCreateTime() : LocalDateTime.now()));
        }
        if (productionOrder.getCompleteQuantity() == null) {
            productionOrder.setCompleteQuantity(BigDecimal.ZERO);
@@ -508,9 +509,10 @@
                .orderByDesc(ProductionOrder::getId);
    }
    private String generateNextOrderNo() {
    private String generateNextOrderNo(LocalDateTime createTime) {
        // ç”Ÿæˆä¸‹ä¸€ä¸ªç”Ÿäº§è®¢å•号
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        LocalDate localDate = createTime.toLocalDate();
        String datePrefix = localDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String prefix = "SC" + datePrefix;
        ProductionOrder latestOrder = this.getOne(Wrappers.<ProductionOrder>lambdaQuery()
                .likeRight(ProductionOrder::getNpsNo, prefix)
src/main/java/com/ruoyi/project/system/mapper/SysUserDeptMapper.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.project.system.domain.SysUserDept;
import com.ruoyi.project.system.domain.vo.SysUserDeptVo;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -15,4 +16,28 @@
    List<SysUserDeptVo> userLoginFacotryList(@Param("userDeptVo") SysUserDeptVo userDeptVo);
    List<Map<String, Object>> setSchemeApplicableStaffUserInfo(@Param("ids") List<Long> ids);
    @Select("<script>" +
            "select count(distinct sud.user_id) " +
            "from sys_user_dept sud " +
            "inner join sys_user su on su.user_id = sud.user_id " +
            "where su.del_flag = '0' " +
            "and sud.dept_id in " +
            "<foreach collection='deptIds' item='deptId' open='(' separator=',' close=')'>" +
            "#{deptId}" +
            "</foreach>" +
            "</script>")
    Long countDistinctUserIdsByDeptIds(@Param("deptIds") List<Long> deptIds);
    @Select("<script>" +
            "select distinct sud.user_id " +
            "from sys_user_dept sud " +
            "inner join sys_user su on su.user_id = sud.user_id " +
            "where su.del_flag = '0' " +
            "and sud.dept_id in " +
            "<foreach collection='deptIds' item='deptId' open='(' separator=',' close=')'>" +
            "#{deptId}" +
            "</foreach>" +
            "</script>")
    List<Long> selectDistinctUserIdsByDeptIds(@Param("deptIds") List<Long> deptIds);
}
src/main/java/com/ruoyi/project/system/service/impl/SysNoticeServiceImpl.java
@@ -4,6 +4,8 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsMapper;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysNotice;
@@ -19,8 +21,12 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
 * å…¬å‘Š æœåŠ¡å±‚å®žçŽ°
@@ -32,10 +38,14 @@
@RequiredArgsConstructor
public class SysNoticeServiceImpl  extends ServiceImpl<SysNoticeMapper, SysNotice> implements ISysNoticeService {
    private static final Pattern ENTERPRISE_NEWS_ID_PATTERN = Pattern.compile("[?&]id=(\\d+)");
    private final SysNoticeMapper noticeMapper;
    private final SysUserMapper userMapper;
    private final SysDeptMapper deptMapper;
    private final SysUserDeptMapper userDeptMapper;
    private final UnipushService unipushService;
    private final EnterpriseNewsMapper enterpriseNewsMapper;
    /**
     * æŸ¥è¯¢å…¬å‘Šä¿¡æ¯
@@ -79,9 +89,24 @@
     * @return ç»“æžœ
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int updateNotice(SysNotice notice)
    {
        return noticeMapper.updateNotice(notice);
        if (notice == null || notice.getNoticeId() == null) {
            return 0;
        }
        SysNotice dbNotice = noticeMapper.selectNoticeById(notice.getNoticeId());
        if (dbNotice == null) {
            return 0;
        }
        boolean needSyncNewsReadCount = isEnterpriseNewsNotice(dbNotice)
                && notice.getStatus() != null
                && !notice.getStatus().equals(dbNotice.getStatus());
        int rows = noticeMapper.updateNotice(notice);
        if (rows > 0 && needSyncNewsReadCount) {
            syncEnterpriseNewsReadCount(dbNotice.getJumpPath());
        }
        return rows;
    }
    /**
@@ -118,10 +143,17 @@
    @Override
    public int readAll() {
        Long userId = SecurityUtils.getUserId();
        return noticeMapper.update(null, Wrappers.<SysNotice>lambdaUpdate()
        List<SysNotice> unreadNotices = noticeMapper.selectList(Wrappers.<SysNotice>lambdaQuery()
                .eq(SysNotice::getConsigneeId, userId)
                .eq(SysNotice::getStatus, "0"));
        int rows = noticeMapper.update(null, Wrappers.<SysNotice>lambdaUpdate()
                .eq(SysNotice::getConsigneeId, userId)
                .eq(SysNotice::getStatus, "0")
                .set(SysNotice::getStatus, "1"));
        if (rows > 0) {
            syncEnterpriseNewsReadCount(unreadNotices);
        }
        return rows;
    }
    @Override
@@ -213,9 +245,63 @@
            return false;
        }
        sysNotice.setStatus("1");
        return noticeMapper.update(null, Wrappers.<SysNotice>lambdaUpdate()
        boolean updated = noticeMapper.update(null, Wrappers.<SysNotice>lambdaUpdate()
                .eq(SysNotice::getNoticeId, noticeId)
                .eq(SysNotice::getStatus, "0")
                .set(SysNotice::getStatus, "1")) > 0;
        if (updated) {
            syncEnterpriseNewsReadCount(sysNotice.getJumpPath());
        }
        return updated;
    }
    private boolean isEnterpriseNewsNotice(SysNotice sysNotice) {
        return sysNotice != null
                && sysNotice.getJumpPath() != null
                && sysNotice.getJumpPath().contains("/enterpriseNews?id=");
    }
    private void syncEnterpriseNewsReadCount(List<SysNotice> notices) {
        if (notices == null || notices.isEmpty()) {
            return;
        }
        Set<String> jumpPaths = new HashSet<>();
        for (SysNotice notice : notices) {
            if (isEnterpriseNewsNotice(notice)) {
                jumpPaths.add(notice.getJumpPath());
            }
        }
        for (String jumpPath : jumpPaths) {
            syncEnterpriseNewsReadCount(jumpPath);
        }
    }
    private void syncEnterpriseNewsReadCount(String jumpPath) {
        Long newsId = parseEnterpriseNewsId(jumpPath);
        if (newsId == null) {
            return;
        }
        long readCount = noticeMapper.selectCount(Wrappers.<SysNotice>lambdaQuery()
                .eq(SysNotice::getStatus, "1")
                .eq(SysNotice::getJumpPath, jumpPath));
        EnterpriseNews enterpriseNews = new EnterpriseNews();
        enterpriseNews.setId(newsId);
        enterpriseNews.setReadCount((int) readCount);
        enterpriseNewsMapper.updateById(enterpriseNews);
    }
    private Long parseEnterpriseNewsId(String jumpPath) {
        if (jumpPath == null || !jumpPath.startsWith("/enterpriseNews")) {
            return null;
        }
        Matcher matcher = ENTERPRISE_NEWS_ID_PATTERN.matcher(jumpPath);
        if (!matcher.find()) {
            return null;
        }
        try {
            return Long.parseLong(matcher.group(1));
        } catch (NumberFormatException e) {
            return null;
        }
    }
}
src/main/java/com/ruoyi/projectManagement/controller/RolesController.java
@@ -7,6 +7,8 @@
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import java.time.LocalDateTime;
import com.ruoyi.projectManagement.dto.RoleDto;
import com.ruoyi.projectManagement.mapper.RolesMapper;
import com.ruoyi.projectManagement.pojo.Roles;
@@ -38,7 +40,7 @@
    @Log(title = "项目角色-新增", businessType = BusinessType.INSERT)
    public AjaxResult add(@RequestBody RoleDto roleDto) {
        if (roleDto.getIsDefaultNo()) {
            roleDto.setNo(OrderUtils.countTodayByCreateTime(rolesMapper, "XMJS","no"));
            roleDto.setNo(OrderUtils.countTodayByCreateTime(rolesMapper, "XMJS","no", roleDto.getCreateTime() != null ? roleDto.getCreateTime() : LocalDateTime.now()));
        }
        return AjaxResult.success(rolesservice.save(roleDto));
    }
src/main/java/com/ruoyi/projectManagement/pojo/Roles.java
@@ -38,7 +38,6 @@
    private Integer status;
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @Schema(description = "创建用户")
src/main/java/com/ruoyi/purchase/controller/PurchaseLedgerController.java
@@ -7,6 +7,7 @@
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
import com.ruoyi.purchase.mapper.PurchaseLedgerTemplateMapper;
@@ -31,6 +32,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Date;
import java.util.List;
/**
@@ -134,6 +136,13 @@
    @PostMapping("/addOrEditPurchase")
    public AjaxResult addOrEditPurchase(@RequestBody PurchaseLedgerDto purchaseLedgerDto) throws Exception {
        return toAjax(purchaseLedgerService.addOrEditPurchase(purchaseLedgerDto));
    }
    @Operation(summary = "批量推进采购台账到入库")
    @Log(title = "批量推进采购台账到入库", businessType = BusinessType.OTHER)
    @PostMapping("/batchInsertPurchaseSteps")
    public R batchInsertPurchaseSteps(@RequestBody PurchaseLedgerDto purchaseLedgerDto) {
        return purchaseLedgerService.batchInsertPurchaseSteps(purchaseLedgerDto == null ? null : purchaseLedgerDto.getIds());
    }
    /**
@@ -247,7 +256,7 @@
    @Operation(summary = "生成采购序列号")
    @GetMapping("/createPurchaseNo")
    @Log(title = "生成采购序列号", businessType = BusinessType.OTHER)
    public AjaxResult createPurchaseNo() {
        return AjaxResult.success("生成成功",purchaseLedgerService.getPurchaseNo());
    public AjaxResult createPurchaseNo(@RequestParam Date entryDate) {
        return AjaxResult.success("生成成功",purchaseLedgerService.getPurchaseNo(entryDate));
    }
}
src/main/java/com/ruoyi/purchase/controller/PurchaseReturnOrdersController.java
@@ -13,6 +13,8 @@
import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
import java.time.LocalDateTime;
import com.ruoyi.purchase.service.PurchaseReturnOrdersService;
import com.ruoyi.purchase.vo.PurchaseStockInProductVo;
import io.swagger.v3.oas.annotations.Operation;
@@ -50,7 +52,7 @@
    @PostMapping("/add")
    public AjaxResult add(@RequestBody PurchaseReturnOrderDto purchaseReturnOrderDto) throws Exception {
        if (purchaseReturnOrderDto.getIsDefaultNo()) {
            purchaseReturnOrderDto.setNo(OrderUtils.countTodayByCreateTime(purchaseReturnOrdersMapper, "CGTL", "no"));
            purchaseReturnOrderDto.setNo(OrderUtils.countTodayByCreateTime(purchaseReturnOrdersMapper, "CGTL", "no", purchaseReturnOrderDto.getCreateTime() != null ? purchaseReturnOrderDto.getCreateTime() : LocalDateTime.now()));
        }
        return AjaxResult.success(purchaseReturnOrdersService.add(purchaseReturnOrderDto));
    }
src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java
@@ -133,6 +133,9 @@
    private List<SalesLedgerProduct> productData;
    @Schema(description = "批量处理采购台账ID列表")
    private List<Long> ids;
    private List<String> tempFileIds;
    private List<CommonFile> SalesLedgerFiles;
src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java
@@ -157,5 +157,4 @@
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/purchase/pojo/PurchaseReturnOrders.java
@@ -82,7 +82,6 @@
    private BigDecimal totalAmount;
    @Schema(description = "录入时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
src/main/java/com/ruoyi/purchase/service/IPurchaseLedgerService.java
@@ -4,11 +4,13 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import org.springframework.web.multipart.MultipartFile;
import java.util.Date;
import java.util.List;
/**
@@ -22,6 +24,8 @@
    List<PurchaseLedger> selectPurchaseLedgerList(PurchaseLedger purchaseLedger);
    int addOrEditPurchase(PurchaseLedgerDto purchaseLedgerDto) throws Exception;
    R batchInsertPurchaseSteps(List<Long> ids);
    void addQualityInspect(PurchaseLedger purchaseLedger, SalesLedgerProduct saleProduct);
@@ -39,7 +43,7 @@
    IPage<PurchaseLedgerDto> selectPurchaseLedgerListPage(IPage ipage, PurchaseLedgerDto purchaseLedger);
    String getPurchaseNo();
    String getPurchaseNo(Date entryDate);
    AjaxResult importData(MultipartFile file);
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -6,8 +6,12 @@
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.vo.ApproveProcessVO;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.mapper.ApprovalTemplateMapper;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.approve.service.ApprovalInstanceService;
import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
@@ -18,16 +22,21 @@
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.enums.ApprovalStatusEnum;
import com.ruoyi.common.enums.ReviewStatusEnum;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
@@ -44,13 +53,15 @@
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
import com.ruoyi.quality.service.IQualityInspectService;
import com.ruoyi.sales.mapper.CommonFileMapper;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.service.impl.CommonFileServiceImpl;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.service.StockInRecordService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
@@ -100,6 +111,11 @@
    private final ApproveProcessServiceImpl approveProcessService;
    private final ProcurementRecordMapper procurementRecordStorageMapper;
    private final FileUtil fileUtil;
    private final ApprovalInstanceService approvalInstanceService;
    private final IQualityInspectService qualityInspectService;
    private final StockInRecordService stockInRecordService;
    private final StockUtils stockUtils;
    private final ApprovalTemplateMapper approvalTemplateMapper;
    @Override
    public List<PurchaseLedger> selectPurchaseLedgerList(PurchaseLedger purchaseLedger) {
@@ -174,6 +190,188 @@
        return 1;
    }
    @Override
    public R batchInsertPurchaseSteps(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return R.fail("请选择采购台账");
        }
        List<Long> distinctIds = ids.stream()
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (CollectionUtils.isEmpty(distinctIds)) {
            return R.fail("请选择采购台账");
        }
        PurchaseLedgerDto queryDto = new PurchaseLedgerDto();
        queryDto.setIds(distinctIds);
        IPage<PurchaseLedgerDto> pageResult = this.selectPurchaseLedgerListPage(
                new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(1, distinctIds.size()),
                queryDto
        );
        List<PurchaseLedgerDto> ledgerDtos = pageResult == null || pageResult.getRecords() == null
                ? Collections.emptyList()
                : pageResult.getRecords();
        Map<Long, PurchaseLedgerDto> ledgerDtoMap = ledgerDtos.stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(PurchaseLedgerDto::getId, item -> item, (left, right) -> left, LinkedHashMap::new));
        List<Map<String, Object>> details = new ArrayList<>();
        int successCount = 0;
        int skipCount = 0;
        int failCount = 0;
        int autoApprovedCount = 0;
        for (Long id : distinctIds) {
            Map<String, Object> detail = new LinkedHashMap<>();
            detail.put("purchaseLedgerId", id);
            PurchaseLedgerDto purchaseLedgerDto = ledgerDtoMap.get(id);
            if (purchaseLedgerDto == null) {
                failCount++;
                detail.put("status", "FAIL");
                detail.put("message", "采购台账不存在或未查询到");
                details.add(detail);
                continue;
            }
            detail.put("purchaseContractNumber", purchaseLedgerDto.getPurchaseContractNumber());
            detail.put("approvalStatus", purchaseLedgerDto.getApprovalStatus());
            detail.put("stockInStatus", purchaseLedgerDto.getStockInStatus());
            if (ApprovalStatusEnum.REJECTED.getCode().equals(purchaseLedgerDto.getApprovalStatus())) {
                skipCount++;
                detail.put("status", "SKIP");
                detail.put("message", "采购单已驳回");
                details.add(detail);
                continue;
            }
            if ("完全入库".equals(purchaseLedgerDto.getStockInStatus())) {
                skipCount++;
                detail.put("status", "SKIP");
                detail.put("message", "采购单已完全入库");
                details.add(detail);
                continue;
            }
            try {
                PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectById(id);
                if (purchaseLedger == null) {
                    failCount++;
                    detail.put("status", "FAIL");
                    detail.put("message", "采购台账不存在");
                    details.add(detail);
                    continue;
                }
                if (!ApprovalStatusEnum.APPROVED.getCode().equals(purchaseLedger.getApprovalStatus())) {
                    ApprovalInstance approvalInstance = approvalInstanceService.getOne(
                            Wrappers.<ApprovalInstance>lambdaQuery()
                                    .eq(ApprovalInstance::getBusinessId, purchaseLedger.getId())
                                    .eq(ApprovalInstance::getBusinessType, 5L)
                                    .eq(ApprovalInstance::getDeleted, 0)
                                    .orderByDesc(ApprovalInstance::getId)
                                    .last("limit 1")
                    );
                    if (approvalInstance == null) {
                        failCount++;
                        detail.put("status", "FAIL");
                        detail.put("message", "未找到对应的采购审批实例");
                        details.add(detail);
                        continue;
                    }
                    if ("APPROVED".equals(approvalInstance.getStatus())
                            && !ApprovalStatusEnum.APPROVED.getCode().equals(purchaseLedger.getApprovalStatus())) {
                        purchaseLedger.setApprovalStatus(ApprovalStatusEnum.APPROVED.getCode());
                        purchaseLedgerMapper.updateById(purchaseLedger);
                    } else if (!"APPROVED".equals(approvalInstance.getStatus())) {
                        R autoApproveResult = approvalInstanceService.autoApprove(approvalInstance.getId());
                        if (autoApproveResult == null || !R.isSuccess(autoApproveResult)) {
                            failCount++;
                            detail.put("status", "FAIL");
                            detail.put("message", autoApproveResult == null ? "采购审批自动通过失败" : autoApproveResult.getMsg());
                            details.add(detail);
                            continue;
                        }
                        autoApprovedCount++;
                    }
                    purchaseLedger = purchaseLedgerMapper.selectById(id);
                    if (purchaseLedger == null || !ApprovalStatusEnum.APPROVED.getCode().equals(purchaseLedger.getApprovalStatus())) {
                        failCount++;
                        detail.put("status", "FAIL");
                        detail.put("message", "采购单审批状态未更新为已通过");
                        details.add(detail);
                        continue;
                    }
                }
                List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(
                        Wrappers.<SalesLedgerProduct>lambdaQuery()
                                .eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedger.getId())
                                .eq(SalesLedgerProduct::getType, 2)
                                .orderByAsc(SalesLedgerProduct::getId)
                );
                if (CollectionUtils.isEmpty(products)) {
                    skipCount++;
                    detail.put("status", "SKIP");
                    detail.put("message", "采购单没有产品明细");
                    details.add(detail);
                    continue;
                }
                int processedProductCount = 0;
                int skippedProductCount = 0;
                int failedProductCount = 0;
                for (SalesLedgerProduct product : products) {
                    try {
                        boolean processed;
                        if (Boolean.TRUE.equals(product.getIsChecked())) {
                            processed = processPurchaseQualityProduct(purchaseLedger, product);
                        } else {
                            processed = processPurchaseDirectProduct(purchaseLedger, product);
                        }
                        if (processed) {
                            processedProductCount++;
                        } else {
                            skippedProductCount++;
                        }
                    } catch (Exception ex) {
                        failedProductCount++;
                        log.error("批量推进采购台账失败, purchaseLedgerId={}, productId={}, productModelId={}",
                                purchaseLedger.getId(), product.getId(), product.getProductModelId(), ex);
                    }
                }
                successCount++;
                detail.put("status", failedProductCount > 0 ? "PARTIAL" : "SUCCESS");
                detail.put("processedProductCount", processedProductCount);
                detail.put("skippedProductCount", skippedProductCount);
                detail.put("failedProductCount", failedProductCount);
                detail.put("message", failedProductCount > 0 ? "部分产品处理失败" : "处理完成");
                details.add(detail);
            } catch (Exception ex) {
                failCount++;
                detail.put("status", "FAIL");
                detail.put("message", ex.getMessage());
                details.add(detail);
                log.error("批量推进采购台账失败, purchaseLedgerId={}", id, ex);
            }
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("totalCount", distinctIds.size());
        summary.put("successCount", successCount);
        summary.put("skipCount", skipCount);
        summary.put("failCount", failCount);
        summary.put("autoApprovedCount", autoApprovedCount);
        summary.put("details", details);
        return R.ok(summary, "批量推进采购步骤完成");
    }
    public void addQualityInspect(PurchaseLedger purchaseLedger, SalesLedgerProduct saleProduct) {
        QualityInspect qualityInspect = new QualityInspect();
@@ -200,6 +398,218 @@
                        param.setInspectId(qualityInspect.getId());
                        qualityInspectParamMapper.insert(param);
                    });
        }
    }
    private boolean processPurchaseQualityProduct(PurchaseLedger purchaseLedger, SalesLedgerProduct product) {
        if (purchaseLedger == null || product == null || product.getProductModelId() == null) {
            return false;
        }
        QualityInspect qualityInspect = findLatestPurchaseQualityInspect(purchaseLedger.getId(), product.getProductModelId());
        if (qualityInspect == null) {
            addQualityInspect(purchaseLedger, product);
            qualityInspect = findLatestPurchaseQualityInspect(purchaseLedger.getId(), product.getProductModelId());
        }
        if (qualityInspect == null) {
            return false;
        }
        LocalDateTime purchaseInspectTime = toStartOfDayPlusDays(purchaseLedger.getEntryDate(), 1);
        if (purchaseInspectTime != null && (qualityInspect.getCheckTime() == null
                || !DateUtils.toLocalDate(qualityInspect.getCheckTime()).equals(purchaseInspectTime.toLocalDate()))) {
            qualityInspect.setCheckTime(DateUtils.toDate(purchaseInspectTime.toLocalDate()));
            qualityInspectMapper.updateById(qualityInspect);
        }
        List<StockInRecord> stockRecords = findQualityStockRecords(qualityInspect.getId(), product.getProductModelId());
        if (hasApprovedStockRecord(stockRecords)) {
            return true;
        }
        if (!Integer.valueOf(1).equals(qualityInspect.getInspectState())) {
            R autoSubmitResult = qualityInspectService.autoSubmit(qualityInspect.getId());
            if (autoSubmitResult == null || !R.isSuccess(autoSubmitResult)) {
                return false;
            }
            qualityInspect = qualityInspectMapper.selectById(qualityInspect.getId());
        }
        stockRecords = findQualityStockRecords(qualityInspect.getId(), product.getProductModelId());
        if (CollectionUtils.isEmpty(stockRecords)
                && qualityInspect.getQualifiedQuantity() != null
                && qualityInspect.getQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
            stockUtils.addStockWithBatchNo(
                    product.getProductModelId(),
                    qualityInspect.getQualifiedQuantity(),
                    StockInQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_OUT.getCode(),
                    qualityInspect.getId(),
                    null,
                    purchaseInspectTime == null ? null : purchaseInspectTime.plusDays(1)
            );
            stockRecords = findQualityStockRecords(qualityInspect.getId(), product.getProductModelId());
        }
        StockInRecord targetStockRecord = findLatestUnapprovedStockRecord(stockRecords);
        if (targetStockRecord == null) {
            return false;
        }
        LocalDateTime qualityStockCreateTime = resolveQualityStockCreateTime(qualityInspect);
        if (qualityStockCreateTime != null && (targetStockRecord.getCreateTime() == null
                || !qualityStockCreateTime.equals(targetStockRecord.getCreateTime()))) {
            targetStockRecord.setCreateTime(qualityStockCreateTime);
            stockInRecordService.updateById(targetStockRecord);
        }
        approveStockRecords(Collections.singletonList(targetStockRecord));
        stockRecords = findQualityStockRecords(qualityInspect.getId(), product.getProductModelId());
        return hasApprovedStockRecord(stockRecords);
    }
    private boolean processPurchaseDirectProduct(PurchaseLedger purchaseLedger, SalesLedgerProduct product) {
        if (purchaseLedger == null || product == null || product.getProductModelId() == null) {
            return false;
        }
        if (product.getQuantity() == null) {
            return false;
        }
        if (!StringUtils.hasText(purchaseLedger.getPurchaseContractNumber())) {
            return false;
        }
        LocalDateTime stockCreateTime = toStartOfDayPlusDays(purchaseLedger.getEntryDate(), 1);
        List<StockInRecord> stockRecords = findDirectStockRecords(purchaseLedger.getId(), purchaseLedger.getPurchaseContractNumber(), product.getProductModelId(), product.getId());
        if (hasApprovedStockRecord(stockRecords)) {
            return true;
        }
        if (CollectionUtils.isEmpty(stockRecords)) {
            stockUtils.addStockWithBatchNo(
                    product.getProductModelId(),
                    product.getQuantity(),
                    StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(),
                    purchaseLedger.getId(),
                    purchaseLedger.getPurchaseContractNumber() + "-" + product.getId(),
                    stockCreateTime
            );
            stockRecords = findDirectStockRecords(purchaseLedger.getId(), purchaseLedger.getPurchaseContractNumber(), product.getProductModelId(), product.getId());
        }
        if (CollectionUtils.isEmpty(stockRecords)) {
            return false;
        }
        StockInRecord targetStockRecord = findLatestUnapprovedStockRecord(stockRecords);
        if (targetStockRecord == null) {
            return false;
        }
        if (stockCreateTime != null && (targetStockRecord.getCreateTime() == null
                || !stockCreateTime.equals(targetStockRecord.getCreateTime()))) {
            targetStockRecord.setCreateTime(stockCreateTime);
            stockInRecordService.updateById(targetStockRecord);
        }
        approveStockRecords(Collections.singletonList(targetStockRecord));
        stockRecords = findDirectStockRecords(purchaseLedger.getId(), purchaseLedger.getPurchaseContractNumber(), product.getProductModelId(), product.getId());
        return hasApprovedStockRecord(stockRecords);
    }
    private LocalDateTime toStartOfDayPlusDays(Date date, int days) {
        if (date == null) {
            return null;
        }
        return DateUtils.toLocalDate(date).plusDays(days).atStartOfDay();
    }
    private LocalDateTime resolveQualityStockCreateTime(QualityInspect qualityInspect) {
        if (qualityInspect == null || qualityInspect.getCheckTime() == null) {
            return null;
        }
        return DateUtils.toLocalDate(qualityInspect.getCheckTime()).plusDays(1).atStartOfDay();
    }
    private QualityInspect findLatestPurchaseQualityInspect(Long purchaseLedgerId, Long productModelId) {
        if (purchaseLedgerId == null || productModelId == null) {
            return null;
        }
        return qualityInspectMapper.selectOne(
                Wrappers.<QualityInspect>lambdaQuery()
                        .eq(QualityInspect::getInspectType, 0)
                        .eq(QualityInspect::getPurchaseLedgerId, purchaseLedgerId)
                        .eq(QualityInspect::getProductModelId, productModelId)
                        .orderByDesc(QualityInspect::getId)
                        .last("limit 1")
        );
    }
    private List<StockInRecord> findQualityStockRecords(Long qualityInspectId, Long productModelId) {
        if (qualityInspectId == null || productModelId == null) {
            return Collections.emptyList();
        }
        return stockInRecordService.list(
                Wrappers.<StockInRecord>lambdaQuery()
                        .eq(StockInRecord::getRecordType, StockInQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_OUT.getCode())
                        .eq(StockInRecord::getRecordId, qualityInspectId)
                        .eq(StockInRecord::getProductModelId, productModelId)
                        .orderByDesc(StockInRecord::getId)
        );
    }
    private List<StockInRecord> findDirectStockRecords(Long purchaseLedgerId, String purchaseContractNumber, Long productModelId, Long purchaseProductId) {
        if (purchaseLedgerId == null || productModelId == null || purchaseProductId == null || !StringUtils.hasText(purchaseContractNumber)) {
            return Collections.emptyList();
        }
        String batchNo = purchaseContractNumber + "-" + purchaseProductId;
        return stockInRecordService.list(
                Wrappers.<StockInRecord>lambdaQuery()
                        .eq(StockInRecord::getRecordType, StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode())
                        .eq(StockInRecord::getRecordId, purchaseLedgerId)
                        .eq(StockInRecord::getProductModelId, productModelId)
                        .eq(StockInRecord::getBatchNo, batchNo)
                        .orderByDesc(StockInRecord::getId)
        );
    }
    private boolean hasApprovedStockRecord(List<StockInRecord> stockRecords) {
        return stockRecords != null && stockRecords.stream()
                .anyMatch(item -> ReviewStatusEnum.APPROVED.getCode().equals(item.getApprovalStatus()));
    }
    private StockInRecord findLatestUnapprovedStockRecord(List<StockInRecord> stockRecords) {
        if (CollectionUtils.isEmpty(stockRecords)) {
            return null;
        }
        return stockRecords.stream()
                .filter(item -> !ReviewStatusEnum.APPROVED.getCode().equals(item.getApprovalStatus()))
                .findFirst()
                .orElse(null);
    }
    private void approveStockRecords(List<StockInRecord> stockRecords) {
        if (CollectionUtils.isEmpty(stockRecords)) {
            return;
        }
        List<Long> rejectedIds = stockRecords.stream()
                .filter(item -> ReviewStatusEnum.REJECTED.getCode().equals(item.getApprovalStatus()))
                .map(StockInRecord::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (!rejectedIds.isEmpty()) {
            stockInRecordService.batchReAudit(rejectedIds);
        }
        List<Long> pendingIds = stockRecords.stream()
                .filter(item -> item.getApprovalStatus() == null
                        || ReviewStatusEnum.PENDING_REVIEW.getCode().equals(item.getApprovalStatus())
                        || ReviewStatusEnum.REJECTED.getCode().equals(item.getApprovalStatus()))
                .map(StockInRecord::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (!pendingIds.isEmpty()) {
            stockInRecordService.batchApprove(pendingIds, ReviewStatusEnum.APPROVED.getCode());
        }
    }
@@ -455,12 +865,13 @@
    }
    @Override
    public String getPurchaseNo() {
    public String getPurchaseNo(Date entryDate) {
        LocalDate localDate = entryDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        // ç”Ÿæˆæ—¥æœŸå‰ç¼€ï¼ˆä¾‹å¦‚:CG20250405)
        String purchaseNo = "CG" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String purchaseNo = "CG" + localDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        // æž„建 Redis Key(按天分隔)
        String redisKey = "purchase_no:" + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
        String redisKey = "purchase_no:" + localDate.format(DateTimeFormatter.ISO_LOCAL_DATE);
        // èŽ·å–å½“å‰åºå·å¹¶é€’å¢žï¼ˆåŽŸå­æ“ä½œï¼‰
        Long sequence = redisTemplate.opsForValue().increment(redisKey);
@@ -616,14 +1027,28 @@
        if (loginUser == null) {
            return;
        }
        ApproveProcessVO approveProcessVO = new ApproveProcessVO();
        approveProcessVO.setApproveType(5);
        approveProcessVO.setApproveDeptId(loginUser.getCurrentDeptId());
        approveProcessVO.setApproveReason(purchaseLedger.getPurchaseContractNumber());
        approveProcessVO.setApproveUserIds(purchaseLedger.getApproveUserIds());
        approveProcessVO.setApproveUser(loginUser.getUserId());
        approveProcessVO.setApproveTime(LocalDate.now().toString());
        approveProcessService.addApprove(approveProcessVO);
        ApprovalTemplate approvalTemplate = approvalTemplateMapper.selectOne(
                new LambdaQueryWrapper<ApprovalTemplate>()
                        .eq(ApprovalTemplate::getBusinessType, 5L)
                        .orderByDesc(ApprovalTemplate::getId)
                        .last("LIMIT 1")
        );
        if (approvalTemplate == null) {
            throw new BaseException("请先配置采购审批模板");
        }
        ApprovalInstanceDto approvalInstance = new ApprovalInstanceDto();
        approvalInstance.setTemplateId(approvalTemplate.getId());
        approvalInstance.setTemplateName(approvalTemplate.getTemplateName());
        approvalInstance.setBusinessId(purchaseLedger.getId());
        approvalInstance.setBusinessType(5L);
        approvalInstance.setCurrentLevel(1);
        approvalInstance.setApplicantId(loginUser.getUserId());
        approvalInstance.setTitle(purchaseLedger.getPurchaseContractNumber()+"审批");
        approvalInstance.setApplicantName(loginUser.getNickName());
        approvalInstance.setApplyTime(LocalDateTime.now());
        approvalInstanceService.add(approvalInstance);
    }
    /**
src/main/java/com/ruoyi/quality/controller/QualityTestStandardParamController.java
@@ -80,7 +80,9 @@
    @Operation(summary = "检测指标维护查询")
    @Log(title = "检测指标维护查询", businessType = BusinessType.OTHER)
    public R<?> list(Long testStandardId) {
        return R.ok(qualityTestStandardParamService.list(Wrappers.<QualityTestStandardParam>lambdaQuery().eq(QualityTestStandardParam::getTestStandardId,testStandardId)));
        return R.ok(qualityTestStandardParamService.list(Wrappers.<QualityTestStandardParam>lambdaQuery()
                .eq(QualityTestStandardParam::getTestStandardId, testStandardId)
                .orderByDesc(QualityTestStandardParam::getCreateTime)));
    }
}
src/main/java/com/ruoyi/quality/service/IQualityInspectService.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.dto.QualityInspectDto;
import com.ruoyi.quality.pojo.QualityInspect;
@@ -23,5 +24,7 @@
    int submit(QualityInspect qualityInspect);
    R autoSubmit(Long id);
    void down(HttpServletResponse response, QualityInspect qualityInspect);
}
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -11,8 +11,10 @@
import com.deepoove.poi.config.Configure;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.HackLoopTableRenderPolicy;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.procurementrecord.service.ProcurementRecordService;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.quality.dto.QualityInspectDto;
@@ -39,6 +41,8 @@
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -123,6 +127,10 @@
            stockInventoryDto.setRecordId(qualityInspect.getId());
            stockInventoryDto.setProductModelId(qualityInspect.getProductModelId());
            stockInventoryDto.setQualitity(qualityInspect.getQualifiedQuantity());
            if (qualityInspect.getCheckTime() != null) {
                LocalDate stockCreateDate = DateUtils.toLocalDate(qualityInspect.getCheckTime()).plusDays(1);
                stockInventoryDto.setCreateTime(LocalDateTime.of(stockCreateDate, java.time.LocalTime.MIDNIGHT));
            }
            stockInventoryDto.setBatchNo(resolveProductionBatchNo(
                    qualityInspect.getProductMainId(),
                    qualityInspect.getId(),
@@ -147,6 +155,33 @@
        return qualityInspectMapper.updateById(qualityInspect);
    }
    @Override
    public R autoSubmit(Long id) {
        if (id == null) {
            return R.fail("检验单ID不能为空");
        }
        QualityInspect qualityInspect = qualityInspectMapper.selectById(id);
        if (qualityInspect == null) {
            return R.fail("检验单不存在");
        }
        if (Integer.valueOf(1).equals(qualityInspect.getInspectState())) {
            return R.ok("检验单已提交");
        }
        if (ObjectUtils.isNull(qualityInspect.getCheckResult())) {
            qualityInspect.setCheckResult("合格");
        }
        if (ObjectUtils.isNull(qualityInspect.getQualifiedQuantity())) {
            qualityInspect.setQualifiedQuantity(qualityInspect.getQuantity() == null ? BigDecimal.ZERO : qualityInspect.getQuantity());
        }
        if (ObjectUtils.isNull(qualityInspect.getUnqualifiedQuantity())) {
            qualityInspect.setUnqualifiedQuantity(BigDecimal.ZERO);
        }
        qualityInspectMapper.updateById(qualityInspect);
        int rows = submit(qualityInspect);
        return rows > 0 ? R.ok("检验单提交成功") : R.fail("检验单提交失败");
    }
    private String resolveProductionBatchNo(Long productionProductMainId,
                                            Long qualityInspectId,
                                            Long productModelId) {
src/main/java/com/ruoyi/quality/utils/QualityInspectHelper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package com.ruoyi.quality.utils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityInspectParamMapper;
import com.ruoyi.quality.mapper.QualityTestStandardMapper;
import com.ruoyi.quality.mapper.QualityTestStandardParamMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.List;
/**
 * è´¨æ£€å•创建工具类
 */
@Component
@RequiredArgsConstructor
public class QualityInspectHelper {
    private final QualityInspectMapper qualityInspectMapper;
    private final QualityTestStandardMapper qualityTestStandardMapper;
    private final QualityTestStandardParamMapper qualityTestStandardParamMapper;
    private final QualityInspectParamMapper qualityInspectParamMapper;
    /**
     * åˆ›å»ºè´¨æ£€å•
     * @param purchaseLedger é‡‡è´­å°è´¦
     * @param saleProduct é‡‡è´­äº§å“
     */
    public void addQualityInspect(PurchaseLedger purchaseLedger, SalesLedgerProduct saleProduct) {
        QualityInspect qualityInspect = new QualityInspect();
        qualityInspect.setInspectType(0);
        qualityInspect.setSupplier(purchaseLedger.getSupplierName());
        qualityInspect.setPurchaseLedgerId(purchaseLedger.getId());
        qualityInspect.setProductId(saleProduct.getProductId());
        qualityInspect.setProductName(saleProduct.getProductCategory());
        qualityInspect.setModel(saleProduct.getSpecificationModel());
        qualityInspect.setProductModelId(saleProduct.getProductModelId());
        qualityInspect.setUnit(saleProduct.getUnit());
        qualityInspect.setQuantity(saleProduct.getQuantity());
        qualityInspectMapper.insert(qualityInspect);
        List<QualityTestStandard> qualityTestStandardList = qualityTestStandardMapper
                .getQualityTestStandardByProductId(saleProduct.getProductId(), 0, null);
        if (qualityTestStandardList.isEmpty()) {
            return;
        }
        QualityTestStandard firstStandard = qualityTestStandardList.get(0);
        qualityInspect.setTestStandardId(firstStandard.getId());
        qualityInspectMapper.updateById(qualityInspect);
        List<QualityTestStandardParam> standardParams = qualityTestStandardParamMapper.selectList(
                Wrappers.<QualityTestStandardParam>lambdaQuery()
                        .eq(QualityTestStandardParam::getTestStandardId, firstStandard.getId()));
        for (QualityTestStandardParam standardParam : standardParams) {
            QualityInspectParam param = new QualityInspectParam();
            BeanUtils.copyProperties(standardParam, param);
            param.setId(null);
            param.setInspectId(qualityInspect.getId());
            qualityInspectParamMapper.insert(param);
        }
    }
}
src/main/java/com/ruoyi/sales/controller/PaymentShippingController.java
@@ -17,6 +17,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -44,7 +45,7 @@
    @Operation(summary = "添加支付与发货信息")
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult add(@RequestBody PaymentShipping paymentShipping) {
        String ord = OrderUtils.countTodayByCreateTime(paymentShippingMapper, "ORD","order_no");
        String ord = OrderUtils.countTodayByCreateTime(paymentShippingMapper, "ORD","order_no", paymentShipping.getCreateTime() != null ? paymentShipping.getCreateTime() : LocalDateTime.now());
        paymentShipping.setOrderNo(ord);
        boolean save = paymentShippingService.save(paymentShipping);
        return save ? success() : error();
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java
@@ -262,7 +262,7 @@
            //  å¦‚果已经有过开票或回款操作,则不允许编辑
            boolean hasReceiptOperation = receiptPaymentAmountTotal.compareTo(BigDecimal.ZERO) > 0;
            salesLedgerVo.setIsEdit(hasReceiptOperation);
            salesLedgerVo.setIsEdit(!hasReceiptOperation);
            salesLedgerVo.setStorageBlobVOs(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.FILE, RecordTypeEnum.SALES_LEDGER, ledgerId));
        }
src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java
@@ -2,14 +2,10 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.vo.ApproveProcessVO;
import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
@@ -25,7 +21,6 @@
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.time.LocalDate;
import java.util.List;
/**
@@ -55,22 +50,7 @@
    @Transactional(rollbackFor = Exception.class)
    @Log(title = "发货信息管理", businessType = BusinessType.INSERT)
    public AjaxResult add(@RequestBody ShippingInfoDto req) throws Exception {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String sh = OrderUtils.countTodayByCreateTime(shippingInfoMapper, "SH","shipping_no");
        // å‘货审批
        ApproveProcessVO approveProcessVO = new ApproveProcessVO();
        approveProcessVO.setApproveType(7);
        approveProcessVO.setApproveDeptId(loginUser.getCurrentDeptId());
        approveProcessVO.setApproveReason(sh);//发货编号
        approveProcessVO.setApproveUserIds(req.getApproveUserIds());
        approveProcessVO.setApproveUser(loginUser.getUserId());
        approveProcessVO.setApproveTime(LocalDate.now().toString());
        approveProcessService.addApprove(approveProcessVO);
        // æ·»åŠ å‘è´§æ¶ˆæ¯
        req.setShippingNo(sh);
        req.setStatus("待审核");
        boolean save = shippingInfoService.add(req);
        return save ? AjaxResult.success() : AjaxResult.error();
        return AjaxResult.success(shippingInfoService.addReq(req) ? "添加成功" : "添加失败");
    }
    @Operation(summary = "发货扣库存")
src/main/java/com/ruoyi/sales/dto/SalesQuotationDto.java
@@ -17,4 +17,6 @@
     */
    // å®¡æ‰¹äºº
    private String approveUserIds;
    private Long templateId;
}
src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java
@@ -47,5 +47,9 @@
    //发货数量
    private BigDecimal totalQuantity;
    private Long templateId;
    private String templateName;
}
src/main/java/com/ruoyi/sales/pojo/PaymentShipping.java
@@ -60,7 +60,6 @@
    private String remark;
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @Schema(description = "创建用户")
src/main/java/com/ruoyi/sales/pojo/SalesQuotation.java
@@ -52,7 +52,6 @@
    @ApiModelProperty(value = "备注")
    private String remark;
    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @ApiModelProperty(value = "修改时间")
src/main/java/com/ruoyi/sales/pojo/ShippingInfo.java
@@ -68,7 +68,6 @@
    private String shippingCarNumber;
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @Schema(description = "修改时间")
src/main/java/com/ruoyi/sales/service/ShippingInfoService.java
@@ -31,4 +31,6 @@
    List<ShippingProductDetailDto> getDetail(Long id);
    ShippingApproveDto getDateilByShippingNo(String shippingNo);
    boolean addReq(ShippingInfoDto req);
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
@@ -35,6 +35,7 @@
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
@@ -229,6 +230,20 @@
            productionPlan.setPromisedDeliveryDate(salesLedger.getDeliveryDate());//承诺日期=交货日期
            productionPlanMapper.insert(productionPlan);
        }
        SalesLedger salesLedger = salesLedgerMapper.selectById(salesLedgerProduct.getSalesLedgerId());
        ProductionPlan productionPlan = new ProductionPlan();
        productionPlan.setSalesLedgerId(salesLedgerProduct.getSalesLedgerId());
        productionPlan.setSalesLedgerProductId(salesLedgerProduct.getId());
        productionPlan.setMpsNo(generateNextPlanNo(salesLedger.getEntryDate().toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDate().format(DateTimeFormatter.ofPattern("yyyyMMdd"))));
        productionPlan.setProductModelId(salesLedgerProduct.getProductModelId());
        productionPlan.setQtyRequired(salesLedgerProduct.getQuantity());
        productionPlan.setSource("销售");
        productionPlan.setStatus(0);
        productionPlan.setRequiredDate(salesLedger.getDeliveryDate());//需求日期=交货日期
        productionPlan.setPromisedDeliveryDate(salesLedger.getDeliveryDate());//承诺日期=交货日期
        productionPlanMapper.insert(productionPlan);
    }
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -63,6 +63,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.io.InputStream;
import java.lang.reflect.Field;
@@ -334,9 +335,17 @@
            if (CollectionUtils.isEmpty(salesLedgerImportDtoList)) return AjaxResult.error("销售台账数据为空!");
            List<SalesLedgerImportDto> salesLedgerProductImportDtoList = stringListMap.get("销售产品数据");
            if (CollectionUtils.isEmpty(salesLedgerProductImportDtoList)) return AjaxResult.error("销售产品数据为空!");
            // å®¢æˆ·æ•°æ®
            List<Customer> customers = customerMapper.selectList(new LambdaQueryWrapper<Customer>().in(Customer::getCustomerName,
                    salesLedgerImportDtoList.stream().map(SalesLedgerImportDto::getCustomerName).collect(Collectors.toList())));
            // å®¢æˆ·æ•°æ® - å‚考 listPage æŸ¥è¯¢ç§æµ·å®¢æˆ·ï¼ˆtype = 0)
            // type = 0(私海客户)或者 type = 1(公海客户)且已被分配,并且是自己领用、自己创建或者共享给自己的客户
            Long loginUserId = loginUser.getUser().getUserId();
            List<Customer> customers = customerMapper.selectList(new QueryWrapper<Customer>()
                    .in("customer_name", salesLedgerImportDtoList.stream()
                            .map(SalesLedgerImportDto::getCustomerName).collect(Collectors.toList()))
                    .and(wrapper -> wrapper.eq("type", 0)
                            .or(wrapper2 -> wrapper2.eq("type", 1).eq("is_assigned", 1)))
                    .and(wrapper -> wrapper.eq("usage_user", loginUserId)
                            .or(wrapper2 -> wrapper2.eq("create_user", loginUserId)
                                    .or(wrapper3 -> wrapper3.exists("select 1 from customer_user cu where cu.customer_id = customer.id and cu.user_id = " + loginUserId)))));
//            // è§„格型号数据
//            List<ProductModel> productModels = productModelMapper.selectList(new LambdaQueryWrapper<ProductModel>().in(ProductModel::getModel,
//                    salesLedgerProductImportDtoList.stream().map(SalesLedgerImportDto::getSpecificationModel).collect(Collectors.toList())));
@@ -354,21 +363,26 @@
                if (salesLedger1 != null) {
                    continue;
                }
                // åˆ¤æ–­ä¸šåŠ¡å‘˜æ˜¯å¦å­˜åœ¨
                SysUser salesman = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
                        .eq(SysUser::getNickName, salesLedgerImportDto.getSalesman()));
                if (salesman == null) {
                    throw new RuntimeException("业务员:" + salesLedgerImportDto.getSalesman() + "不存在!");
                }
                SalesLedger salesLedger = new SalesLedger();
                BeanUtils.copyProperties(salesLedgerImportDto, salesLedger);
                salesLedger.setExecutionDate(DateUtils.toLocalDate(salesLedgerImportDto.getExecutionDate()));
                salesLedger.setDeliveryDate(DateUtils.toLocalDate(salesLedgerImportDto.getDeliveryDate()));
                // é€šè¿‡å®¢æˆ·åç§°æŸ¥è¯¢å®¢æˆ·ID,客户合同号
                salesLedger.setCustomerId(customers.stream()
                Optional<Customer> customerOptional = customers.stream()
                        .filter(customer -> customer.getCustomerName().equals(salesLedger.getCustomerName()))
                        .findFirst()
                        .map(Customer::getId)
                        .orElse(null));
                salesLedger.setCustomerContractNo(customers.stream()
                        .filter(customer -> customer.getCustomerName().equals(salesLedger.getCustomerName()))
                        .findFirst()
                        .map(Customer::getTaxpayerIdentificationNumber)
                        .orElse(null));
                        .findFirst();
                if (customerOptional.isEmpty()) {
                    throw new RuntimeException("客户:" + salesLedger.getCustomerName() + "不存在!或者非私海用户");
                }
                salesLedger.setCustomerId(customerOptional.get().getId());
                salesLedger.setCustomerContractNo(customerOptional.get().getTaxpayerIdentificationNumber());
                Long aLong = sysUsers.stream()
                        .filter(sysUser -> sysUser.getNickName().equals(salesLedger.getEntryPerson()))
                        .findFirst()
@@ -396,13 +410,16 @@
                    salesLedgerProduct.setType(1);
                    // è®¡ç®—不含税总价
                    salesLedgerProduct.setTaxExclusiveTotalPrice(salesLedgerProduct.getTaxInclusiveTotalPrice().divide(new BigDecimal(1).add(salesLedgerProduct.getTaxRate().divide(new BigDecimal(100))), 2, RoundingMode.HALF_UP));
                    list.stream()
                    // æ ¡éªŒäº§å“è§„格是否存在
                    Optional<Map<String, Object>> productModelOptional = list.stream()
                            .filter(map -> Objects.equals(map.get("productName"), salesLedgerProduct.getProductCategory()) && Objects.equals(map.get("model"), salesLedgerProduct.getSpecificationModel()))
                            .findFirst()
                            .ifPresent(map -> {
                                salesLedgerProduct.setProductModelId(Long.parseLong(map.get("modelId").toString()));
                                salesLedgerProduct.setProductId(Long.parseLong(map.get("id").toString()));
                            });
                            .findFirst();
                    if (productModelOptional.isEmpty()) {
                        throw new RuntimeException("产品大类:" + salesLedgerProduct.getProductCategory() + ",规格型号:" + salesLedgerProduct.getSpecificationModel() + "不存在!");
                    }
                    Map<String, Object> productModelMap = productModelOptional.get();
                    salesLedgerProduct.setProductModelId(Long.parseLong(productModelMap.get("modelId").toString()));
                    salesLedgerProduct.setProductId(Long.parseLong(productModelMap.get("id").toString()));
//                    salesLedgerProduct.setProductId(productList.stream()
//                            .filter(product -> product.getProductName().equals(salesLedgerProduct.getProductCategory()))
//                            .findFirst()
@@ -564,7 +581,7 @@
        if (salesLedger.getId() == null) {
            String contractNo = salesLedger.getSalesContractNo();
            if (StringUtils.isEmpty(contractNo)) {
                contractNo = generateSalesContractNo();
                contractNo = generateSalesContractNo(salesLedgerDto.getEntryDate());
            }
            salesLedger.setSalesContractNo(contractNo);
            salesLedgerMapper.insert(salesLedger);
@@ -627,8 +644,8 @@
    }
    @Transactional(readOnly = true)
    public String generateSalesContractNo() {
        LocalDate currentDate = LocalDate.now();
    public String generateSalesContractNo(Date entryDate) {
        LocalDate currentDate = entryDate != null ? DateUtils.toLocalDate(entryDate) : LocalDate.now();
        String datePart = currentDate.format(DateTimeFormatter.BASIC_ISO_DATE);
        String lockKey = LOCK_PREFIX + datePart;
        String lockValue = Thread.currentThread().getId() + "-" + System.nanoTime(); // å”¯ä¸€æ ‡è¯†é”æŒæœ‰è€…
src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java
@@ -7,10 +7,14 @@
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.vo.ApproveGetAndUpdateVo;
import com.ruoyi.approve.bean.vo.ApproveProcessVO;
import com.ruoyi.approve.mapper.ApprovalTemplateMapper;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.approve.service.ApprovalInstanceService;
import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.common.enums.IsDeleteEnum;
@@ -29,9 +33,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
@@ -44,6 +47,8 @@
    private final ApproveProcessServiceImpl approveProcessService;
    private final CustomerMapper customerMapper;
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final ApprovalInstanceService approvalInstanceService;
    @Override
    public IPage<SalesQuotationDto> listPage(Page page, SalesQuotationDto salesQuotationDto) {
@@ -51,10 +56,26 @@
        if(CollectionUtils.isEmpty(salesQuotationDtoIPage.getRecords())){
            return salesQuotationDtoIPage;
        }
        salesQuotationDtoIPage.getRecords().forEach(record -> {
            List<SalesQuotationProduct> products = salesQuotationProductMapper.selectBySalesQuotationId(record.getId());
            record.setProducts(products);
        });
        // æ‰¹é‡æŸ¥è¯¢äº§å“ï¼Œé¿å… N+1 é—®é¢˜
        List<Long> quotationIds = salesQuotationDtoIPage.getRecords().stream()
                .map(SalesQuotationDto::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (!quotationIds.isEmpty()) {
            List<SalesQuotationProduct> allProducts = salesQuotationProductMapper.selectList(
                    new LambdaQueryWrapper<SalesQuotationProduct>()
                            .in(SalesQuotationProduct::getSalesQuotationId, quotationIds)
            );
            Map<Long, List<SalesQuotationProduct>> productMap = allProducts.stream()
                    .collect(Collectors.groupingBy(SalesQuotationProduct::getSalesQuotationId));
            salesQuotationDtoIPage.getRecords().forEach(record ->
                    record.setProducts(productMap.getOrDefault(record.getId(), new ArrayList<>()))
            );
        }
        return salesQuotationDtoIPage;
    }
@@ -68,7 +89,7 @@
        if (ObjectUtils.isNotEmpty(customer))  {
            salesQuotation.setCustomer(customer.getCustomerName());
        }
        String quotationNo = OrderUtils.countTodayByCreateTime(salesQuotationMapper, "QT","quotation_no");
        String quotationNo = OrderUtils.countTodayByCreateTime(salesQuotationMapper, "QT","quotation_no", salesQuotationDto.getCreateTime() != null ? salesQuotationDto.getCreateTime() : LocalDateTime.now());
        salesQuotation.setQuotationNo(quotationNo);
        salesQuotation.setStatus("待审批");
        salesQuotationMapper.insert(salesQuotation);
@@ -83,19 +104,21 @@
        }).collect(Collectors.toList());
        salesQuotationProductService.saveBatch(products);
        // æŠ¥ä»·å®¡æ‰¹
        ApproveProcessVO approveProcessVO = new ApproveProcessVO();
        approveProcessVO.setApproveType(6);
        approveProcessVO.setApproveDeptId(loginUser.getCurrentDeptId());
        approveProcessVO.setApproveReason(quotationNo);
        approveProcessVO.setApproveUserIds(salesQuotationDto.getApproveUserIds());
        approveProcessVO.setApproveUser(loginUser.getUserId());
        approveProcessVO.setApproveTime(LocalDate.now().toString());
        approveProcessVO.setPrice(salesQuotationDto.getTotalAmount());
        ApprovalInstanceDto approvalInstance = new ApprovalInstanceDto();
        approvalInstance.setTemplateId(approvalTemplateMapper.selectOne(new LambdaQueryWrapper<ApprovalTemplate>().eq(ApprovalTemplate::getBusinessType,6L).orderByDesc(ApprovalTemplate::getId).last("LIMIT 1")).getId());
        approvalInstance.setTemplateName(approvalTemplateMapper.selectOne(new LambdaQueryWrapper<ApprovalTemplate>().eq(ApprovalTemplate::getBusinessType,6L).orderByDesc(ApprovalTemplate::getId).last("LIMIT 1")).getTemplateName());
        approvalInstance.setBusinessId(salesQuotation.getId());
        approvalInstance.setBusinessType(6L);
        approvalInstance.setCurrentLevel(1);
        approvalInstance.setTitle(quotationNo+"审批");
        approvalInstance.setApplicantId(loginUser.getUserId());
        approvalInstance.setApplicantName(loginUser.getNickName());
        approvalInstance.setApplyTime(LocalDateTime.now());
        try {
            approveProcessService.addApprove(approveProcessVO);
            approvalInstanceService.add(approvalInstance);
        }catch (Exception e){
            log.error("SalesQuotationServiceImpl error:{}", e);
            throw                                new RuntimeException("审批失败");
            log.error("SalesQuotationServiceImpl approve error for quotationNo: {}", e);
            throw new RuntimeException("审批失败: " + e.getMessage(), e);
        }
        return true;
    }
@@ -124,10 +147,25 @@
        salesQuotationProductService.saveBatch(products);
        // ä¿®æ”¹æŠ¥ä»·å®¡æ‰¹
        vo.setApproveUserIds(salesQuotationDto.getApproveUserIds());
        vo.setApproveType(6);
        vo.setApproveReason(salesQuotationDto.getQuotationNo());
        approveProcessService.updateApproveUser(vo);
        // å…ˆç»“束之前未结束的报价审批
        approvalInstanceService.lambdaUpdate().set(ApprovalInstance::getStatus,"REJECTED").eq(ApprovalInstance::getBusinessId,salesQuotation.getId()).eq(ApprovalInstance::getBusinessType,6L).update();
        ApprovalInstanceDto approvalInstance = new ApprovalInstanceDto();
        approvalInstance.setTemplateId(approvalTemplateMapper.selectOne(new LambdaQueryWrapper<ApprovalTemplate>().eq(ApprovalTemplate::getBusinessType,6L).orderByDesc(ApprovalTemplate::getId).last("LIMIT 1")).getId());
        approvalInstance.setTemplateName(approvalTemplateMapper.selectOne(new LambdaQueryWrapper<ApprovalTemplate>().eq(ApprovalTemplate::getBusinessType,6L).orderByDesc(ApprovalTemplate::getId).last("LIMIT 1")).getTemplateName());
        approvalInstance.setBusinessId(salesQuotation.getId());
        approvalInstance.setBusinessType(6L);
        approvalInstance.setCurrentLevel(1);
        approvalInstance.setTitle(salesQuotation.getQuotationNo()+"审批");
        approvalInstance.setApplicantId(SecurityUtils.getUserId());
        approvalInstance.setApplicantName(SecurityUtils.getLoginUser().getNickName());
        approvalInstance.setApplyTime(LocalDateTime.now());
        try {
            approvalInstanceService.add(approvalInstance);
        } catch (Exception e) {
            log.error("SalesQuotationServiceImpl approve error for quotationNo: {}", e);
            throw new RuntimeException("审批失败: " + e.getMessage(), e);
        }
        return true;
    }
    @Override
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -4,13 +4,20 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.mapper.ApprovalTemplateMapper;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.approve.service.ApprovalInstanceService;
import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.procurementrecord.bean.vo.ShippingProductVo;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.sales.dto.ShippingApproveDto;
@@ -28,6 +35,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -52,6 +60,8 @@
    private final ApproveProcessServiceImpl approveProcessService;
    private final FileUtil fileUtil;
    private final ShippingProductDetailMapper shippingProductDetailMapper;
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final ApprovalInstanceService approvalInstanceService;
    @Override
    public IPage<ShippingInfoDto> listPage(Page page, ShippingInfo req) {
@@ -158,4 +168,28 @@
        shippingApproveDto.setShippingProductDetailDtoList(dateilByShippingNo);
        return shippingApproveDto;
    }
    @Override
    public boolean addReq(ShippingInfoDto req) {
                LoginUser loginUser = SecurityUtils.getLoginUser();
        String sh = OrderUtils.countTodayByCreateTime(shippingInfoMapper, "SH","shipping_no",req.getCreateTime());
        // å…ˆä¿å­˜å‘货单,再发起审批;无审核人自动通过时需要按发货编号回写发货状态。
        req.setShippingNo(sh);
        req.setStatus("待审核");
        boolean save = this.add(req);
        // å‘货审批
        ApprovalInstanceDto approvalInstance = new ApprovalInstanceDto();
        approvalInstance.setTemplateId(approvalTemplateMapper.selectOne(new LambdaQueryWrapper<ApprovalTemplate>().eq(ApprovalTemplate::getBusinessType,6L).orderByDesc(ApprovalTemplate::getId).last("LIMIT 1")).getId());
        approvalInstance.setTemplateName(approvalTemplateMapper.selectOne(new LambdaQueryWrapper<ApprovalTemplate>().eq(ApprovalTemplate::getBusinessType,6L).orderByDesc(ApprovalTemplate::getId).last("LIMIT 1")).getTemplateName());
        approvalInstance.setBusinessId(req.getId());
        approvalInstance.setBusinessType(7L);
        approvalInstance.setCurrentLevel(1);
        approvalInstance.setTitle(sh+"审批");
        approvalInstance.setApplicantId(loginUser.getUserId());
        approvalInstance.setApplicantName(loginUser.getNickName());
        approvalInstance.setApplyTime(LocalDateTime.now());
        approvalInstanceService.add(approvalInstance);
        return true;
    }
}
src/main/java/com/ruoyi/staff/controller/PersonalAttendanceLocationConfigController.java
@@ -1,5 +1,6 @@
package com.ruoyi.staff.controller;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
@@ -45,6 +46,7 @@
    @Operation(summary = "分页查询人员打卡规则配置")
    @GetMapping("/listPage")
    public R listPage(Page page){
        page.addOrder(OrderItem.desc("id"));
        return R.ok(personalAttendanceLocationConfigService.page(page));
    }
src/main/java/com/ruoyi/staff/controller/StaffOnJobController.java
@@ -39,7 +39,7 @@
     * @return
     */
    @GetMapping("/listPage")
    public AjaxResult staffOnJobListPage(Page page, StaffOnJob staffOnJob) {
    public AjaxResult staffOnJobListPage(Page page, StaffOnJobDto staffOnJob) {
        return AjaxResult.success(staffOnJobService.staffOnJobListPage(page, staffOnJob));
    }
src/main/java/com/ruoyi/staff/dto/StaffOnJobDto.java
@@ -25,4 +25,6 @@
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date contractEndTime;
    private Long sysDeptId;
}
src/main/java/com/ruoyi/staff/mapper/StaffOnJobMapper.java
@@ -14,7 +14,7 @@
@Mapper
public interface StaffOnJobMapper extends BaseMapper<StaffOnJob> {
    IPage<StaffOnJobDto> staffOnJobListPage(Page page, @Param("staffOnJob") StaffOnJob staffOnJob);
    IPage<StaffOnJobDto> staffOnJobListPage(Page page, @Param("staffOnJob") StaffOnJobDto staffOnJob);
    List<StaffOnJobDto> staffOnJobList(@Param("staffOnJob") StaffOnJob staffOnJob);
src/main/java/com/ruoyi/staff/service/IStaffOnJobService.java
@@ -14,7 +14,7 @@
public interface IStaffOnJobService extends IService<StaffOnJob> {
    IPage<StaffOnJobDto> staffOnJobListPage(Page page, StaffOnJob staffOnJob);
    IPage<StaffOnJobDto> staffOnJobListPage(Page page, StaffOnJobDto staffOnJob);
     StaffOnJobDto staffOnJobDetail(Long id);
src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java
@@ -59,6 +59,7 @@
                schemeApplicableStaffLambdaQueryWrapper.like(SchemeApplicableStaff::getTitle, schemeApplicableStaff.getTitle());
            }
        }
        schemeApplicableStaffLambdaQueryWrapper.orderByDesc(SchemeApplicableStaff::getId);
        Page<SchemeApplicableStaff> page1 = schemeApplicableStaffMapper.selectPage(page, schemeApplicableStaffLambdaQueryWrapper);
        List<Long> collect = page1.getRecords().stream().map(SchemeApplicableStaff::getId).collect(Collectors.toList());
        if(CollectionUtils.isEmpty(collect)){
src/main/java/com/ruoyi/staff/service/impl/StaffLeaveServiceImpl.java
@@ -59,6 +59,7 @@
        StaffLeave staffLeave = new StaffLeave();
        staffLeave.setStaffOnJobId(staffLeaveDto.getStaffOnJobId());
        staffLeave.setReason(staffLeaveDto.getReason());
        staffLeave.setLeaveDate(staffLeaveDto.getLeaveDate());
        String reason = staffLeaveDto.getReason();
        if (StaffLeaveReasonOther.getCode().equals(reason)){
            staffLeave.setRemark(staffLeaveDto.getRemark());
src/main/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java
@@ -7,6 +7,7 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.dto.WordDateDto;
import com.ruoyi.project.system.domain.SysDept;
@@ -67,8 +68,9 @@
    //在职员工台账分页查询
    @Override
    public IPage<StaffOnJobDto> staffOnJobListPage(Page page, StaffOnJob staffOnJob) {
        return staffOnJobMapper.staffOnJobListPage(page,staffOnJob);
    public IPage<StaffOnJobDto> staffOnJobListPage(Page page, StaffOnJobDto staffOnJob) {
        IPage<StaffOnJobDto> staffOnJobDtoIPage = staffOnJobMapper.staffOnJobListPage(page, staffOnJob);
        return staffOnJobDtoIPage;
    }
    //新增入职
@@ -83,6 +85,7 @@
        }
        // åˆ›å»ºå…¥èŒæ•°æ®
        syncStudyInfoFromEducation(staffOnJobPrams);
        staffOnJobPrams.setContractExpireTime(staffOnJobPrams.getContractEndTime());
        staffOnJobPrams.setStaffState(1);
        staffOnJobMapper.insert(staffOnJobPrams);
@@ -141,6 +144,7 @@
        // ç»‘定子表数据
        bingingStaffOnJobExtra(id,staffOnJobParams);
        // æ›´æ–°å‘˜å·¥æ•°æ®
        syncStudyInfoFromEducation(staffOnJobParams);
        staffOnJobParams.setContractExpireTime(staffOnJobParams.getContractEndTime());
        return staffOnJobMapper.updateById(staffOnJobParams);
    }
@@ -158,6 +162,7 @@
                    .forEach(staff -> staff.setStaffOnJobId(id)); // èµ‹å€¼
            staffEducationService.saveBatch(staffOnJobPrams.getStaffEducationList());
        }
        // æ–°å¢žå·¥ä½œç»åކ
        if(CollectionUtils.isNotEmpty(staffOnJobPrams.getStaffWorkExperienceList())){
            staffOnJobPrams.getStaffWorkExperienceList().stream()
@@ -174,6 +179,28 @@
        }
    }
    private void syncStudyInfoFromEducation(StaffOnJob staffOnJobPrams) {
        if (staffOnJobPrams == null || CollectionUtils.isEmpty(staffOnJobPrams.getStaffEducationList())) {
            if (staffOnJobPrams != null) {
                staffOnJobPrams.setFirstStudy("/");
                staffOnJobPrams.setProfession("/");
            }
            return;
        }
        Optional<StaffEducation> matchedEducation = staffOnJobPrams.getStaffEducationList().stream()
                .filter(Objects::nonNull)
                .filter(education -> StringUtils.isNotEmpty(education.getMajor()))
                .findFirst();
        if (matchedEducation.isPresent()) {
            StaffEducation education = matchedEducation.get();
            staffOnJobPrams.setFirstStudy(education.getEducation());
            staffOnJobPrams.setProfession(education.getMajor());
            return;
        }
        staffOnJobPrams.setFirstStudy("/");
        staffOnJobPrams.setProfession("/");
    }
    /**
     * é€šè¿‡å‘˜å·¥id删除教育经历,工作经历,紧急联系人
src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java
@@ -56,6 +56,7 @@
                staffSalaryMainLambdaQueryWrapper.eq(StaffSalaryMain::getStatus, staffSalaryMain.getStatus());
            }
        }
        staffSalaryMainLambdaQueryWrapper.orderByDesc(StaffSalaryMain::getId);
        Page<StaffSalaryMain> page1 = staffSalaryMainMapper.selectPage(page, staffSalaryMainLambdaQueryWrapper);
        page1.getRecords().forEach(main -> {
            List<StaffSalaryDetail> staffSalaryDetailList = staffSalaryDetailMapper.selectList(new LambdaQueryWrapper<StaffSalaryDetail>().eq(StaffSalaryDetail::getMainId, main.getId()));
src/main/java/com/ruoyi/stock/pojo/StockInRecord.java
@@ -47,14 +47,12 @@
    private String remark;
    @Schema(description = "预警数量")
    @TableField(exist = false)
    private BigDecimal warnNum;
    @Schema(description = "类型  0合格入库 1不合格入库")
    private String type;
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
src/main/java/com/ruoyi/stock/pojo/StockOutRecord.java
@@ -53,7 +53,6 @@
    private String remark;
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
@@ -13,6 +13,8 @@
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import java.time.LocalDateTime;
import com.ruoyi.stock.dto.StockInRecordDto;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
@@ -53,7 +55,7 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int add(StockInRecordDto stockInRecordDto) {
        String no = OrderUtils.countTodayByCreateTime(stockInRecordMapper, "RK","inbound_batches");
        String no = OrderUtils.countTodayByCreateTime(stockInRecordMapper, "RK","inbound_batches", stockInRecordDto.getCreateTime() != null ? stockInRecordDto.getCreateTime() : LocalDateTime.now());
        stockInRecordDto.setInboundBatches(no);
        StockInRecord stockInRecord = new StockInRecord();
        BeanUtils.copyProperties(stockInRecordDto, stockInRecord);
@@ -293,6 +295,7 @@
                            setWarnNum(stockInRecord.getWarnNum());
                            setBatchNo(stockInRecord.getBatchNo());
                            setRemark(stockInRecord.getRemark());
                            setWarnNum(stockInRecord.getWarnNum());
                            setVersion(1);
                        }});
                    } else {
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -169,6 +169,7 @@
        stockInRecordDto.setWarnNum(stockInventoryDto.getWarnNum());
        stockInRecordDto.setType("0");
        stockInRecordDto.setRemark(stockInventoryDto.getRemark());
        stockInRecordDto.setWarnNum(stockInventoryDto.getWarnNum());
        stockInRecordService.add(stockInRecordDto);
        return true;
    }
src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java
@@ -15,6 +15,8 @@
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import java.time.LocalDateTime;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockOutRecordDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
@@ -58,7 +60,7 @@
    @Override
    public int add(StockOutRecordDto stockOutRecordDto) {
        String no = OrderUtils.countTodayByCreateTime(stockOutRecordMapper, "CK","outbound_batches");
        String no = OrderUtils.countTodayByCreateTime(stockOutRecordMapper, "CK","outbound_batches", stockOutRecordDto.getCreateTime() != null ? stockOutRecordDto.getCreateTime() : LocalDateTime.now());
        stockOutRecordDto.setOutboundBatches(no);
        if (StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode().equals(stockOutRecordDto.getRecordType())){
            stockOutRecordDto.setApprovalStatus(3);
src/main/java/com/ruoyi/technology/controller/TechnologyOperationController.java
@@ -24,28 +24,28 @@
    private final TechnologyOperationService technologyOperationService;
    @GetMapping("/listPage")
    @Log(title = "Technology operation page", businessType = BusinessType.OTHER)
    @Log(title = "工序分页查询", businessType = BusinessType.OTHER)
    @Operation(summary = "工序分页查询")
    public R<IPage<TechnologyOperationVo>> listPage(Page<TechnologyOperationDto> page, TechnologyOperationDto technologyOperationDto) {
        return R.ok(technologyOperationService.listPage(page, technologyOperationDto));
    }
    @PostMapping("/add")
    @Log(title = "Add technology operation", businessType = BusinessType.INSERT)
    @Log(title = "工序新增", businessType = BusinessType.INSERT)
    @Operation(summary = "新增工序")
    public R add(@RequestBody TechnologyOperationDto technologyOperationDto) {
        return technologyOperationService.add(technologyOperationDto);
    }
    @PutMapping("/update")
    @Log(title = "Update technology operation", businessType = BusinessType.UPDATE)
    @Log(title = "工序更新", businessType = BusinessType.UPDATE)
    @Operation(summary = "修改工序")
    public R update(@RequestBody com.ruoyi.technology.pojo.TechnologyOperation technologyOperation) {
        return R.ok(technologyOperationService.updateById(technologyOperation));
    }
    @DeleteMapping("/batchDelete")
    @Log(title = "Delete technology operation", businessType = BusinessType.DELETE)
    @Log(title = "删除工序", businessType = BusinessType.DELETE)
    @Operation(summary = "批量删除工序")
    public R batchDelete(@RequestBody List<Long> ids) {
        return R.ok(technologyOperationService.batchDelete(ids));
src/main/java/com/ruoyi/technology/controller/TechnologyOperationParamController.java
@@ -36,7 +36,7 @@
    }
    @DeleteMapping("/batchDelete/{id}")
    @Log(title = "Delete technology operation param", businessType = BusinessType.DELETE)
    @Log(title = "删除工序参数", businessType = BusinessType.DELETE)
    @Operation(summary = "删除工序参数")
    public AjaxResult batchDelete(@PathVariable("id") Long id) {
        return AjaxResult.success(technologyOperationParamService.batchDelete(id));
src/main/java/com/ruoyi/technology/pojo/TechnologyParam.java
@@ -1,8 +1,10 @@
package com.ruoyi.technology.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
@@ -53,6 +55,8 @@
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @Schema(description = "修改人")
@@ -61,6 +65,8 @@
    @Schema(description = "修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @Schema(description = "部门ID")
src/main/java/com/ruoyi/technology/pojo/TechnologyRouting.java
@@ -32,7 +32,6 @@
    private String description;
    @Schema(description = "录入时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @Schema(description = "更新时间")
src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java
@@ -7,6 +7,8 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.OrderUtils;
import java.time.LocalDateTime;
import com.ruoyi.technology.bean.dto.TechnologyRoutingDto;
import com.ruoyi.technology.bean.vo.TechnologyRoutingVo;
import com.ruoyi.production.mapper.ProductionOrderRoutingMapper;
@@ -60,7 +62,7 @@
    @Override
    public Long saveTechnologyRouting(TechnologyRouting technologyRouting) {
        String code = OrderUtils.countTodayByCreateTime(technologyRoutingMapper, "GYLX", "process_route_code");
        String code = OrderUtils.countTodayByCreateTime(technologyRoutingMapper, "GYLX", "process_route_code", technologyRouting.getCreateTime() != null ? technologyRouting.getCreateTime() : LocalDateTime.now());
        technologyRouting.setProcessRouteCode(code);
        technologyRoutingMapper.insert(technologyRouting);
        // å¸¦å…¥bom产品结构
src/main/resources/application-ckgm.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,268 @@
# é¡¹ç›®ç›¸å…³é…ç½®
ruoyi:
  # åç§°
  name: RuoYi
  # ç‰ˆæœ¬
  version: 3.8.9
  # ç‰ˆæƒå¹´ä»½
  copyrightYear: 2025
  # æ–‡ä»¶è·¯å¾„ ç¤ºä¾‹ï¼ˆ Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
  profile: /javaWork/product-inventory-management/file
  # èŽ·å–ip地址开关
  addressEnabled: false
  # éªŒè¯ç ç±»åž‹ math æ•°å­—计算 char å­—符验证
  captchaType: math
  # ååŒå®¡æ‰¹ç¼–号前缀(配置文件后缀命名)
  approvalNumberPrefix: NEW
  # ä¸ªæŽ¨ Unipush é…ç½®
  getui:
    appId: PfjyAAE0FK64FaO1w2CMb1
    appKey: zTMb831OEL6J4GK1uE3Ob4
    masterSecret: K1GFtsv42v61tXGnF7SGE5
    domain: https://restapi.getui.cn/v2/
    # ç¦»çº¿æŽ¨é€ä½¿ç”¨çš„包名/组件名
    intentComponent: uni.app.UNI099A590/io.dcloud.PandoraEntry
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口,默认为8080
  port: 9003
  servlet:
    # åº”用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # è¿žæŽ¥æ•°æ»¡åŽçš„æŽ’队数,默认为100
    accept-count: 1000
    threads:
      # tomcat最大线程数,默认为200
      max: 800
      # Tomcat启动初始化的线程数,默认值10
      min-spare: 100
# æ—¥å¿—配置
logging:
  level:
    com.ruoyi: warn
    org.springframework: warn
minio:
  endpoint: http://114.132.189.42/
  port: 7019
  secure: false
  accessKey: admin
  secretKey: 12345678
  preview-expiry: 24 # é¢„览地址默认24小时
  default-bucket: jxc
# ç”¨æˆ·é…ç½®
user:
  password:
    # å¯†ç æœ€å¤§é”™è¯¯æ¬¡æ•°
    maxRetryCount: 5
    # å¯†ç é”å®šæ—¶é—´ï¼ˆé»˜è®¤10分钟)
    lockTime: 10
# Spring配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://172.17.0.1:3306/product-inventory-management-ckgm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: xd@123456..
      # ä»Žåº“数据源
      slave:
        # ä»Žæ•°æ®æºå¼€å…³/默认关闭
        enabled: false
        url:
        username:
        password:
      # åˆå§‹è¿žæŽ¥æ•°
      initialSize: 5
      # æœ€å°è¿žæŽ¥æ± æ•°é‡
      minIdle: 10
      # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
      maxActive: 20
      # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´
      maxWait: 60000
      # é…ç½®è¿žæŽ¥è¶…æ—¶æ—¶é—´
      connectTimeout: 30000
      # é…ç½®ç½‘络超时时间
      socketTimeout: 60000
      # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      minEvictableIdleTimeMillis: 300000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # è®¾ç½®ç™½åå•,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # æŽ§åˆ¶å°ç®¡ç†ç”¨æˆ·åå’Œå¯†ç 
        login-username: ruoyi
        login-password: 123456
      filter:
        stat:
          enabled: true
          # æ…¢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
  # èµ„源信息
  messages:
    # å›½é™…化资源文件路径
    basename: i18n/messages
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
      # å•个文件大小
      max-file-size: 1GB
      # è®¾ç½®æ€»ä¸Šä¼ çš„æ–‡ä»¶å¤§å°
      max-request-size: 2GB
  # æœåŠ¡æ¨¡å—
  devtools:
    restart:
      # çƒ­éƒ¨ç½²å¼€å…³
      enabled: false
  # redis é…ç½®
  data:
    mongodb:
      uri: mongodb://114.132.189.42:9028/chat_memory_db_ckgm
    # redis é…ç½®
    redis:
      # åœ°å€
#      host: 127.0.0.1
      host: 172.17.0.1
      # ç«¯å£ï¼Œé»˜è®¤ä¸º6379
      port: 6379
      # æ•°æ®åº“索引
      database: 0
      # å¯†ç 
      #    password: root2022!
      password:
      # è¿žæŽ¥è¶…æ—¶æ—¶é—´
      timeout: 10s
      lettuce:
        pool:
          # è¿žæŽ¥æ± ä¸­çš„æœ€å°ç©ºé—²è¿žæŽ¥
          min-idle: 0
          # è¿žæŽ¥æ± ä¸­çš„æœ€å¤§ç©ºé—²è¿žæŽ¥
          max-idle: 8
          # è¿žæŽ¥æ± çš„æœ€å¤§æ•°æ®åº“连接数
          max-active: 8
          # #连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
  # Quartz定时任务配置(新增部分)
  quartz:
    job-store-type: jdbc  # ä½¿ç”¨æ•°æ®åº“存储
    jdbc:
      initialize-schema: never  # é¦–次运行时自动创建表结构,成功后改为never
      schema: classpath:org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql  # MySQL表结构脚本
    properties:
      org:
        quartz:
          scheduler:
            instanceName: RuoYiScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate  # MySQL适配
            tablePrefix: qrtz_  # è¡¨åå‰ç¼€ï¼Œä¸Žè„šæœ¬ä¸€è‡´
            isClustered: false  # å•节点模式(集群需改为true)
            clusterCheckinInterval: 10000
            txIsolationLevelSerializable: true
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10  # çº¿ç¨‹æ± å¤§å°
            threadPriority: 5
            makeThreadsDaemons: true
          updateCheck: false  # å…³é—­ç‰ˆæœ¬æ£€æŸ¥
# token配置
token:
  # ä»¤ç‰Œè‡ªå®šä¹‰æ ‡è¯†
  header: Authorization
  # ä»¤ç‰Œå¯†é’¥
  secret: xpAVjhCjQDaDB7mjPAzMDSbQWXNu2zYkTdDNUsPMS5Xx8QMmQVYN7n74eZrYJxDJ
  # ä»¤ç‰Œæœ‰æ•ˆæœŸï¼ˆé»˜è®¤30分钟)
  expireTime: 450
# MyBatis Plus配置
mybatis-plus:
  # æœç´¢æŒ‡å®šåŒ…别名   æ ¹æ®è‡ªå·±çš„项目来
  typeAliasesPackage: com.ruoyi.**.pojo
  # é…ç½®mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # åŠ è½½å…¨å±€çš„é…ç½®æ–‡ä»¶
  configLocation: classpath:mybatis/mybatis-config.xml
  global-config:
    enable-sql-runner: true
    db-config:
      id-type: auto
# PageHelper分页插件
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true
  params: count=countSql
# Swagger配置
swagger:
  # æ˜¯å¦å¼€å¯swagger
  enabled: true
  # è¯·æ±‚前缀
  pathMapping: /dev-api
# é˜²æ­¢XSS攻击
xss:
  # è¿‡æ»¤å¼€å…³
  enabled: true
  # æŽ’除链接(多个用逗号分隔)
  excludes: /system/notice
  # åŒ¹é…é“¾æŽ¥
  urlPatterns: /system/*,/monitor/*,/tool/*
# ä»£ç ç”Ÿæˆ
gen:
  # ä½œè€…
  author: ruoyi
  # é»˜è®¤ç”ŸæˆåŒ…路径 system éœ€æ”¹æˆè‡ªå·±çš„æ¨¡å—名称 å¦‚ system monitor tool
  packageName: com.ruoyi.project.system
  # è‡ªåŠ¨åŽ»é™¤è¡¨å‰ç¼€ï¼Œé»˜è®¤æ˜¯true
  autoRemovePre: false
  # è¡¨å‰ç¼€ï¼ˆç”Ÿæˆç±»åä¸ä¼šåŒ…含表前缀,多个用逗号分隔)
  tablePrefix: sys_
  # æ˜¯å¦å…è®¸ç”Ÿæˆæ–‡ä»¶è¦†ç›–到本地(自定义路径),默认不允许
  allowOverwrite: false
# æ–‡ä»¶ä¸Šä¼ é…ç½®
file:
  temp-dir: /javaWork/product-inventory-management/file/temp/uploads   # ä¸´æ—¶ç›®å½•
  upload-dir: /javaWork/product-inventory-management/file/prod/uploads # æ­£å¼ç›®å½•
  path: /javaWork/product-inventory-management/file # ä¸Šä¼ ç›®å½•
  urlPrefix: /prod-api/common # é“¾æŽ¥å‰ç¼€
  domain: http://1.15.17.182:9071 # åŸŸåå‰ç¼€
  expired: 120 # è¿‡æœŸæ—¶é—´(单位:分钟)
  useLimit: 10 # ä½¿ç”¨æ¬¡æ•°
  compress: true # æ˜¯å¦åŽ‹ç¼©
  needCompressSize: 10MB # åŽ‹ç¼©é˜ˆå€¼
  compressQuality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
src/main/resources/application-hqjc.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,268 @@
# é¡¹ç›®ç›¸å…³é…ç½®
ruoyi:
  # åç§°
  name: RuoYi
  # ç‰ˆæœ¬
  version: 3.8.9
  # ç‰ˆæƒå¹´ä»½
  copyrightYear: 2025
  # æ–‡ä»¶è·¯å¾„ ç¤ºä¾‹ï¼ˆ Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
  profile: /javaWork/product-inventory-management/file
  # èŽ·å–ip地址开关
  addressEnabled: false
  # éªŒè¯ç ç±»åž‹ math æ•°å­—计算 char å­—符验证
  captchaType: math
  # ååŒå®¡æ‰¹ç¼–号前缀(配置文件后缀命名)
  approvalNumberPrefix: NEW
  # ä¸ªæŽ¨ Unipush é…ç½®
  getui:
    appId: PfjyAAE0FK64FaO1w2CMb1
    appKey: zTMb831OEL6J4GK1uE3Ob4
    masterSecret: K1GFtsv42v61tXGnF7SGE5
    domain: https://restapi.getui.cn/v2/
    # ç¦»çº¿æŽ¨é€ä½¿ç”¨çš„包名/组件名
    intentComponent: uni.app.UNI099A590/io.dcloud.PandoraEntry
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口,默认为8080
  port: 9003
  servlet:
    # åº”用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # è¿žæŽ¥æ•°æ»¡åŽçš„æŽ’队数,默认为100
    accept-count: 1000
    threads:
      # tomcat最大线程数,默认为200
      max: 800
      # Tomcat启动初始化的线程数,默认值10
      min-spare: 100
# æ—¥å¿—配置
logging:
  level:
    com.ruoyi: warn
    org.springframework: warn
minio:
  endpoint: http://114.132.189.42/
  port: 7019
  secure: false
  accessKey: admin
  secretKey: 12345678
  preview-expiry: 24 # é¢„览地址默认24小时
  default-bucket: jxc
# ç”¨æˆ·é…ç½®
user:
  password:
    # å¯†ç æœ€å¤§é”™è¯¯æ¬¡æ•°
    maxRetryCount: 5
    # å¯†ç é”å®šæ—¶é—´ï¼ˆé»˜è®¤10分钟)
    lockTime: 10
# Spring配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://172.17.0.1:9002/product-inventory-management-hqjc?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: hqjc@123456..
      # ä»Žåº“数据源
      slave:
        # ä»Žæ•°æ®æºå¼€å…³/默认关闭
        enabled: false
        url:
        username:
        password:
      # åˆå§‹è¿žæŽ¥æ•°
      initialSize: 5
      # æœ€å°è¿žæŽ¥æ± æ•°é‡
      minIdle: 10
      # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
      maxActive: 20
      # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´
      maxWait: 60000
      # é…ç½®è¿žæŽ¥è¶…æ—¶æ—¶é—´
      connectTimeout: 30000
      # é…ç½®ç½‘络超时时间
      socketTimeout: 60000
      # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      minEvictableIdleTimeMillis: 300000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # è®¾ç½®ç™½åå•,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # æŽ§åˆ¶å°ç®¡ç†ç”¨æˆ·åå’Œå¯†ç 
        login-username: ruoyi
        login-password: 123456
      filter:
        stat:
          enabled: true
          # æ…¢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
  # èµ„源信息
  messages:
    # å›½é™…化资源文件路径
    basename: i18n/messages
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
      # å•个文件大小
      max-file-size: 1GB
      # è®¾ç½®æ€»ä¸Šä¼ çš„æ–‡ä»¶å¤§å°
      max-request-size: 2GB
  # æœåŠ¡æ¨¡å—
  devtools:
    restart:
      # çƒ­éƒ¨ç½²å¼€å…³
      enabled: false
  # redis é…ç½®
  data:
    mongodb:
      uri: mongodb://114.132.189.42:9028/chat_memory_db_hqjc
    # redis é…ç½®
    redis:
      # åœ°å€
#      host: 127.0.0.1
      host: 172.17.0.1
      # ç«¯å£ï¼Œé»˜è®¤ä¸º6379
      port: 6379
      # æ•°æ®åº“索引
      database: 0
      # å¯†ç 
      #    password: root2022!
      password:
      # è¿žæŽ¥è¶…æ—¶æ—¶é—´
      timeout: 10s
      lettuce:
        pool:
          # è¿žæŽ¥æ± ä¸­çš„æœ€å°ç©ºé—²è¿žæŽ¥
          min-idle: 0
          # è¿žæŽ¥æ± ä¸­çš„æœ€å¤§ç©ºé—²è¿žæŽ¥
          max-idle: 8
          # è¿žæŽ¥æ± çš„æœ€å¤§æ•°æ®åº“连接数
          max-active: 8
          # #连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
  # Quartz定时任务配置(新增部分)
  quartz:
    job-store-type: jdbc  # ä½¿ç”¨æ•°æ®åº“存储
    jdbc:
      initialize-schema: never  # é¦–次运行时自动创建表结构,成功后改为never
      schema: classpath:org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql  # MySQL表结构脚本
    properties:
      org:
        quartz:
          scheduler:
            instanceName: RuoYiScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate  # MySQL适配
            tablePrefix: qrtz_  # è¡¨åå‰ç¼€ï¼Œä¸Žè„šæœ¬ä¸€è‡´
            isClustered: false  # å•节点模式(集群需改为true)
            clusterCheckinInterval: 10000
            txIsolationLevelSerializable: true
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10  # çº¿ç¨‹æ± å¤§å°
            threadPriority: 5
            makeThreadsDaemons: true
          updateCheck: false  # å…³é—­ç‰ˆæœ¬æ£€æŸ¥
# token配置
token:
  # ä»¤ç‰Œè‡ªå®šä¹‰æ ‡è¯†
  header: Authorization
  # ä»¤ç‰Œå¯†é’¥
  secret: xpAVjhCjQDaDB7mjPAzMDSbQWXNu2zYkTdDNUsPMS5Xx8QMmQVYN7n74eZrYJxDJ
  # ä»¤ç‰Œæœ‰æ•ˆæœŸï¼ˆé»˜è®¤30分钟)
  expireTime: 450
# MyBatis Plus配置
mybatis-plus:
  # æœç´¢æŒ‡å®šåŒ…别名   æ ¹æ®è‡ªå·±çš„项目来
  typeAliasesPackage: com.ruoyi.**.pojo
  # é…ç½®mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # åŠ è½½å…¨å±€çš„é…ç½®æ–‡ä»¶
  configLocation: classpath:mybatis/mybatis-config.xml
  global-config:
    enable-sql-runner: true
    db-config:
      id-type: auto
# PageHelper分页插件
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true
  params: count=countSql
# Swagger配置
swagger:
  # æ˜¯å¦å¼€å¯swagger
  enabled: true
  # è¯·æ±‚前缀
  pathMapping: /dev-api
# é˜²æ­¢XSS攻击
xss:
  # è¿‡æ»¤å¼€å…³
  enabled: true
  # æŽ’除链接(多个用逗号分隔)
  excludes: /system/notice
  # åŒ¹é…é“¾æŽ¥
  urlPatterns: /system/*,/monitor/*,/tool/*
# ä»£ç ç”Ÿæˆ
gen:
  # ä½œè€…
  author: ruoyi
  # é»˜è®¤ç”ŸæˆåŒ…路径 system éœ€æ”¹æˆè‡ªå·±çš„æ¨¡å—名称 å¦‚ system monitor tool
  packageName: com.ruoyi.project.system
  # è‡ªåŠ¨åŽ»é™¤è¡¨å‰ç¼€ï¼Œé»˜è®¤æ˜¯true
  autoRemovePre: false
  # è¡¨å‰ç¼€ï¼ˆç”Ÿæˆç±»åä¸ä¼šåŒ…含表前缀,多个用逗号分隔)
  tablePrefix: sys_
  # æ˜¯å¦å…è®¸ç”Ÿæˆæ–‡ä»¶è¦†ç›–到本地(自定义路径),默认不允许
  allowOverwrite: false
# æ–‡ä»¶ä¸Šä¼ é…ç½®
file:
  temp-dir: /javaWork/product-inventory-management/file/temp/uploads   # ä¸´æ—¶ç›®å½•
  upload-dir: /javaWork/product-inventory-management/file/prod/uploads # æ­£å¼ç›®å½•
  path: /javaWork/product-inventory-management/file # ä¸Šä¼ ç›®å½•
  urlPrefix: /prod-api/common # é“¾æŽ¥å‰ç¼€
  domain: http://36.134.77.64:9001 # åŸŸåå‰ç¼€
  expired: 120 # è¿‡æœŸæ—¶é—´(单位:分钟)
  useLimit: 10 # ä½¿ç”¨æ¬¡æ•°
  compress: true # æ˜¯å¦åŽ‹ç¼©
  needCompressSize: 10MB # åŽ‹ç¼©é˜ˆå€¼
  compressQuality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
src/main/resources/financial-agent-prompt.txt
@@ -2,10 +2,10 @@
当前日期:{{currentDate}}(中国时区)。
工作规则:
1. ç”¨æˆ·æå‡ºâ€œæŸ¥ã€é—®ã€ç»Ÿè®¡ã€åˆ†æžã€é¢„警、建议、报告”需求时,优先调用工具返回结构化 JSON,不编造业务数据。
2. å‘½ä¸­æˆæœ¬ã€åˆ©æ¶¦ã€åº“存资金、现金流、预警、驾驶舱、日报周报场景时,优先调用对应工具。
1. ç”¨æˆ·æå‡ºâ€œæŸ¥ã€é—®ã€ç»Ÿè®¡ã€åˆ†æžã€é¢„警、建议、报告”类需求时,优先调用工具返回结构化 JSON,不编造业务数据。
2. å‘½ä¸­æˆæœ¬ã€åˆ©æ¶¦ã€åº“存资金、现金流、预警、驾驶舱、日报、周报等场景时,优先调用对应工具。
3. å·¥å…·è¿”回 JSON æ—¶ï¼Œç›´æŽ¥è¾“出原始 JSON å­—符串,不要额外包裹 Markdown,也不要在前后追加解释文本。
4. å½“用户问题缺少时间范围时,默认使用工具内置口径(如近30天、本月、近90天),并在后续可提醒用户补充范围。
5. ç”¨æˆ·é—®â€œä¸ºä»€ä¹ˆåˆ©æ¶¦ä¸‹é™â€â€œå“ªä¸ªè®¢å•亏损”“哪个客户最赚钱”“哪个客户利润贡献最高”“哪个车间/工序成本最高”等问题时,优先基于订单利润与工序成本分析工具作答。
6. å›žç­”必须使用中文;若数据不足以得出结论,明确指出缺少哪些关键字段或筛选条件。
7. ç”¨æˆ·æåˆ°â€œä»Šå¹´/本月/今天/最近/上月/去年”等相对时间时,必须严格基于“当前日期”换算,禁止自行假设年份。
4. ç”¨æˆ·æ²¡æœ‰æ˜Žç¡®ç»™å‡ºæ—¶é—´ã€å®¢æˆ·ã€ä¾›åº”商、产品、订单、数量等筛选条件时,不要自行补充条件;工具参数保持为空,由工具按当前业务口径查询。
5. ç”¨æˆ·æåˆ°â€œæœ¬å‘¨ã€æœ¬æœˆã€ä»Šå¹´ã€ä»Šå¤©ã€æœ€è¿‘、近30天、上月、去年”等相对时间时,必须严格基于“当前日期”换算,禁止自行假设年份。
6. ç”¨æˆ·é—®â€œä¸ºä»€ä¹ˆåˆ©æ¶¦ä¸‹é™â€â€œå“ªä¸ªè®¢å•亏损”“哪个客户最赚钱”“哪个客户利润贡献最高”“哪个工序成本最高”等问题时,优先基于订单利润与工序成本分析工具作答。
7. å›žç­”必须使用中文;若数据不足以得出结论,明确指出缺少哪些关键字段或筛选条件。
src/main/resources/mapper/account/purchase/AccountPaymentApplicationMapper.xml
@@ -44,6 +44,7 @@
                AND A.apply_date BETWEEN #{req.startDate} AND #{req.endDate}
            </if>
        </where>
        order by A.id desc
    </select>
    <select id="getInboundBatchesBySupplier"
src/main/resources/mapper/account/purchase/AccountPurchaseInvoiceMapper.xml
@@ -42,6 +42,7 @@
                AND api.issue_date BETWEEN #{req.startDate} AND #{req.endDate}
            </if>
        </where>
        order by api.id desc
    </select>
    <select id="getInboundBatchesBySupplier"
            resultType="com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo">
src/main/resources/mapper/account/purchase/AccountPurchasePaymentMapper.xml
@@ -44,6 +44,7 @@
                AND app.payment_date BETWEEN #{req.startDate} AND #{req.endDate}
            </if>
        </where>
        order by app.id desc
    </select>
    <select id="selectPayment" resultType="com.ruoyi.home.dto.IncomeExpenseAnalysisDto">
        SELECT DATE_FORMAT(payment_date, #{dateFormat}) AS dateStr,
src/main/resources/mapper/account/sales/AccountInvoiceApplicationMapper.xml
@@ -25,6 +25,7 @@
                AND A.apply_date BETWEEN #{req.startDate} AND #{req.endDate}
            </if>
        </where>
        order by A.id desc
    </select>
    <select id="getOutboundBatchesByCustomer"
            resultType="com.ruoyi.account.bean.vo.sales.SalesOutboundVo">
src/main/resources/mapper/account/sales/AccountSalesCollectionMapper.xml
@@ -45,7 +45,7 @@
                AND A.collection_date BETWEEN #{req.startDate} AND #{req.endDate}
            </if>
        </where>
        order by A.id desc
    </select>
    <select id="existsByStockOutRecordId" resultType="java.lang.Boolean">
        SELECT COUNT(*) > 0
src/main/resources/mapper/account/sales/AccountSalesInvoiceMapper.xml
@@ -42,6 +42,7 @@
                AND asi.issue_date BETWEEN #{req.startDate} AND #{req.endDate}
            </if>
        </where>
        order by asi.id desc
    </select>
</mapper>
src/main/resources/mapper/aftersalesservice/AfterSalesNearExpiryMapper.xml
@@ -25,7 +25,7 @@
    <select id="listPage" resultType="com.ruoyi.aftersalesservice.pojo.AfterSalesNearExpiry">
        select * from after_sales_near_expiry
        where 1 = 1
        <where>
        <if test="req.expireDate != null">
            AND expire_date = #{req.expireDate}
        </if>
@@ -35,6 +35,7 @@
        <if test="req.status != null">
            AND status = #{req.status}
        </if>
        </where>
        order by create_time desc
    </select>
src/main/resources/mapper/aftersalesservice/AfterSalesServiceMapper.xml
@@ -32,7 +32,7 @@
        <if test="req.serviceType != null">
            and service_type = #{req.serviceType}
        </if>
        order by update_time desc
        order by create_time desc
    </select>
    <select id="countAfterSalesService" resultType="com.ruoyi.aftersalesservice.dto.CountDto">
        select
src/main/resources/mapper/approve/ApprovalInstanceMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.ApprovalInstanceMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.ApprovalInstance">
        <id column="id" property="id" />
        <result column="instance_no" property="instanceNo" />
        <result column="template_id" property="templateId" />
        <result column="template_name" property="templateName" />
        <result column="business_id" property="businessId" />
        <result column="business_type" property="businessType" />
        <result column="title" property="title" />
        <result column="status" property="status" />
        <result column="current_level" property="currentLevel" />
        <result column="applicant_id" property="applicantId" />
        <result column="applicant_name" property="applicantName" />
        <result column="apply_time" property="applyTime" />
        <result column="finish_time" property="finishTime" />
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="update_user" property="updateUser" />
        <result column="update_time" property="updateTime" />
        <result column="deleted" property="deleted" />
    </resultMap>
    <select id="listPage" resultType="com.ruoyi.approve.bean.vo.ApprovalInstanceVo">
        select ai.*,su.nick_name as create_user_name from
        approval_instance ai
        left join sys_user su on ai.create_user = su.user_id
        <where>
            deleted = 0
            <if test="ew.instanceNo != null">
                and ai.instance_no like concat('%',#{ew.instanceNo},'%')
            </if>
            <if test="ew.templateName != null">
                and ai.template_name like concat('%',#{ew.templateName},'%')
            </if>
            <if test="ew.templateId != null ">
                and ai. template_id = #{ew.templateId}
            </if>
            <if test="ew.businessType != null ">
                and ai.business_type = #{ew.businessType}
            </if>
            <if test="ew.createTimeStart != null and ew.createTimeEnd != null">
                and ai.apply_time &gt;= #{ew.createTimeStart}
                and ai.apply_time &lt;= #{ew.createTimeEnd}
            </if>
            <if test="ew.status != null">
                and ai.status = #{ew.status}
            </if>
            <if test="ew.applicantName != null and ew.applicantName !=''">
                and ai.applicant_name = #{ew.applicantName}
            </if>
        </where>
        order by ai.create_time desc
    </select>
</mapper>
src/main/resources/mapper/approve/ApprovalInstanceNodeMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.ApprovalInstanceNodeMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.ApprovalInstanceNode">
        <id column="id" property="id" />
        <result column="instance_id" property="instanceId" />
        <result column="level_no" property="levelNo" />
        <result column="approve_type" property="approveType" />
        <result column="status" property="status" />
        <result column="start_time" property="startTime" />
        <result column="finish_time" property="finishTime" />
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="update_user" property="updateUser" />
        <result column="update_time" property="updateTime" />
        <result column="deleted" property="deleted" />
    </resultMap>
</mapper>
src/main/resources/mapper/approve/ApprovalRecordMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.ApprovalRecordMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.ApprovalRecord">
        <id column="id" property="id" />
        <result column="instance_id" property="instanceId" />
        <result column="node_id" property="nodeId" />
        <result column="task_id" property="taskId" />
        <result column="operator_id" property="operatorId" />
        <result column="operator_name" property="operatorName" />
        <result column="action" property="action" />
        <result column="comment" property="comment" />
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="deleted" property="deleted" />
    </resultMap>
</mapper>
src/main/resources/mapper/approve/ApprovalTaskMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.ApprovalTaskMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.ApprovalTask">
        <id column="id" property="id" />
        <result column="instance_id" property="instanceId" />
        <result column="node_id" property="nodeId" />
        <result column="level_no" property="levelNo" />
        <result column="approver_id" property="approverId" />
        <result column="approver_name" property="approverName" />
        <result column="task_status" property="taskStatus" />
        <result column="approve_time" property="approveTime" />
        <result column="comment" property="comment" />
        <result column="is_read" property="isRead" />
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="update_user" property="updateUser" />
        <result column="update_time" property="updateTime" />
        <result column="deleted" property="deleted" />
    </resultMap>
</mapper>
src/main/resources/mapper/approve/ApprovalTemplateMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.ApprovalTemplateMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.ApprovalTemplate">
        <id column="id" property="id" />
        <result column="template_name" property="templateName" />
        <result column="enabled" property="enabled" />
        <result column="description" property="description" />
        <result column="deleted" property="deleted" />
        <result column="dept_id" property="deptId" />
    </resultMap>
    <select id="listPage" resultType="com.ruoyi.approve.bean.vo.ApprovalTemplateVo">
        select at.*,su.nick_name as create_user_name  from
        approval_template at
        left join sys_user su on at.create_user = su.user_id
        <where>
            deleted = 0
            <if test="ew.templateName != null">
                and template_name like concat('%',#{ew.templateName},'%')
            </if>
            <if test="ew.templateType != null">
                and template_type = #{ew.templateType}
            </if>
            <if test="ew.enabled != null">
                and enabled = #{ew.enabled}
            </if>
        </where>
        order by at.id desc
    </select>
</mapper>
src/main/resources/mapper/approve/ApprovalTemplateNodeApproverMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.ApprovalTemplateNodeApproverMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover">
        <id column="id" property="id" />
        <result column="node_id" property="nodeId" />
        <result column="template_id" property="templateId" />
        <result column="approver_id" property="approverId" />
        <result column="approver_name" property="approverName" />
        <result column="sort_no" property="sortNo" />
        <result column="created_time" property="createdTime" />
    </resultMap>
</mapper>
src/main/resources/mapper/approve/ApprovalTemplateNodeMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.ApprovalTemplateNodeMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.ApprovalTemplateNode">
        <id column="id" property="id" />
        <result column="template_id" property="templateId" />
        <result column="level_no" property="levelNo" />
        <result column="approve_type" property="approveType" />
        <result column="created_time" property="createdTime" />
        <result column="updated_time" property="updatedTime" />
    </resultMap>
</mapper>
src/main/resources/mapper/approve/ApproveProcessMapper.xml
@@ -41,7 +41,7 @@
        <if test="req.approveType != null ">
            and approve_type = #{req.approveType}
        </if>
        order by approve_time desc
        order by id desc
    </select>
</mapper>
src/main/resources/mapper/approve/FinReimbursementDetailMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.FinReimbursementDetailMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.FinReimbursementDetail">
        <id column="id" property="id" />
        <result column="reimbursement_id" property="reimbursementId" />
        <result column="row_no" property="rowNo" />
        <result column="invoice_date" property="invoiceDate" />
        <result column="expense_category" property="expenseCategory" />
        <result column="amount" property="amount" />
        <result column="description" property="description" />
        <result column="invoice_no" property="invoiceNo" />
        <result column="invoice_type" property="invoiceType" />
        <result column="invoice_amount" property="invoiceAmount" />
        <result column="tax_rate" property="taxRate" />
        <result column="tax_amount" property="taxAmount" />
        <result column="remark" property="remark" />
        <result column="tenant_id" property="tenantId" />
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="update_user" property="updateUser" />
        <result column="update_time" property="updateTime" />
        <result column="dept_id" property="deptId" />
        <result column="deleted" property="deleted" />
    </resultMap>
</mapper>
src/main/resources/mapper/approve/FinReimbursementMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.FinReimbursementMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.FinReimbursement">
        <id column="id" property="id" />
        <result column="bill_no" property="billNo" />
        <result column="reimbursement_type" property="reimbursementType" />
        <result column="expense_type" property="expenseType" />
        <result column="applicant_id" property="applicantId" />
        <result column="applicant_code" property="applicantCode" />
        <result column="applicant_name" property="applicantName" />
        <result column="applicant_dept_id" property="applicantDeptId" />
        <result column="applicant_dept_name" property="applicantDeptName" />
        <result column="reason" property="reason" />
        <result column="apply_amount" property="applyAmount" />
        <result column="detail_total_amount" property="detailTotalAmount" />
        <result column="payee_name" property="payeeName" />
        <result column="payee_account" property="payeeAccount" />
        <result column="payee_bank" property="payeeBank" />
        <result column="approval_instance_id" property="approvalInstanceId" />
        <result column="approve_process_id" property="approveProcessId" />
        <result column="bill_status" property="billStatus" />
        <result column="approved_time" property="approvedTime" />
        <result column="paid_time" property="paidTime" />
        <result column="account_expense_id" property="accountExpenseId" />
        <result column="remark" property="remark" />
        <result column="tenant_id" property="tenantId" />
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="update_user" property="updateUser" />
        <result column="update_time" property="updateTime" />
        <result column="dept_id" property="deptId" />
        <result column="deleted" property="deleted" />
    </resultMap>
    <select id="listPage" resultType="com.ruoyi.approve.bean.vo.FinReimbursementVo">
        select fin_reimbursement.*,
        fin_reimbursement_travel.start_time ,
        fin_reimbursement_travel.end_time
               from
                fin_reimbursement
            left join fin_reimbursement_travel on fin_reimbursement.id = fin_reimbursement_travel.reimbursement_id
        <where>
            <if test="ew.billNo != null and ew.billNo != ''">
                bill_no like concat('%',#{ew.billNo},'%')
            </if>
            <if test="ew.applicantName != null and ew.applicantName != ''">
                and applicant_name like concat('%',#{ew.applicantName},'%')
            </if>
            <if test="ew.applicantCode != null and ew.applicantCode != ''">
                and applicant_code like concat('%',#{ew.applicantCode},'%')
            </if>
            <if test="ew.createTimeStart != null and ew.createTimeStart !='' and ew.createTimeEnd != null and ew.createTimeEnd != ''">
                and create_time &gt;= #{ew.createTimeStart}
                and create_time &lt;= #{ew.createTimeEnd}
            </if>
        </where>
        order by create_time desc
    </select>
</mapper>
src/main/resources/mapper/approve/FinReimbursementTravelMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.approve.mapper.FinReimbursementTravelMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.approve.pojo.FinReimbursementTravel">
        <id column="id" property="id" />
        <result column="reimbursement_id" property="reimbursementId" />
        <result column="start_time" property="startTime" />
        <result column="end_time" property="endTime" />
        <result column="travel_days" property="travelDays" />
        <result column="departure_city" property="departureCity" />
        <result column="destination_city" property="destinationCity" />
        <result column="hotel_standard" property="hotelStandard" />
        <result column="lodging_days" property="lodgingDays" />
        <result column="meal_allowance" property="mealAllowance" />
        <result column="transport_allowance" property="transportAllowance" />
        <result column="lodging_limit" property="lodgingLimit" />
        <result column="standard_tag" property="standardTag" />
        <result column="within_standard" property="withinStandard" />
        <result column="tenant_id" property="tenantId" />
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="update_user" property="updateUser" />
        <result column="update_time" property="updateTime" />
        <result column="dept_id" property="deptId" />
    </resultMap>
</mapper>
src/main/resources/mapper/approve/KnowledgeBaseMapper.xml
@@ -16,6 +16,6 @@
                and type = #{knowledgeBase.type}
            </if>
        </where>
        order by id desc
    </select>
</mapper>
src/main/resources/mapper/basic/CustomerMapper.xml
@@ -52,6 +52,7 @@
                )
            </if>
        </where>
        order by c.id desc
    </select>
    <select id="list" resultType="com.ruoyi.basic.vo.CustomerVo">
@@ -106,6 +107,7 @@
                )
            </if>
        </where>
        order by c.id desc
    </select>
    <select id="customewTransactions" resultType="com.ruoyi.sales.vo.CustomerTransactionsVo">
@@ -134,6 +136,7 @@
                AND c.customer_name LIKE CONCAT('%', #{customerName}, '%')
            </if>
        </where>
        order by T1.customer_id desc
    </select>
    <select id="customewTransactionsDetails"
@@ -159,5 +162,6 @@
            group by  sl.id
        )T2 on T2.id = sl.id
        where sl.customer_id = #{customerId}
        order by sl.id desc
    </select>
</mapper>
src/main/resources/mapper/basic/SupplierManageMapper.xml
@@ -35,6 +35,7 @@
                AND T1.is_white = #{supplierManageDto.isWhite}
            </if>
        </where>
        order by T1.id desc
    </select>
    <select id="supplierExportList" resultType="com.ruoyi.basic.excel.SupplierManageExcelDto">
@@ -115,6 +116,7 @@
                AND sm.supplier_name LIKE CONCAT('%',#{supplierName},'%')
            </if>
        </where>
        order by T1.supplier_id desc
    </select>
    <select id="supplierTransactionsDetails"
@@ -123,42 +125,51 @@
              pl.purchase_contract_number,
              pl.execution_date,
              pl.contract_amount,
              IFNULL(T2.InboundAmount, 0) AS shippedAmount,
              pl.contract_amount - IFNULL(T2.InboundAmount, 0) AS unshippedAmount
       FROM purchase_ledger pl
       LEFT JOIN (
           SELECT t.sales_ledger_id,
                  SUM(t.inbound_amount) AS InboundAmount
           FROM (
               SELECT sir.stock_in_num * slp.tax_inclusive_unit_price AS inbound_amount, slp.sales_ledger_id
               FROM stock_in_record sir
               INNER JOIN sales_ledger_product slp ON slp.id = sir.record_id
               WHERE sir.approval_status = 1 AND sir.record_type = 7 AND slp.type = 2
               UNION ALL
              IFNULL(T1.paymentAmount, 0) AS paymentAmount,
              IFNULL(T2.InboundAmount, 0) - IFNULL(T3.returnAmount, 0) AS payableAmount
       from purchase_ledger pl
       left join (
           select
               pl.id,
               sum(app.payment_amount) as paymentAmount
           from account_purchase_payment app
           left join account_payment_application apa on app.account_payment_application_id = apa.id
           left join stock_in_record sir on FIND_IN_SET(sir.id, apa.stock_in_record_ids) > 0
               -- 10 ç±»åž‹æ‰å…³è”质检表
           LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
               -- åŠ¨æ€å…³è”é‡‡è´­ï¼ˆè‡ªåŠ¨é€‚é… 7 å’Œ 10)
           LEFT JOIN purchase_ledger pl
                     ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
           WHERE sir.approval_status = 1
             AND sir.record_type IN ('7','10')
           group by pl.id
       )T1 on T1.id = pl.id
       left join (
               SELECT
                   sir.stock_in_num * slp_agg.tax_inclusive_unit_price AS inbound_amount,
                   slp_agg.sales_ledger_id
               pl.id,
               sum(sir.stock_in_num * slp.tax_inclusive_unit_price) AS InboundAmount
               FROM stock_in_record sir
                        INNER JOIN quality_inspect qi
                                   ON qi.id = sir.record_id
                        INNER JOIN purchase_ledger pl2
                                   ON pl2.id = qi.purchase_ledger_id
                        INNER JOIN (
                   SELECT
                       sales_ledger_id,
                       product_model_id,
                       MIN(tax_inclusive_unit_price) AS tax_inclusive_unit_price
                   FROM sales_ledger_product
                   WHERE type = 2
                   GROUP BY sales_ledger_id, product_model_id
               ) slp_agg
                                   ON slp_agg.sales_ledger_id = pl2.id
                                       AND slp_agg.product_model_id = sir.product_model_id
               WHERE sir.approval_status = 1 AND sir.record_type = 10
           ) t
           GROUP BY t.sales_ledger_id
       ) T2 ON T2.sales_ledger_id = pl.id
       WHERE pl.supplier_id = #{supplierId}
                    -- 10 ç±»åž‹æ‰å…³è”质检表
                    LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
               -- åŠ¨æ€å…³è”é‡‡è´­ï¼ˆè‡ªåŠ¨é€‚é… 7 å’Œ 10)
                    LEFT JOIN purchase_ledger pl
                              ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
               -- äº§å“å…³è”不动
                    LEFT JOIN sales_ledger_product slp ON pl.id = slp.sales_ledger_id
           -- æ¡ä»¶
           WHERE sir.approval_status = 1 AND slp.type = 2
             AND sir.record_type IN ('7','10')
           group by pl.id
       )T2 on T2.id = pl.id
       left join (
           select pl.id,
                  sum(pro.total_amount) as returnAmount
           from purchase_return_orders pro
                    left join purchase_ledger pl on pro.purchase_ledger_id = pl.id
           group by pl.id
       )T3 on T3.id = pl.id
       where pl.supplier_id = #{supplierId}
       order by pl.id desc
    </select>
</mapper>
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.collaborativeApproval.pojo.EnterpriseNews">
        <id column="id" property="id" />
        <result column="title" property="title" />
        <result column="summary" property="summary" />
        <result column="content" property="content" />
        <result column="category" property="category" />
        <result column="read_scope" property="readScope" />
        <result column="is_required" property="isRequired" />
        <result column="status" property="status" />
        <result column="required_read_count" property="requiredReadCount" />
        <result column="read_count" property="readCount" />
        <result column="create_user" property="createUser" />
        <result column="create_time" property="createTime" />
        <result column="update_user" property="updateUser" />
        <result column="update_time" property="updateTime" />
        <result column="dept_id" property="deptId" />
    </resultMap>
    <select id="listPage" resultType="com.ruoyi.collaborativeApproval.vo.EnterpriseNewsVo">
        select en.*, u.nick_name as create_user_name from
        enterprise_news en
        left join sys_user u on en.create_user = u.user_id
        <where>
            <if test="enterpriseNewsDto.title != null and enterpriseNewsDto.title != ''">
                and en.title like concat('%',#{enterpriseNewsDto.title},'%')
            </if>
            <if test="enterpriseNewsDto.category != null and enterpriseNewsDto.category != ''">
                and en.category = #{enterpriseNewsDto.category}
            </if>
            <if test="enterpriseNewsDto.status != null ">
                and en.status = #{enterpriseNewsDto.status}
            </if>
            <if test="enterpriseNewsDto.createUser != null and enterpriseNewsDto.createUser != ''">
                and en.create_user = #{enterpriseNewsDto.createUser}
            </if>
            <if test="enterpriseNewsDto.createTimeStart != null and enterpriseNewsDto.createTimeEnd != null">
                and en.create_time between #{enterpriseNewsDto.createTimeStart} and #{enterpriseNewsDto.createTimeEnd}
            </if>
        </where>
        order by en.create_time desc
    </select>
</mapper>
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeDeptMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeDeptMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept">
        <id column="id" property="id" />
        <result column="news_id" property="newsId" />
        <result column="dept_id" property="deptId" />
        <result column="create_time" property="createTime" />
    </resultMap>
</mapper>
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeUserMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeUserMapper">
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser">
        <id column="id" property="id" />
        <result column="news_id" property="newsId" />
        <result column="user_id" property="userId" />
        <result column="create_time" property="createTime" />
    </resultMap>
</mapper>
src/main/resources/mapper/collaborativeApproval/NoticeMapper.xml
@@ -17,5 +17,6 @@
                and n.status = #{ew.status}
            </if>
        </where>
        order by n.create_time desc
    </select>
</mapper>
src/main/resources/mapper/collaborativeApproval/RulesRegulationsManagementMapper.xml
@@ -27,5 +27,6 @@
                and rrm.category = #{ew.category}
            </if>
        </where>
        order by rrm.id desc
    </select>
</mapper>
src/main/resources/mapper/collaborativeApproval/SealApplicationManagementMapper.xml
@@ -37,5 +37,6 @@
            </if>
        </where>
        GROUP BY sam.id
        order by sam.id desc
    </select>
</mapper>
src/main/resources/mapper/device/DeviceMaintenanceMapper.xml
@@ -26,7 +26,7 @@
        left join device_ledger dl on dm.device_ledger_id = dl.id
        left join sys_user su on dm.create_user = su.user_id
        <where>
            <if test="deviceMaintenanceDto.deviceName != null and deviceMaintenanceDto.deviceName != ''">
            <if test="deviceMaintenanceDto.deviceName != null">
                and dl.device_name like concat('%', #{deviceMaintenanceDto.deviceName}, '%')
            </if>
            <if test="deviceMaintenanceDto.deviceModel != null and deviceMaintenanceDto.deviceModel != ''">
@@ -48,17 +48,7 @@
                date_add(str_to_date(#{deviceMaintenanceDto.maintenanceActuallyTime}, '%Y-%m-%d'), interval 1 day)
            </if>
        </where>
        order by
        <!--    å¾…保养(0)优先排在上面,已完结(1)在下面 -->
        dm.status asc,
        case
        <!-- å½“状态是 0(待保养)时,按计划时间升序,即时间最远的单子在最上面 -->
        when dm.status = 0 then dm.maintenance_plan_time
        end asc,
        case
        <!-- å½“状态是 1(已完结)时,按实际保养时间降序,最近刚保养完的单子在已完结里排最前 -->
        when dm.status = 1 then dm.maintenance_actually_time
        end desc
        order by dm.create_time desc
    </select>
    <select id="detailById" resultType="com.ruoyi.device.vo.DeviceMaintenanceVo">
src/main/resources/mapper/device/DeviceRepairMapper.xml
@@ -29,7 +29,6 @@
        from device_repair dr
        left join device_ledger dl on dr.device_ledger_id = dl.id
        <where>
            1 = 1
            <if test="deviceRepairDto.deviceName != null">
                and dl.device_name like concat('%',#{deviceRepairDto.deviceName},'%')
            </if>
@@ -53,6 +52,7 @@
                and dr.maintenance_time like concat('%',#{deviceRepairDto.maintenanceTimeStr},'%')
            </if>
        </where>
        order by dr.create_time desc
    </select>
    <select id="detailById" resultType="com.ruoyi.device.vo.DeviceRepairVo">
        select dr.id,
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml
@@ -62,6 +62,6 @@
                AND record_date = DATE_FORMAT(#{req.recordDate},'%Y-%m-%d')
            </if>
        </where>
        ORDER BY update_time DESC
        ORDER BY create_time DESC
    </select>
</mapper>
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerRecordMapper.xml
@@ -32,7 +32,7 @@
                AND t1.record_date = DATE_FORMAT(#{req.recordDate},'%Y-%m-%d')
            </if>
        </where>
        ORDER BY t1.update_time DESC
        ORDER BY t1.create_time DESC
    </select>
    <select id="list" resultType="com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedgerRecord">
        SELECT
src/main/resources/mapper/measuringinstrumentledger/SparePartsMapper.xml
@@ -10,5 +10,6 @@
                and sp.name like concat('%',#{spareParts.name},'%')
            </if>
        </where>
        order by sp.create_time desc
    </select>
</mapper>
src/main/resources/mapper/measuringinstrumentledger/SparePartsRequisitionRecordMapper.xml
@@ -32,5 +32,6 @@
                and sprr.source_type = #{params.sourceType}
            </if>
        </where>
        order by sprr.create_time desc
    </select>
</mapper>
src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml
@@ -39,6 +39,7 @@
                and sl.sales_contract_no like concat('%',#{req.salesContractNo},'%')
            </if>
        </where>
        order by rm.id desc
    </select>
    <select id="getReturnManagementDtoById" resultType="com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto">
     select rm.*,
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -71,7 +71,7 @@
                and pot.work_order_no like concat('%', #{c.workOrderNo}, '%')
            </if>
        </where>
        order by pot.id desc
        order by pot.production_order_id desc, poro.drag_sort
    </select>
    <select id="selectTaskStatisticsByDate" resultType="com.ruoyi.home.dto.ProductionTaskStatisticsDto">
src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml
@@ -120,6 +120,12 @@
                <if test="c.supplierId != null">
                    AND pl.supplier_id = #{c.supplierId}
                </if>
                <if test="c.ids != null and c.ids.size() > 0">
                    AND pl.id IN
                    <foreach collection="c.ids" item="id" open="(" separator="," close=")">
                        #{id}
                    </foreach>
                </if>
                <if test="c.approvalStatus != null">
                    AND pl.approval_status = #{c.approvalStatus}
                </if>
src/main/resources/mapper/quality/QualityInspectMapper.xml
@@ -58,6 +58,15 @@
        <if test="qualityInspect.entryDateEnd != null and qualityInspect.entryDateEnd != '' ">
            AND qi.check_time &lt;= DATE_FORMAT(#{qualityInspect.entryDateEnd},'%Y-%m-%d')
        </if>
        <if test="qualityInspect.purchaseContractNo != null and qualityInspect.purchaseContractNo != '' ">
            AND pl.purchase_contract_number like concat('%',#{qualityInspect.purchaseContractNo},'%')
        </if>
        <if test="qualityInspect.workOrderNo != null and qualityInspect.workOrderNo != '' ">
            AND pot.work_order_no like concat('%',#{qualityInspect.workOrderNo},'%')
        </if>
        <if test="qualityInspect.salesContractNo != null and qualityInspect.salesContractNo != '' ">
            AND po_sales.sales_contract_no like concat('%',#{qualityInspect.salesContractNo},'%')
        </if>
        ORDER BY qi.check_time DESC
    </select>
src/main/resources/mapper/quality/QualityTestStandardMapper.xml
@@ -19,6 +19,7 @@
        <if test="c.inspectType != null ">
            AND inspect_type =#{c.inspectType}
        </if>
        order by create_time desc
    </select>
    <select id="getQualityTestStandardByProductId" resultType="com.ruoyi.quality.pojo.QualityTestStandard">
        SELECT qts.*
src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml
@@ -26,8 +26,7 @@
        END AS method
        FROM quality_unqualified qu
        LEFT JOIN product_model pm ON qu.model = pm.id
        where
        1=1
        <where>
        <if test="qualityUnqualified.inspectType != null ">
            AND inspect_type = #{qualityUnqualified.inspectType}
        </if>
@@ -43,6 +42,8 @@
        <if test="qualityUnqualified.entryDateEnd != null and qualityUnqualified.entryDateEnd != '' ">
            AND  check_time &lt;= DATE_FORMAT(#{qualityUnqualified.entryDateEnd},'%Y-%m-%d')
        </if>
        </where>
        order by qu.create_time desc
    </select>
    <select id="qualityUnqualifiedExport" resultType="com.ruoyi.quality.pojo.QualityUnqualified">
        SELECT
src/main/resources/mapper/sales/SalesLedgerMapper.xml
@@ -134,10 +134,12 @@
        from sales_ledger sl
        left join purchase_ledger pl on sl.id = pl.sales_ledger_id
        left join customer c on sl.customer_id = c.id
        where 1=1
        <where>
        <if test="customerName != null and customerName != '' ">
            and c.customer_name like concat('%',#{customerName},'%')
        </if>
        </where>
        order by sl.entry_date desc
    </select>
</mapper>
src/main/resources/mapper/sales/SalesQuotationMapper.xml
@@ -4,12 +4,9 @@
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.sales.mapper.SalesQuotationMapper">
    <select id="listPage" resultType="com.ruoyi.sales.dto.SalesQuotationDto">
        SELECT t1.*,
               t2.approve_user_ids
        SELECT t1.*
        FROM sales_quotation t1
        LEFT JOIN approve_process t2 ON t1.quotation_no = t2.approve_reason and t2.approve_type = 6
        WHERE 1=1
          and t2.approve_delete = 0
        <where>
        <if test="salesQuotationDto.quotationNo != null and salesQuotationDto.quotationNo != '' ">
            AND t1.quotation_no LIKE CONCAT('%',#{salesQuotationDto.quotationNo},'%')
        </if>
@@ -19,5 +16,7 @@
        <if test="salesQuotationDto.status != null and salesQuotationDto.status != '' ">
            AND t1.status = #{salesQuotationDto.status}
        </if>
        </where>
        order by t1.id desc
    </select>
</mapper>
src/main/resources/mapper/staff/PersonalAttendanceRecordsMapper.xml
@@ -39,6 +39,7 @@
            and personal_attendance_records.date &gt;= #{params.date}
            and personal_attendance_records.date &lt; DATE_ADD(DATE(#{params.date}), INTERVAL 1 DAY)
        </if>
        order by personal_attendance_records.id desc
    </select>
    <!-- æŸ¥è¯¢æŒ‡å®šæ—¥æœŸæ²¡æœ‰è€ƒå‹¤è®°å½•的在职员工(在指定时间之前入职的) -->
src/main/resources/mapper/staff/PersonalShiftMapper.xml
@@ -31,7 +31,7 @@
            </if>
        </where>
        GROUP BY u.id, u.staff_name
        ORDER BY MAX(s.create_time)
        ORDER BY MAX(s.create_time) desc
    </select>
   
    <select id="performanceShiftYear" resultType="java.util.Map">
src/main/resources/mapper/staff/StaffLeaveMapper.xml
@@ -18,18 +18,24 @@
        soj.emergency_contact as emergencyContact,
        soj.emergency_contact_phone as emergencyContactPhone,
        sp.post_name as postName,
        sd.dept_name as deptName
        sd.dept_name as deptName,
        se.education as first_study,
        se.major as profession
        FROM staff_leave
        LEFT JOIN
        staff_on_job soj ON soj.id = staff_leave.staff_on_job_id
        LEFT JOIN
        sys_post sp ON sp.post_id = soj.sys_post_id
        LEFT JOIN
        sys_dept sd ON sd.dept_id = soj.sys_dept_id
        where 1=1
        LEFT JOIN staff_on_job soj ON soj.id = staff_leave.staff_on_job_id
        LEFT JOIN sys_post sp ON sp.post_id = soj.sys_post_id
        LEFT JOIN sys_dept sd ON sd.dept_id = soj.sys_dept_id
        LEFT JOIN staff_education se ON se.staff_on_job_id = staff_leave.staff_on_job_id
        AND se.id = (
        SELECT MAX(se2.id)
        FROM staff_education se2
        WHERE se2.staff_on_job_id = staff_leave.staff_on_job_id
        )
        WHERE 1=1
        <if test="c.staffName != null and c.staffName != '' ">
            AND soj.staff_name LIKE CONCAT('%',#{c.staffName},'%')
        </if>
        order by staff_leave.create_time desc
    </select>
    <select id="staffLeaveList" resultType="com.ruoyi.staff.dto.StaffLeaveDto">
        SELECT
@@ -56,10 +62,12 @@
        sys_post sp ON sp.post_id = soj.sys_post_id
        LEFT JOIN
        sys_dept sd ON sd.dept_id = soj.sys_dept_id
        where 1=1
        <where>
        <if test="c.staffName != null and c.staffName != '' ">
            AND soj.staff_name LIKE CONCAT('%',#{c.staffName},'%')
        </if>
        </where>
        order by staff_leave.create_time desc
    </select>
    <select id="staffLeaveReasonAnalytics" resultType="com.ruoyi.staff.dto.StaffLeaveDto">
src/main/resources/mapper/staff/StaffOnJobMapper.xml
@@ -3,29 +3,83 @@
<mapper namespace="com.ruoyi.staff.mapper.StaffOnJobMapper">
    <select id="staffOnJobListPage" resultType="com.ruoyi.staff.dto.StaffOnJobDto">
        SELECT
        staff_on_job.*,
        sp.post_name as postName,
        sd.dept_name as deptName,
        MIN(t1.contract_start_time) as contract_start_time,  -- å–最早合同开始时间
        MAX(t1.contract_end_time) as contract_end_time
        staff_on_job.id,
        staff_on_job.staff_state,
        staff_on_job.staff_no,
        staff_on_job.staff_name,
        staff_on_job.sex,
        staff_on_job.native_place,
        staff_on_job.sys_post_id,
        staff_on_job.sys_dept_id,
        staff_on_job.role_id,
        staff_on_job.adress,
        staff_on_job.first_study,
        staff_on_job.profession,
        staff_on_job.identity_card,
        staff_on_job.age,
        staff_on_job.phone,
        staff_on_job.contract_term,
        staff_on_job.contract_expire_time,
        staff_on_job.trial_end_date,
        staff_on_job.trial_start_date,
        staff_on_job.sign_date,
        staff_on_job.salary_select,
        staff_on_job.pro_salary,
        staff_on_job.date_select,
        staff_on_job.remark,
        staff_on_job.create_time,
        staff_on_job.create_user,
        staff_on_job.update_time,
        staff_on_job.update_user,
        staff_on_job.tenant_id,
        staff_on_job.alias,
        staff_on_job.birth_date,
        staff_on_job.nation,
        staff_on_job.marital_status,
        staff_on_job.pro_term,
        staff_on_job.positive_date,
        staff_on_job.basic_salary,
        staff_on_job.dept_id,
        sp.post_name AS post_name,
        sd.dept_name AS dept_name,
        MIN(t1.contract_start_time) AS contract_start_time,
        MAX(t1.contract_end_time) AS contract_end_time,
        (
        SELECT GROUP_CONCAT(sec.contact_name SEPARATOR ',')
        FROM staff_emergency_contact sec
        WHERE sec.staff_on_job_id = staff_on_job.id
        ) AS emergency_contact,
        (
        SELECT GROUP_CONCAT(sec.contact_phone SEPARATOR ',')
        FROM staff_emergency_contact sec
        WHERE sec.staff_on_job_id = staff_on_job.id
        ) AS emergency_contact_phone
        FROM staff_on_job
        LEFT JOIN sys_post sp ON sp.post_id = staff_on_job.sys_post_id
        LEFT JOIN sys_dept sd ON sd.dept_id = staff_on_job.sys_dept_id
        LEFT JOIN staff_contract as t1 ON t1.staff_on_job_id = staff_on_job.id
        WHERE 1=1
        LEFT JOIN staff_contract AS t1 ON t1.staff_on_job_id = staff_on_job.id
        <where>
        <if test="staffOnJob.staffState != null">
            AND staff_state = #{staffOnJob.staffState}
                AND staff_on_job.staff_state = #{staffOnJob.staffState}
        </if>
        <if test="staffOnJob.staffName != null and staffOnJob.staffName != '' ">
            AND staff_name LIKE CONCAT('%',#{staffOnJob.staffName},'%')
                AND staff_on_job.staff_name LIKE CONCAT('%',#{staffOnJob.staffName},'%')
        </if>
        <if test="staffOnJob.entryDateStart != null and staffOnJob.entryDateStart != '' ">
            AND contract_expire_time &gt;= DATE_FORMAT(#{staffOnJob.entryDateStart},'%Y-%m-%d')
                AND staff_on_job.contract_expire_time &gt;= DATE_FORMAT(#{staffOnJob.entryDateStart},'%Y-%m-%d')
        </if>
        <if test="staffOnJob.entryDateEnd != null and staffOnJob.entryDateEnd != '' ">
            AND contract_expire_time &lt;= DATE_FORMAT(#{staffOnJob.entryDateEnd},'%Y-%m-%d')
                AND staff_on_job.contract_expire_time &lt;= DATE_FORMAT(#{staffOnJob.entryDateEnd},'%Y-%m-%d')
        </if>
            <if test="staffOnJob.sysDeptId != null">
                AND staff_on_job.sys_dept_id = #{staffOnJob.sysDeptId}
            </if>
        </where>
        GROUP BY staff_on_job.id
        <if test="staffOnJob.contractStartTime != null">
            HAVING MIN(t1.contract_start_time) = #{staffOnJob.contractStartTime}
        </if>
        order by staff_on_job.create_time desc
    </select>
    <select id="staffOnJobList" resultType="com.ruoyi.staff.dto.StaffOnJobDto">
        SELECT
@@ -37,13 +91,15 @@
        sys_post sp ON sp.post_id = staff_on_job.sys_post_id
        LEFT JOIN
        sys_dept sd ON sd.dept_id = staff_on_job.sys_dept_id
        where 1=1
        <where>
        <if test="staffOnJob.staffState != null">
            AND staff_state = #{staffOnJob.staffState}
        </if>
        <if test="staffOnJob.staffName != null and staffOnJob.staffName != '' ">
            AND staff_name LIKE CONCAT('%',#{staffOnJob.staffName},'%')
        </if>
        </where>
        order by staff_on_job.create_time desc
    </select>
    <!-- ç»Ÿè®¡æŒ‡å®šæ—¥æœŸçš„在职员工数 -->
    <select id="countOnJobStaffByDate" resultType="java.lang.Integer">
src/main/resources/mapper/stock/StockInventoryMapper.xml
@@ -222,6 +222,7 @@
        unit,
        product_name,
        product_id
        order by combined.create_time desc
    </select>
    <select id="listStockInventoryExportData" resultType="com.ruoyi.stock.execl.StockInventoryExportData">
@@ -367,6 +368,7 @@
                and sir.create_time &lt;= #{ew.endMonth}
            </if>
        </where>
        order by sir.id desc
    </select>
    <select id="stockInAndOutRecord" resultType="com.ruoyi.stock.dto.StockInventoryDto">
src/main/resources/mapper/system/SysDeptMapper.xml
@@ -26,6 +26,7 @@
    <sql id="selectDeptVo">
        select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time 
        from sys_dept d
        order by d.create_time desc
    </sql>
    <select id="selectDeptList" parameterType="com.ruoyi.project.system.domain.SysDept" resultMap="SysDeptResult">
@@ -49,7 +50,7 @@
        <!-- æ•°æ®èŒƒå›´è¿‡æ»¤ -->
        ${params.dataScope}
        group by d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time
        order by d.parent_id, d.order_num
        order by d.parent_id asc, d.order_num asc, d.create_time desc
    </select>
    
    <select id="selectDeptListByRoleId" resultType="java.lang.Long">
@@ -60,7 +61,7 @@
            <if test="deptCheckStrictly">
              and d.dept_id not in (select d.parent_id from sys_dept d inner join sys_role_dept rd on d.dept_id = rd.dept_id and rd.role_id = #{roleId})
            </if>
        order by d.parent_id, d.order_num
        order by d.parent_id asc, d.order_num asc, d.create_time desc
    </select>
    
    <select id="selectDeptById" parameterType="Long" resultMap="SysDeptResult">
@@ -69,6 +70,7 @@
            d.dept_nick
        from sys_dept d
        where d.dept_id = #{deptId}
        order by d.create_time desc
    </select>
    
    <select id="checkDeptExistUser" parameterType="Long" resultType="int">
src/main/resources/mapper/system/SysDictTypeMapper.xml
@@ -39,6 +39,7 @@
                and date_format(create_time,'%Y%m%d') &lt;= date_format(#{params.endTime},'%Y%m%d')
            </if>
        </where>
        order by create_time desc
    </select>
    
    <select id="selectDictTypeAll" resultMap="SysDictTypeResult">
src/main/resources/mapper/system/SysNoticeMapper.xml
@@ -10,11 +10,16 @@
        <result property="noticeType"     column="notice_type"     />
        <result property="noticeContent"  column="notice_content"  />
        <result property="status"         column="status"          />
        <result property="senderId"       column="sender_id"       />
        <result property="consigneeId"    column="consignee_id"    />
        <result property="jumpPath"       column="jump_path"       />
        <result property="appJumpPath"    column="app_jump_path"   />
        <result property="createBy"       column="create_by"       />
        <result property="createTime"     column="create_time"     />
        <result property="updateBy"       column="update_by"       />
        <result property="updateTime"     column="update_time"     />
        <result property="remark"         column="remark"          />
        <result property="tenantId"       column="tenant_id"       />
    </resultMap>
    <sql id="selectNoticeVo">
src/main/resources/mapper/system/SysPostMapper.xml
@@ -35,6 +35,7 @@
                AND post_name like concat('%', #{postName}, '%')
            </if>
        </where>
        order by create_time desc
    </select>
    
    <select id="selectPostAll" resultMap="SysPostResult">
src/main/resources/mapper/system/SysRoleMapper.xml
src/main/resources/mapper/system/SysUserMapper.xml
@@ -78,8 +78,7 @@
    </sql>
    <select id="selectUserList" parameterType="com.ruoyi.project.system.domain.SysUser" resultMap="SysUserResult">
        select u.user_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag,
        u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,T2.dept_names from sys_user u
        select u.user_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,T2.dept_names from sys_user u
        left join
        ( SELECT T1.user_id,GROUP_CONCAT(T2.dept_name SEPARATOR ', ') AS dept_names
        FROM
@@ -119,6 +118,7 @@
        </if>
        <!-- æ•°æ®èŒƒå›´è¿‡æ»¤ -->
        ${params.dataScope}
        ORDER BY u.create_time DESC
    </select>
    <select id="selectAllocatedList" parameterType="com.ruoyi.project.system.domain.SysUser" resultMap="SysUserResult">
@@ -137,15 +137,13 @@
        ${params.dataScope}
    </select>
    <select id="selectUnallocatedList" parameterType="com.ruoyi.project.system.domain.SysUser"
            resultMap="SysUserResult">
    <select id="selectUnallocatedList" parameterType="com.ruoyi.project.system.domain.SysUser" resultMap="SysUserResult">
        select distinct u.user_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time
        from sys_user u
        left join sys_user_role ur on u.user_id = ur.user_id
        left join sys_role r on r.role_id = ur.role_id
        where u.del_flag = '0' and (r.role_id != #{roleId} or r.role_id IS NULL)
        and u.user_id not in (select u.user_id from sys_user u inner join sys_user_role ur on u.user_id = ur.user_id and
        ur.role_id = #{roleId})
        and u.user_id not in (select u.user_id from sys_user u inner join sys_user_role ur on u.user_id = ur.user_id and ur.role_id = #{roleId})
        <if test="userName != null and userName != ''">
            AND u.user_name like concat('%', #{userName}, '%')
        </if>
@@ -154,17 +152,20 @@
        </if>
        <!-- æ•°æ®èŒƒå›´è¿‡æ»¤ -->
        ${params.dataScope}
        ORDER BY u.create_time DESC
    </select>
    <select id="selectUserByUserName" parameterType="String" resultMap="SysUserResult">
        <include refid="selectUserVo"/>
        where u.user_name = #{userName} and u.del_flag = '0'
        ORDER BY u.create_time DESC
    </select>
    <select id="selectUserById" parameterType="Long" resultMap="SysUserResult">
        <include refid="selectUserVo"/>
        where u.user_id = #{userId}
        and u.del_flag = '0'
        ORDER BY u.create_time DESC
    </select>
    <select id="checkUserNameUnique" parameterType="String" resultMap="SysUserResult">
@@ -198,6 +199,7 @@
            </if>
            and u.del_flag = '0'
        </where>
        ORDER BY u.create_time DESC
    </select>
    <select id="selectRegistrantIds" resultType="com.ruoyi.project.system.domain.SysUser">
        SELECT user_id, nick_name FROM sys_user
@@ -212,6 +214,7 @@
                1=0  <!-- ç©ºåˆ—表时返回空结果 -->
            </if>
        </where>
        ORDER BY sys_user.create_time DESC
    </select>
    <select id="selectUsersByIds" resultType="com.ruoyi.project.system.domain.SysUser">
        SELECT user_id, nick_name
@@ -221,6 +224,7 @@
            #{id}
        </foreach>
        and del_flag = '0'
        ORDER BY sys_user.create_time DESC
    </select>
    <select id="selectUserByNickName" resultType="com.ruoyi.project.system.domain.SysUser"
            parameterType="java.lang.String">
@@ -229,8 +233,7 @@
        limit 1
    </select>
    <insert id="insertUser" parameterType="com.ruoyi.project.system.domain.SysUser" useGeneratedKeys="true"
            keyProperty="userId">
    <insert id="insertUser" parameterType="com.ruoyi.project.system.domain.SysUser" useGeneratedKeys="true" keyProperty="userId">
        insert into sys_user(
        <if test="userId != null and userId != 0">user_id,</if>
        <if test="userName != null and userName != ''">user_name,</if>
@@ -283,27 +286,19 @@
    </update>
    <update id="updateUserStatus" parameterType="com.ruoyi.project.system.domain.SysUser">
        update sys_user
        set status = #{status}
        where user_id = #{userId}
         update sys_user set status = #{status} where user_id = #{userId}
    </update>
    <update id="updateUserAvatar" parameterType="com.ruoyi.project.system.domain.SysUser">
        update sys_user
        set avatar = #{avatar}
        where user_name = #{userName}
         update sys_user set avatar = #{avatar} where user_name = #{userName}
    </update>
    <update id="resetUserPwd" parameterType="com.ruoyi.project.system.domain.SysUser">
        update sys_user
        set password = #{password}
        where user_name = #{userName}
         update sys_user set password = #{password} where user_name = #{userName}
    </update>
    <delete id="deleteUserById" parameterType="Long">
        update sys_user
        set del_flag = '2'
        where user_id = #{userId}
         update sys_user set del_flag = '2' where user_id = #{userId}
    </delete>
    <delete id="deleteUserByIds" parameterType="Long">
@@ -311,7 +306,14 @@
        <foreach collection="array" item="userId" open="(" separator="," close=")">
            #{userId}
        </foreach>
    </delete>
        and del_flag = '0'
    </select>
    <select id="selectUserByNickName" resultType="com.ruoyi.project.system.domain.SysUser"
            parameterType="java.lang.String">
        <include refid="selectUserVo"/>
        where u.nick_name = #{nickName} and u.del_flag = '0'
        limit 1
    </select>
    <select id="getUserByRole" resultType="java.lang.Long">
        select distinct su.user_id
@@ -323,6 +325,7 @@
          and sr.status = '0'
          and su.status = '0'
          and su.del_flag = '0'
        ORDER BY su.create_time DESC
    </select>
    <select id="getUserByPerms" resultType="java.lang.Long">
        select distinct t5.user_id
src/main/resources/mapper/technology/TechnologyOperationMapper.xml
@@ -32,6 +32,6 @@
                and t.type = #{c.type}
            </if>
        </where>
        order by t.id asc
        order by t.id desc
    </select>
</mapper>
src/main/resources/mapper/technology/TechnologyOperationParamMapper.xml
@@ -28,6 +28,6 @@
                and top1.technology_param_id = #{paramId}
            </if>
        </where>
        order by top1.id asc
        order by top1.id desc
    </select>
</mapper>
src/main/resources/mapper/warehouse/DocumentationBorrowManagementMapper.xml
@@ -51,6 +51,7 @@
        <if test="documentationBorrowManagement.entryDateEnd != null and documentationBorrowManagement.entryDateEnd != ''">
            and borrow_date &lt;= DATE_FORMAT(#{documentationBorrowManagement.entryDateEnd},'%Y-%m-%d')
        </if>
        order by dbm.create_time desc
    </select>
    <select id="export" resultType="com.ruoyi.warehouse.dto.DocumentationBorrowManagementDto">
        select dbm.*,doc.doc_name
src/main/resources/mapper/warehouse/DocumentationMapper.xml
@@ -32,6 +32,7 @@
        <if test="documentation.id != null">
            and doc.id = #{documentation.id}
        </if>
        order by doc.create_time desc
    </select>
    <select id="listByDocumentClassificationId" resultType="com.ruoyi.warehouse.dto.DocumentationDto">
        SELECT
src/main/resources/mapper/warehouse/DocumentationReturnManagementMapper.xml
@@ -28,6 +28,7 @@
        <if test="documentationReturnManagement.entryDateEnd != null and documentationReturnManagement.entryDateEnd != ''">
            and return_date &lt;= DATE_FORMAT(#{documentationReturnManagement.entryDateEnd},'%Y-%m-%d')
        </if>
        order by dbm.create_time desc
    </select>
    <select id="exportrevent" resultType="com.ruoyi.warehouse.dto.ReturnExportDto">
src/main/resources/mapper/warehouse/WarehouseGoodsShelvesRowcolMapper.xml
@@ -36,6 +36,7 @@
                and wgsr.id = #{warehouseGoodsShelvesRowcol.id}
            </if>
        </where>
        order by wgsr.create_time desc
    </select>
</mapper>