liyong
8 天以前 a5d7ef82a926d72651bb35779a59883528a9d641
Merge remote-tracking branch 'origin/dev_New_pro' into dev_New_pro

# Conflicts:
# src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
已添加52个文件
已重命名11个文件
已修改124个文件
已删除1个文件
8355 ■■■■■ 文件已修改
.gitignore 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260512_AccountSubject树形改造前端修改文档.md 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260512_add_parent_id_to_account_subject.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260512_create_financial_management_tables.sql 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260512_财务管理模块前端联调文档.md 288 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260515_device_maintenance_inspection_abnormal_acceptance.sql 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260515_设备巡检异常联动维修单_前端联调文档.md 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260516_制造智能助手前端联调文档.md 258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/前端联调文档-设备报修保养财务模块改造.md 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/PurchaseInboundDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/PurchaseReturnDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/SalesOutboundDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/SalesReturnDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/FinDetailLedgerQueryDto.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/FinFixedAssetDto.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/FinIdBatchDto.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/FinIntangibleAssetDto.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/FinLedgerQueryDto.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherDto.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherEntryDto.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherPageDto.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherStatusDto.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerEntryRecordVo.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerRowVo.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/financial/FinVoucherDetailVo.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountSubjectController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/financial/FinFixedAssetController.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/financial/FinIntangibleAssetController.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/financial/FinLedgerController.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/financial/FinVoucherController.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/financial/FinFixedAssetMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/financial/FinIntangibleAssetMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/financial/FinVoucherEntryMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/financial/FinVoucherMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/AccountSubject.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/financial/FinFixedAsset.java 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/financial/FinIntangibleAsset.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/financial/FinVoucher.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/financial/FinVoucherEntry.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountSubjectService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/financial/FinFixedAssetService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/financial/FinIntangibleAssetService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/financial/FinLedgerService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/financial/FinVoucherService.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java 362 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/financial/FinFixedAssetServiceImpl.java 231 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/financial/FinIntangibleAssetServiceImpl.java 250 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/financial/FinLedgerServiceImpl.java 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java 307 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java 1035 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApproveProcessVO.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/ProductModelDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/ProductModelMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/ProductModel.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/SealApplicationManagementController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/dto/SealApplicationManagementDTO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/SealApplicationManagementServiceImpl.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/controller/DeviceRepairController.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/execl/DeviceRepairExeclDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/DeviceRepair.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/MaintenanceTask.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/IDeviceRepairService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/framework/security/LoginUser.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/pojo/InspectionTask.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/pojo/TimingTask.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/Details.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/InventoryInformationDto.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementAddDto.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementDto.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementManagementUpdateDto.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementPageDto.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementPageDtoCopy.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutAdd.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutPageDto.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementUpdateDto.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ReturnManagementDto.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ReturnSaleProductDto.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/vo/ShippingInfoVo.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/vo/ShippingProductVo.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementExceptionRecordController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementRecordController.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementRecordOutController.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/ReturnManagementController.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/ReturnSaleProductDto.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/mapper/ProcurementRecordMapper.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/mapper/ProcurementRecordOutMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/mapper/ReturnManagementMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/mapper/ReturnSaleProductMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/pojo/ReturnManagement.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/pojo/ReturnSaleProduct.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/ProcurementRecordOutService.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/ProcurementRecordService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/ReturnManagementService.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/ReturnSaleProductService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordOutServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnManagementServiceImpl.java 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnSaleProductServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionAccount.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionProductInput.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionProductOutput.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java 596 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysLoginController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/service/impl/PlanServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/service/impl/handle/InfoStageHandleService.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/PurchaseReturnOrdersController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/PurchaseReturnOrderProductsDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/SimpleReturnOrderGroupDto.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/PurchaseReturnOrdersMapper.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/ProductRecord.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseReturnOrderProducts.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/SalesLedgerProductTemplate.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/PurchaseReturnOrdersService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/PurchaseReturnDetailsVo.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/PurchaseReturnOrderProductsDetailVo.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/PurchaseStockInProductVo.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 77 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityUnqualifiedServiceImpl.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/SalesLedgerProductController.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/ShippingInfoMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesQuotationProduct.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ShippingInfoService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 100 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/execl/StockInRecordExportData.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/execl/StockInventoryExportData.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/execl/StockOutRecordExportData.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/execl/StockUnInventoryExportData.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/StockInventoryService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockUninventoryServiceImpl.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/bean/vo/TechnologyBomStructureVo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyBomController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/TechnologyBomService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyBomServiceImpl.java 125 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/warehouse/mapper/DocumentationMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/logback.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/manufacturing-agent-prompt.txt 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountSubjectMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/SealApplicationManagementMapper.xml 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceRepairMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ProcurementRecordMapper.xml 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ProcurementRecordOutMapper.xml 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ReturnSaleProductMapper.xml 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseReturnOrderProductsMapper.xml 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesQuotationMapper.xml 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ShippingInfoMapper.xml 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInRecordMapper.xml 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 345 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockOutRecordMapper.xml 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/technology/TechnologyBomStructureMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -4,7 +4,7 @@
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
claude.md
target/
!.mvn/wrapper/maven-wrapper.jar
doc/20260512_AccountSubjectÊ÷ÐθÄÔìǰ¶ËÐÞ¸ÄÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,185 @@
# AccountSubject æ ‘形改造前端修改文档
更新时间:2026-05-12
## 1. å˜æ›´èƒŒæ™¯
`AccountSubjectController` å·²æ”¹ä¸ºçˆ¶å­å±‚级递归模式,`/accountSubject/list` çŽ°åœ¨è¿”å›žæ ‘å½¢ç»“æž„ï¼ˆ`children` é€’归),不再是单纯的平铺列表。
---
## 2. åŽç«¯æŽ¥å£å˜åŒ–
### 2.1 æŸ¥è¯¢æŽ¥å£ï¼ˆå·²å˜æ›´ä¸ºæ ‘)
- URL:`GET /accountSubject/list`
- å…¥å‚:保持不变(`current,size,subjectCode,subjectName,subjectType,status`)
- å‡ºå‚:仍是分页壳(`records,total`),但 `records` å˜ä¸ºæ ‘节点数组(根节点分页,子节点递归内嵌)
示例:
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "id": 1,
        "parentId": null,
        "subjectCode": "1002",
        "subjectName": "银行存款",
        "subjectType": "资产类",
        "balanceDirection": "借方",
        "status": 0,
        "leaf": false,
        "children": [
          {
            "id": 2,
            "parentId": 1,
            "subjectCode": "100201",
            "subjectName": "工行存款",
            "subjectType": "资产类",
            "balanceDirection": "借方",
            "status": 0,
            "leaf": true,
            "children": []
          }
        ]
      }
    ],
    "total": 1
  }
}
```
### 2.2 æ–°å¢ž/编辑接口字段变化
- `POST /accountSubject/add`
- `PUT /accountSubject/edit`
新增支持字段:
- `parentId`:父节点ID(为空表示根节点)
示例:
```json
{
  "id": 2,
  "parentId": 1,
  "subjectCode": "100201",
  "subjectName": "工行存款",
  "subjectType": "资产类",
  "balanceDirection": "借方",
  "status": 0,
  "remark": ""
}
```
### 2.3 åˆ é™¤æŽ¥å£è¡Œä¸ºå˜åŒ–
- `DELETE /accountSubject/remove/{ids}`
行为:
1. åˆ é™¤çˆ¶èŠ‚ç‚¹æ—¶ä¼šé€’å½’åˆ é™¤æ‰€æœ‰å­å­™èŠ‚ç‚¹ã€‚
2. è‹¥ä»»æ„å¾…删除节点(含子孙)已被 `fin_voucher_entry.subject_code` å¼•用,则整体删除失败。
---
## 3. å‰ç«¯æ”¹é€ æ¸…单
## 3.1 æ€»è´¦ç§‘目管理页
文件:`src/views/financialManagement/generalLedger/index.vue`
### å¿…改项
1. æ–°å¢žâ€œçˆ¶ç§‘目”选择控件(`el-cascader` æˆ– `el-tree-select`),保存时带 `parentId`。
2. åˆ—表改为树表展示(推荐 `el-table` + `row-key` + `tree-props`)。
3. æœç´¢é€»è¾‘保持不变,但要兼容 `records` ä¸ºæ ‘结构。
---
## 3.2 å‡­è¯é¡µç§‘目下拉
文件:`src/views/financialManagement/voucher/index.vue`
当前凭证分录使用 `el-select`(平铺)。
后端已返回树,需要前端扁平化后再绑定下拉。
示例(可复用):
```js
const flattenSubjectTree = (nodes, result = []) => {
  (nodes || []).forEach(node => {
    result.push({
      code: node.subjectCode,
      name: node.subjectName,
      id: node.id,
      parentId: node.parentId
    });
    if (node.children?.length) {
      flattenSubjectTree(node.children, result);
    }
  });
  return result;
};
// list æŽ¥å£è¿”回后:
const treeRecords = response.data?.records || [];
subjectList.value = flattenSubjectTree(treeRecords);
```
---
## 3.3 ç§‘目总账/明细账页级联
文件:
- `src/views/financialManagement/voucher/generalLedger.vue`
- `src/views/financialManagement/voucher/detailLedger.vue`
当前逻辑是把 `records` å¼ºåˆ¶æ˜ å°„成 `children: []`,需要删除这段“平铺改造”,直接使用后端树。
示例(可复用):
```js
const toCascaderTree = (nodes = []) =>
  nodes
    .filter(item => item.subjectCode && item.subjectName)
    .map(item => ({
      code: item.subjectCode,
      name: item.subjectName,
      children: toCascaderTree(item.children || [])
    }));
subjectOptions.value = toCascaderTree(response.data?.records || []);
```
---
## 4. å»ºè®®çš„前端字段约定
建议在前端 `form` å¢žåŠ ï¼š
- `parentId: null`
并在编辑回填时保持 `parentId`。
---
## 5. è”调注意事项
1. `/accountSubject/list` çš„ `total` æ˜¯æ ¹èŠ‚ç‚¹æ•°é‡ï¼Œä¸æ˜¯å…¨é‡èŠ‚ç‚¹æ•°ã€‚
2. è‹¥é¡µé¢ä»æŒ‰å¹³é“º `records.map(...)` å¤„理,会丢失子节点。
3. åˆ é™¤ç§‘目失败时,优先检查是否被凭证分录引用。
4. ä¿å­˜å¤±è´¥å‡ºçŽ°â€œçˆ¶ç§‘ç›®ä¸èƒ½æ˜¯å½“å‰ç§‘ç›®æˆ–å…¶å­ç§‘ç›®â€æ—¶ï¼Œéœ€è¦å‰ç«¯é™åˆ¶çˆ¶èŠ‚ç‚¹å¯é€‰èŒƒå›´ã€‚
---
## 6. æ•°æ®åº“字段要求
`account_subject` éœ€è¦åŒ…含 `parent_id` å­—段(`bigint`,可空)。
若线上库尚未添加,请先执行 DDL å†è”调。
doc/20260512_add_parent_id_to_account_subject.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
-- account_subject å¢žåŠ çˆ¶çº§ç§‘ç›®å­—æ®µï¼ˆæ ‘å½¢ç»“æž„ï¼‰
ALTER TABLE `account_subject`
ADD COLUMN `parent_id` bigint NULL COMMENT '父科目ID(为空表示根节点)' AFTER `id`;
CREATE INDEX `idx_account_subject_parent_id` ON `account_subject` (`parent_id`);
doc/20260512_create_financial_management_tables.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
-- è´¢åŠ¡ç®¡ç†æ¨¡å—å»ºè¡¨è„šæœ¬ï¼ˆå›ºå®šèµ„äº§/无形资产/凭证/科目账)
-- è¯´æ˜Žï¼š
-- 1) æ€»è´¦ç§‘目继续复用已有表 account_subject,不重复创建 fin_account_subject。
-- 2) é‡‘额字段统一 decimal(18,2)。
CREATE TABLE IF NOT EXISTS `fin_fixed_asset` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `asset_code` varchar(64) NOT NULL COMMENT '资产编号',
  `asset_name` varchar(128) NOT NULL COMMENT '资产名称',
  `category` varchar(64) NOT NULL COMMENT '资产类别',
  `specification` varchar(255) DEFAULT NULL COMMENT '规格型号',
  `purchase_date` date NOT NULL COMMENT '购置日期',
  `original_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '资产原值',
  `useful_life` int NOT NULL DEFAULT '1' COMMENT '使用年限(å¹´)',
  `residual_rate` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '残值率(%)',
  `accumulated_depreciation` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '累计折旧',
  `net_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '净值',
  `location` varchar(255) DEFAULT NULL COMMENT '存放地点',
  `department` varchar(128) DEFAULT NULL COMMENT '使用部门',
  `keeper` varchar(64) DEFAULT NULL COMMENT '保管人',
  `status` varchar(32) NOT NULL DEFAULT 'in_use' COMMENT '状态: in_use/idle/repair/scrapped',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_user` varchar(64) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_user` varchar(64) DEFAULT NULL COMMENT '修改人',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `dept_id` bigint DEFAULT NULL COMMENT '部门ID',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_fin_fixed_asset_code` (`asset_code`),
  KEY `idx_fin_fixed_asset_status` (`status`),
  KEY `idx_fin_fixed_asset_category` (`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='固定资产';
CREATE TABLE IF NOT EXISTS `fin_intangible_asset` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `asset_code` varchar(64) NOT NULL COMMENT '资产编号',
  `asset_name` varchar(128) NOT NULL COMMENT '资产名称',
  `category` varchar(64) NOT NULL COMMENT '资产类别',
  `certificate_no` varchar(128) DEFAULT NULL COMMENT '证书编号',
  `acquisition_date` date NOT NULL COMMENT '取得日期',
  `original_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '资产原值',
  `amortization_period` int NOT NULL DEFAULT '1' COMMENT '摊销年限(å¹´)',
  `residual_rate` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '残值率(%)',
  `accumulated_amortization` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '累计摊销',
  `net_value` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '净值',
  `validity_date` date DEFAULT NULL COMMENT '有效期至',
  `status` varchar(32) NOT NULL DEFAULT 'in_use' COMMENT '状态: in_use/expired/amortized',
  `description` varchar(1000) DEFAULT NULL COMMENT '资产描述',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_user` varchar(64) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_user` varchar(64) DEFAULT NULL COMMENT '修改人',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `dept_id` bigint DEFAULT NULL COMMENT '部门ID',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_fin_intangible_asset_code` (`asset_code`),
  KEY `idx_fin_intangible_asset_status` (`status`),
  KEY `idx_fin_intangible_asset_category` (`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='无形资产';
CREATE TABLE IF NOT EXISTS `fin_voucher` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `voucher_no` varchar(64) NOT NULL COMMENT '凭证字号',
  `voucher_date` date NOT NULL COMMENT '凭证日期',
  `summary` varchar(500) DEFAULT NULL COMMENT '摘要',
  `debit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '借方合计',
  `credit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '贷方合计',
  `creator` varchar(64) DEFAULT NULL COMMENT '制单人',
  `status` varchar(32) NOT NULL DEFAULT 'unposted' COMMENT '状态: unposted/posted/cancelled',
  `attachment_count` int NOT NULL DEFAULT '0' COMMENT '附件张数',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `create_user` varchar(64) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_user` varchar(64) DEFAULT NULL COMMENT '修改人',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `dept_id` bigint DEFAULT NULL COMMENT '部门ID',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_fin_voucher_no` (`voucher_no`),
  KEY `idx_fin_voucher_date` (`voucher_date`),
  KEY `idx_fin_voucher_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='凭证主表';
CREATE TABLE IF NOT EXISTS `fin_voucher_entry` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `voucher_id` bigint NOT NULL COMMENT '凭证ID',
  `row_no` int NOT NULL DEFAULT '1' COMMENT '行号',
  `subject_code` varchar(64) NOT NULL COMMENT '科目编码',
  `subject_name` varchar(128) DEFAULT NULL COMMENT '科目名称',
  `summary` varchar(500) DEFAULT NULL COMMENT '摘要',
  `debit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '借方金额',
  `credit` decimal(18,2) NOT NULL DEFAULT '0.00' COMMENT '贷方金额',
  `auxiliary_type` varchar(32) DEFAULT NULL COMMENT '辅助核算类型',
  `auxiliary_id` varchar(64) DEFAULT NULL COMMENT '辅助核算对象ID',
  `auxiliary_name` varchar(128) DEFAULT NULL COMMENT '辅助核算对象名称',
  `create_user` varchar(64) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_user` varchar(64) DEFAULT NULL COMMENT '修改人',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `dept_id` bigint DEFAULT NULL COMMENT '部门ID',
  PRIMARY KEY (`id`),
  KEY `idx_fin_voucher_entry_voucher` (`voucher_id`),
  KEY `idx_fin_voucher_entry_subject` (`subject_code`),
  KEY `idx_fin_voucher_entry_aux` (`auxiliary_type`, `auxiliary_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='凭证分录';
doc/20260512_²ÆÎñ¹ÜÀíÄ£¿éǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,288 @@
# è´¢åŠ¡ç®¡ç†æ¨¡å—å‰ç«¯è”è°ƒæ–‡æ¡£ï¼ˆaccount æ¨¡å—)
更新时间:2026-05-12
## 1. é€šç”¨è¯´æ˜Ž
### 1.1 å“åº”结构
成功响应:
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {}
}
```
业务校验失败(例如借贷不平衡、必填缺失)由全局异常返回:
```json
{
  "code": 500,
  "msg": "错误信息",
  "data": null
}
```
### 1.2 åˆ†é¡µç»“æž„
分页接口统一使用 MyBatis-Plus `Page`:
- è¯·æ±‚参数:`current`、`size`
- è¿”回:`data.records`、`data.total`
---
## 2. æ€»è´¦ç§‘目(已在原接口上增强校验)
接口保持不变:
- `GET /accountSubject/list`
- `POST /accountSubject/add`
- `PUT /accountSubject/edit`
- `DELETE /accountSubject/remove/{ids}`
- `POST /accountSubject/export`
新增规则:
1. `subjectCode`、`subjectName`、`subjectType` å¿…填。
2. `subjectCode` å”¯ä¸€æ ¡éªŒã€‚
3. åˆ é™¤å‰åšå¼•用校验:若被凭证分录(`fin_voucher_entry.subject_code`)引用,禁止删除。
---
## 3. å›ºå®šèµ„产
Base URL:`/financial/fixedAsset`
### 3.1 åˆ†é¡µæŸ¥è¯¢
- `GET /page`
- Query:`current,size,assetCode,assetName,category,status`
### 3.2 æ–°å¢ž
- `POST /add`
- Body(JSON):
```json
{
  "assetCode": "GD20260512001",
  "assetName": "办公电脑",
  "category": "electronic",
  "specification": "ThinkPad X1",
  "purchaseDate": "2026-05-01",
  "originalValue": 8000.00,
  "usefulLife": 5,
  "residualRate": 5.00,
  "location": "财务部",
  "department": "财务部",
  "keeper": "张三",
  "status": "in_use",
  "remark": "示例"
}
```
### 3.3 ä¿®æ”¹
- `PUT /update`
- Body:同新增,需包含 `id`
### 3.4 åˆ é™¤
- `DELETE /delete?ids=1&ids=2`
### 3.5 æŠ˜æ—§è®¡æï¼ˆæŒ‰æœˆï¼‰
- `POST /depreciate`
- Body å¯é€‰ï¼š
  - å…¨éƒ¨åœ¨ç”¨èµ„产:`{}`
  - æŒ‡å®šèµ„产:`{"ids":[1,2,3]}`
核心公式:
- `monthlyDepreciation = originalValue * (1 - residualRate/100) / (usefulLife*12)`
- `accumulatedDepreciation += monthlyDepreciation`
- `netValue = originalValue - accumulatedDepreciation`
状态建议值:
- `in_use`、`idle`、`repair`、`scrapped`
---
## 4. æ— å½¢èµ„产
Base URL:`/financial/intangibleAsset`
### 4.1 åˆ†é¡µæŸ¥è¯¢
- `GET /page`
- Query:`current,size,assetCode,assetName,category,status`
### 4.2 æ–°å¢ž
- `POST /add`
```json
{
  "assetCode": "WX20260512001",
  "assetName": "ERP软件许可",
  "category": "software",
  "certificateNo": "SW-001",
  "acquisitionDate": "2026-05-01",
  "originalValue": 50000.00,
  "amortizationPeriod": 10,
  "residualRate": 0.00,
  "validityDate": "2036-05-01",
  "status": "in_use",
  "description": "示例",
  "remark": ""
}
```
### 4.3 ä¿®æ”¹
- `PUT /update`
- Body:同新增,需包含 `id`
### 4.4 åˆ é™¤
- `DELETE /delete?ids=1&ids=2`
### 4.5 æ‘Šé”€è®¡æï¼ˆæŒ‰æœˆï¼‰
- `POST /amortize`
- Body å¯é€‰ï¼š
  - å…¨éƒ¨åœ¨ç”¨èµ„产:`{}`
  - æŒ‡å®šèµ„产:`{"ids":[1,2,3]}`
核心公式:
- `monthlyAmortization = originalValue * (1 - residualRate/100) / (amortizationPeriod*12)`
- `accumulatedAmortization += monthlyAmortization`
- `netValue = originalValue - accumulatedAmortization`
- å½“ `netValue <= 0` æ—¶ï¼š`netValue=0` ä¸” `status=amortized`
状态建议值:
- `in_use`、`expired`、`amortized`
---
## 5. å‡­è¯
Base URL:`/financial/voucher`
### 5.1 åˆ†é¡µæŸ¥è¯¢
- `GET /page`
- Query:`current,size,voucherNo,creator,status,startDate,endDate`
### 5.2 æ–°å¢ž
- `POST /add`
```json
{
  "voucherNo": "è®°-0001",
  "voucherDate": "2026-05-12",
  "summary": "销售收入",
  "creator": "张三",
  "attachmentCount": 0,
  "remark": "",
  "entries": [
    {
      "subjectCode": "1002",
      "subjectName": "银行存款",
      "summary": "收到货款",
      "debit": 1000.00,
      "credit": 0
    },
    {
      "subjectCode": "6001",
      "subjectName": "主营业务收入",
      "summary": "确认收入",
      "debit": 0,
      "credit": 1000.00
    }
  ]
}
```
### 5.3 ä¿®æ”¹
- `PUT /update`
- Body:同新增,需包含 `id`
- ä»… `unposted` çŠ¶æ€å…è®¸ä¿®æ”¹
### 5.4 è¿‡è´¦
- `POST /post`
```json
{
  "id": 1
}
```
### 5.5 ä½œåºŸ
- `POST /cancel`
```json
{
  "id": 1
}
```
### 5.6 è¯¦æƒ…
- `GET /detail/{id}`
关键校验:
1. åˆ†å½•至少一条有效行(科目不空,且借方或贷方 > 0)。
2. æ¯æ¡æœ‰æ•ˆåˆ†å½•不能借贷同时大于 0。
3. å€Ÿè´·å¹³è¡¡ï¼š`sum(debit) == sum(credit)` ä¸”都 > 0。
4. `subjectCode` å¿…须存在于 `account_subject`。
状态流转:
- `unposted -> posted`
- `unposted -> cancelled`
---
## 6. ç§‘目总账
### 6.1 æŸ¥è¯¢æŽ¥å£
- `GET /financial/ledger/general`
### 6.2 Query å‚æ•°
- `subjectCode`(必填)
- `startMonth`(YYYY-MM)
- `endMonth`(YYYY-MM)
### 6.3 è¿”回字段
- `rowType`:`opening` / `entry` / `monthly_total` / `yearly_total`
- `date`
- `voucherNo`
- `summary`
- `debit`
- `credit`
- `direction`(借/贷)
- `balance`(借正贷负)
说明:
- ç§‘目支持“指定科目及其下级(前缀匹配)”查询。
- è‡ªåŠ¨è¾“å‡ºâ€œæœŸåˆä½™é¢ / æœ¬æœˆåˆè®¡ / æœ¬å¹´ç´¯è®¡â€ã€‚
---
## 7. ç§‘目明细账
### 7.1 æŸ¥è¯¢æŽ¥å£
- `GET /financial/ledger/detail`
### 7.2 Query å‚æ•°
- `subjectCode`(必填)
- `auxiliaryType`(可选:`customer/supplier/department/employee/project`)
- `auxiliaryId`(可选)
- `startMonth`(YYYY-MM)
- `endMonth`(YYYY-MM)
### 7.3 è¿”回字段
同科目总账:
- `rowType,date,voucherNo,summary,debit,credit,direction,balance`
---
## 8. æ•°æ®åº“脚本
已提供脚本:
- `doc/20260512_create_financial_management_tables.sql`
包含:
- `fin_fixed_asset`
- `fin_intangible_asset`
- `fin_voucher`
- `fin_voucher_entry`
总账科目复用现有 `account_subject`。
doc/20260515_device_maintenance_inspection_abnormal_acceptance.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
ALTER TABLE `inspection_task`
    ADD COLUMN `inspection_project` VARCHAR(100) NULL COMMENT '巡检项目' AFTER `task_name`;
ALTER TABLE `inspection_task`
    ADD COLUMN `inspection_result` VARCHAR(1) NULL COMMENT '巡检结果 0异常 1正常' AFTER `remarks`,
    ADD COLUMN `abnormal_description` VARCHAR(500) NULL COMMENT '异常描述' AFTER `inspection_result`,
    ADD COLUMN `device_repair_id` BIGINT NULL COMMENT '关联维修单ID' AFTER `abnormal_description`,
    ADD COLUMN `acceptance_user_id` BIGINT NULL COMMENT '验收人ID' AFTER `device_repair_id`,
    ADD COLUMN `acceptance_name` VARCHAR(100) NULL COMMENT '验收人' AFTER `acceptance_user_id`;
ALTER TABLE `timing_task`
    ADD COLUMN `inspection_project` VARCHAR(100) NULL COMMENT '巡检项目' AFTER `task_name`,
    ADD COLUMN `is_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用 0否 1是' AFTER `is_active`;
CREATE INDEX `idx_inspection_task_device_repair_id`
    ON `inspection_task` (`device_repair_id`);
CREATE INDEX `idx_inspection_task_inspection_result`
    ON `inspection_task` (`inspection_result`);
CREATE INDEX `idx_timing_task_is_enabled`
    ON `timing_task` (`is_enabled`);
doc/20260515_É豸Ѳ¼ìÒì³£Áª¶¯Î¬ÐÞµ¥_ǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,170 @@
# è®¾å¤‡å·¡æ£€ä¸Žå®šæ—¶å·¡æ£€å‰ç«¯è”调文档(inspectiontask)
> æ›´æ–°æ—¥æœŸï¼š2026-05-15
> é€‚用模块:设备巡检任务 `inspectiontask`(`/inspectionTask`)与定时巡检任务(`/timingTask`)
## 1. æœ¬æ¬¡æ”¹åЍ
1. å·¡æ£€ä»»åŠ¡æ–°å¢žå­—æ®µï¼š
   - `inspectionProject`(巡检项目)
   - `inspectionResult`(巡检结果,`0`异常 / `1`正常,必填)
   - `abnormalDescription`(异常描述)
   - `deviceRepairId`(关联维修单ID,异常时后端自动回填)
   - `acceptanceUserId`(验收人ID)
   - `acceptanceName`(验收人)
2. å¼‚常校验规则:
   - `inspectionResult=1`(正常):照片非必填。
   - `inspectionResult=0`(异常):必须有照片,且必须填写 `abnormalDescription`。
3. å¼‚常联动规则:
   - å¼‚常保存后自动生成 `device_repair` å¹¶å›žå¡« `deviceRepairId`。
4. å®šæ—¶ä»»åŠ¡æ–°å¢žå­—æ®µï¼š
   - `inspectionProject`(巡检项目)
   - `isEnabled`(是否启用,`0`否 / `1`是)
5. å¤‡æ³¨å¸¦å…¥è§„则:
   - å®šæ—¶ä»»åŠ¡è‡ªåŠ¨ç”Ÿæˆå·¡æ£€è®°å½•æ—¶ï¼Œè‹¥å®šæ—¶ä»»åŠ¡ `remarks` æœ‰å€¼ï¼Œä¼šæ‹¼æŽ¥åˆ°å·¡æ£€è®°å½•备注中。
## 2. æ•°æ®åº“变更
联调前执行 SQL:
- [doc/20260515_device_maintenance_inspection_abnormal_acceptance.sql](/D:/牛马/南通/后端/product-inventory-management-after-jdk25/doc/20260515_device_maintenance_inspection_abnormal_acceptance.sql)
> è¯´æ˜Žï¼šè¯¥è„šæœ¬å½“å‰ä½œç”¨äºŽ `inspection_task` ä¸Ž `timing_task` ä¸¤å¼ è¡¨ï¼Œæ–‡ä»¶ååŽ†å²ä¿ç•™æœªæ”¹ã€‚
## 3. å·¡æ£€ä»»åŠ¡æŽ¥å£
### 3.1 ä¿å­˜æŽ¥å£
`POST /inspectionTask/addOrEditInspectionTask`
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|---|---|---|---|
| id | long | å¦ | æœ‰å€¼=修改,无值=新增 |
| taskId | int | å»ºè®®å¿…å¡« | è®¾å¤‡ID(用于异常自动建维修单) |
| taskName | string | å»ºè®®å¿…å¡« | è®¾å¤‡åç§° |
| inspectionProject | string | å¦ | å·¡æ£€é¡¹ç›® |
| inspectorId | string | å¦ | å·¡æ£€äººID,支持逗号分隔 |
| inspectionResult | string | æ˜¯ | `0`=异常,`1`=正常 |
| abnormalDescription | string | æ¡ä»¶å¿…å¡« | å¼‚常时必填 |
| acceptanceUserId | long | å¦ | éªŒæ”¶äººID |
| acceptanceName | string | å¦ | éªŒæ”¶äººå§“名 |
| commonFileListDTO | array | æ¡ä»¶å¿…å¡« | é™„件组1(异常时三组至少一组有图) |
| commonFileListAfterDTO | array | æ¡ä»¶å¿…å¡« | é™„件组2(异常时三组至少一组有图) |
| commonFileListBeforeDTO | array | æ¡ä»¶å¿…å¡« | é™„件组3(异常时三组至少一组有图) |
异常示例:
```json
{
  "taskId": 1001,
  "taskName": "空压机A-01",
  "inspectionProject": "润滑系统",
  "inspectorId": "12",
  "inspectionResult": "0",
  "abnormalDescription": "电机异响,温升偏高",
  "acceptanceUserId": 20,
  "commonFileListDTO": [
    {
      "id": 90001,
      "application": "file"
    }
  ]
}
```
正常示例:
```json
{
  "taskId": 1001,
  "taskName": "空压机A-01",
  "inspectionProject": "点检",
  "inspectorId": "12",
  "inspectionResult": "1",
  "acceptanceUserId": 20
}
```
### 3.2 åˆ—表接口
`GET /inspectionTask/list`
返回包含新增字段:
- `inspectionProject`
- `inspectionResult`
- `abnormalDescription`
- `deviceRepairId`
- `acceptanceUserId`
- `acceptanceName`
## 4. å®šæ—¶ä»»åŠ¡æŽ¥å£
### 4.1 ä¿å­˜æŽ¥å£
`POST /timingTask/addOrEditTimingTask`
新增/更新字段:
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|---|---|---|---|
| inspectionProject | string | å¦ | å·¡æ£€é¡¹ç›® |
| remarks | string | å¦ | å¤‡æ³¨ |
| isEnabled | int | å¦ | `0`=禁用,`1`=启用;不传默认启用 |
示例:
```json
{
  "taskName": "空压机A-01定时巡检",
  "inspectionProject": "月度巡检",
  "taskId": 1001,
  "inspectorIds": "12,13",
  "frequencyType": "DAILY",
  "frequencyDetail": "09:00",
  "remarks": "重点检查轴承温度",
  "isEnabled": 1
}
```
### 4.2 å¯ç”¨çŠ¶æ€è¡Œä¸º
1. `isEnabled=1`:任务进入调度,按频次自动生成巡检记录。
2. `isEnabled=0`:任务不调度;已存在调度会被移除。
### 4.3 å¤‡æ³¨å¸¦å…¥è§„则
定时任务自动生成巡检记录时:
1. å·¡æ£€è®°å½•备注固定前缀:`自动生成自定时任务ID: {id}`
2. å½“定时任务 `remarks` éžç©ºæ—¶ï¼Œæ‹¼æŽ¥ä¸ºï¼š
   `自动生成自定时任务ID: {id};{remarks}`
## 5. å¼‚常自动建维修单规则
当巡检记录 `inspectionResult=0` æ—¶ï¼š
1. è‹¥ `deviceRepairId` ä¸ºç©ºï¼ŒåŽç«¯è‡ªåŠ¨åˆ›å»º `device_repair`:
   - `deviceLedgerId`:来自 `taskId`
   - `deviceName`:优先 `taskName`,否则取设备台账名称
   - `remark`:异常描述
   - `status`:`0`(待维修)
2. è‹¥å·²æœ‰å…³è”维修单,仅同步更新维修单 `remark`。
## 6. å‰ç«¯æ”¹é€ å»ºè®®
1. å·¡æ£€è¡¨å•新增 `inspectionProject` è¾“入框。
2. å·¡æ£€è¡¨å•保留“正常/异常”联动校验:
   - å¼‚常时强制异常描述 + è‡³å°‘一组图片。
3. å®šæ—¶ä»»åŠ¡è¡¨å•æ–°å¢žâ€œæ˜¯å¦å¯ç”¨â€å¼€å…³å¹¶æ˜ å°„ `isEnabled`。
4. å®šæ—¶ä»»åŠ¡è¡¨å•æ–°å¢ž `inspectionProject` ä¸Ž `remarks` è¾“入项。
5. å·¡æ£€åˆ—表展示 `inspectionProject` å’Œ `deviceRepairId`(支持跳转维修单详情)。
## 7. è”调验收清单
1. å·¡æ£€æ–°å¢ž/修改可正确提交 `inspectionProject` å¹¶åœ¨åˆ—表回显。
2. å¼‚常巡检(有描述+有图)保存成功并回填 `deviceRepairId`。
3. å¼‚常巡检缺描述或缺图片时被拦截。
4. å®šæ—¶ä»»åŠ¡ `isEnabled=0` æ—¶ä¸å†è§¦å‘自动巡检记录。
5. å®šæ—¶ä»»åŠ¡ `isEnabled=1` æ—¶æŒ‰é¢‘次生成巡检记录。
6. å®šæ—¶ä»»åŠ¡æœ‰ `remarks` æ—¶ï¼Œè‡ªåŠ¨å·¡æ£€è®°å½•å¤‡æ³¨å¸¦ä¸Šè¯¥å†…å®¹ã€‚
doc/20260516_ÖÆÔìÖÇÄÜÖúÊÖǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,258 @@
# åˆ¶é€ æ™ºèƒ½åŠ©æ‰‹å‰ç«¯è”è°ƒæ–‡æ¡£ï¼ˆ`manufacturing-ai`)
> æ›´æ–°æ—¥æœŸï¼š2026-05-16
> é€‚用模块:生产现场、计划、工单、设备、质量、物料、异常处理
> èƒ½åŠ›èŒƒå›´ï¼šæŸ¥ã€é—®ã€åŠžã€é¢„è­¦ã€åˆ†æž
## 1. æŽ¥å£æ€»è§ˆ
1. æµå¼å¯¹è¯ï¼š`POST /manufacturing-ai/chat`
2. ä¼šè¯åˆ—表:`GET /manufacturing-ai/history/sessions`
3. ä¼šè¯æ¶ˆæ¯ï¼š`GET /manufacturing-ai/history/messages/{memoryId}`
4. åˆ é™¤ä¼šè¯ï¼š`DELETE /manufacturing-ai/history/{memoryId}`
说明:
- `/chat` ä¸º **SSE/流式文本** è¿”回(`text/stream;charset=utf-8`)。
- å‘½ä¸­â€œæŸ¥/预警/分析/办”工具时,流式最终内容是 **JSON å­—符串**(不是 `AjaxResult`)。
- æœªå‘½ä¸­å·¥å…·æ—¶ï¼Œè¿”回普通自然语言文本。
## 2. é‰´æƒä¸Žè¯·æ±‚头
- ç»Ÿä¸€ä½¿ç”¨ç³»ç»Ÿç™»å½•态(`Authorization` ä¸ŽçŽ°æœ‰æŽ¥å£ä¸€è‡´ï¼‰ã€‚
- `POST /manufacturing-ai/chat` è¯·æ±‚头:`Content-Type: application/json`。
## 3. å¯¹è¯æŽ¥å£
### 3.1 è¯·æ±‚
```http
POST /manufacturing-ai/chat
Content-Type: application/json
```
```json
{
  "memoryId": "mfg-ai-001",
  "message": "查设备西门子变频器的维修情况"
}
```
字段说明:
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
| --- | --- | --- | --- |
| memoryId | string | æ˜¯ | ä¼šè¯ ID,前端生成并复用 |
| message | string | æ˜¯ | ç”¨æˆ·è¾“å…¥ |
### 3.2 è¿”回(流式)
```http
Content-Type: text/stream;charset=utf-8
```
前端处理建议:
1. æŒ‰æµæ‹¼æŽ¥å®Œæ•´æ–‡æœ¬ã€‚
2. å°è¯• `JSON.parse(fullText)`:
   - æˆåŠŸï¼šæŒ‰ç»“æž„åŒ–ç»“æžœæ¸²æŸ“ã€‚
   - å¤±è´¥ï¼šæŒ‰æ™®é€šèŠå¤©æ–‡æœ¬æ¸²æŸ“。
## 4. ç»“构化响应协议
### 4.1 é€šç”¨ç»“æž„
```json
{
  "success": true,
  "type": "manufacturing_device_repair_list",
  "description": "已返回设备维修记录。",
  "summary": {},
  "data": {},
  "charts": {}
}
```
### 4.2 `type` æžšä¸¾
| type | åœºæ™¯ |
| --- | --- |
| manufacturing_site_snapshot | ç”Ÿäº§çŽ°åœºæ¦‚è§ˆ |
| manufacturing_plan_list | ç”Ÿäº§è®¡åˆ’查询 |
| manufacturing_workorder_list | å·¥å•查询 |
| manufacturing_device_list | è®¾å¤‡å°è´¦æŸ¥è¯¢ |
| manufacturing_device_repair_list | è®¾å¤‡ç»´ä¿®è®°å½•查询 |
| manufacturing_quality_list | è´¨é‡æŸ¥è¯¢ |
| manufacturing_material_list | ç‰©æ–™åº“存查询 |
| manufacturing_exception_list | å¼‚常处理查询 |
| manufacturing_warning | é¢„警看板 |
| manufacturing_analysis | ç»è¥åˆ†æž |
| manufacturing_action_plan | åŠžç†å»ºè®®ï¼ˆåŠ¨ä½œå¡ï¼‰ |
## 5. â€œæŸ¥â€èƒ½åŠ›è”è°ƒè¦ç‚¹
### 5.1 è®¾å¤‡ç›¸å…³è·¯ç”±è§„则(关键)
- å½“用户输入包含 `ç»´ä¿®/报修/检修/维护`,设备域会返回 `manufacturing_device_repair_list`(查 `device_repair`)。
- æœªåŒ…含以上词时,返回 `manufacturing_device_list`(查设备台账)。
示例:
- `查设备A-01` -> `manufacturing_device_list`
- `查设备A-01维修情况` -> `manufacturing_device_repair_list`
### 5.2 ç»´ä¿®è®°å½•时间过滤规则(关键)
- ç”¨æˆ·æ˜Žç¡®å¸¦æ—¶é—´æ¡ä»¶ï¼ˆå¦‚“本月/上周/近7天/2026-05-01 åˆ° 2026-05-16”)才按时间过滤维修记录。
- æœªå¸¦æ—¶é—´æ¡ä»¶æ—¶ï¼Œä¸é»˜è®¤æŒ‰è¿‘ 30 å¤©æˆªæ–­ï¼Œé¿å…åŽ†å²ç»´ä¿®è®°å½•è¢«è¯¯è¿‡æ»¤ã€‚
### 5.3 å…³é”®è¯å¤„理规则(设备/维修)
- ç³»ç»Ÿä¼šæ¸…洗噪音词:`查询/查看/请/设备/维修情况/记录/信息` ç­‰ã€‚
- åŒæ—¶ä¼šé€šè¿‡è®¾å¤‡å°è´¦åŒ¹é… `deviceLedgerId` å…œåº•,再回查维修记录,降低“有数据但查不到”的概率。
### 5.4 åˆ—表结果约定
- åˆ—表数据统一在 `data.items`
- ç»Ÿè®¡æ‘˜è¦åœ¨ `summary`
常用字段:
| type | å¸¸ç”¨å­—段 |
| --- | --- |
| manufacturing_plan_list | `mpsNo`, `requiredDate`, `status` |
| manufacturing_workorder_list | `workOrderNo`, `planStartTime`, `planEndTime`, `status` |
| manufacturing_device_list | `deviceName`, `deviceModel`, `pendingRepairCount` |
| manufacturing_device_repair_list | `deviceName`, `deviceModel`, `repairTime`, `repairName`, `maintenanceName`, `status`, `createTime` |
## 6. â€œé¢„警”联调要点
- `type = manufacturing_warning`
- é¢„警明细在 `data.items`,每项包含:
  - `level`:`high` / `medium`
  - `title`
  - `count`
  - `detail`
状态口径:
- è®¾å¤‡â€œå¾…维修”统计按 `status = 0` è®¡ç®—(不再把其他状态计入待维修)。
## 7. â€œåˆ†æžâ€è”调要点
- `type = manufacturing_analysis`
- å…³é”®æŒ‡æ ‡åœ¨ `summary`
- æŒ‡æ ‡å¡åœ¨ `data.coreMetrics`
- å›¾è¡¨é…ç½®åœ¨ `charts`:
  - `charts.domainBarOption`
  - `charts.qualityPieOption`
图表配置可直接给 ECharts ä½¿ç”¨ã€‚
## 8. â€œåŠžâ€èƒ½åŠ›è”è°ƒè¦ç‚¹
当前“办”为 **办理建议模式**(AI è¾“出动作卡,前端确认后调用目标业务接口)。
- `type = manufacturing_action_plan`
- åŠ¨ä½œå¡æ•°ç»„ï¼š`data.actionCards`
动作卡字段:
| å­—段 | è¯´æ˜Ž |
| --- | --- |
| code | åŠ¨ä½œç¼–ç  |
| name | åŠ¨ä½œåç§° |
| method | è¯·æ±‚方法 |
| targetApi | ç›®æ ‡ä¸šåŠ¡æŽ¥å£ |
| requiredFields | å¿…填字段 |
| examplePayload | ç¤ºä¾‹å‚æ•° |
| description | è¯´æ˜Ž |
内置动作示例:
1. `POST /productionOperationTask/assign`
2. `POST /device/repair`
3. `POST /quality/qualityUnqualified/deal`
4. `POST /stockInventory/addstockInventory`
5. `POST /procurementExceptionRecord/add`
## 9. ä¼šè¯ç®¡ç†æŽ¥å£
### 9.1 ä¼šè¯åˆ—表
```http
GET /manufacturing-ai/history/sessions
```
`AjaxResult.data` å­—段:
- `memoryId`
- `title`
- `lastMessage`
- `messageCount`
- `lastChatTime`
### 9.2 ä¼šè¯æ¶ˆæ¯
```http
GET /manufacturing-ai/history/messages/{memoryId}
```
`AjaxResult.data` å­—段:
- `role`:`user` / `assistant` / `system` / `tool`
- `content`
- `filePaths`
### 9.3 åˆ é™¤ä¼šè¯
```http
DELETE /manufacturing-ai/history/{memoryId}
```
返回标准 `AjaxResult`。
## 10. é”™è¯¯ä¸Žè¾¹ç•Œ
`/chat` å¸¸è§è¿”回文本:
- `memoryId不能为空`
- `message不能为空`
建议前端发送前先做必填校验。
## 11. å‰ç«¯è”调流程建议
1. ç™»å½•后创建并复用 `memoryId`。
2. è°ƒç”¨ `/manufacturing-ai/chat`,按 SSE æ‹¼æŽ¥å®Œæ•´æ–‡æœ¬ã€‚
3. å…ˆå°è¯• JSON è§£æžï¼š
   - æˆåŠŸï¼šæŒ‰ `type` è·¯ç”±åˆ°å¯¹åº” UI(列表/预警/分析/动作卡)。
   - å¤±è´¥ï¼šæŒ‰æ™®é€šèŠå¤©æ¶ˆæ¯å±•示。
4. â€œåŠžâ€åœºæ™¯ç”±ç”¨æˆ·ç¡®è®¤åŠ¨ä½œå¡åŽï¼Œå‰ç«¯è°ƒç”¨ `targetApi` å®Œæˆä¸šåŠ¡æäº¤ã€‚
5. é€šè¿‡åŽ†å²æŽ¥å£åšä¼šè¯å›žæ˜¾ä¸Žåˆ é™¤ã€‚
## 12. å‰ç«¯é›†æˆçº¦æŸï¼ˆæœ¬æ¬¡è¡¥å……)
### 12.1 æ™ºèƒ½ä½“新增与弹窗同步规则(强制)
1. å½“ `src/views/aiIndustrialBrain/index.vue` æ–°å¢žæ™ºèƒ½ä½“(`agents`)逻辑时,必须同步确认弹窗助手可用性。
2. å¼¹çª—助手统一由 `src/components/AIChatSidebar/assistants/index.js` çš„ `assistantRegistry` æ³¨å†Œã€‚
3. æ–°å¢žæ™ºèƒ½ä½“çš„ `key` è‹¥è¦åœ¨å¼¹çª—中可用,必须在 `assistantRegistry` ä¸­æä¾›åŒåé…ç½®ã€‚
4. æœªåœ¨ `assistantRegistry` æ³¨å†Œçš„æ™ºèƒ½ä½“,弹窗显示为 `pending`(开发中)态。
### 12.2 ç”Ÿäº§åŠ©æ‰‹æŽ¥å…¥çº¦å®š
1. ç”Ÿäº§åŠ©æ‰‹é…ç½®ä½äºŽ `src/components/AIChatSidebar/assistants/productionAssistant.js`,`apiBase = /manufacturing-ai`。
2. AI å·¥ä¸šå¤§è„‘中生产智能体进入弹窗后,默认使用 `production` åŠ©æ‰‹ã€‚
3. å…¨å±€å³ä¾§å¯¹è¯æ¡†åŠ©æ‰‹åˆ‡æ¢åˆ—è¡¨å·²åŒ…å«ï¼š
   - `general`(待办助理)
   - `purchase`(采购助理)
   - `production`(生产助理)
### 12.3 å­—段中文化展示规则
1. é¢å‘业务用户的字段名、标签、必填提示不直接展示英文 key。
2. `requiredFields`、`missingFields` æç¤ºéœ€è½¬æ¢ä¸ºä¸­æ–‡è·¯å¾„标签(示例:`缺少必填字段:工单号、计划结束时间`)。
3. ç»“构化列表列名、摘要指标、动作卡字段优先显示中文;英文 key ä»…用于接口通信与调试。
## 13. æœ¬æ¬¡æ›´æ–°è®°å½•(2026-05-16)
1. æ–°å¢žè®¾å¤‡ç»´ä¿®è®°å½•返回类型:`manufacturing_device_repair_list`。
2. ä¿®æ­£è®¾å¤‡åŸŸæ„å›¾åˆ†æµï¼š`ç»´ä¿®/报修/检修/维护` èµ°ç»´ä¿®è®°å½•,不再误走设备列表。
3. ä¿®æ­£ç»´ä¿®è®°å½•时间过滤:仅在用户明确时间条件时生效。
4. ä¿®æ­£å¾…维修统计口径:按 `status = 0` ç»Ÿè®¡ã€‚
5. æ–°å¢ž AI å·¥ä¸šå¤§è„‘智能体与弹窗同步维护规则:新增智能体必须同步注册弹窗助手。
6. ç”Ÿäº§åŠ©æ‰‹å·²æŽ¥å…¥å·¥ä¸šå¤§è„‘å¼¹çª—ä¸Žå…¨å±€å³ä¾§å¯¹è¯æ¡†åŠ©æ‰‹åˆ‡æ¢ã€‚
7. å¢žåŠ å­—æ®µä¸­æ–‡åŒ–å±•ç¤ºçº¦æŸï¼šé¿å…è‹±æ–‡å­—æ®µå¯¹ä¸šåŠ¡ç”¨æˆ·ç›´å‡ºã€‚
doc/ǰ¶ËÁªµ÷Îĵµ-É豸±¨ÐÞ±£Ñø²ÆÎñÄ£¿é¸ÄÔì.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,233 @@
# å‰ç«¯è”调文档(设备报修 / è®¾å¤‡ä¿å…»å®šæ—¶ä»»åŠ¡ / è´¢åŠ¡ç§‘ç›®æ€»è´¦ï¼‰
## 1. å˜æ›´èŒƒå›´
本次联调涉及 3 ä¸ªæ¨¡å—:
1. è´¢åŠ¡æ¨¡å—ï¼šç§‘ç›®æ€»è´¦åŽ»æŽ‰å‡­è¯å­—å·ã€æ‘˜è¦ï¼Œåªè¿”å›ž 1 æ¡åˆè®¡æ•°æ®ã€‚
2. è®¾å¤‡ä¿å…»å®šæ—¶ä»»åŠ¡ï¼šæ–°å¢ž `保养人` å­—段,定时任务生成保养记录时带入。
3. è®¾å¤‡æŠ¥ä¿®ï¼šç¡®è®¤æŠ¥ä¿®åŽæ–°å¢žéªŒæ”¶å®¡æ‰¹ï¼ŒéªŒæ”¶é€šè¿‡åŽæ‰ç®—完结。
---
## 2. æŽ¥å£æ¸…单
### 2.1 è´¢åŠ¡-科目总账
- **GET** `/financial/ledger/general`
- è¯´æ˜Žï¼šè¿”回科目总账合计,仅 1 æ¡è®°å½•。
#### è¯·æ±‚参数(Query)
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|---|---|---|---|
| `subjectCode` | string | æ˜¯ | ç§‘目编码 |
| `startMonth` | string | æ˜¯ | å¼€å§‹æœˆä»½ï¼Œæ ¼å¼ `YYYY-MM` |
| `endMonth` | string | æ˜¯ | ç»“束月份,格式 `YYYY-MM` |
#### è¿”回结构
`R<List<FinLedgerRowVo>>`
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": [
    {
      "rowType": "yearly_total",
      "date": "2026-05-31",
      "debit": 12000.00,
      "credit": 8000.00,
      "direction": "借",
      "balance": 4000.00
    }
  ]
}
```
#### è”调注意
1. `data` å›ºå®šåªæœ‰ 1 æ¡ï¼ˆåˆè®¡ï¼‰ã€‚
2. `voucherNo`、`summary` ä¸è¿”回(不再展示凭证字号、摘要)。
---
### 2.2 è®¾å¤‡ä¿å…»å®šæ—¶ä»»åŠ¡ï¼ˆæ–°å¢žä¿å…»äººï¼‰
- åŸºç¡€è·¯å¾„:`/deviceMaintenanceTask`
- ç›¸å…³æŽ¥å£ï¼š
  - **POST** `/add`
  - **POST** `/update`
  - **GET** `/listPage`
#### æ–°å¢žå­—段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|---|---|---|
| `maintenancePerson` | string | ä¿å…»äºº |
#### æ–°å¢ž/更新请求示例
```json
{
  "id": 1,
  "taskName": "空压机保养任务",
  "taskId": 1001,
  "maintenancePerson": "张三",
  "frequencyType": "MONTHLY",
  "frequencyDetail": "10,09:00",
  "remarks": "每月例行保养"
}
```
#### å®šæ—¶ä»»åŠ¡ä¸‹å‘è¡Œä¸º
定时任务执行后,系统自动创建保养记录(`device_maintenance`)时会写入:
- `maintenanceActuallyName = maintenancePerson`
即前端在定时任务里维护的保养人,会自动带入到保养记录。
---
### 2.3 è®¾å¤‡æŠ¥ä¿®ï¼ˆç¡®è®¤åŽéªŒæ”¶å®¡æ‰¹ï¼‰
- åŸºç¡€è·¯å¾„:`/device/repair`
#### çŠ¶æ€å®šä¹‰
| çŠ¶æ€å€¼ | å«ä¹‰ |
|---|---|
| `0` | å¾…ç»´ä¿® |
| `3` | å¾…验收 |
| `1` | å®Œç»“ |
| `2` | å¤±è´¥ |
#### 2.3.1 ç»´ä¿®ç¡®è®¤ï¼ˆåŽŸç¡®è®¤æŠ¥ä¿®ï¼‰
- **POST** `/device/repair/repair`
- è¯´æ˜Žï¼šæäº¤åŽçŠ¶æ€ä»Ž `待维修(0)` è¿›å…¥ `待验收(3)`,不再直接完结。
请求示例:
```json
{
  "id": 10001,
  "maintenanceName": "李四",
  "maintenanceTime": "2026-05-14 10:30:00",
  "maintenanceResult": "更换轴承并试运行正常",
  "sparePartsUseList": [
    {
      "id": 501,
      "quantity": 2
    }
  ]
}
```
常见失败提示(用于前端弹窗):
- `报修记录不存在`
- `该报修已完结,不能重复确认维修`
- `该报修已提交验收审批`
- `备件 xxx æ•°é‡ä¸è¶³`
#### 2.3.2 éªŒæ”¶å®¡æ‰¹ï¼ˆæ–°å¢žï¼‰
- **POST** `/device/repair/acceptance`
- è¯´æ˜Žï¼šä»… `待验收(3)` å¯å®¡æ‰¹ï¼›å®¡æ‰¹é€šè¿‡åŽçŠ¶æ€æ”¹ä¸º `完结(1)`。
请求参数(Body):
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|---|---|---|---|
| `id` | long | æ˜¯ | æŠ¥ä¿®è®°å½•ID |
| `acceptanceName` | string | æ˜¯ | éªŒæ”¶äºº |
| `acceptanceTime` | string | æ˜¯ | éªŒæ”¶æ—¶é—´ï¼Œæ ¼å¼ `yyyy-MM-dd HH:mm:ss` |
| `acceptanceRemark` | string | æ˜¯ | éªŒæ”¶å¤‡æ³¨ |
请求示例:
```json
{
  "id": 10001,
  "acceptanceName": "王五",
  "acceptanceTime": "2026-05-14 11:00:00",
  "acceptanceRemark": "维修项核验通过,设备运行正常"
}
```
常见失败提示:
- `报修记录id不能为空`
- `报修记录不存在`
- `该报修未进入待验收状态,不能审批`
- `验收人不能为空`
- `验收时间不能为空`
- `验收备注不能为空`
#### 2.3.3 æ™®é€šæ›´æ–°æŽ¥å£é™åˆ¶
- **PUT** `/device/repair`
- é™åˆ¶ï¼šä¸èƒ½é€šè¿‡æ™®é€šæ›´æ–°ç›´æŽ¥æŠŠçŠ¶æ€æ”¹æˆ `完结(1)`(必须走验收审批接口)。
- å¤±è´¥æç¤ºï¼š`请先提交验收审批,验收通过后才可完结`
---
## 3. è¿”回字段变更(报修列表/详情)
以下接口返回已新增验收字段:
- **GET** `/device/repair/page`
- **GET** `/device/repair/{id}`
新增返回字段:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|---|---|---|
| `acceptanceName` | string | éªŒæ”¶äºº |
| `acceptanceTime` | string | éªŒæ”¶æ—¶é—´ |
| `acceptanceRemark` | string | éªŒæ”¶å¤‡æ³¨ |
---
## 4. å‰ç«¯æ”¹é€ å»ºè®®
1. æŠ¥ä¿®åˆ—表增加状态值 `3=待验收` çš„展示文案与筛选项。
2. â€œç¡®è®¤ç»´ä¿®â€æŒ‰é’®è°ƒç”¨ `/device/repair/repair`,成功后刷新为待验收状态。
3. æ–°å¢žâ€œéªŒæ”¶å®¡æ‰¹â€å¼¹çª—,必填:
   - éªŒæ”¶äºº
   - éªŒæ”¶æ—¶é—´
   - éªŒæ”¶å¤‡æ³¨
4. ç¦æ­¢åœ¨æ™®é€šç¼–辑页直接将状态置为完结。
5. è®¾å¤‡ä¿å…»å®šæ—¶ä»»åŠ¡æ–°å¢žâ€œä¿å…»äººâ€è¾“å…¥é¡¹ï¼Œå¹¶åœ¨åˆ—è¡¨/详情展示。
6. ç§‘目总账页面按单行合计渲染,不再显示凭证字号、摘要列。
---
## 5. è”调检查清单
1. ç§‘目总账查询返回 `data.length === 1`,且无 `voucherNo/summary`。
2. æ–°å¢žä¿å…»å®šæ—¶ä»»åŠ¡æ—¶ä¼  `maintenancePerson`,列表能回显。
3. å®šæ—¶ä»»åŠ¡è§¦å‘åŽï¼Œç”Ÿæˆçš„ä¿å…»è®°å½• `maintenanceActuallyName` ä¸Žå®šæ—¶ä»»åŠ¡ä¿å…»äººä¸€è‡´ã€‚
4. æŠ¥ä¿®å•流程:`0待维修 -> 3待验收 -> 1完结`。
5. å¾…验收单据未填验收人/验收时间/验收备注时,后端返回对应错误提示。
6. å°è¯•通过 `PUT /device/repair` ç›´æŽ¥è®¾ä¸ºå®Œç»“时,后端返回拦截提示。
---
## 6. æ•°æ®åº“变更(联调前确认)
```sql
ALTER TABLE maintenance_task
  ADD COLUMN maintenance_person VARCHAR(100) NULL COMMENT '保养人';
ALTER TABLE device_repair
  ADD COLUMN acceptance_name VARCHAR(100) NULL COMMENT '验收人',
  ADD COLUMN acceptance_time DATETIME NULL COMMENT '验收时间',
  ADD COLUMN acceptance_remark VARCHAR(500) NULL COMMENT '验收备注';
```
> è‹¥æœªæ‰§è¡Œä»¥ä¸Š SQL,相关接口会出现字段不存在异常。
src/main/java/com/ruoyi/account/bean/dto/PurchaseInboundDto.java
@@ -3,6 +3,7 @@
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.util.Date;
@@ -18,9 +19,11 @@
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
}
src/main/java/com/ruoyi/account/bean/dto/PurchaseReturnDto.java
@@ -3,6 +3,7 @@
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.util.Date;
@@ -18,9 +19,11 @@
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
}
src/main/java/com/ruoyi/account/bean/dto/SalesOutboundDto.java
@@ -3,6 +3,7 @@
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.util.Date;
@@ -18,9 +19,11 @@
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
}
src/main/java/com/ruoyi/account/bean/dto/SalesReturnDto.java
@@ -3,6 +3,7 @@
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.util.Date;
@@ -18,9 +19,11 @@
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
}
src/main/java/com/ruoyi/account/bean/dto/financial/FinDetailLedgerQueryDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package com.ruoyi.account.bean.dto.financial;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * ç§‘目明细账查询参数(含辅助核算条件)。
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class FinDetailLedgerQueryDto extends FinLedgerQueryDto {
    /**
     * è¾…助核算类型:customer/supplier/department/employee/project。
     */
    private String auxiliaryType;
    /**
     * è¾…助核算对象ID。
     */
    private String auxiliaryId;
}
src/main/java/com/ruoyi/account/bean/dto/financial/FinFixedAssetDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package com.ruoyi.account.bean.dto.financial;
import com.ruoyi.account.pojo.financial.FinFixedAsset;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * å›ºå®šèµ„产查询与保存 DTO。
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class FinFixedAssetDto extends FinFixedAsset {
}
src/main/java/com/ruoyi/account/bean/dto/financial/FinIdBatchDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package com.ruoyi.account.bean.dto.financial;
import lombok.Data;
import java.util.List;
/**
 * æ‰¹é‡ID请求参数。
 */
@Data
public class FinIdBatchDto {
    /**
     * ID集合。
     */
    private List<Long> ids;
}
src/main/java/com/ruoyi/account/bean/dto/financial/FinIntangibleAssetDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package com.ruoyi.account.bean.dto.financial;
import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * æ— å½¢èµ„产查询与保存 DTO。
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class FinIntangibleAssetDto extends FinIntangibleAsset {
}
src/main/java/com/ruoyi/account/bean/dto/financial/FinLedgerQueryDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.ruoyi.account.bean.dto.financial;
import lombok.Data;
/**
 * ç§‘目账查询参数。
 */
@Data
public class FinLedgerQueryDto {
    /**
     * ç§‘目编码(支持末级或指定科目)。
     */
    private String subjectCode;
    /**
     * å¼€å§‹æœˆä»½ï¼Œæ ¼å¼ï¼šYYYY-MM。
     */
    private String startMonth;
    /**
     * ç»“束月份,格式:YYYY-MM。
     */
    private String endMonth;
}
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package com.ruoyi.account.bean.dto.financial;
import com.ruoyi.account.pojo.financial.FinVoucher;
import com.ruoyi.basic.dto.StorageBlobDTO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
 * å‡­è¯ä¿å­˜ DTO(主表 + åˆ†å½•)。
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class FinVoucherDto extends FinVoucher {
    /**
     * å‡­è¯æ˜Žç»†åˆ†å½•。
     */
    private List<FinVoucherEntryDto> entries;
    private List<StorageBlobDTO> storageBlobDTOs;
}
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherEntryDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package com.ruoyi.account.bean.dto.financial;
import com.ruoyi.account.pojo.financial.FinVoucherEntry;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * å‡­è¯åˆ†å½• DTO。
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class FinVoucherEntryDto extends FinVoucherEntry {
}
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherPageDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package com.ruoyi.account.bean.dto.financial;
import lombok.Data;
import java.time.LocalDate;
/**
 * å‡­è¯åˆ†é¡µæŸ¥è¯¢å‚数。
 */
@Data
public class FinVoucherPageDto {
    /**
     * å‡­è¯å­—号(模糊匹配)。
     */
    private String voucherNo;
    /**
     * åˆ¶å•人。
     */
    private String creator;
    /**
     * å‡­è¯çŠ¶æ€ã€‚
     */
    private String status;
    /**
     * å¼€å§‹æ—¥æœŸï¼ˆå«ï¼‰ã€‚
     */
    private LocalDate startDate;
    /**
     * ç»“束日期(含)。
     */
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/dto/financial/FinVoucherStatusDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package com.ruoyi.account.bean.dto.financial;
import lombok.Data;
/**
 * å‡­è¯çŠ¶æ€å˜æ›´å‚æ•°ã€‚
 */
@Data
public class FinVoucherStatusDto {
    /**
     * å‡­è¯ID。
     */
    private Long id;
}
src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java
@@ -3,6 +3,19 @@
import com.ruoyi.account.pojo.AccountSubject;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class AccountSubjectVo extends AccountSubject {
    /**
     * å­ç§‘目列表(递归结构)。
     */
    private List<AccountSubjectVo> children = new ArrayList<>();
    /**
     * æ˜¯å¦å¶å­èŠ‚ç‚¹ã€‚
     */
    private Boolean leaf;
}
src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerEntryRecordVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package com.ruoyi.account.bean.vo.financial;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * ç§‘目账基础分录查询对象(SQL映射使用)。
 */
@Data
public class FinLedgerEntryRecordVo {
    /**
     * å‡­è¯æ—¥æœŸã€‚
     */
    private LocalDate voucherDate;
    /**
     * å‡­è¯å­—号。
     */
    private String voucherNo;
    /**
     * æ‘˜è¦ã€‚
     */
    private String summary;
    /**
     * å€Ÿæ–¹é‡‘额。
     */
    private BigDecimal debit;
    /**
     * è´·æ–¹é‡‘额。
     */
    private BigDecimal credit;
    /**
     * è¡Œå·ï¼ˆæŽ’序字段)。
     */
    private Integer rowNo;
}
src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerRowVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package com.ruoyi.account.bean.vo.financial;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * ç§‘目账行数据返回对象。
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class FinLedgerRowVo {
    /**
     * è¡Œç±»åž‹ï¼šopening/entry/monthly_total/yearly_total。
     */
    private String rowType;
    /**
     * æ—¥æœŸã€‚
     */
    private LocalDate date;
    /**
     * å‡­è¯å­—号。
     */
    private String voucherNo;
    /**
     * æ‘˜è¦ã€‚
     */
    private String summary;
    /**
     * å€Ÿæ–¹é‡‘额。
     */
    private BigDecimal debit;
    /**
     * è´·æ–¹é‡‘额。
     */
    private BigDecimal credit;
    /**
     * ä½™é¢æ–¹å‘:借/贷。
     */
    private String direction;
    /**
     * ä½™é¢ï¼ˆå€Ÿæ­£è´·è´Ÿï¼‰ã€‚
     */
    private BigDecimal balance;
}
src/main/java/com/ruoyi/account/bean/vo/financial/FinVoucherDetailVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.account.bean.vo.financial;
import com.ruoyi.account.pojo.financial.FinVoucher;
import com.ruoyi.account.pojo.financial.FinVoucherEntry;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.basic.dto.StorageBlobVO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
 * å‡­è¯è¯¦æƒ…返回对象。
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class FinVoucherDetailVo extends FinVoucher {
    /**
     * å‡­è¯åˆ†å½•列表。
     */
    private List<FinVoucherEntry> entries;
    private List<StorageBlobVO> storageBlobVOList;
}
src/main/java/com/ruoyi/account/controller/AccountSubjectController.java
@@ -33,7 +33,7 @@
    @GetMapping("/list")
    @Log(title = "总账科目数据集合", businessType = BusinessType.OTHER)
    @Operation(summary = "总账科目分页查询")
    @Operation(summary = "总账科目树形查询(递归)")
    public R<IPage<AccountSubjectVo>> AccountSubjectDtoList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto) {
        IPage<AccountSubjectVo> paramList = accountSubjectService.baseList(page, accountSubjectDto);
        return R.ok(paramList);
@@ -43,21 +43,21 @@
    @Log(title = "新增总账科目", businessType = BusinessType.INSERT)
    @Operation(summary = "新增总账科目")
    public R AccountSubjectDtoAdd(@RequestBody AccountSubjectDto accountSubjectDto) {
        return R.ok(accountSubjectService.save(accountSubjectDto));
        return R.ok(accountSubjectService.saveAccountSubject(accountSubjectDto));
    }
    @PutMapping("/edit")
    @Log(title = "修改总账科目", businessType = BusinessType.UPDATE)
    @Operation(summary = "修改总账科目")
    public R AccountSubjectDtoEdit(@RequestBody AccountSubjectDto accountSubjectDto) {
        return R.ok(accountSubjectService.updateById(accountSubjectDto));
        return R.ok(accountSubjectService.updateAccountSubject(accountSubjectDto));
    }
    @DeleteMapping("/remove/{ids}")
    @Log(title = "删除总账科目", businessType = BusinessType.DELETE)
    @Operation(summary = "删除总账科目")
    public R AccountSubjectDtooRemove(@PathVariable Long[] ids) {
        return R.ok(accountSubjectService.removeBatchByIds(Arrays.asList(ids)));
        return R.ok(accountSubjectService.removeAccountSubjectByIds(Arrays.asList(ids)));
    }
    @PostMapping("/export")
src/main/java/com/ruoyi/account/controller/financial/FinFixedAssetController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package com.ruoyi.account.controller.financial;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.financial.FinFixedAssetDto;
import com.ruoyi.account.bean.dto.financial.FinIdBatchDto;
import com.ruoyi.account.pojo.financial.FinFixedAsset;
import com.ruoyi.account.service.financial.FinFixedAssetService;
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.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
/**
 * å›ºå®šèµ„产控制器。
 */
@RestController
@RequestMapping("/financial/fixedAsset")
@RequiredArgsConstructor
@Tag(name = "财务管理-固定资产")
public class FinFixedAssetController {
    private final FinFixedAssetService finFixedAssetService;
    @GetMapping("/page")
    @Operation(summary = "固定资产分页查询")
    public R<IPage<FinFixedAsset>> page(Page<FinFixedAsset> page, FinFixedAssetDto queryDto) {
        return R.ok(finFixedAssetService.pageList(page, queryDto));
    }
    @PostMapping("/add")
    @Log(title = "固定资产", businessType = BusinessType.INSERT)
    @Operation(summary = "新增固定资产")
    public R<Boolean> add(@RequestBody FinFixedAssetDto dto) {
        return R.ok(finFixedAssetService.add(dto));
    }
    @PutMapping("/update")
    @Log(title = "固定资产", businessType = BusinessType.UPDATE)
    @Operation(summary = "修改固定资产")
    public R<Boolean> update(@RequestBody FinFixedAssetDto dto) {
        return R.ok(finFixedAssetService.update(dto));
    }
    @DeleteMapping("/delete")
    @Log(title = "固定资产", businessType = BusinessType.DELETE)
    @Operation(summary = "删除固定资产")
    public R<Boolean> delete(@RequestParam("ids") Long[] ids) {
        return R.ok(finFixedAssetService.deleteByIds(Arrays.asList(ids)));
    }
    @PostMapping("/depreciate")
    @Log(title = "固定资产折旧计提", businessType = BusinessType.UPDATE)
    @Operation(summary = "固定资产按月计提折旧")
    public R depreciate(@RequestBody(required = false) FinIdBatchDto dto) {
        return R.ok(finFixedAssetService.depreciate(dto == null ? null : dto.getIds()));
    }
}
src/main/java/com/ruoyi/account/controller/financial/FinIntangibleAssetController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package com.ruoyi.account.controller.financial;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.financial.FinIdBatchDto;
import com.ruoyi.account.bean.dto.financial.FinIntangibleAssetDto;
import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
import com.ruoyi.account.service.financial.FinIntangibleAssetService;
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.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
/**
 * æ— å½¢èµ„产控制器。
 */
@RestController
@RequestMapping("/financial/intangibleAsset")
@RequiredArgsConstructor
@Tag(name = "财务管理-无形资产")
public class FinIntangibleAssetController {
    private final FinIntangibleAssetService finIntangibleAssetService;
    @GetMapping("/page")
    @Operation(summary = "无形资产分页查询")
    public R<IPage<FinIntangibleAsset>> page(Page<FinIntangibleAsset> page, FinIntangibleAssetDto queryDto) {
        return R.ok(finIntangibleAssetService.pageList(page, queryDto));
    }
    @PostMapping("/add")
    @Log(title = "无形资产", businessType = BusinessType.INSERT)
    @Operation(summary = "新增无形资产")
    public R<Boolean> add(@RequestBody FinIntangibleAssetDto dto) {
        return R.ok(finIntangibleAssetService.add(dto));
    }
    @PutMapping("/update")
    @Log(title = "无形资产", businessType = BusinessType.UPDATE)
    @Operation(summary = "修改无形资产")
    public R<Boolean> update(@RequestBody FinIntangibleAssetDto dto) {
        return R.ok(finIntangibleAssetService.update(dto));
    }
    @DeleteMapping("/delete")
    @Log(title = "无形资产", businessType = BusinessType.DELETE)
    @Operation(summary = "删除无形资产")
    public R<Boolean> delete(@RequestParam("ids") Long[] ids) {
        return R.ok(finIntangibleAssetService.deleteByIds(Arrays.asList(ids)));
    }
    @PostMapping("/amortize")
    @Log(title = "无形资产摊销计提", businessType = BusinessType.UPDATE)
    @Operation(summary = "无形资产按月计提摊销")
    public R amortize(@RequestBody(required = false) FinIdBatchDto dto) {
        return R.ok(finIntangibleAssetService.amortize(dto == null ? null : dto.getIds()));
    }
}
src/main/java/com/ruoyi/account/controller/financial/FinLedgerController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package com.ruoyi.account.controller.financial;
import com.ruoyi.account.bean.dto.financial.FinDetailLedgerQueryDto;
import com.ruoyi.account.bean.dto.financial.FinLedgerQueryDto;
import com.ruoyi.account.bean.vo.financial.FinLedgerRowVo;
import com.ruoyi.account.service.financial.FinLedgerService;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
 * ç§‘目总账/明细账控制器。
 */
@RestController
@RequestMapping("/financial/ledger")
@RequiredArgsConstructor
@Tag(name = "财务管理-科目账")
public class FinLedgerController {
    private final FinLedgerService finLedgerService;
    @GetMapping("/general")
    @Operation(summary = "科目总账查询")
    public R<List<FinLedgerRowVo>> general(FinLedgerQueryDto queryDto) {
        return R.ok(finLedgerService.queryGeneralLedger(queryDto));
    }
    @GetMapping("/detail")
    @Operation(summary = "科目明细账查询")
    public R<List<FinLedgerRowVo>> detail(FinDetailLedgerQueryDto queryDto) {
        return R.ok(finLedgerService.queryDetailLedger(queryDto));
    }
}
src/main/java/com/ruoyi/account/controller/financial/FinVoucherController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
package com.ruoyi.account.controller.financial;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.financial.FinVoucherDto;
import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto;
import com.ruoyi.account.bean.dto.financial.FinVoucherStatusDto;
import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo;
import com.ruoyi.account.pojo.financial.FinVoucher;
import com.ruoyi.account.service.financial.FinVoucherService;
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.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
 * å‡­è¯æŽ§åˆ¶å™¨ã€‚
 */
@RestController
@RequestMapping("/financial/voucher")
@RequiredArgsConstructor
@Tag(name = "财务管理-凭证")
public class FinVoucherController {
    private final FinVoucherService finVoucherService;
    @GetMapping("/page")
    @Operation(summary = "凭证分页查询")
    public R<IPage<FinVoucher>> page(Page<FinVoucher> page, FinVoucherPageDto queryDto) {
        return R.ok(finVoucherService.pageList(page, queryDto));
    }
    @PostMapping("/add")
    @Log(title = "凭证", businessType = BusinessType.INSERT)
    @Operation(summary = "新增凭证")
    public R<Boolean> add(@RequestBody FinVoucherDto dto) {
        return R.ok(finVoucherService.addVoucher(dto));
    }
    @PutMapping("/update")
    @Log(title = "凭证", businessType = BusinessType.UPDATE)
    @Operation(summary = "修改凭证")
    public R<Boolean> update(@RequestBody FinVoucherDto dto) {
        return R.ok(finVoucherService.updateVoucher(dto));
    }
    @PostMapping("/post")
    @Log(title = "凭证过账", businessType = BusinessType.UPDATE)
    @Operation(summary = "凭证过账")
    public R<Boolean> post(@RequestBody FinVoucherStatusDto dto) {
        return R.ok(finVoucherService.postVoucher(dto.getId()));
    }
    @PostMapping("/cancel")
    @Log(title = "凭证作废", businessType = BusinessType.UPDATE)
    @Operation(summary = "凭证作废")
    public R<Boolean> cancel(@RequestBody FinVoucherStatusDto dto) {
        return R.ok(finVoucherService.cancelVoucher(dto.getId()));
    }
    @GetMapping("/detail/{id}")
    @Operation(summary = "凭证详情")
    public R<FinVoucherDetailVo> detail(@PathVariable("id") Long id) {
        return R.ok(finVoucherService.detail(id));
    }
}
src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java
@@ -3,6 +3,9 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.AccountSubject;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
@@ -15,4 +18,6 @@
@Mapper
public interface AccountSubjectMapper extends BaseMapper<AccountSubject> {
    Long countReferencedBySubjectCodes(@Param("subjectCodes") List<String> subjectCodes);
}
src/main/java/com/ruoyi/account/mapper/financial/FinFixedAssetMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package com.ruoyi.account.mapper.financial;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.financial.FinFixedAsset;
import org.apache.ibatis.annotations.Mapper;
/**
 * å›ºå®šèµ„产 Mapper。
 */
@Mapper
public interface FinFixedAssetMapper extends BaseMapper<FinFixedAsset> {
}
src/main/java/com/ruoyi/account/mapper/financial/FinIntangibleAssetMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package com.ruoyi.account.mapper.financial;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
import org.apache.ibatis.annotations.Mapper;
/**
 * æ— å½¢èµ„产 Mapper。
 */
@Mapper
public interface FinIntangibleAssetMapper extends BaseMapper<FinIntangibleAsset> {
}
src/main/java/com/ruoyi/account/mapper/financial/FinVoucherEntryMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.account.mapper.financial;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo;
import com.ruoyi.account.pojo.financial.FinVoucherEntry;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
/**
 * å‡­è¯åˆ†å½• Mapper。
 */
@Mapper
public interface FinVoucherEntryMapper extends BaseMapper<FinVoucherEntry> {
    List<FinLedgerEntryRecordVo> listPostedEntries(@Param("subjectCode") String subjectCode,
                                                   @Param("startDate") LocalDate startDate,
                                                   @Param("endDate") LocalDate endDate,
                                                   @Param("auxiliaryType") String auxiliaryType,
                                                   @Param("auxiliaryId") String auxiliaryId);
    List<FinLedgerEntryRecordVo> listPostedEntriesBefore(@Param("subjectCode") String subjectCode,
                                                         @Param("beforeDate") LocalDate beforeDate,
                                                         @Param("auxiliaryType") String auxiliaryType,
                                                         @Param("auxiliaryId") String auxiliaryId);
}
src/main/java/com/ruoyi/account/mapper/financial/FinVoucherMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package com.ruoyi.account.mapper.financial;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.financial.FinVoucher;
import org.apache.ibatis.annotations.Mapper;
/**
 * å‡­è¯ä¸»è¡¨ Mapper。
 */
@Mapper
public interface FinVoucherMapper extends BaseMapper<FinVoucher> {
}
src/main/java/com/ruoyi/account/pojo/AccountSubject.java
@@ -39,6 +39,12 @@
    private Long id;
    /**
     * çˆ¶ç§‘ç›®ID(为空表示根节点)
     */
    @ApiModelProperty("父科目ID(为空表示根节点)")
    private Long parentId;
    /**
     * ç§‘目编码(唯一标识)
     */
    @ApiModelProperty("科目编码(唯一标识)")
src/main/java/com/ruoyi/account/pojo/financial/FinFixedAsset.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,101 @@
package com.ruoyi.account.pojo.financial;
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.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * å›ºå®šèµ„产实体。
 */
@Getter
@Setter
@ToString
@TableName("fin_fixed_asset")
@ApiModel(value = "FinFixedAsset对象", description = "固定资产")
public class FinFixedAsset implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    @ApiModelProperty("主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("资产编号")
    private String assetCode;
    @ApiModelProperty("资产名称")
    private String assetName;
    @ApiModelProperty("资产类别")
    private String category;
    @ApiModelProperty("规格型号")
    private String specification;
    @ApiModelProperty("购置日期")
    private LocalDate purchaseDate;
    @ApiModelProperty("资产原值")
    private BigDecimal originalValue;
    @ApiModelProperty("使用年限(å¹´)")
    private Integer usefulLife;
    @ApiModelProperty("残值率(%)")
    private BigDecimal residualRate;
    @ApiModelProperty("累计折旧")
    private BigDecimal accumulatedDepreciation;
    @ApiModelProperty("净值")
    private BigDecimal netValue;
    @ApiModelProperty("存放地点")
    private String location;
    @ApiModelProperty("使用部门")
    private String department;
    @ApiModelProperty("保管人")
    private String keeper;
    @ApiModelProperty("状态")
    private String status;
    @ApiModelProperty("备注")
    private String remark;
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private String createUser;
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updateUser;
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/account/pojo/financial/FinIntangibleAsset.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
package com.ruoyi.account.pojo.financial;
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.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * æ— å½¢èµ„产实体。
 */
@Getter
@Setter
@ToString
@TableName("fin_intangible_asset")
@ApiModel(value = "FinIntangibleAsset对象", description = "无形资产")
public class FinIntangibleAsset implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    @ApiModelProperty("主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("资产编号")
    private String assetCode;
    @ApiModelProperty("资产名称")
    private String assetName;
    @ApiModelProperty("资产类别")
    private String category;
    @ApiModelProperty("证书编号")
    private String certificateNo;
    @ApiModelProperty("取得日期")
    private LocalDate acquisitionDate;
    @ApiModelProperty("资产原值")
    private BigDecimal originalValue;
    @ApiModelProperty("摊销年限(å¹´)")
    private Integer amortizationPeriod;
    @ApiModelProperty("残值率(%)")
    private BigDecimal residualRate;
    @ApiModelProperty("累计摊销")
    private BigDecimal accumulatedAmortization;
    @ApiModelProperty("净值")
    private BigDecimal netValue;
    @ApiModelProperty("有效期至")
    private LocalDate validityDate;
    @ApiModelProperty("状态")
    private String status;
    @ApiModelProperty("资产描述")
    private String description;
    @ApiModelProperty("备注")
    private String remark;
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private String createUser;
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updateUser;
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/account/pojo/financial/FinVoucher.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
package com.ruoyi.account.pojo.financial;
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.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * å‡­è¯ä¸»è¡¨å®žä½“。
 */
@Getter
@Setter
@ToString
@TableName("fin_voucher")
@ApiModel(value = "FinVoucher对象", description = "凭证主表")
public class FinVoucher implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    @ApiModelProperty("主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("凭证字号")
    private String voucherNo;
    @ApiModelProperty("凭证日期")
    private LocalDate voucherDate;
    @ApiModelProperty("摘要")
    private String summary;
    @ApiModelProperty("借方合计")
    private BigDecimal debit;
    @ApiModelProperty("贷方合计")
    private BigDecimal credit;
    @ApiModelProperty("制单人")
    private String creator;
    @ApiModelProperty("状态: unposted/posted/cancelled")
    private String status;
    @ApiModelProperty("附件数量")
    private Integer attachmentCount;
    @ApiModelProperty("备注")
    private String remark;
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private String createUser;
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updateUser;
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/account/pojo/financial/FinVoucherEntry.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
package com.ruoyi.account.pojo.financial;
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.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * å‡­è¯åˆ†å½•实体。
 */
@Getter
@Setter
@ToString
@TableName("fin_voucher_entry")
@ApiModel(value = "FinVoucherEntry对象", description = "凭证分录")
public class FinVoucherEntry implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    @ApiModelProperty("主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("凭证ID")
    private Long voucherId;
    @ApiModelProperty("行号")
    private Integer rowNo;
    @ApiModelProperty("科目编码")
    private String subjectCode;
    @ApiModelProperty("科目名称")
    private String subjectName;
    @ApiModelProperty("摘要")
    private String summary;
    @ApiModelProperty("借方金额")
    private BigDecimal debit;
    @ApiModelProperty("贷方金额")
    private BigDecimal credit;
    @ApiModelProperty("辅助核算类型")
    private String auxiliaryType;
    @ApiModelProperty("辅助核算对象ID")
    private String auxiliaryId;
    @ApiModelProperty("辅助核算对象名称")
    private String auxiliaryName;
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private String createUser;
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updateUser;
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/account/service/AccountSubjectService.java
@@ -8,6 +8,8 @@
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * <p>
 * æ€»è´¦ç§‘目表 æœåŠ¡ç±»
@@ -20,5 +22,11 @@
    IPage<AccountSubjectVo> baseList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto);
    Boolean saveAccountSubject(AccountSubjectDto accountSubjectDto);
    Boolean updateAccountSubject(AccountSubjectDto accountSubjectDto);
    Boolean removeAccountSubjectByIds(List<Long> ids);
    void exportAccountSubject(HttpServletResponse response);
}
src/main/java/com/ruoyi/account/service/financial/FinFixedAssetService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.ruoyi.account.service.financial;
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.account.bean.dto.financial.FinFixedAssetDto;
import com.ruoyi.account.pojo.financial.FinFixedAsset;
import java.util.List;
import java.util.Map;
/**
 * å›ºå®šèµ„产服务。
 */
public interface FinFixedAssetService extends IService<FinFixedAsset> {
    IPage<FinFixedAsset> pageList(Page<FinFixedAsset> page, FinFixedAssetDto queryDto);
    Boolean add(FinFixedAssetDto dto);
    Boolean update(FinFixedAssetDto dto);
    Boolean deleteByIds(List<Long> ids);
    Map<String, Object> depreciate(List<Long> ids);
}
src/main/java/com/ruoyi/account/service/financial/FinIntangibleAssetService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.ruoyi.account.service.financial;
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.account.bean.dto.financial.FinIntangibleAssetDto;
import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
import java.util.List;
import java.util.Map;
/**
 * æ— å½¢èµ„产服务。
 */
public interface FinIntangibleAssetService extends IService<FinIntangibleAsset> {
    IPage<FinIntangibleAsset> pageList(Page<FinIntangibleAsset> page, FinIntangibleAssetDto queryDto);
    Boolean add(FinIntangibleAssetDto dto);
    Boolean update(FinIntangibleAssetDto dto);
    Boolean deleteByIds(List<Long> ids);
    Map<String, Object> amortize(List<Long> ids);
}
src/main/java/com/ruoyi/account/service/financial/FinLedgerService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package com.ruoyi.account.service.financial;
import com.ruoyi.account.bean.dto.financial.FinDetailLedgerQueryDto;
import com.ruoyi.account.bean.dto.financial.FinLedgerQueryDto;
import com.ruoyi.account.bean.vo.financial.FinLedgerRowVo;
import java.util.List;
/**
 * ç§‘目账服务。
 */
public interface FinLedgerService {
    List<FinLedgerRowVo> queryGeneralLedger(FinLedgerQueryDto queryDto);
    List<FinLedgerRowVo> queryDetailLedger(FinDetailLedgerQueryDto queryDto);
}
src/main/java/com/ruoyi/account/service/financial/FinVoucherService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package com.ruoyi.account.service.financial;
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.account.bean.dto.financial.FinVoucherDto;
import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto;
import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo;
import com.ruoyi.account.pojo.financial.FinVoucher;
/**
 * å‡­è¯æœåŠ¡ã€‚
 */
public interface FinVoucherService extends IService<FinVoucher> {
    IPage<FinVoucher> pageList(Page<FinVoucher> page, FinVoucherPageDto queryDto);
    Boolean addVoucher(FinVoucherDto dto);
    Boolean updateVoucher(FinVoucherDto dto);
    Boolean postVoucher(Long id);
    Boolean cancelVoucher(Long id);
    FinVoucherDetailVo detail(Long id);
}
src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java
@@ -10,6 +10,7 @@
import com.ruoyi.account.mapper.AccountSubjectMapper;
import com.ruoyi.account.pojo.AccountSubject;
import com.ruoyi.account.service.AccountSubjectService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import jakarta.servlet.http.HttpServletResponse;
@@ -18,7 +19,16 @@
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -37,30 +47,78 @@
    @Override
    public IPage<AccountSubjectVo> baseList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto) {
        LambdaQueryWrapper<AccountSubject> queryWrapper = new LambdaQueryWrapper<>();
        if (accountSubjectDto != null && StringUtils.isNotEmpty(accountSubjectDto.getSubjectCode())) {
            queryWrapper.like(AccountSubject::getSubjectCode, accountSubjectDto.getSubjectCode());
        }
        if (accountSubjectDto != null && StringUtils.isNotEmpty(accountSubjectDto.getSubjectName())) {
            queryWrapper.like(AccountSubject::getSubjectName, accountSubjectDto.getSubjectName());
        }
        if (accountSubjectDto != null && StringUtils.isNotEmpty(accountSubjectDto.getSubjectType())) {
            queryWrapper.eq(AccountSubject::getSubjectType, accountSubjectDto.getSubjectType());
        }
        queryWrapper.orderByDesc(AccountSubject::getId);
        Page<AccountSubjectDto> requestPage = page == null ? new Page<>(1, 10) : page;
        List<AccountSubject> allSubjects = list(loadBaseQueryWrapper(accountSubjectDto));
        List<AccountSubject> filteredSubjects = applyTreeFilter(allSubjects, accountSubjectDto);
        List<AccountSubjectVo> fullTree = buildTree(filteredSubjects);
        Page<AccountSubject> entityPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        Page<AccountSubject> paramPage = page(entityPage, queryWrapper);
        long current = requestPage.getCurrent() <= 0 ? 1 : requestPage.getCurrent();
        long size = requestPage.getSize() <= 0 ? 10 : requestPage.getSize();
        int fromIndex = (int) Math.min((current - 1) * size, fullTree.size());
        int toIndex = (int) Math.min(fromIndex + size, fullTree.size());
        List<AccountSubjectVo> pagedRoots = fromIndex >= toIndex
                ? Collections.emptyList()
                : fullTree.subList(fromIndex, toIndex);
        Page<AccountSubjectVo> resultPage = new Page<>(paramPage.getCurrent(), paramPage.getSize(), paramPage.getTotal());
        List<AccountSubjectVo> records = new ArrayList<>(paramPage.getRecords().size());
        for (AccountSubject item : paramPage.getRecords()) {
            AccountSubjectVo vo = new AccountSubjectVo();
            BeanUtils.copyProperties(item, vo);
            records.add(vo);
        }
        resultPage.setRecords(records);
        Page<AccountSubjectVo> resultPage = new Page<>(current, size, fullTree.size());
        resultPage.setRecords(pagedRoots);
        return resultPage;
    }
    @Override
    public Boolean saveAccountSubject(AccountSubjectDto accountSubjectDto) {
        validateSubjectRequiredFields(accountSubjectDto);
        validateSubjectCodeUnique(accountSubjectDto, false);
        validateParent(accountSubjectDto.getParentId(), null);
        if (accountSubjectDto.getStatus() == null) {
            accountSubjectDto.setStatus(0);
        }
        return save(accountSubjectDto);
    }
    @Override
    public Boolean updateAccountSubject(AccountSubjectDto accountSubjectDto) {
        if (accountSubjectDto == null || accountSubjectDto.getId() == null) {
            throw new ServiceException("修改失败,科目ID不能为空");
        }
        if (getById(accountSubjectDto.getId()) == null) {
            throw new ServiceException("修改失败,未找到对应科目");
        }
        validateParent(accountSubjectDto.getParentId(), accountSubjectDto.getId());
        validateSubjectRequiredFields(accountSubjectDto);
        validateSubjectCodeUnique(accountSubjectDto, true);
        return updateById(accountSubjectDto);
    }
    @Override
    public Boolean removeAccountSubjectByIds(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return true;
        }
        List<AccountSubject> allSubjects = list();
        if (allSubjects == null || allSubjects.isEmpty()) {
            return true;
        }
        Map<Long, List<Long>> childrenIdMap = buildChildrenIdMap(allSubjects);
        Set<Long> removeIds = new LinkedHashSet<>();
        for (Long id : ids) {
            collectDescendantIds(id, childrenIdMap, removeIds);
        }
        if (removeIds.isEmpty()) {
            return true;
        }
        List<String> subjectCodes = allSubjects.stream()
                .filter(subject -> removeIds.contains(subject.getId()))
                .map(AccountSubject::getSubjectCode)
                .filter(StringUtils::isNotEmpty)
                .collect(Collectors.toList());
        if (!subjectCodes.isEmpty()) {
            Long referencedCount = accountSubjectMapper.countReferencedBySubjectCodes(subjectCodes);
            if (referencedCount != null && referencedCount > 0) {
                throw new ServiceException("删除失败,科目已被凭证分录引用");
            }
        }
        return removeByIds(removeIds);
    }
    @Override
@@ -74,4 +132,266 @@
        ExcelUtil<AccountSubjectImportDto> util = new ExcelUtil<>(AccountSubjectImportDto.class);
        util.exportExcel(response, importDtos , "总账科目");
    }
    /**
     * æ ¡éªŒç§‘目必填字段,避免脏数据写入。
     */
    private void validateSubjectRequiredFields(AccountSubjectDto accountSubjectDto) {
        if (accountSubjectDto == null) {
            throw new ServiceException("总账科目数据不能为空");
        }
        if (StringUtils.isEmpty(accountSubjectDto.getSubjectCode())) {
            throw new ServiceException("科目编码不能为空");
        }
        if (StringUtils.isEmpty(accountSubjectDto.getSubjectName())) {
            throw new ServiceException("科目名称不能为空");
        }
        if (StringUtils.isEmpty(accountSubjectDto.getSubjectType())) {
            throw new ServiceException("科目类型不能为空");
        }
    }
    /**
     * æ ¡éªŒç§‘目编码唯一,新增和修改都要执行。
     */
    private void validateSubjectCodeUnique(AccountSubjectDto accountSubjectDto, boolean isUpdate) {
        LambdaQueryWrapper<AccountSubject> codeQueryWrapper = new LambdaQueryWrapper<>();
        codeQueryWrapper.eq(AccountSubject::getSubjectCode, accountSubjectDto.getSubjectCode());
        if (isUpdate) {
            codeQueryWrapper.ne(AccountSubject::getId, accountSubjectDto.getId());
        }
        AccountSubject exists = getOne(codeQueryWrapper, false);
        if (Objects.nonNull(exists)) {
            throw new ServiceException("科目编码已存在,请勿重复提交");
        }
    }
    /**
     * ä»…按通用过滤条件查询基础数据(树形过滤后续再做)。
     */
    private LambdaQueryWrapper<AccountSubject> loadBaseQueryWrapper(AccountSubjectDto accountSubjectDto) {
        LambdaQueryWrapper<AccountSubject> queryWrapper = new LambdaQueryWrapper<>();
        if (accountSubjectDto != null && accountSubjectDto.getStatus() != null) {
            queryWrapper.eq(AccountSubject::getStatus, accountSubjectDto.getStatus());
        }
        queryWrapper.orderByAsc(AccountSubject::getSubjectCode).orderByAsc(AccountSubject::getId);
        return queryWrapper;
    }
    /**
     * æ ‘形过滤:命中节点后保留其父链与子树,保证递归结构完整。
     */
    private List<AccountSubject> applyTreeFilter(List<AccountSubject> allSubjects, AccountSubjectDto queryDto) {
        if (allSubjects == null || allSubjects.isEmpty()) {
            return Collections.emptyList();
        }
        boolean hasFilter = queryDto != null && (
                StringUtils.isNotEmpty(queryDto.getSubjectCode())
                        || StringUtils.isNotEmpty(queryDto.getSubjectName())
                        || StringUtils.isNotEmpty(queryDto.getSubjectType())
        );
        if (!hasFilter) {
            return allSubjects;
        }
        Map<Long, AccountSubject> subjectMap = allSubjects.stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(AccountSubject::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
        Map<Long, List<AccountSubject>> childrenMap = buildChildrenMap(allSubjects);
        Set<Long> matchedIds = new LinkedHashSet<>();
        for (AccountSubject subject : allSubjects) {
            if (subject.getId() == null) {
                continue;
            }
            if (matchesFilter(subject, queryDto)) {
                matchedIds.add(subject.getId());
            }
        }
        if (matchedIds.isEmpty()) {
            return Collections.emptyList();
        }
        Set<Long> resultIds = new LinkedHashSet<>(matchedIds);
        for (Long matchedId : matchedIds) {
            addAncestors(matchedId, subjectMap, resultIds);
            addDescendants(matchedId, childrenMap, resultIds);
        }
        return allSubjects.stream()
                .filter(item -> item.getId() != null && resultIds.contains(item.getId()))
                .collect(Collectors.toList());
    }
    private boolean matchesFilter(AccountSubject subject, AccountSubjectDto queryDto) {
        if (queryDto == null) {
            return true;
        }
        if (StringUtils.isNotEmpty(queryDto.getSubjectCode())
                && (subject.getSubjectCode() == null || !subject.getSubjectCode().contains(queryDto.getSubjectCode()))) {
            return false;
        }
        if (StringUtils.isNotEmpty(queryDto.getSubjectName())
                && (subject.getSubjectName() == null || !subject.getSubjectName().contains(queryDto.getSubjectName()))) {
            return false;
        }
        if (StringUtils.isNotEmpty(queryDto.getSubjectType())
                && !queryDto.getSubjectType().equals(subject.getSubjectType())) {
            return false;
        }
        return true;
    }
    private void addAncestors(Long subjectId, Map<Long, AccountSubject> subjectMap, Set<Long> resultIds) {
        AccountSubject current = subjectMap.get(subjectId);
        if (current == null) {
            return;
        }
        Long parentId = current.getParentId();
        while (parentId != null && parentId > 0) {
            AccountSubject parent = subjectMap.get(parentId);
            if (parent == null) {
                break;
            }
            if (!resultIds.add(parent.getId())) {
                break;
            }
            parentId = parent.getParentId();
        }
    }
    private void addDescendants(Long subjectId, Map<Long, List<AccountSubject>> childrenMap, Set<Long> resultIds) {
        List<AccountSubject> children = childrenMap.getOrDefault(subjectId, Collections.emptyList());
        for (AccountSubject child : children) {
            if (child.getId() == null) {
                continue;
            }
            if (resultIds.add(child.getId())) {
                addDescendants(child.getId(), childrenMap, resultIds);
            }
        }
    }
    private Map<Long, List<AccountSubject>> buildChildrenMap(List<AccountSubject> subjects) {
        Map<Long, List<AccountSubject>> childrenMap = new HashMap<>();
        for (AccountSubject subject : subjects) {
            if (subject.getId() == null) {
                continue;
            }
            Long parentId = subject.getParentId();
            if (parentId == null || parentId <= 0) {
                continue;
            }
            childrenMap.computeIfAbsent(parentId, key -> new ArrayList<>()).add(subject);
        }
        return childrenMap;
    }
    /**
     * åŸºäºŽ parentId é€’归构建科目树。
     */
    private List<AccountSubjectVo> buildTree(List<AccountSubject> subjects) {
        if (subjects == null || subjects.isEmpty()) {
            return Collections.emptyList();
        }
        List<AccountSubject> sortedSubjects = new ArrayList<>(subjects);
        sortedSubjects.sort(Comparator
                .comparing(AccountSubject::getSubjectCode, Comparator.nullsLast(String::compareTo))
                .thenComparing(AccountSubject::getId, Comparator.nullsLast(Long::compareTo)));
        Map<Long, AccountSubjectVo> subjectVoMap = new LinkedHashMap<>();
        for (AccountSubject subject : sortedSubjects) {
            if (subject.getId() == null) {
                continue;
            }
            AccountSubjectVo vo = new AccountSubjectVo();
            BeanUtils.copyProperties(subject, vo);
            subjectVoMap.put(subject.getId(), vo);
        }
        List<AccountSubjectVo> roots = new ArrayList<>();
        for (AccountSubject subject : sortedSubjects) {
            if (subject.getId() == null) {
                continue;
            }
            AccountSubjectVo current = subjectVoMap.get(subject.getId());
            Long parentId = subject.getParentId();
            if (parentId != null && parentId > 0 && subjectVoMap.containsKey(parentId)) {
                subjectVoMap.get(parentId).getChildren().add(current);
            } else {
                roots.add(current);
            }
        }
        markLeafRecursively(roots);
        return roots;
    }
    private void markLeafRecursively(List<AccountSubjectVo> nodes) {
        for (AccountSubjectVo node : nodes) {
            List<AccountSubjectVo> children = node.getChildren();
            node.setLeaf(children == null || children.isEmpty());
            if (children != null && !children.isEmpty()) {
                markLeafRecursively(children);
            }
        }
    }
    /**
     * æ ¡éªŒçˆ¶å­å…³ç³»ï¼šçˆ¶èŠ‚ç‚¹å¿…é¡»å­˜åœ¨ï¼Œä¸”ä¸èƒ½å½¢æˆå¾ªçŽ¯å¼•ç”¨ã€‚
     */
    private void validateParent(Long parentId, Long currentId) {
        if (parentId == null || parentId <= 0) {
            return;
        }
        if (currentId != null && parentId.equals(currentId)) {
            throw new ServiceException("父科目不能选择自身");
        }
        AccountSubject parent = getById(parentId);
        if (parent == null) {
            throw new ServiceException("父科目不存在,请重新选择");
        }
        // é˜²æ­¢å½¢æˆçŽ¯ï¼šæ›´æ–°æ—¶ï¼Œçˆ¶èŠ‚ç‚¹ä¸èƒ½æ˜¯å½“å‰èŠ‚ç‚¹çš„ä»»æ„å­å­™èŠ‚ç‚¹ã€‚
        if (currentId != null) {
            Set<Long> visited = new HashSet<>();
            Long traceParentId = parentId;
            while (traceParentId != null && traceParentId > 0) {
                if (!visited.add(traceParentId)) {
                    throw new ServiceException("科目层级存在循环引用,请检查父科目设置");
                }
                if (traceParentId.equals(currentId)) {
                    throw new ServiceException("父科目不能是当前科目或其子科目");
                }
                AccountSubject traceNode = getById(traceParentId);
                if (traceNode == null) {
                    break;
                }
                traceParentId = traceNode.getParentId();
            }
        }
    }
    private Map<Long, List<Long>> buildChildrenIdMap(List<AccountSubject> subjects) {
        Map<Long, List<Long>> map = new HashMap<>();
        for (AccountSubject subject : subjects) {
            if (subject.getId() == null || subject.getParentId() == null || subject.getParentId() <= 0) {
                continue;
            }
            map.computeIfAbsent(subject.getParentId(), key -> new ArrayList<>()).add(subject.getId());
        }
        return map;
    }
    /**
     * æ”¶é›†å¾…删除节点及其所有子孙节点。
     */
    private void collectDescendantIds(Long id, Map<Long, List<Long>> childrenIdMap, Set<Long> result) {
        if (id == null || !result.add(id)) {
            return;
        }
        List<Long> children = childrenIdMap.getOrDefault(id, Collections.emptyList());
        for (Long childId : children) {
            collectDescendantIds(childId, childrenIdMap, result);
        }
    }
}
src/main/java/com/ruoyi/account/service/impl/financial/FinFixedAssetServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,231 @@
package com.ruoyi.account.service.impl.financial;
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.account.bean.dto.financial.FinFixedAssetDto;
import com.ruoyi.account.mapper.financial.FinFixedAssetMapper;
import com.ruoyi.account.pojo.financial.FinFixedAsset;
import com.ruoyi.account.service.financial.FinFixedAssetService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
 * å›ºå®šèµ„产服务实现。
 */
@Service
@RequiredArgsConstructor
public class FinFixedAssetServiceImpl extends ServiceImpl<FinFixedAssetMapper, FinFixedAsset> implements FinFixedAssetService {
    private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    @Override
    public IPage<FinFixedAsset> pageList(Page<FinFixedAsset> page, FinFixedAssetDto queryDto) {
        LambdaQueryWrapper<FinFixedAsset> wrapper = new LambdaQueryWrapper<>();
        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetCode())) {
            wrapper.like(FinFixedAsset::getAssetCode, queryDto.getAssetCode());
        }
        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetName())) {
            wrapper.like(FinFixedAsset::getAssetName, queryDto.getAssetName());
        }
        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getCategory())) {
            wrapper.eq(FinFixedAsset::getCategory, queryDto.getCategory());
        }
        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getStatus())) {
            wrapper.eq(FinFixedAsset::getStatus, queryDto.getStatus());
        }
        wrapper.orderByDesc(FinFixedAsset::getId);
        return page(page, wrapper);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean add(FinFixedAssetDto dto) {
        validateForSave(dto, false);
        if (StringUtils.isEmpty(dto.getAssetCode())) {
            dto.setAssetCode(generateAssetCode());
        }
        BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
        dto.setResidualRate(residualRate);
        BigDecimal accumulatedDepreciation = defaultMoney(dto.getAccumulatedDepreciation());
        dto.setAccumulatedDepreciation(accumulatedDepreciation);
        dto.setNetValue(calculateNetValue(dto.getOriginalValue(), accumulatedDepreciation));
        if (StringUtils.isEmpty(dto.getStatus())) {
            dto.setStatus("in_use");
        }
        return save(dto);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean update(FinFixedAssetDto dto) {
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("修改失败,资产ID不能为空");
        }
        FinFixedAsset existed = getById(dto.getId());
        if (existed == null) {
            throw new ServiceException("修改失败,固定资产不存在");
        }
        if (StringUtils.isEmpty(dto.getAssetCode())) {
            dto.setAssetCode(existed.getAssetCode());
        }
        if (StringUtils.isEmpty(dto.getStatus())) {
            dto.setStatus(existed.getStatus());
        }
        validateForSave(dto, true);
        BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
        dto.setResidualRate(residualRate);
        if (dto.getAccumulatedDepreciation() == null) {
            dto.setAccumulatedDepreciation(defaultMoney(existed.getAccumulatedDepreciation()));
        }
        dto.setNetValue(calculateNetValue(dto.getOriginalValue(), dto.getAccumulatedDepreciation()));
        return updateById(dto);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean deleteByIds(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new ServiceException("删除失败,请选择要删除的数据");
        }
        return removeByIds(ids);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> depreciate(List<Long> ids) {
        LambdaQueryWrapper<FinFixedAsset> wrapper = new LambdaQueryWrapper<>();
        if (ids != null && !ids.isEmpty()) {
            wrapper.in(FinFixedAsset::getId, ids);
        } else {
            wrapper.eq(FinFixedAsset::getStatus, "in_use");
        }
        List<FinFixedAsset> assets = list(wrapper);
        BigDecimal totalMonthlyDepreciation = ZERO;
        int processedCount = 0;
        for (FinFixedAsset asset : assets) {
            if (!"in_use".equals(asset.getStatus())) {
                continue;
            }
            BigDecimal monthlyDepreciation = calculateMonthlyDepreciation(
                    asset.getOriginalValue(),
                    asset.getResidualRate(),
                    asset.getUsefulLife()
            );
            BigDecimal accumulatedDepreciation = defaultMoney(asset.getAccumulatedDepreciation()).add(monthlyDepreciation);
            if (accumulatedDepreciation.compareTo(defaultMoney(asset.getOriginalValue())) > 0) {
                accumulatedDepreciation = defaultMoney(asset.getOriginalValue());
            }
            asset.setAccumulatedDepreciation(roundMoney(accumulatedDepreciation));
            asset.setNetValue(calculateNetValue(asset.getOriginalValue(), asset.getAccumulatedDepreciation()));
            updateById(asset);
            processedCount++;
            totalMonthlyDepreciation = totalMonthlyDepreciation.add(monthlyDepreciation);
        }
        Map<String, Object> result = new HashMap<>(4);
        result.put("processedCount", processedCount);
        result.put("totalMonthlyDepreciation", roundMoney(totalMonthlyDepreciation));
        result.put("executionTime", LocalDateTime.now());
        return result;
    }
    /**
     * æŒ‰æ–‡æ¡£è§„则校验固定资产数据。
     */
    private void validateForSave(FinFixedAssetDto dto, boolean isUpdate) {
        if (dto == null) {
            throw new ServiceException("固定资产数据不能为空");
        }
        if (isUpdate && dto.getId() == null) {
            throw new ServiceException("修改失败,资产ID不能为空");
        }
        if (StringUtils.isEmpty(dto.getAssetName())) {
            throw new ServiceException("资产名称不能为空");
        }
        if (StringUtils.isEmpty(dto.getCategory())) {
            throw new ServiceException("资产类别不能为空");
        }
        if (dto.getPurchaseDate() == null) {
            throw new ServiceException("购置日期不能为空");
        }
        if (dto.getOriginalValue() == null || dto.getOriginalValue().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("资产原值不能为空且不能小于0");
        }
        if (dto.getUsefulLife() == null || dto.getUsefulLife() <= 0) {
            throw new ServiceException("使用年限必须大于0");
        }
        if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("残值率不能小于0");
        }
        if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(ONE_HUNDRED) > 0) {
            throw new ServiceException("残值率不能大于100%");
        }
        if (StringUtils.isNotEmpty(dto.getAssetCode())) {
            LambdaQueryWrapper<FinFixedAsset> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(FinFixedAsset::getAssetCode, dto.getAssetCode());
            if (isUpdate) {
                wrapper.ne(FinFixedAsset::getId, dto.getId());
            }
            if (count(wrapper) > 0) {
                throw new ServiceException("资产编号已存在,请勿重复提交");
            }
        }
    }
    /**
     * å›ºå®šèµ„产折旧公式:
     * monthlyDepreciation = originalValue * (1 - residualRate/100) / (usefulLife*12)
     */
    private BigDecimal calculateMonthlyDepreciation(BigDecimal originalValue, BigDecimal residualRate, Integer usefulLife) {
        BigDecimal normalizedOriginalValue = defaultMoney(originalValue);
        BigDecimal normalizedResidualRate = normalizeResidualRate(residualRate);
        BigDecimal depreciableRatio = BigDecimal.ONE.subtract(normalizedResidualRate.divide(ONE_HUNDRED, 8, RoundingMode.HALF_UP));
        BigDecimal months = BigDecimal.valueOf((long) usefulLife * 12L);
        if (months.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("使用年限无效,无法计提折旧");
        }
        return roundMoney(normalizedOriginalValue.multiply(depreciableRatio).divide(months, 8, RoundingMode.HALF_UP));
    }
    /**
     * å‡€å€¼ = åŽŸå€¼ - ç´¯è®¡æŠ˜æ—§ã€‚
     */
    private BigDecimal calculateNetValue(BigDecimal originalValue, BigDecimal accumulatedDepreciation) {
        BigDecimal value = defaultMoney(originalValue).subtract(defaultMoney(accumulatedDepreciation));
        if (value.compareTo(BigDecimal.ZERO) < 0) {
            value = BigDecimal.ZERO;
        }
        return roundMoney(value);
    }
    private BigDecimal normalizeResidualRate(BigDecimal residualRate) {
        return residualRate == null ? BigDecimal.ZERO : residualRate;
    }
    private BigDecimal defaultMoney(BigDecimal value) {
        return value == null ? ZERO : roundMoney(value);
    }
    private BigDecimal roundMoney(BigDecimal value) {
        if (value == null) {
            return ZERO;
        }
        return value.setScale(2, RoundingMode.HALF_UP);
    }
    private String generateAssetCode() {
        return "GD" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
}
src/main/java/com/ruoyi/account/service/impl/financial/FinIntangibleAssetServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,250 @@
package com.ruoyi.account.service.impl.financial;
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.account.bean.dto.financial.FinIntangibleAssetDto;
import com.ruoyi.account.mapper.financial.FinIntangibleAssetMapper;
import com.ruoyi.account.pojo.financial.FinIntangibleAsset;
import com.ruoyi.account.service.financial.FinIntangibleAssetService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
 * æ— å½¢èµ„产服务实现。
 */
@Service
@RequiredArgsConstructor
public class FinIntangibleAssetServiceImpl extends ServiceImpl<FinIntangibleAssetMapper, FinIntangibleAsset> implements FinIntangibleAssetService {
    private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    @Override
    public IPage<FinIntangibleAsset> pageList(Page<FinIntangibleAsset> page, FinIntangibleAssetDto queryDto) {
        LambdaQueryWrapper<FinIntangibleAsset> wrapper = new LambdaQueryWrapper<>();
        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetCode())) {
            wrapper.like(FinIntangibleAsset::getAssetCode, queryDto.getAssetCode());
        }
        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getAssetName())) {
            wrapper.like(FinIntangibleAsset::getAssetName, queryDto.getAssetName());
        }
        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getCategory())) {
            wrapper.eq(FinIntangibleAsset::getCategory, queryDto.getCategory());
        }
        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getStatus())) {
            wrapper.eq(FinIntangibleAsset::getStatus, queryDto.getStatus());
        }
        wrapper.orderByDesc(FinIntangibleAsset::getId);
        return page(page, wrapper);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean add(FinIntangibleAssetDto dto) {
        validateForSave(dto, false);
        if (StringUtils.isEmpty(dto.getAssetCode())) {
            dto.setAssetCode(generateAssetCode());
        }
        BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
        dto.setResidualRate(residualRate);
        BigDecimal accumulatedAmortization = defaultMoney(dto.getAccumulatedAmortization());
        dto.setAccumulatedAmortization(accumulatedAmortization);
        dto.setNetValue(calculateNetValue(dto.getOriginalValue(), accumulatedAmortization));
        if (StringUtils.isEmpty(dto.getStatus())) {
            dto.setStatus("in_use");
        }
        return save(dto);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean update(FinIntangibleAssetDto dto) {
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("修改失败,资产ID不能为空");
        }
        FinIntangibleAsset existed = getById(dto.getId());
        if (existed == null) {
            throw new ServiceException("修改失败,无形资产不存在");
        }
        if (StringUtils.isEmpty(dto.getAssetCode())) {
            dto.setAssetCode(existed.getAssetCode());
        }
        if (StringUtils.isEmpty(dto.getStatus())) {
            dto.setStatus(existed.getStatus());
        }
        validateForSave(dto, true);
        BigDecimal residualRate = normalizeResidualRate(dto.getResidualRate());
        dto.setResidualRate(residualRate);
        if (dto.getAccumulatedAmortization() == null) {
            dto.setAccumulatedAmortization(defaultMoney(existed.getAccumulatedAmortization()));
        }
        dto.setNetValue(calculateNetValue(dto.getOriginalValue(), dto.getAccumulatedAmortization()));
        if (dto.getNetValue().compareTo(BigDecimal.ZERO) <= 0) {
            dto.setStatus("amortized");
        } else if ("amortized".equals(dto.getStatus())) {
            dto.setStatus("in_use");
        }
        if (dto.getValidityDate() != null
                && dto.getValidityDate().isBefore(LocalDate.now())
                && !"amortized".equals(dto.getStatus())) {
            dto.setStatus("expired");
        }
        return updateById(dto);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean deleteByIds(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new ServiceException("删除失败,请选择要删除的数据");
        }
        return removeByIds(ids);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> amortize(List<Long> ids) {
        LambdaQueryWrapper<FinIntangibleAsset> wrapper = new LambdaQueryWrapper<>();
        if (ids != null && !ids.isEmpty()) {
            wrapper.in(FinIntangibleAsset::getId, ids);
        } else {
            wrapper.eq(FinIntangibleAsset::getStatus, "in_use");
        }
        List<FinIntangibleAsset> assets = list(wrapper);
        BigDecimal totalMonthlyAmortization = ZERO;
        int processedCount = 0;
        for (FinIntangibleAsset asset : assets) {
            if (!"in_use".equals(asset.getStatus())) {
                continue;
            }
            BigDecimal monthlyAmortization = calculateMonthlyAmortization(
                    asset.getOriginalValue(),
                    asset.getResidualRate(),
                    asset.getAmortizationPeriod()
            );
            BigDecimal accumulatedAmortization = defaultMoney(asset.getAccumulatedAmortization()).add(monthlyAmortization);
            if (accumulatedAmortization.compareTo(defaultMoney(asset.getOriginalValue())) > 0) {
                accumulatedAmortization = defaultMoney(asset.getOriginalValue());
            }
            asset.setAccumulatedAmortization(roundMoney(accumulatedAmortization));
            asset.setNetValue(calculateNetValue(asset.getOriginalValue(), asset.getAccumulatedAmortization()));
            // è§„则:当净值 <= 0 æ—¶ï¼Œå‡€å€¼å½’零并标记为已摊销完。
            if (asset.getNetValue().compareTo(BigDecimal.ZERO) <= 0) {
                asset.setNetValue(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
                asset.setStatus("amortized");
            } else if (asset.getValidityDate() != null && asset.getValidityDate().isBefore(LocalDate.now())) {
                asset.setStatus("expired");
            }
            updateById(asset);
            processedCount++;
            totalMonthlyAmortization = totalMonthlyAmortization.add(monthlyAmortization);
        }
        Map<String, Object> result = new HashMap<>(4);
        result.put("processedCount", processedCount);
        result.put("totalMonthlyAmortization", roundMoney(totalMonthlyAmortization));
        result.put("executionTime", LocalDateTime.now());
        return result;
    }
    /**
     * æŒ‰æ–‡æ¡£è§„则校验无形资产数据。
     */
    private void validateForSave(FinIntangibleAssetDto dto, boolean isUpdate) {
        if (dto == null) {
            throw new ServiceException("无形资产数据不能为空");
        }
        if (isUpdate && dto.getId() == null) {
            throw new ServiceException("修改失败,资产ID不能为空");
        }
        if (StringUtils.isEmpty(dto.getAssetName())) {
            throw new ServiceException("资产名称不能为空");
        }
        if (StringUtils.isEmpty(dto.getCategory())) {
            throw new ServiceException("资产类别不能为空");
        }
        if (dto.getAcquisitionDate() == null) {
            throw new ServiceException("取得日期不能为空");
        }
        if (dto.getOriginalValue() == null || dto.getOriginalValue().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("资产原值不能为空且不能小于0");
        }
        if (dto.getAmortizationPeriod() == null || dto.getAmortizationPeriod() <= 0) {
            throw new ServiceException("摊销年限必须大于0");
        }
        if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(BigDecimal.ZERO) < 0) {
            throw new ServiceException("残值率不能小于0");
        }
        if (dto.getResidualRate() != null && dto.getResidualRate().compareTo(ONE_HUNDRED) > 0) {
            throw new ServiceException("残值率不能大于100%");
        }
        if (StringUtils.isNotEmpty(dto.getAssetCode())) {
            LambdaQueryWrapper<FinIntangibleAsset> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(FinIntangibleAsset::getAssetCode, dto.getAssetCode());
            if (isUpdate) {
                wrapper.ne(FinIntangibleAsset::getId, dto.getId());
            }
            if (count(wrapper) > 0) {
                throw new ServiceException("资产编号已存在,请勿重复提交");
            }
        }
    }
    /**
     * æ— å½¢èµ„产摊销公式:
     * monthlyAmortization = originalValue * (1 - residualRate/100) / (amortizationPeriod*12)
     */
    private BigDecimal calculateMonthlyAmortization(BigDecimal originalValue, BigDecimal residualRate, Integer amortizationPeriod) {
        BigDecimal normalizedOriginalValue = defaultMoney(originalValue);
        BigDecimal normalizedResidualRate = normalizeResidualRate(residualRate);
        BigDecimal amortizableRatio = BigDecimal.ONE.subtract(normalizedResidualRate.divide(ONE_HUNDRED, 8, RoundingMode.HALF_UP));
        BigDecimal months = BigDecimal.valueOf((long) amortizationPeriod * 12L);
        if (months.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("摊销年限无效,无法计提摊销");
        }
        return roundMoney(normalizedOriginalValue.multiply(amortizableRatio).divide(months, 8, RoundingMode.HALF_UP));
    }
    /**
     * å‡€å€¼ = åŽŸå€¼ - ç´¯è®¡æ‘Šé”€ã€‚
     */
    private BigDecimal calculateNetValue(BigDecimal originalValue, BigDecimal accumulatedAmortization) {
        BigDecimal value = defaultMoney(originalValue).subtract(defaultMoney(accumulatedAmortization));
        if (value.compareTo(BigDecimal.ZERO) < 0) {
            value = BigDecimal.ZERO;
        }
        return roundMoney(value);
    }
    private BigDecimal normalizeResidualRate(BigDecimal residualRate) {
        return residualRate == null ? BigDecimal.ZERO : residualRate;
    }
    private BigDecimal defaultMoney(BigDecimal value) {
        return value == null ? ZERO : roundMoney(value);
    }
    private BigDecimal roundMoney(BigDecimal value) {
        if (value == null) {
            return ZERO;
        }
        return value.setScale(2, RoundingMode.HALF_UP);
    }
    private String generateAssetCode() {
        return "WX" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
}
src/main/java/com/ruoyi/account/service/impl/financial/FinLedgerServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,237 @@
package com.ruoyi.account.service.impl.financial;
import com.ruoyi.account.bean.dto.financial.FinDetailLedgerQueryDto;
import com.ruoyi.account.bean.dto.financial.FinLedgerQueryDto;
import com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo;
import com.ruoyi.account.bean.vo.financial.FinLedgerRowVo;
import com.ruoyi.account.mapper.financial.FinVoucherEntryMapper;
import com.ruoyi.account.service.financial.FinLedgerService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
/**
 * ç§‘目总账/明细账服务实现。
 */
@Service
@RequiredArgsConstructor
public class FinLedgerServiceImpl implements FinLedgerService {
    private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    private final FinVoucherEntryMapper finVoucherEntryMapper;
    @Override
    public List<FinLedgerRowVo> queryGeneralLedger(FinLedgerQueryDto queryDto) {
        if (queryDto == null || StringUtils.isEmpty(queryDto.getSubjectCode())) {
            return Collections.emptyList();
        }
        YearMonth startMonth = parseMonth(queryDto.getStartMonth(), "开始月份");
        YearMonth endMonth = parseMonth(queryDto.getEndMonth(), "结束月份");
        if (startMonth.isAfter(endMonth)) {
            throw new ServiceException("开始月份不能大于结束月份");
        }
        return Collections.singletonList(buildGeneralLedgerTotalRow(queryDto.getSubjectCode(), startMonth, endMonth));
    }
    @Override
    public List<FinLedgerRowVo> queryDetailLedger(FinDetailLedgerQueryDto queryDto) {
        if (queryDto == null || StringUtils.isEmpty(queryDto.getSubjectCode())) {
            return Collections.emptyList();
        }
        YearMonth startMonth = parseMonth(queryDto.getStartMonth(), "开始月份");
        YearMonth endMonth = parseMonth(queryDto.getEndMonth(), "结束月份");
        if (startMonth.isAfter(endMonth)) {
            throw new ServiceException("开始月份不能大于结束月份");
        }
        return buildLedgerRows(queryDto.getSubjectCode(), startMonth, endMonth, queryDto.getAuxiliaryType(), queryDto.getAuxiliaryId());
    }
    /**
     * æž„建账簿行数据,输出期初、分录、本月合计、本年累计。
     */
    private List<FinLedgerRowVo> buildLedgerRows(String subjectCode,
                                                 YearMonth startMonth,
                                                 YearMonth endMonth,
                                                 String auxiliaryType,
                                                 String auxiliaryId) {
        LocalDate startDate = startMonth.atDay(1);
        LocalDate endDate = endMonth.atEndOfMonth();
        List<FinLedgerEntryRecordVo> openingEntries = finVoucherEntryMapper.listPostedEntriesBefore(
                subjectCode, startDate, auxiliaryType, auxiliaryId
        );
        BigDecimal openingBalance = calculateBalance(openingEntries);
        List<FinLedgerEntryRecordVo> currentPeriodEntries = finVoucherEntryMapper.listPostedEntries(
                subjectCode, startDate, endDate, auxiliaryType, auxiliaryId
        );
        Map<YearMonth, List<FinLedgerEntryRecordVo>> monthEntriesMap = groupEntriesByMonth(currentPeriodEntries);
        List<FinLedgerRowVo> rows = new ArrayList<>();
        BigDecimal runningBalance = openingBalance;
        BigDecimal yearDebit = ZERO;
        BigDecimal yearCredit = ZERO;
        for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) {
            rows.add(buildOpeningRow(month.atDay(1), runningBalance));
            List<FinLedgerEntryRecordVo> monthEntries = monthEntriesMap.getOrDefault(month, Collections.emptyList());
            BigDecimal monthDebit = ZERO;
            BigDecimal monthCredit = ZERO;
            for (FinLedgerEntryRecordVo entry : monthEntries) {
                BigDecimal debit = money(entry.getDebit());
                BigDecimal credit = money(entry.getCredit());
                runningBalance = runningBalance.add(debit).subtract(credit);
                monthDebit = monthDebit.add(debit);
                monthCredit = monthCredit.add(credit);
                FinLedgerRowVo row = new FinLedgerRowVo();
                row.setRowType("entry");
                row.setDate(entry.getVoucherDate());
                row.setVoucherNo(entry.getVoucherNo());
                row.setSummary(StringUtils.isNotEmpty(entry.getSummary()) ? entry.getSummary() : "");
                row.setDebit(debit);
                row.setCredit(credit);
                row.setBalance(money(runningBalance));
                row.setDirection(resolveDirection(runningBalance));
                rows.add(row);
            }
            rows.add(buildMonthlyTotalRow(month.atEndOfMonth(), monthDebit, monthCredit, runningBalance));
            yearDebit = yearDebit.add(monthDebit);
            yearCredit = yearCredit.add(monthCredit);
        }
        rows.add(buildYearlyTotalRow(endMonth.atEndOfMonth(), yearDebit, yearCredit, runningBalance));
        return rows;
    }
    private FinLedgerRowVo buildGeneralLedgerTotalRow(String subjectCode, YearMonth startMonth, YearMonth endMonth) {
        LocalDate startDate = startMonth.atDay(1);
        LocalDate endDate = endMonth.atEndOfMonth();
        List<FinLedgerEntryRecordVo> openingEntries = finVoucherEntryMapper.listPostedEntriesBefore(
                subjectCode, startDate, null, null
        );
        BigDecimal openingBalance = calculateBalance(openingEntries);
        List<FinLedgerEntryRecordVo> currentPeriodEntries = finVoucherEntryMapper.listPostedEntries(
                subjectCode, startDate, endDate, null, null
        );
        BigDecimal totalDebit = ZERO;
        BigDecimal totalCredit = ZERO;
        for (FinLedgerEntryRecordVo entry : currentPeriodEntries) {
            totalDebit = totalDebit.add(money(entry.getDebit()));
            totalCredit = totalCredit.add(money(entry.getCredit()));
        }
        BigDecimal endingBalance = openingBalance.add(totalDebit).subtract(totalCredit);
        FinLedgerRowVo totalRow = new FinLedgerRowVo();
        totalRow.setRowType("yearly_total");
        totalRow.setDate(endDate);
        totalRow.setDebit(money(totalDebit));
        totalRow.setCredit(money(totalCredit));
        totalRow.setBalance(money(endingBalance));
        totalRow.setDirection(resolveDirection(endingBalance));
        return totalRow;
    }
    private Map<YearMonth, List<FinLedgerEntryRecordVo>> groupEntriesByMonth(List<FinLedgerEntryRecordVo> entries) {
        Map<YearMonth, List<FinLedgerEntryRecordVo>> map = new LinkedHashMap<>();
        for (FinLedgerEntryRecordVo entry : entries) {
            if (entry.getVoucherDate() == null) {
                continue;
            }
            YearMonth month = YearMonth.from(entry.getVoucherDate());
            map.computeIfAbsent(month, key -> new ArrayList<>()).add(entry);
        }
        return map;
    }
    private FinLedgerRowVo buildOpeningRow(LocalDate date, BigDecimal openingBalance) {
        FinLedgerRowVo row = new FinLedgerRowVo();
        row.setRowType("opening");
        row.setDate(date);
        row.setVoucherNo("-");
        row.setSummary("期初余额");
        row.setDebit(ZERO);
        row.setCredit(ZERO);
        row.setBalance(money(openingBalance));
        row.setDirection(resolveDirection(openingBalance));
        return row;
    }
    private FinLedgerRowVo buildMonthlyTotalRow(LocalDate date,
                                                BigDecimal monthDebit,
                                                BigDecimal monthCredit,
                                                BigDecimal monthBalance) {
        FinLedgerRowVo row = new FinLedgerRowVo();
        row.setRowType("monthly_total");
        row.setDate(date);
        row.setVoucherNo("-");
        row.setSummary("本月合计");
        row.setDebit(money(monthDebit));
        row.setCredit(money(monthCredit));
        row.setBalance(money(monthBalance));
        row.setDirection(resolveDirection(monthBalance));
        return row;
    }
    private FinLedgerRowVo buildYearlyTotalRow(LocalDate date,
                                               BigDecimal yearDebit,
                                               BigDecimal yearCredit,
                                               BigDecimal yearBalance) {
        FinLedgerRowVo row = new FinLedgerRowVo();
        row.setRowType("yearly_total");
        row.setDate(date);
        row.setVoucherNo("-");
        row.setSummary("合计");
        row.setDebit(money(yearDebit));
        row.setCredit(money(yearCredit));
        row.setBalance(money(yearBalance));
        row.setDirection(resolveDirection(yearBalance));
        return row;
    }
    private BigDecimal calculateBalance(List<FinLedgerEntryRecordVo> entries) {
        BigDecimal balance = ZERO;
        for (FinLedgerEntryRecordVo entry : entries) {
            balance = balance.add(money(entry.getDebit())).subtract(money(entry.getCredit()));
        }
        return money(balance);
    }
    private String resolveDirection(BigDecimal balance) {
        return money(balance).compareTo(BigDecimal.ZERO) >= 0 ? "借" : "è´·";
    }
    private YearMonth parseMonth(String value, String fieldLabel) {
        if (StringUtils.isEmpty(value)) {
            throw new ServiceException(fieldLabel + "不能为空,格式应为YYYY-MM");
        }
        try {
            return YearMonth.parse(value, MONTH_FORMATTER);
        } catch (DateTimeParseException ex) {
            throw new ServiceException(fieldLabel + "格式错误,格式应为YYYY-MM");
        }
    }
    private BigDecimal money(BigDecimal value) {
        if (value == null) {
            return ZERO;
        }
        return value.setScale(2, RoundingMode.HALF_UP);
    }
}
src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,307 @@
package com.ruoyi.account.service.impl.financial;
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.account.bean.dto.financial.FinVoucherDto;
import com.ruoyi.account.bean.dto.financial.FinVoucherEntryDto;
import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto;
import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo;
import com.ruoyi.account.mapper.AccountSubjectMapper;
import com.ruoyi.account.mapper.financial.FinVoucherEntryMapper;
import com.ruoyi.account.mapper.financial.FinVoucherMapper;
import com.ruoyi.account.pojo.AccountSubject;
import com.ruoyi.account.pojo.financial.FinVoucher;
import com.ruoyi.account.pojo.financial.FinVoucherEntry;
import com.ruoyi.account.service.financial.FinVoucherService;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
/**
 * å‡­è¯æœåŠ¡å®žçŽ°ã€‚
 */
@Service
@RequiredArgsConstructor
public class FinVoucherServiceImpl extends ServiceImpl<FinVoucherMapper, FinVoucher> implements FinVoucherService {
    private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    private final FinVoucherEntryMapper finVoucherEntryMapper;
    private final AccountSubjectMapper accountSubjectMapper;
    private final FileUtil fileUtil;
    @Override
    public IPage<FinVoucher> pageList(Page<FinVoucher> page, FinVoucherPageDto queryDto) {
        LambdaQueryWrapper<FinVoucher> wrapper = new LambdaQueryWrapper<>();
        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getVoucherNo())) {
            wrapper.like(FinVoucher::getVoucherNo, queryDto.getVoucherNo());
        }
        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getCreator())) {
            wrapper.eq(FinVoucher::getCreator, queryDto.getCreator());
        }
        if (queryDto != null && StringUtils.isNotEmpty(queryDto.getStatus())) {
            wrapper.eq(FinVoucher::getStatus, queryDto.getStatus());
        }
        if (queryDto != null && queryDto.getStartDate() != null) {
            wrapper.ge(FinVoucher::getVoucherDate, queryDto.getStartDate());
        }
        if (queryDto != null && queryDto.getEndDate() != null) {
            wrapper.le(FinVoucher::getVoucherDate, queryDto.getEndDate());
        }
        wrapper.orderByDesc(FinVoucher::getVoucherDate).orderByDesc(FinVoucher::getId);
        return page(page, wrapper);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean addVoucher(FinVoucherDto dto) {
        validateVoucherBasicInfo(dto, false);
        List<FinVoucherEntry> validEntries = buildAndValidateEntries(dto);
        FinVoucher voucher = new FinVoucher();
        BeanUtils.copyProperties(dto, voucher);
        voucher.setStatus("unposted");
        voucher.setAttachmentCount(voucher.getAttachmentCount() == null ? 0 : voucher.getAttachmentCount());
        BigDecimal totalDebit = calculateTotalDebit(validEntries);
        BigDecimal totalCredit = calculateTotalCredit(validEntries);
        voucher.setDebit(totalDebit);
        voucher.setCredit(totalCredit);
        if (StringUtils.isEmpty(voucher.getSummary())) {
            voucher.setSummary(findDefaultSummary(validEntries));
        }
        save(voucher);
        saveEntries(voucher.getId(), validEntries);
        // 5. ä¿å­˜é”€å”®å°è´¦é™„ä»¶
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_VOUCHER, voucher.getId(), dto.getStorageBlobDTOs());
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean updateVoucher(FinVoucherDto dto) {
        validateVoucherBasicInfo(dto, true);
        FinVoucher existed = getById(dto.getId());
        if (existed == null) {
            throw new ServiceException("修改失败,凭证不存在");
        }
        if (!"unposted".equals(existed.getStatus())) {
            throw new ServiceException("仅未过账凭证允许修改");
        }
        List<FinVoucherEntry> validEntries = buildAndValidateEntries(dto);
        FinVoucher voucher = new FinVoucher();
        BeanUtils.copyProperties(dto, voucher);
        voucher.setStatus(existed.getStatus());
        voucher.setAttachmentCount(voucher.getAttachmentCount() == null ? 0 : voucher.getAttachmentCount());
        BigDecimal totalDebit = calculateTotalDebit(validEntries);
        BigDecimal totalCredit = calculateTotalCredit(validEntries);
        voucher.setDebit(totalDebit);
        voucher.setCredit(totalCredit);
        if (StringUtils.isEmpty(voucher.getSummary())) {
            voucher.setSummary(findDefaultSummary(validEntries));
        }
        updateById(voucher);
        LambdaQueryWrapper<FinVoucherEntry> deleteWrapper = new LambdaQueryWrapper<>();
        deleteWrapper.eq(FinVoucherEntry::getVoucherId, voucher.getId());
        finVoucherEntryMapper.delete(deleteWrapper);
        saveEntries(voucher.getId(), validEntries);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_VOUCHER, voucher.getId(), dto.getStorageBlobDTOs());
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean postVoucher(Long id) {
        FinVoucher voucher = getById(id);
        if (voucher == null) {
            throw new ServiceException("过账失败,凭证不存在");
        }
        if (!"unposted".equals(voucher.getStatus())) {
            throw new ServiceException("仅未过账凭证允许过账");
        }
        voucher.setStatus("posted");
        return updateById(voucher);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean cancelVoucher(Long id) {
        FinVoucher voucher = getById(id);
        if (voucher == null) {
            throw new ServiceException("作废失败,凭证不存在");
        }
        if (!"unposted".equals(voucher.getStatus())) {
            throw new ServiceException("仅未过账凭证允许作废");
        }
        voucher.setStatus("cancelled");
        return updateById(voucher);
    }
    @Override
    public FinVoucherDetailVo detail(Long id) {
        FinVoucher voucher = getById(id);
        if (voucher == null) {
            throw new ServiceException("查询失败,凭证不存在");
        }
        LambdaQueryWrapper<FinVoucherEntry> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(FinVoucherEntry::getVoucherId, id)
                .orderByAsc(FinVoucherEntry::getRowNo)
                .orderByAsc(FinVoucherEntry::getId);
        List<FinVoucherEntry> entries = finVoucherEntryMapper.selectList(wrapper);
        FinVoucherDetailVo vo = new FinVoucherDetailVo();
        BeanUtils.copyProperties(voucher, vo);
        vo.setEntries(entries);
        vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.FIN_VOUCHER, id));
        return vo;
    }
    /**
     * æ ¡éªŒå‡­è¯ä¸»è¡¨å­—段、状态字段与唯一性。
     */
    private void validateVoucherBasicInfo(FinVoucherDto dto, boolean isUpdate) {
        if (dto == null) {
            throw new ServiceException("凭证数据不能为空");
        }
        if (isUpdate && dto.getId() == null) {
            throw new ServiceException("修改失败,凭证ID不能为空");
        }
        if (StringUtils.isEmpty(dto.getVoucherNo())) {
            throw new ServiceException("凭证字号不能为空");
        }
        if (dto.getVoucherDate() == null) {
            throw new ServiceException("凭证日期不能为空");
        }
        LambdaQueryWrapper<FinVoucher> uniqueWrapper = new LambdaQueryWrapper<>();
        uniqueWrapper.eq(FinVoucher::getVoucherNo, dto.getVoucherNo());
        if (isUpdate) {
            uniqueWrapper.ne(FinVoucher::getId, dto.getId());
        }
        if (count(uniqueWrapper) > 0) {
            throw new ServiceException("凭证字号已存在,请勿重复提交");
        }
    }
    /**
     * è¿‡æ»¤æœ‰æ•ˆåˆ†å½•并执行借贷平衡校验。
     */
    private List<FinVoucherEntry> buildAndValidateEntries(FinVoucherDto dto) {
        List<FinVoucherEntryDto> rawEntries = dto.getEntries();
        if (rawEntries == null || rawEntries.isEmpty()) {
            throw new ServiceException("分录不能为空,至少需要一条有效分录");
        }
        List<FinVoucherEntry> validEntries = new ArrayList<>();
        int rowNo = 1;
        for (FinVoucherEntryDto entryDto : rawEntries) {
            if (entryDto == null || StringUtils.isEmpty(entryDto.getSubjectCode())) {
                continue;
            }
            BigDecimal debit = defaultMoney(entryDto.getDebit());
            BigDecimal credit = defaultMoney(entryDto.getCredit());
            if (debit.compareTo(BigDecimal.ZERO) <= 0 && credit.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            if (debit.compareTo(BigDecimal.ZERO) > 0 && credit.compareTo(BigDecimal.ZERO) > 0) {
                throw new ServiceException("分录借方和贷方不能同时大于0");
            }
            FinVoucherEntry entry = new FinVoucherEntry();
            BeanUtils.copyProperties(entryDto, entry);
            entry.setDebit(debit);
            entry.setCredit(credit);
            entry.setRowNo(rowNo++);
            validEntries.add(entry);
        }
        if (validEntries.isEmpty()) {
            throw new ServiceException("分录至少需要一条有效行(科目不空,且借方或贷方大于0)");
        }
        // åˆ†å½•科目必须存在,避免脏科目编码入账。
        Set<String> subjectCodes = validEntries.stream()
                .map(FinVoucherEntry::getSubjectCode)
                .filter(StringUtils::isNotEmpty)
                .collect(Collectors.toSet());
        if (subjectCodes.isEmpty()) {
            throw new ServiceException("分录科目不能为空");
        }
        LambdaQueryWrapper<AccountSubject> subjectWrapper = new LambdaQueryWrapper<>();
        subjectWrapper.in(AccountSubject::getSubjectCode, subjectCodes);
        List<AccountSubject> subjects = accountSubjectMapper.selectList(subjectWrapper);
        Map<String, AccountSubject> subjectMap = subjects.stream()
                .collect(Collectors.toMap(AccountSubject::getSubjectCode, it -> it, (a, b) -> a));
        for (FinVoucherEntry entry : validEntries) {
            AccountSubject accountSubject = subjectMap.get(entry.getSubjectCode());
            if (accountSubject == null) {
                throw new ServiceException("科目编码不存在:" + entry.getSubjectCode());
            }
            if (StringUtils.isEmpty(entry.getSubjectName())) {
                entry.setSubjectName(accountSubject.getSubjectName());
            }
        }
        BigDecimal totalDebit = calculateTotalDebit(validEntries);
        BigDecimal totalCredit = calculateTotalCredit(validEntries);
        if (totalDebit.compareTo(BigDecimal.ZERO) <= 0 || totalCredit.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("借贷金额必须大于0");
        }
        if (totalDebit.compareTo(totalCredit) != 0) {
            throw new ServiceException("借贷不平衡,禁止保存");
        }
        return validEntries;
    }
    private void saveEntries(Long voucherId, List<FinVoucherEntry> entries) {
        if (voucherId == null) {
            throw new ServiceException("凭证ID不能为空");
        }
        for (FinVoucherEntry entry : entries) {
            entry.setVoucherId(voucherId);
            finVoucherEntryMapper.insert(entry);
        }
    }
    private String findDefaultSummary(List<FinVoucherEntry> entries) {
        for (FinVoucherEntry entry : entries) {
            if (StringUtils.isNotEmpty(entry.getSummary())) {
                return entry.getSummary();
            }
        }
        return "";
    }
    private BigDecimal calculateTotalDebit(List<FinVoucherEntry> entries) {
        BigDecimal total = BigDecimal.ZERO;
        for (FinVoucherEntry entry : entries) {
            total = total.add(defaultMoney(entry.getDebit()));
        }
        return total.setScale(2, RoundingMode.HALF_UP);
    }
    private BigDecimal calculateTotalCredit(List<FinVoucherEntry> entries) {
        BigDecimal total = BigDecimal.ZERO;
        for (FinVoucherEntry entry : entries) {
            total = total.add(defaultMoney(entry.getCredit()));
        }
        return total.setScale(2, RoundingMode.HALF_UP);
    }
    private BigDecimal defaultMoney(BigDecimal value) {
        if (value == null) {
            return ZERO;
        }
        return value.setScale(2, RoundingMode.HALF_UP);
    }
}
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.ai.assistant;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
@AiService(
        wiringMode = EXPLICIT,
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "chatMemoryProviderManufacturing",
        tools = "manufacturingAgentTools"
)
public interface ManufacturingAgent {
    @SystemMessage(fromResource = "manufacturing-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
}
src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,136 @@
package com.ruoyi.ai.assistant;
import com.ruoyi.ai.tools.ManufacturingAgentTools;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class ManufacturingIntentExecutor {
    private static final Pattern LIMIT_PATTERN = Pattern.compile("(前|最近)?(\\d{1,2})条");
    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
    private final ManufacturingAgentTools manufacturingAgentTools;
    public ManufacturingIntentExecutor(ManufacturingAgentTools manufacturingAgentTools) {
        this.manufacturingAgentTools = manufacturingAgentTools;
    }
    public String tryExecute(String memoryId, String message) {
        if (!StringUtils.hasText(message)) {
            return null;
        }
        String text = message.trim();
        String keyword = extractKeyword(text);
        Integer limit = extractLimit(text);
        String startDate = extractStartDate(text);
        String endDate = extractEndDate(text);
        if (containsAny(text, "预警", "告警", "风险", "提醒")) {
            return manufacturingAgentTools.getWarningBoard(memoryId, startDate, endDate, text);
        }
        if (containsAny(text, "分析", "统计", "趋势", "看板", "报表", "总览")) {
            return manufacturingAgentTools.analyzeFactory(memoryId, startDate, endDate, text);
        }
        if (containsAny(text, "办", "处理", "派工", "安排", "闭环", "跟进", "处置")) {
            return manufacturingAgentTools.planActions(memoryId, text);
        }
        if (containsAny(text, "生产现场", "现场", "车间")) {
            return manufacturingAgentTools.queryDomain(memoryId, "site", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "计划", "排产", "mps")) {
            return manufacturingAgentTools.queryDomain(memoryId, "plan", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "工单", "作业单", "任务单", "任务")) {
            return manufacturingAgentTools.queryDomain(memoryId, "workorder", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "设备", "ç»´ä¿®", "保养", "故障")) {
            return manufacturingAgentTools.queryDomain(memoryId, "device", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "质量", "质检", "不合格", "检验")) {
            return manufacturingAgentTools.queryDomain(memoryId, "quality", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "物料", "库存", "库位", "入库", "出库")) {
            return manufacturingAgentTools.queryDomain(memoryId, "material", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "异常", "例外", "偏差")) {
            return manufacturingAgentTools.queryDomain(memoryId, "exception", keyword, limit, startDate, endDate, text);
        }
        return null;
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.toLowerCase().contains(keyword.toLowerCase())) {
                return true;
            }
        }
        return false;
    }
    private Integer extractLimit(String text) {
        Matcher matcher = LIMIT_PATTERN.matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(2)) : 10;
    }
    private String extractStartDate(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        return matcher.find() ? matcher.group(1) : null;
    }
    private String extractEndDate(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        if (!matcher.find()) {
            return null;
        }
        return matcher.find() ? matcher.group(1) : null;
    }
    private String extractKeyword(String text) {
        String cleaned = text
                .replace("查询", "")
                .replace("查看", "")
                .replace("帮我", "")
                .replace("请", "")
                .replace("一下", "")
                .replace("所有", "")
                .replace("全部", "")
                .replace("今年", "")
                .replace("本年", "")
                .replace("去年", "")
                .replace("本月", "")
                .replace("上月", "")
                .replace("本周", "")
                .replace("上周", "")
                .replace("今天", "")
                .replace("昨天", "")
                .replace("近30天", "")
                .replace("近7天", "")
                .replace("近15天", "")
                .replace("近60天", "")
                .replace("最近30天", "")
                .replace("最近7天", "")
                .replace("最近15天", "")
                .replace("最近60天", "")
                .replace("生产现场", "")
                .replace("现场", "")
                .replace("生产工单", "")
                .replace("生产", "")
                .replace("计划", "")
                .replace("排产", "")
                .replace("工单", "")
                .replace("设备", "")
                .replace("质量", "")
                .replace("物料", "")
                .replace("库存", "")
                .replace("异常", "")
                .replace("前10条", "")
                .replace("最近10条", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
}
src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.ai.config;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ManufacturingAgentConfig {
    @Bean
    ChatMemoryProvider chatMemoryProviderManufacturing(MongoChatMemoryStore mongoChatMemoryStore) {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(30)
                .chatMemoryStore(mongoChatMemoryStore)
                .build();
    }
}
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package com.ruoyi.ai.controller;
import com.ruoyi.ai.assistant.ManufacturingAgent;
import com.ruoyi.ai.assistant.ManufacturingIntentExecutor;
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.util.List;
@Tag(name = "制造智能助手")
@RestController
@RequestMapping("/manufacturing-ai")
public class ManufacturingAiController extends BaseController {
    private final ManufacturingAgent manufacturingAgent;
    private final ManufacturingIntentExecutor manufacturingIntentExecutor;
    private final AiSessionUserContext aiSessionUserContext;
    private final MongoChatMemoryStore mongoChatMemoryStore;
    private final AiChatSessionService aiChatSessionService;
    public ManufacturingAiController(ManufacturingAgent manufacturingAgent,
                                     ManufacturingIntentExecutor manufacturingIntentExecutor,
                                     AiSessionUserContext aiSessionUserContext,
                                     MongoChatMemoryStore mongoChatMemoryStore,
                                     AiChatSessionService aiChatSessionService) {
        this.manufacturingAgent = manufacturingAgent;
        this.manufacturingIntentExecutor = manufacturingIntentExecutor;
        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 = manufacturingIntentExecutor.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);
        }
        return manufacturingAgent.chat(memoryId, userMessage)
                .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()));
    }
}
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1035 @@
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.ai.context.AiSessionUserContext;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.mapper.DeviceDefectRecordMapper;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.device.pojo.DeviceDefectRecord;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.pojo.DeviceRepair;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.procurementrecord.mapper.ProcurementExceptionRecordMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementExceptionRecord;
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.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionPlan;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityUnqualifiedMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityUnqualified;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInventory;
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.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Component
public class ManufacturingAgentTools {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final int DEFAULT_LIMIT = 10;
    private static final int MAX_LIMIT = 30;
    private static final int DEVICE_REPAIR_STATUS_PENDING = 0;
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final DeviceRepairMapper deviceRepairMapper;
    private final DeviceDefectRecordMapper deviceDefectRecordMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final QualityUnqualifiedMapper qualityUnqualifiedMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final ProcurementExceptionRecordMapper procurementExceptionRecordMapper;
    private final AiSessionUserContext aiSessionUserContext;
    public ManufacturingAgentTools(ProductionPlanMapper productionPlanMapper,
                                   ProductionOrderMapper productionOrderMapper,
                                   ProductionOperationTaskMapper productionOperationTaskMapper,
                                   ProductionProductMainMapper productionProductMainMapper,
                                   DeviceLedgerMapper deviceLedgerMapper,
                                   DeviceRepairMapper deviceRepairMapper,
                                   DeviceDefectRecordMapper deviceDefectRecordMapper,
                                   QualityInspectMapper qualityInspectMapper,
                                   QualityUnqualifiedMapper qualityUnqualifiedMapper,
                                   StockInventoryMapper stockInventoryMapper,
                                   ProcurementExceptionRecordMapper procurementExceptionRecordMapper,
                                   AiSessionUserContext aiSessionUserContext) {
        this.productionPlanMapper = productionPlanMapper;
        this.productionOrderMapper = productionOrderMapper;
        this.productionOperationTaskMapper = productionOperationTaskMapper;
        this.productionProductMainMapper = productionProductMainMapper;
        this.deviceLedgerMapper = deviceLedgerMapper;
        this.deviceRepairMapper = deviceRepairMapper;
        this.deviceDefectRecordMapper = deviceDefectRecordMapper;
        this.qualityInspectMapper = qualityInspectMapper;
        this.qualityUnqualifiedMapper = qualityUnqualifiedMapper;
        this.stockInventoryMapper = stockInventoryMapper;
        this.procurementExceptionRecordMapper = procurementExceptionRecordMapper;
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Tool(name = "查询制造业务域数据", value = "按业务域查询生产现场、计划、工单、设备、质量、物料、异常处理相关数据。")
    public String queryDomain(@ToolMemoryId String memoryId,
                              @P(value = "业务域,site/plan/workorder/device/quality/material/exception") String domain,
                              @P(value = "关键字,可不传", required = false) String keyword,
                              @P(value = "返回条数,默认10,最大30", required = false) Integer limit,
                              @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);
        int finalLimit = normalizeLimit(limit);
        DateRange range = resolveDateRange(startDate, endDate, timeRange);
        boolean hasTimeConstraint = hasTimeConstraint(startDate, endDate, timeRange);
        String normalizedDomain = normalizeDomain(domain);
        return switch (normalizedDomain) {
            case "site" -> siteSnapshot(loginUser, range);
            case "plan" -> listProductionPlans(loginUser, keyword, finalLimit, range);
            case "workorder" -> listWorkOrders(loginUser, keyword, finalLimit, range);
            case "device" -> isRepairIntent(keyword, timeRange)
                    ? listDeviceRepairs(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit, range, hasTimeConstraint)
                    : listDevices(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit);
            case "repair" -> listDeviceRepairs(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit, range, hasTimeConstraint);
            case "quality" -> listQualityIssues(loginUser, keyword, finalLimit, range);
            case "material" -> listMaterialInventory(loginUser, keyword, finalLimit);
            case "exception" -> listExceptions(loginUser, keyword, finalLimit, range);
            default -> jsonResponse(false, "manufacturing_query", "不支持的业务域: " + safe(domain), Map.of(), Map.of(), Map.of());
        };
    }
    @Tool(name = "制造预警看板", value = "计算计划、工单、设备、质量、物料、异常处理的预警信息。")
    public String getWarningBoard(@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);
        LocalDate today = LocalDate.now();
        long overduePlanCount = countOverduePlans(loginUser, today);
        long overdueWorkOrderCount = countOverdueWorkOrders(loginUser, today);
        long pendingRepairCount = countPendingRepairs(loginUser);
        long qualityOpenCount = countOpenQualityIssues(loginUser, range);
        long lowStockCount = countLowStock(loginUser);
        long exceptionCount = countExceptionRecords(loginUser, range);
        List<Map<String, Object>> warningItems = new ArrayList<>();
        if (overduePlanCount > 0) {
            warningItems.add(warningItem("high", "计划逾期", overduePlanCount, "有生产计划超过需求日期仍未完成"));
        }
        if (overdueWorkOrderCount > 0) {
            warningItems.add(warningItem("high", "工单逾期", overdueWorkOrderCount, "有工单计划结束日期已过仍未完工"));
        }
        if (pendingRepairCount > 0) {
            warningItems.add(warningItem("medium", "设备待维修", pendingRepairCount, "存在待维修/维修中的设备"));
        }
        if (qualityOpenCount > 0) {
            warningItems.add(warningItem("high", "质量未闭环", qualityOpenCount, "存在未处理完成的不合格记录"));
        }
        if (lowStockCount > 0) {
            warningItems.add(warningItem("medium", "物料低库存", lowStockCount, "库存数量低于或等于预警阈值"));
        }
        if (exceptionCount > 0) {
            warningItems.add(warningItem("medium", "异常记录", exceptionCount, "时间范围内存在异常处理记录"));
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("warningCount", warningItems.size());
        summary.put("overduePlanCount", overduePlanCount);
        summary.put("overdueWorkOrderCount", overdueWorkOrderCount);
        summary.put("pendingRepairCount", pendingRepairCount);
        summary.put("qualityOpenCount", qualityOpenCount);
        summary.put("lowStockCount", lowStockCount);
        summary.put("exceptionCount", exceptionCount);
        return jsonResponse(true, "manufacturing_warning", "已返回制造预警看板。", summary,
                Map.of("items", warningItems), Map.of());
    }
    @Tool(name = "制造经营分析", value = "按时间范围输出制造关键指标,支持查、问、分析场景。")
    public String analyzeFactory(@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);
        long planTotal = countPlans(loginUser, range);
        long planCompleted = countPlansByStatus(loginUser, range, 2);
        long workOrderTotal = countWorkOrders(loginUser, range);
        long workOrderCompleted = countWorkOrdersByStatus(loginUser, range, 2);
        long workOrderInProgress = countWorkOrdersByStatus(loginUser, range, 1);
        long outputCount = countOutputs(loginUser, range);
        long deviceTotal = countDevices(loginUser);
        long pendingRepairCount = countPendingRepairs(loginUser);
        long qualityInspectTotal = countQualityInspect(loginUser, range);
        long qualityNgCount = countOpenQualityIssues(loginUser, range);
        long materialSkuCount = countInventorySku(loginUser);
        long lowStockCount = countLowStock(loginUser);
        long exceptionCount = countExceptionRecords(loginUser, range);
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("planTotal", planTotal);
        summary.put("planCompleted", planCompleted);
        summary.put("planCompletionRate", toRate(planCompleted, planTotal));
        summary.put("workOrderTotal", workOrderTotal);
        summary.put("workOrderCompleted", workOrderCompleted);
        summary.put("workOrderInProgress", workOrderInProgress);
        summary.put("workOrderCompletionRate", toRate(workOrderCompleted, workOrderTotal));
        summary.put("outputCount", outputCount);
        summary.put("deviceTotal", deviceTotal);
        summary.put("pendingRepairCount", pendingRepairCount);
        summary.put("qualityInspectTotal", qualityInspectTotal);
        summary.put("qualityNgCount", qualityNgCount);
        summary.put("qualityIssueRate", toRate(qualityNgCount, qualityInspectTotal));
        summary.put("materialSkuCount", materialSkuCount);
        summary.put("lowStockCount", lowStockCount);
        summary.put("exceptionCount", exceptionCount);
        List<Map<String, Object>> coreMetrics = List.of(
                metric("计划完成率", toRate(planCompleted, planTotal)),
                metric("工单完成率", toRate(workOrderCompleted, workOrderTotal)),
                metric("质量异常率", toRate(qualityNgCount, qualityInspectTotal)),
                metric("低库存占比", toRate(lowStockCount, materialSkuCount))
        );
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("domainBarOption", buildDomainBarOption(summary));
        charts.put("qualityPieOption", buildQualityPieOption(qualityInspectTotal, qualityNgCount));
        return jsonResponse(true, "manufacturing_analysis", "已返回制造分析结果。", summary,
                Map.of("coreMetrics", coreMetrics), charts);
    }
    @Tool(name = "生成制造办理建议", value = "根据用户问题输出可执行的办理动作建议,包括目标业务接口、必填字段和示例。")
    public String planActions(@ToolMemoryId String memoryId,
                              @P("用户诉求原文") String userQuery) {
        LoginUser loginUser = currentLoginUser(memoryId);
        List<Map<String, Object>> actionCards = new ArrayList<>();
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "工单", "派工", "作业")) {
            actionCards.add(actionCard(
                    "workorder_assign",
                    "工单派工",
                    "POST",
                    "/productionOperationTask/assign",
                    List.of("id", "userIds"),
                    Map.of("id", 10001, "userIds", "12,13"),
                    "将工单分配给指定人员,适用于现场调度。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "设备", "ç»´ä¿®", "故障")) {
            actionCards.add(actionCard(
                    "device_repair_create",
                    "创建设备维修单",
                    "POST",
                    "/device/repair",
                    List.of("deviceLedgerId", "deviceName", "repairName", "remark"),
                    Map.of("deviceLedgerId", 1001, "deviceName", "空压机A-01", "repairName", "张三", "remark", "异响并伴随温升"),
                    "新建维修单,进入设备异常处理闭环。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "质量", "不合格", "闭环")) {
            actionCards.add(actionCard(
                    "quality_unqualified_deal",
                    "处理不合格单",
                    "POST",
                    "/quality/qualityUnqualified/deal",
                    List.of("id", "dealResult", "dealName"),
                    Map.of("id", 3001, "dealResult", "返工后复检", "dealName", "李四"),
                    "对不合格记录执行处置并闭环。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "物料", "库存", "补料")) {
            actionCards.add(actionCard(
                    "material_inbound",
                    "补充库存",
                    "POST",
                    "/stockInventory/addstockInventory",
                    List.of("productModelId", "batchNo", "qualitity"),
                    Map.of("productModelId", 5001, "batchNo", "B2026051601", "qualitity", 120),
                    "当低库存预警触发时,增加库存数量。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "异常", "采购异常", "来料异常")) {
            actionCards.add(actionCard(
                    "procurement_exception_add",
                    "登记异常记录",
                    "POST",
                    "/procurementExceptionRecord/add",
                    List.of("purchaseLedgerId", "exceptionReason", "exceptionNum"),
                    Map.of("purchaseLedgerId", 888, "exceptionReason", "到料短缺", "exceptionNum", 24),
                    "登记采购/来料异常,便于后续追踪和分析。"));
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("actionCount", actionCards.size());
        summary.put("userId", loginUser.getUserId());
        summary.put("tenantId", loginUser.getTenantId());
        return jsonResponse(true, "manufacturing_action_plan", "已生成办理建议,请前端引导用户确认后调用目标业务接口。",
                summary, Map.of("actionCards", actionCards), Map.of());
    }
    private String siteSnapshot(LoginUser loginUser, DateRange range) {
        long planTotal = countPlans(loginUser, range);
        long workOrderTotal = countWorkOrders(loginUser, range);
        long outputCount = countOutputs(loginUser, range);
        long deviceTotal = countDevices(loginUser);
        long pendingRepairCount = countPendingRepairs(loginUser);
        long qualityOpenCount = countOpenQualityIssues(loginUser, range);
        long lowStockCount = countLowStock(loginUser);
        long exceptionCount = countExceptionRecords(loginUser, range);
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("planTotal", planTotal);
        summary.put("workOrderTotal", workOrderTotal);
        summary.put("outputCount", outputCount);
        summary.put("deviceTotal", deviceTotal);
        summary.put("pendingRepairCount", pendingRepairCount);
        summary.put("qualityOpenCount", qualityOpenCount);
        summary.put("lowStockCount", lowStockCount);
        summary.put("exceptionCount", exceptionCount);
        return jsonResponse(true, "manufacturing_site_snapshot", "已返回生产现场概览。", summary, Map.of(), Map.of());
    }
    private String listProductionPlans(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.ge(ProductionPlan::getRequiredDate, range.start()).le(ProductionPlan::getRequiredDate, range.end());
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(ProductionPlan::getMpsNo, keyword)
                    .or().like(ProductionPlan::getRemark, keyword)
                    .or().like(ProductionPlan::getSource, keyword));
        }
        wrapper.orderByDesc(ProductionPlan::getRequiredDate, ProductionPlan::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(productionPlanMapper.selectList(wrapper)).stream()
                .map(this::toPlanItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_plan_list", "已返回生产计划列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listWorkOrders(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
                .le(ProductionOperationTask::getPlanEndTime, range.end());
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(ProductionOperationTask::getWorkOrderNo, keyword)
                    .or().like(ProductionOperationTask::getUserIds, keyword));
        }
        wrapper.orderByDesc(ProductionOperationTask::getPlanEndTime, ProductionOperationTask::getId)
                .last("limit " + limit);
        List<Map<String, Object>> items = defaultList(productionOperationTaskMapper.selectList(wrapper)).stream()
                .map(this::toWorkOrderItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_workorder_list", "已返回工单列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listDevices(LoginUser loginUser, String keyword, int limit) {
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(DeviceLedger::getDeviceName, keyword)
                    .or().like(DeviceLedger::getDeviceModel, keyword)
                    .or().like(DeviceLedger::getDeviceBrand, keyword));
        }
        wrapper.orderByDesc(DeviceLedger::getId).last("limit " + limit);
        Map<Long, Long> pendingRepairMap = pendingRepairCountByDevice(loginUser);
        List<Map<String, Object>> items = defaultList(deviceLedgerMapper.selectList(wrapper)).stream()
                .map(item -> toDeviceItem(item, pendingRepairMap.getOrDefault(item.getId(), 0L)))
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_device_list", "已返回设备列表。",
                Map.of("count", items.size(), "keyword", safe(keyword)), Map.of("items", items), Map.of());
    }
    private String listDeviceRepairs(LoginUser loginUser, String keyword, int limit, DateRange range, boolean hasTimeConstraint) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        Long currentDeptId = loginUser.getCurrentDeptId();
        if (currentDeptId != null) {
            wrapper.and(w -> w.eq(DeviceRepair::getDeptId, currentDeptId).or().isNull(DeviceRepair::getDeptId));
        }
        if (hasTimeConstraint) {
            wrapper.ge(DeviceRepair::getCreateTime, range.start().atStartOfDay())
                    .lt(DeviceRepair::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        if (StringUtils.hasText(keyword)) {
            List<Long> matchedDeviceIds = findDeviceLedgerIdsByKeyword(loginUser, keyword);
            wrapper.and(w -> {
                w.like(DeviceRepair::getDeviceName, keyword)
                        .or().like(DeviceRepair::getDeviceModel, keyword)
                        .or().like(DeviceRepair::getRemark, keyword)
                        .or().like(DeviceRepair::getRepairName, keyword)
                        .or().like(DeviceRepair::getMaintenanceName, keyword);
                if (!matchedDeviceIds.isEmpty()) {
                    w.or().in(DeviceRepair::getDeviceLedgerId, matchedDeviceIds);
                }
            });
        }
        wrapper.orderByDesc(DeviceRepair::getCreateTime, DeviceRepair::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(deviceRepairMapper.selectList(wrapper)).stream()
                .map(this::toDeviceRepairItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_device_repair_list", "已返回设备维修记录。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listQualityIssues(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<QualityUnqualified> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityUnqualified::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityUnqualified::getDeptId);
        wrapper.ge(QualityUnqualified::getCheckTime, toDate(range.start()))
                .lt(QualityUnqualified::getCheckTime, toExclusiveEndDate(range.end()));
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(QualityUnqualified::getProductName, keyword)
                    .or().like(QualityUnqualified::getDefectivePhenomena, keyword)
                    .or().like(QualityUnqualified::getDealResult, keyword));
        }
        wrapper.orderByDesc(QualityUnqualified::getCheckTime, QualityUnqualified::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(qualityUnqualifiedMapper.selectList(wrapper)).stream()
                .map(this::toQualityItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_quality_list", "已返回质量异常列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listMaterialInventory(LoginUser loginUser, String keyword, int limit) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(StockInventory::getBatchNo, keyword)
                    .or().like(StockInventory::getProductModelId, keyword));
        }
        wrapper.orderByDesc(StockInventory::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(stockInventoryMapper.selectList(wrapper)).stream()
                .map(this::toMaterialItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_material_list", "已返回物料库存列表。",
                Map.of("count", items.size(), "keyword", safe(keyword)), Map.of("items", items), Map.of());
    }
    private String listExceptions(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<ProcurementExceptionRecord> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementExceptionRecord::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementExceptionRecord::getDeptId);
        wrapper.ge(ProcurementExceptionRecord::getCreateTime, range.start().atStartOfDay())
                .lt(ProcurementExceptionRecord::getCreateTime, range.end().plusDays(1).atStartOfDay());
        if (StringUtils.hasText(keyword)) {
            wrapper.like(ProcurementExceptionRecord::getExceptionReason, keyword);
        }
        wrapper.orderByDesc(ProcurementExceptionRecord::getCreateTime, ProcurementExceptionRecord::getId)
                .last("limit " + limit);
        List<Map<String, Object>> items = defaultList(procurementExceptionRecordMapper.selectList(wrapper)).stream()
                .map(this::toExceptionItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_exception_list", "已返回异常处理列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private long countPlans(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.ge(ProductionPlan::getRequiredDate, range.start()).le(ProductionPlan::getRequiredDate, range.end());
        return productionPlanMapper.selectCount(wrapper);
    }
    private long countPlansByStatus(LoginUser loginUser, DateRange range, int status) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.ge(ProductionPlan::getRequiredDate, range.start())
                .le(ProductionPlan::getRequiredDate, range.end())
                .eq(ProductionPlan::getStatus, status);
        return productionPlanMapper.selectCount(wrapper);
    }
    private long countWorkOrders(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
                .le(ProductionOperationTask::getPlanEndTime, range.end());
        return productionOperationTaskMapper.selectCount(wrapper);
    }
    private long countWorkOrdersByStatus(LoginUser loginUser, DateRange range, int status) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
                .le(ProductionOperationTask::getPlanEndTime, range.end())
                .eq(ProductionOperationTask::getStatus, status);
        return productionOperationTaskMapper.selectCount(wrapper);
    }
    private long countOutputs(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProductionProductMain> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionProductMain::getDeptId);
        wrapper.ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay())
                .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay());
        return productionProductMainMapper.selectCount(wrapper);
    }
    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 countPendingRepairs(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
        wrapper.eq(DeviceRepair::getStatus, DEVICE_REPAIR_STATUS_PENDING);
        return deviceRepairMapper.selectCount(wrapper);
    }
    private long countQualityInspect(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<QualityInspect> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityInspect::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityInspect::getDeptId);
        wrapper.ge(QualityInspect::getCheckTime, toDate(range.start()))
                .lt(QualityInspect::getCheckTime, toExclusiveEndDate(range.end()));
        return qualityInspectMapper.selectCount(wrapper);
    }
    private long countOpenQualityIssues(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<QualityUnqualified> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityUnqualified::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityUnqualified::getDeptId);
        wrapper.ge(QualityUnqualified::getCheckTime, toDate(range.start()))
                .lt(QualityUnqualified::getCheckTime, toExclusiveEndDate(range.end()))
                .ne(QualityUnqualified::getInspectState, 2);
        return qualityUnqualifiedMapper.selectCount(wrapper);
    }
    private long countInventorySku(LoginUser loginUser) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        return stockInventoryMapper.selectCount(wrapper);
    }
    private long countLowStock(LoginUser loginUser) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        wrapper.isNotNull(StockInventory::getWarnNum);
        List<StockInventory> stocks = defaultList(stockInventoryMapper.selectList(wrapper));
        return stocks.stream()
                .filter(this::isLowStock)
                .count();
    }
    private long countExceptionRecords(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProcurementExceptionRecord> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementExceptionRecord::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementExceptionRecord::getDeptId);
        wrapper.ge(ProcurementExceptionRecord::getCreateTime, range.start().atStartOfDay())
                .lt(ProcurementExceptionRecord::getCreateTime, range.end().plusDays(1).atStartOfDay());
        return procurementExceptionRecordMapper.selectCount(wrapper);
    }
    private long countOverduePlans(LoginUser loginUser, LocalDate today) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.lt(ProductionPlan::getRequiredDate, today).ne(ProductionPlan::getStatus, 2);
        return productionPlanMapper.selectCount(wrapper);
    }
    private long countOverdueWorkOrders(LoginUser loginUser, LocalDate today) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.lt(ProductionOperationTask::getPlanEndTime, today).ne(ProductionOperationTask::getStatus, 2);
        return productionOperationTaskMapper.selectCount(wrapper);
    }
    private Map<Long, Long> pendingRepairCountByDevice(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
        wrapper.eq(DeviceRepair::getStatus, DEVICE_REPAIR_STATUS_PENDING);
        return defaultList(deviceRepairMapper.selectList(wrapper)).stream()
                .filter(item -> item.getDeviceLedgerId() != null)
                .collect(Collectors.groupingBy(DeviceRepair::getDeviceLedgerId, Collectors.counting()));
    }
    private Map<String, Object> toPlanItem(ProductionPlan item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("mpsNo", safe(item.getMpsNo()));
        map.put("requiredDate", formatDate(item.getRequiredDate()));
        map.put("promisedDeliveryDate", formatDate(item.getPromisedDeliveryDate()));
        map.put("qtyRequired", item.getQtyRequired());
        map.put("quantityIssued", item.getQuantityIssued());
        map.put("status", item.getStatus());
        map.put("source", safe(item.getSource()));
        map.put("remark", safe(item.getRemark()));
        return map;
    }
    private Map<String, Object> toWorkOrderItem(ProductionOperationTask item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("workOrderNo", safe(item.getWorkOrderNo()));
        map.put("productionOrderId", item.getProductionOrderId());
        map.put("planStartTime", formatDate(item.getPlanStartTime()));
        map.put("planEndTime", formatDate(item.getPlanEndTime()));
        map.put("actualStartTime", formatDate(item.getActualStartTime()));
        map.put("actualEndTime", formatDate(item.getActualEndTime()));
        map.put("planQuantity", item.getPlanQuantity());
        map.put("completeQuantity", item.getCompleteQuantity());
        map.put("status", item.getStatus());
        map.put("userIds", safe(item.getUserIds()));
        return map;
    }
    private Map<String, Object> toDeviceItem(DeviceLedger item, long pendingRepairCount) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("deviceName", safe(item.getDeviceName()));
        map.put("deviceModel", safe(item.getDeviceModel()));
        map.put("deviceBrand", safe(item.getDeviceBrand()));
        map.put("status", safe(item.getStatus()));
        map.put("storageLocation", safe(item.getStorageLocation()));
        map.put("supplierName", safe(item.getSupplierName()));
        map.put("pendingRepairCount", pendingRepairCount);
        return map;
    }
    private Map<String, Object> toDeviceRepairItem(DeviceRepair item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("deviceLedgerId", item.getDeviceLedgerId());
        map.put("deviceName", safe(item.getDeviceName()));
        map.put("deviceModel", safe(item.getDeviceModel()));
        map.put("repairTime", formatDate(item.getRepairTime()));
        map.put("repairName", safe(item.getRepairName()));
        map.put("maintenanceName", safe(item.getMaintenanceName()));
        map.put("maintenanceTime", formatDateTime(item.getMaintenanceTime()));
        map.put("maintenanceResult", safe(item.getMaintenanceResult()));
        map.put("acceptanceName", safe(item.getAcceptanceName()));
        map.put("acceptanceTime", formatDateTime(item.getAcceptanceTime()));
        map.put("status", item.getStatus());
        map.put("remark", safe(item.getRemark()));
        map.put("createTime", formatDateTime(item.getCreateTime()));
        return map;
    }
    private Map<String, Object> toQualityItem(QualityUnqualified item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("checkTime", formatDate(item.getCheckTime()));
        map.put("inspectState", item.getInspectState());
        map.put("productId", item.getProductId());
        map.put("productName", safe(item.getProductName()));
        map.put("model", safe(item.getModel()));
        map.put("quantity", item.getQuantity());
        map.put("defectivePhenomena", safe(item.getDefectivePhenomena()));
        map.put("dealResult", safe(item.getDealResult()));
        map.put("dealName", safe(item.getDealName()));
        map.put("dealTime", formatDate(item.getDealTime()));
        return map;
    }
    private Map<String, Object> toMaterialItem(StockInventory item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("productModelId", item.getProductModelId());
        map.put("batchNo", safe(item.getBatchNo()));
        map.put("qualitity", item.getQualitity());
        map.put("lockedQuantity", item.getLockedQuantity());
        map.put("warnNum", item.getWarnNum());
        map.put("lowStock", isLowStock(item));
        map.put("remark", safe(item.getRemark()));
        return map;
    }
    private Map<String, Object> toExceptionItem(ProcurementExceptionRecord item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("purchaseLedgerId", item.getPurchaseLedgerId());
        map.put("exceptionReason", safe(item.getExceptionReason()));
        map.put("exceptionNum", item.getExceptionNum());
        map.put("createTime", formatDateTime(item.getCreateTime()));
        return map;
    }
    private boolean isLowStock(StockInventory item) {
        BigDecimal quantity = item.getQualitity();
        BigDecimal warnNum = item.getWarnNum();
        if (quantity == null || warnNum == null) {
            return false;
        }
        return quantity.compareTo(warnNum) <= 0;
    }
    private Map<String, Object> warningItem(String level, String title, long count, String detail) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("level", level);
        map.put("title", title);
        map.put("count", count);
        map.put("detail", detail);
        return map;
    }
    private Map<String, Object> actionCard(String code,
                                           String name,
                                           String method,
                                           String targetApi,
                                           List<String> requiredFields,
                                           Map<String, Object> examplePayload,
                                           String description) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("code", code);
        map.put("name", name);
        map.put("method", method);
        map.put("targetApi", targetApi);
        map.put("requiredFields", requiredFields);
        map.put("examplePayload", examplePayload);
        map.put("description", description);
        return map;
    }
    private Map<String, Object> metric(String label, String value) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("label", label);
        map.put("value", value);
        return map;
    }
    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", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("count", count);
        summary.put("keyword", safe(keyword));
        return summary;
    }
    private Map<String, Object> buildDomainBarOption(Map<String, Object> summary) {
        List<String> xData = List.of("计划", "工单", "设备", "质量", "物料", "异常");
        List<Number> yData = List.of(
                numberValue(summary.get("planTotal")),
                numberValue(summary.get("workOrderTotal")),
                numberValue(summary.get("deviceTotal")),
                numberValue(summary.get("qualityNgCount")),
                numberValue(summary.get("lowStockCount")),
                numberValue(summary.get("exceptionCount"))
        );
        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> buildQualityPieOption(long inspectTotal, long ngCount) {
        long passCount = Math.max(inspectTotal - ngCount, 0);
        List<Map<String, Object>> data = List.of(
                Map.of("name", "不合格", "value", ngCount),
                Map.of("name", "非不合格", "value", passCount)
        );
        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 int numberValue(Object value) {
        if (value instanceof Number number) {
            return number.intValue();
        }
        return 0;
    }
    private String toRate(long numerator, long denominator) {
        if (denominator <= 0) {
            return "0.00%";
        }
        BigDecimal rate = new BigDecimal(numerator)
                .multiply(new BigDecimal("100"))
                .divide(new BigDecimal(denominator), 2, RoundingMode.HALF_UP);
        return rate.toPlainString() + "%";
    }
    private String normalizeDomain(String domain) {
        if (!StringUtils.hasText(domain)) {
            return "";
        }
        String value = domain.trim().toLowerCase();
        return switch (value) {
            case "生产现场", "site", "factory", "workshop" -> "site";
            case "计划", "plan", "schedule" -> "plan";
            case "工单", "workorder", "work_order", "task" -> "workorder";
            case "设备", "device", "equipment" -> "device";
            case "ç»´ä¿®", "repair", "maintenance" -> "repair";
            case "质量", "quality", "qc" -> "quality";
            case "物料", "material", "inventory", "stock" -> "material";
            case "异常", "exception", "abnormal" -> "exception";
            default -> value;
        };
    }
    private boolean isRepairIntent(String keyword, String userQuery) {
        String query = safe(userQuery);
        return containsAny(safe(keyword), "ç»´ä¿®", "报修", "检修", "维护")
                || containsAny(query, "ç»´ä¿®", "报修", "检修", "维护");
    }
    private String normalizeDeviceQueryKeyword(String keyword, String userQuery) {
        String source = StringUtils.hasText(keyword) ? keyword : userQuery;
        if (!StringUtils.hasText(source)) {
            return null;
        }
        String cleaned = source
                .replace("查询", "")
                .replace("查看", "")
                .replace("帮我", "")
                .replace("请", "")
                .replace("查", "")
                .replace("设备", "")
                .replace("维修记录", "")
                .replace("维修情况", "")
                .replace("报修记录", "")
                .replace("报修情况", "")
                .replace("ç»´ä¿®", "")
                .replace("报修", "")
                .replace("情况", "")
                .replace("记录", "")
                .replace("信息", "")
                .replace("的", "")
                .replace("一下", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
    private List<Long> findDeviceLedgerIdsByKeyword(LoginUser loginUser, String keyword) {
        if (!StringUtils.hasText(keyword)) {
            return List.of();
        }
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        Long currentDeptId = loginUser.getCurrentDeptId();
        if (currentDeptId != null) {
            wrapper.and(w -> w.eq(DeviceLedger::getDeptId, currentDeptId).or().isNull(DeviceLedger::getDeptId));
        }
        wrapper.and(w -> w.like(DeviceLedger::getDeviceName, keyword)
                .or().like(DeviceLedger::getDeviceModel, keyword)
                .or().like(DeviceLedger::getDeviceBrand, keyword));
        wrapper.orderByDesc(DeviceLedger::getId).last("limit 200");
        return defaultList(deviceLedgerMapper.selectList(wrapper)).stream()
                .map(DeviceLedger::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }
    private boolean hasTimeConstraint(String startDate, String endDate, String userQuery) {
        if (StringUtils.hasText(startDate) || StringUtils.hasText(endDate)) {
            return true;
        }
        if (!StringUtils.hasText(userQuery)) {
            return false;
        }
        String text = userQuery.trim();
        return containsAny(text, "今天", "昨天", "本周", "上周", "本月", "上月", "今年", "去年", "近", "最近");
    }
    private DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
        LocalDate today = LocalDate.now();
        LocalDate start = parseLocalDate(startDate);
        LocalDate end = parseLocalDate(endDate);
        if (start != null || end != null) {
            LocalDate s = start != null ? start : end;
            LocalDate e = end != null ? end : start;
            if (s.isAfter(e)) {
                LocalDate temp = s;
                s = e;
                e = temp;
            }
            return new DateRange(s, e, s + "至" + e);
        }
        if (!StringUtils.hasText(timeRange)) {
            return new DateRange(today.minusDays(29), today, "近30天");
        }
        String text = timeRange.trim();
        if (text.contains("今天")) {
            return new DateRange(today, today, "今天");
        }
        if (text.contains("本周")) {
            LocalDate startOfWeek = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            return new DateRange(startOfWeek, today, "本周");
        }
        if (text.contains("本月")) {
            return new DateRange(today.withDayOfMonth(1), today, "本月");
        }
        if (text.contains("本年") || text.contains("今年")) {
            return new DateRange(today.withDayOfYear(1), today, "今年");
        }
        if (text.contains("去年")) {
            LocalDate firstDay = today.minusYears(1).withDayOfYear(1);
            LocalDate lastDay = today.minusYears(1).withMonth(12).withDayOfMonth(31);
            return new DateRange(firstDay, lastDay, "去年");
        }
        if (text.contains("上月")) {
            LocalDate startOfLastMonth = today.minusMonths(1).withDayOfMonth(1);
            return new DateRange(startOfLastMonth, startOfLastMonth.withDayOfMonth(startOfLastMonth.lengthOfMonth()), "上月");
        }
        java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(近|最近)(\\d+)(天|周|个月|月|å¹´)").matcher(text);
        if (matcher.find()) {
            int amount = Integer.parseInt(matcher.group(2));
            String unit = matcher.group(3);
            LocalDate relativeStart = 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(relativeStart, today, "近" + amount + unit);
        }
        return new DateRange(today.minusDays(29), today, "近30天");
    }
    private LocalDate parseLocalDate(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        return LocalDate.parse(text.trim(), DATE_FMT);
    }
    private Date toDate(LocalDate date) {
        return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private Date toExclusiveEndDate(LocalDate date) {
        return Date.from(date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private String formatDate(LocalDate date) {
        return date == null ? "" : DATE_FMT.format(date);
    }
    private String formatDate(Date date) {
        if (date == null) {
            return "";
        }
        return DATE_FMT.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
    }
    private String formatDateTime(LocalDateTime time) {
        if (time == null) {
            return "";
        }
        return time.truncatedTo(ChronoUnit.SECONDS).toString().replace('T', ' ');
    }
    private int normalizeLimit(Integer limit) {
        if (limit == null || limit <= 0) {
            return DEFAULT_LIMIT;
        }
        return Math.min(limit, MAX_LIMIT);
    }
    private boolean containsAny(String text, String... values) {
        for (String value : values) {
            if (text.contains(value)) {
                return true;
            }
        }
        return false;
    }
    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 LoginUser currentLoginUser(String memoryId) {
        LoginUser loginUser = aiSessionUserContext.get(memoryId);
        if (loginUser != null) {
            return loginUser;
        }
        return SecurityUtils.getLoginUser();
    }
    private String safe(Object value) {
        return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ');
    }
    private <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    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) {
    }
}
src/main/java/com/ruoyi/approve/bean/vo/ApproveProcessVO.java
@@ -75,5 +75,5 @@
     */
    private BigDecimal maintenancePrice;
    private List<StorageBlobDTO> storageBlobDTOList;
    private List<StorageBlobDTO> storageBlobDTOS;
}
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java
@@ -216,9 +216,12 @@
            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("审核中");
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
@@ -138,7 +138,7 @@
                .collect(Collectors.joining(","));
        approveNodeService.initApproveNodes(nodeIdStr, no, approveProcessVO.getApproveDeptId());
        // é™„件绑定
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVE_PROCESS, approveProcess.getId(), approveProcessVO.getStorageBlobDTOList());
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVE_PROCESS, approveProcess.getId(), approveProcessVO.getStorageBlobDTOS());
        /*消息通知*/
        Long id = nodeIds.getFirst();
        if (approveProcess.getApproveType() == 8) {
src/main/java/com/ruoyi/basic/dto/ProductModelDto.java
@@ -3,9 +3,11 @@
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.production.bean.dto.ProductStructureDto;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
public class ProductModelDto extends ProductModel {
    private List<ProductStructureDto> productStructureList;
src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java
@@ -204,6 +204,7 @@
    SALES_REFUND_AMOUNT_ORDER("sales_refund_amount_order"),
    SALES_RECEIPT_RETURN("sales_receipt_return"),
    ACCOUNT_EXPENSE("account_expense"),
    FIN_VOUCHER("fin_voucher"),
    ACCOUNT_FILE("account_file");
    private final String type;
src/main/java/com/ruoyi/basic/mapper/ProductModelMapper.java
@@ -5,7 +5,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.vo.ProductModelVo;
import com.ruoyi.procurementrecord.dto.ProcurementPageDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementPageDto;
import org.apache.ibatis.annotations.Param;
import java.util.List;
src/main/java/com/ruoyi/basic/pojo/ProductModel.java
@@ -38,7 +38,6 @@
    private String model;
    @Excel(name = "产品编码")
    @TableField("product_code")
    private String productCode;
    /**
src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java
@@ -18,8 +18,11 @@
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.sales.dto.LossProductModelDto;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.technology.mapper.TechnologyBomMapper;
import com.ruoyi.technology.pojo.TechnologyBom;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -43,10 +46,16 @@
    private final ProductMapper productMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final TechnologyBomMapper technologyBomMapper;
    private ProductModelMapper productModelMapper;
    @Override
    public int addOrEditProductModel(ProductModelDto productModelDto) {
        String model = StringUtils.trim(productModelDto.getModel());
        String productCode = StringUtils.trim(productModelDto.getProductCode());
        productModelDto.setModel(model);
        productModelDto.setProductCode(productCode);
        checkModelAndProductCodeUnique(model, productCode, productModelDto.getId());
        if (productModelDto.getId() == null) {
            ProductModel productModel = new ProductModel();
@@ -54,6 +63,21 @@
            return productModelMapper.insert(productModel);
        } else {
            return productModelMapper.updateById(productModelDto);
        }
    }
    private void checkModelAndProductCodeUnique(String model, String productCode, Long currentId) {
        if (StringUtils.isEmpty(model) || StringUtils.isEmpty(productCode)) {
            return;
        }
        LambdaQueryWrapper<ProductModel> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ProductModel::getModel, model)
                .eq(ProductModel::getProductCode, productCode)
                .ne(currentId != null, ProductModel::getId, currentId)
                .last("limit 1");
        ProductModel duplicateProductModel = productModelMapper.selectOne(queryWrapper);
        if (duplicateProductModel != null) {
            throw new ServiceException("对应的型号" + model + "的产品编码" + productCode + "已经存在");
        }
    }
@@ -66,6 +90,14 @@
            throw new RuntimeException("已经存在该产品的销售台账和采购台账");
        }
        // æ˜¯å¦å­˜åœ¨BOM
        List<TechnologyBom> technologyBoms = technologyBomMapper.selectList(new QueryWrapper<TechnologyBom>()
                .lambda().in(TechnologyBom::getProductModelId, ids));
        if (CollectionUtils.isNotEmpty(technologyBoms)) {
            throw new RuntimeException("已经存在该产品的BOM数据");
        }
        return productModelMapper.deleteBatchIds(Arrays.asList(ids));
    }
src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java
@@ -13,12 +13,10 @@
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.service.IProductService;
import com.ruoyi.basic.vo.ProductModelVo;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.AjaxResult;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.Arrays;
@@ -94,17 +92,18 @@
        if (ObjectUtils.isEmpty(productDto.getParentId())) {
            throw new IllegalArgumentException("请选择父节点");
        }
        String productName = StringUtils.trim(productDto.getProductName());
        if (StringUtils.isEmpty(productName)) {
            throw new IllegalArgumentException("产品名称不能为空");
        }
        productDto.setProductName(productName);
        checkProductNameUnique(productDto.getParentId(), productName, productDto.getId());
        if (productDto.getId() == null) {
            // æ–°å¢žäº§å“é€»è¾‘
            if (productDto.getParentId() == null) {
                // è‹¥æœªæŒ‡å®šçˆ¶èŠ‚ç‚¹ï¼Œé»˜è®¤ä¸ºæ ¹èŠ‚ç‚¹ï¼ˆparentId è®¾ä¸º null)
                productDto.setParentId(null);
            } else {
                // æ£€æŸ¥çˆ¶èŠ‚ç‚¹æ˜¯å¦å­˜åœ¨ï¼ˆå¯é€‰ï¼Œæ ¹æ®ä¸šåŠ¡éœ€æ±‚ï¼‰
                Product parent = productMapper.selectById(productDto.getParentId());
                if (parent == null) {
                    throw new IllegalArgumentException("父节点不存在,无法添加子产品");
                }
            }
            return productMapper.insert(productDto);
        } else {
@@ -118,6 +117,18 @@
        }
    }
    private void checkProductNameUnique(Long parentId, String productName, Long currentId) {
        LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Product::getParentId, parentId)
                .eq(Product::getProductName, productName)
                .ne(currentId != null, Product::getId, currentId)
                .last("limit 1");
        Product duplicateProduct = productMapper.selectOne(queryWrapper);
        if (duplicateProduct != null) {
            throw new IllegalArgumentException("对应的" + productName + "已经存在");
        }
    }
    @Override
    public int delProductByIds(Long[] ids) {
        // 1. åˆ é™¤å­è¡¨ product_model ä¸­å…³è”的数据
src/main/java/com/ruoyi/collaborativeApproval/controller/SealApplicationManagementController.java
@@ -2,6 +2,10 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.pojo.KnowledgeBase;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.collaborativeApproval.dto.SealApplicationManagementDTO;
import com.ruoyi.collaborativeApproval.pojo.SealApplicationManagement;
import com.ruoyi.collaborativeApproval.service.SealApplicationManagementService;
import com.ruoyi.common.utils.SecurityUtils;
@@ -27,6 +31,7 @@
public class SealApplicationManagementController {
    private SealApplicationManagementService sealApplicationManagementService;
    private ISysNoticeService sysNoticeService;
    private FileUtil fileUtil;
    @GetMapping("/getList")
    @Operation(summary = "分页查询")
@@ -36,8 +41,13 @@
    @PostMapping("/add")
    @Operation(summary = "新增")
    public AjaxResult add(@RequestBody SealApplicationManagement sealApplicationManagement){
    public AjaxResult add(@RequestBody SealApplicationManagementDTO sealApplicationManagement){
        sealApplicationManagementService.save(sealApplicationManagement);
        // 5. ä¿å­˜é”€å”®å°è´¦é™„ä»¶
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE,
                RecordTypeEnum.SEAL_APPLICATION_MANAGEMENT,
                sealApplicationManagement.getId(),
                sealApplicationManagement.getStorageBlobDTOs());
        //消息通知
        sysNoticeService.simpleNoticeByUser("用印审批",
                "申请编号:"+sealApplicationManagement.getApplicationNum()+"\t"
@@ -49,7 +59,12 @@
    @PostMapping("/update")
    @Operation(summary = "修改")
    public AjaxResult update(@RequestBody SealApplicationManagement sealApplicationManagement){
    public AjaxResult update(@RequestBody SealApplicationManagementDTO sealApplicationManagement){
        // 5. ä¿å­˜é”€å”®å°è´¦é™„ä»¶
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE,
                RecordTypeEnum.SEAL_APPLICATION_MANAGEMENT,
                sealApplicationManagement.getId(),
                sealApplicationManagement.getStorageBlobDTOs());
        return AjaxResult.success(sealApplicationManagementService.updateById(sealApplicationManagement));
    }
@@ -59,6 +74,9 @@
        if (CollectionUtils.isEmpty(ids)) {
            throw new RuntimeException("请传入要删除的ID");
        }
        fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum.FILE,
                RecordTypeEnum.SEAL_APPLICATION_MANAGEMENT,
                ids);
        return AjaxResult.success(sealApplicationManagementService.removeBatchByIds(ids));
    }
src/main/java/com/ruoyi/collaborativeApproval/dto/SealApplicationManagementDTO.java
@@ -1,7 +1,11 @@
package com.ruoyi.collaborativeApproval.dto;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.collaborativeApproval.pojo.SealApplicationManagement;
import lombok.Data;
import java.util.List;
@Data
public class SealApplicationManagementDTO extends SealApplicationManagement {
@@ -11,4 +15,8 @@
    //审批人
    private String approveUserName;
    private List<StorageBlobDTO> storageBlobDTOs;
    private List<StorageBlobVO> storageBlobVOList;
}
src/main/java/com/ruoyi/collaborativeApproval/service/impl/SealApplicationManagementServiceImpl.java
@@ -3,6 +3,8 @@
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.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.collaborativeApproval.dto.SealApplicationManagementDTO;
import com.ruoyi.collaborativeApproval.mapper.SealApplicationManagementMapper;
import com.ruoyi.collaborativeApproval.pojo.SealApplicationManagement;
@@ -14,9 +16,14 @@
@RequiredArgsConstructor
public class SealApplicationManagementServiceImpl extends ServiceImpl<SealApplicationManagementMapper, SealApplicationManagement> implements SealApplicationManagementService {
    private final SealApplicationManagementMapper sealApplicationManagementMapper;
    private final FileUtil fileUtil;
    @Override
    public IPage<SealApplicationManagementDTO> listPage(Page page, SealApplicationManagement sealApplicationManagement) {
        return sealApplicationManagementMapper.listPage(page, sealApplicationManagement);
        IPage<SealApplicationManagementDTO> sealApplicationManagementDTOIPage = sealApplicationManagementMapper.listPage(page, sealApplicationManagement);
        sealApplicationManagementDTOIPage.getRecords().forEach(item -> {
            item.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.SEAL_APPLICATION_MANAGEMENT, item.getId()));
        });
        return sealApplicationManagementDTOIPage;
    }
}
src/main/java/com/ruoyi/device/controller/DeviceRepairController.java
@@ -46,10 +46,16 @@
        return deviceRepairService.updateDeviceRepair(deviceRepairDto);
    }
    @PostMapping ("repair")
    @PostMapping ("/repair")
    @Operation(summary = "设备维修")
    public AjaxResult repair( @RequestBody DeviceRepairDto deviceRepairDto) {
        return deviceRepairService.updateDeviceRepair(deviceRepairDto);
        return deviceRepairService.confirmRepair(deviceRepairDto);
    }
    @PostMapping ("/acceptance")
    @Operation(summary = "设备报修验收审批")
    public AjaxResult acceptance(@RequestBody DeviceRepairDto deviceRepairDto) {
        return deviceRepairService.approveRepairAcceptance(deviceRepairDto);
    }
    @DeleteMapping("/{ids}")
src/main/java/com/ruoyi/device/execl/DeviceRepairExeclDto.java
@@ -47,6 +47,18 @@
    @Excel(name = "维修结果")
    private String maintenanceResult;
    @Schema(description = "验收人")
    @Excel(name = "验收人")
    private String acceptanceName;
    @Schema(description = "验收时间")
    @Excel(name = "验收时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime acceptanceTime;
    @Schema(description = "验收备注")
    @Excel(name = "验收备注")
    private String acceptanceRemark;
    @Schema(description = "状态")
    @Excel(name = "状态")
    private String statusStr;
src/main/java/com/ruoyi/device/pojo/DeviceRepair.java
@@ -50,7 +50,19 @@
    @Schema(description = "维修结果")
    private String maintenanceResult;
    @Schema(description = "验收人")
    private String acceptanceName;
    @Schema(description = "验收时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime acceptanceTime;
    @Schema(description = "验收备注")
    private String acceptanceRemark;
    @Schema(description = "状态 0 å¾…ç»´ä¿® 1完结 2 å¤±è´¥")
    // 0:待维修 1:完结 2:失败 3:待验收
    private Integer status;
    @Schema(description = "创建时间")
src/main/java/com/ruoyi/device/pojo/MaintenanceTask.java
@@ -44,6 +44,10 @@
    @Schema(description = "设备id")
    private Long taskId;
    @Schema(description = "保养人")
    @Excel(name = "保养人")
    private String maintenancePerson;
    @Schema(description = "频次")
    @Excel(name = "频次")
    private String frequencyType;
src/main/java/com/ruoyi/device/service/IDeviceRepairService.java
@@ -19,6 +19,10 @@
    AjaxResult updateDeviceRepair(DeviceRepairDto deviceRepairDto);
    AjaxResult confirmRepair(DeviceRepairDto deviceRepairDto);
    AjaxResult approveRepairAcceptance(DeviceRepairDto deviceRepairDto);
    void export(HttpServletResponse response, Long[] ids);
    DeviceRepairVo detailById(Long id);
src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java
@@ -47,6 +47,11 @@
    private final SparePartsRequisitionRecordService sparePartsRequisitionRecordService;
    private final FileUtil fileUtil;
    private static final int STATUS_PENDING_REPAIR = 0;
    private static final int STATUS_COMPLETED = 1;
    private static final int STATUS_FAILED = 2;
    private static final int STATUS_PENDING_ACCEPTANCE = 3;
    @Override
    public IPage<DeviceRepairVo> queryPage(Page page, DeviceRepairDto deviceRepairDto) {
        IPage<DeviceRepairVo> pageDto = deviceRepairMapper.queryPage(page, deviceRepairDto);
@@ -62,6 +67,9 @@
        DeviceLedger byId = deviceLedgerService.getById(deviceRepairDto.getDeviceLedgerId());
        deviceRepairDto.setDeviceName(byId.getDeviceName());
        deviceRepairDto.setDeviceModel(byId.getDeviceModel());
        if (deviceRepairDto.getStatus() == null) {
            deviceRepairDto.setStatus(STATUS_PENDING_REPAIR);
        }
        boolean save = this.save(deviceRepairDto);
        if (save) {
            // å¤„理图片上传
@@ -75,6 +83,15 @@
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult updateDeviceRepair(DeviceRepairDto deviceRepairDto) {
        DeviceRepair oldDeviceRepair = this.getById(deviceRepairDto.getId());
        if (oldDeviceRepair == null) {
            return AjaxResult.error("报修记录不存在");
        }
        if (deviceRepairDto.getStatus() != null
                && deviceRepairDto.getStatus() == STATUS_COMPLETED
                && (oldDeviceRepair.getStatus() == null
                || oldDeviceRepair.getStatus() != STATUS_COMPLETED)) {
            return AjaxResult.error("请先提交验收审批,验收通过后才可完结");
        }
        // å¤„理备件使用情况
        if (CollectionUtils.isNotEmpty(deviceRepairDto.getSparePartsUseList())) {
            List<Long> sparePartIds = new ArrayList<>();
@@ -131,6 +148,58 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult confirmRepair(DeviceRepairDto deviceRepairDto) {
        DeviceRepair oldDeviceRepair = this.getById(deviceRepairDto.getId());
        if (oldDeviceRepair == null) {
            return AjaxResult.error("报修记录不存在");
        }
        if (oldDeviceRepair.getStatus() != null && oldDeviceRepair.getStatus() == STATUS_COMPLETED) {
            return AjaxResult.error("该报修已完结,不能重复确认维修");
        }
        if (oldDeviceRepair.getStatus() != null && oldDeviceRepair.getStatus() == STATUS_PENDING_ACCEPTANCE) {
            return AjaxResult.error("该报修已提交验收审批");
        }
        deviceRepairDto.setStatus(STATUS_PENDING_ACCEPTANCE);
        return updateDeviceRepair(deviceRepairDto);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult approveRepairAcceptance(DeviceRepairDto deviceRepairDto) {
        if (deviceRepairDto.getId() == null) {
            return AjaxResult.error("报修记录id不能为空");
        }
        DeviceRepair oldDeviceRepair = this.getById(deviceRepairDto.getId());
        if (oldDeviceRepair == null) {
            return AjaxResult.error("报修记录不存在");
        }
        if (oldDeviceRepair.getStatus() == null || oldDeviceRepair.getStatus() != STATUS_PENDING_ACCEPTANCE) {
            return AjaxResult.error("该报修未进入待验收状态,不能审批");
        }
        if (StringUtils.isBlank(deviceRepairDto.getAcceptanceName())) {
            return AjaxResult.error("验收人不能为空");
        }
        if (deviceRepairDto.getAcceptanceTime() == null) {
            return AjaxResult.error("验收时间不能为空");
        }
        if (StringUtils.isBlank(deviceRepairDto.getAcceptanceRemark())) {
            return AjaxResult.error("验收备注不能为空");
        }
        DeviceRepair update = new DeviceRepair();
        update.setId(deviceRepairDto.getId());
        update.setAcceptanceName(deviceRepairDto.getAcceptanceName());
        update.setAcceptanceTime(deviceRepairDto.getAcceptanceTime());
        update.setAcceptanceRemark(deviceRepairDto.getAcceptanceRemark());
        update.setStatus(STATUS_COMPLETED);
        if (this.updateById(update)) {
            return AjaxResult.success();
        }
        return AjaxResult.error("验收审批失败");
    }
    @Override
    public void export(HttpServletResponse response, Long[] ids) {
        if (ids == null || ids.length == 0) {
            List<DeviceRepair> supplierManageList = this.list();
@@ -138,24 +207,19 @@
            supplierManageList.stream().forEach(deviceRepair -> {
                DeviceRepairExeclDto deviceRepairExeclDto = new DeviceRepairExeclDto();
                BeanUtils.copyProperties(deviceRepair,deviceRepairExeclDto);
                deviceRepairExeclDto.setStatusStr(deviceRepair.getStatus() == 0 ? "待维修" : deviceRepair.getStatus() == 1 ? "完结" : "失败");
                deviceRepairExeclDto.setStatusStr(resolveStatusText(deviceRepair.getStatus()));
                deviceLedgerExeclDtos.add(deviceRepairExeclDto);
            });
            ExcelUtil<DeviceRepairExeclDto> util = new ExcelUtil<DeviceRepairExeclDto>(DeviceRepairExeclDto.class);
            util.exportExcel(response, deviceLedgerExeclDtos, "设备报修导出");
        }else {
            ArrayList<Long> arrayList = new ArrayList<>();
            Arrays.stream(ids).map(id -> {
                return arrayList.add( id);
            });
            ArrayList<Long> arrayList = new ArrayList<>(Arrays.asList(ids));
            List<DeviceRepair> supplierManageList = deviceRepairMapper.selectBatchIds(arrayList);
            ArrayList<DeviceRepairExeclDto> deviceLedgerExeclDtos = new ArrayList<>();
            supplierManageList.stream().forEach(deviceRepair -> {
                DeviceRepairExeclDto deviceRepairExeclDto = new DeviceRepairExeclDto();
                BeanUtils.copyProperties(deviceRepair,deviceRepairExeclDto);
                deviceRepairExeclDto.setStatusStr(deviceRepair.getStatus() == 0 ? "待维修" : deviceRepair.getStatus() == 1 ? "完结" : "失败");
                deviceRepairExeclDto.setStatusStr(resolveStatusText(deviceRepair.getStatus()));
                deviceLedgerExeclDtos.add(deviceRepairExeclDto);
            });
            ExcelUtil<DeviceRepairExeclDto> util = new ExcelUtil<DeviceRepairExeclDto>(DeviceRepairExeclDto.class);
@@ -164,6 +228,25 @@
    }
    private String resolveStatusText(Integer status) {
        if (status == null) {
            return "";
        }
        if (status == STATUS_PENDING_REPAIR) {
            return "待维修";
        }
        if (status == STATUS_COMPLETED) {
            return "完结";
        }
        if (status == STATUS_FAILED) {
            return "失败";
        }
        if (status == STATUS_PENDING_ACCEPTANCE) {
            return "待验收";
        }
        return "未知";
    }
    @Override
    public DeviceRepairVo detailById(Long id) {
        DeviceRepairVo vo = deviceRepairMapper.detailById(id);
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java
@@ -93,6 +93,7 @@
        inspectionTask.setMaintenanceTaskId(timingTask.getId());
        inspectionTask.setDeviceLedgerId(timingTask.getTaskId());
        inspectionTask.setMaintenancePlanTime(LocalDateTime.now());
        inspectionTask.setMaintenanceActuallyName(timingTask.getMaintenancePerson());
        inspectionTask.setFrequencyType(timingTask.getFrequencyType());
        inspectionTask.setFrequencyDetail(timingTask.getFrequencyDetail());
        inspectionTask.setTenantId(timingTask.getTenantId());
src/main/java/com/ruoyi/framework/security/LoginUser.java
@@ -301,7 +301,10 @@
    public void setUser(SysUser user)
    {
        this.user = user;
        this.aiEnabled = user == null ? null : user.getAiEnabled();
        if (user != null && user.getAiEnabled() != null)
        {
            this.aiEnabled = user.getAiEnabled();
        }
    }
    @Override
src/main/java/com/ruoyi/inspectiontask/pojo/InspectionTask.java
@@ -30,6 +30,10 @@
    @Excel(name = "巡检任务名称")
    private String taskName;
    @Schema(description = "巡检项目")
    @Excel(name = "巡检项目")
    private String inspectionProject;
    @Schema(description = "设备id")
    private Integer taskId;
@@ -44,6 +48,22 @@
    @Excel(name = "备注")
    private String remarks;
    @Schema(description = "巡检结果 0 å¼‚常 1 æ­£å¸¸")
    private String inspectionResult;
    @Schema(description = "异常描述")
    private String abnormalDescription;
    @Schema(description = "关联维修单ID")
    private Long deviceRepairId;
    @Schema(description = "验收人ID")
    private Long acceptanceUserId;
    @Schema(description = "验收人")
    @Excel(name = "验收人")
    private String acceptanceName;
    @Schema(description = "任务登记人ID")
    private Long registrantId;
src/main/java/com/ruoyi/inspectiontask/pojo/TimingTask.java
@@ -33,6 +33,10 @@
    @Excel(name = "巡检任务名称")
    private String taskName;
    @Schema(description = "巡检项目")
    @Excel(name = "巡检项目")
    private String inspectionProject;
    @Schema(description = "设备id")
    private Integer taskId;
@@ -60,6 +64,10 @@
    @Schema(description = "是否激活")
    private boolean isActive;
    @Schema(description = "是否启用 0否 1是")
    @Excel(name = "是否启用", readConverterExp = "0=否,1=是")
    private Integer isEnabled;
    @Schema(description = "备注")
    @Excel(name = "备注")
    private String remarks;
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java
@@ -5,12 +5,17 @@
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
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.inspectiontask.dto.InspectionTaskDto;
import com.ruoyi.inspectiontask.mapper.InspectionTaskMapper;
import com.ruoyi.inspectiontask.pojo.InspectionTask;
@@ -23,6 +28,7 @@
import org.springframework.transaction.annotation.Transactional;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -43,6 +49,14 @@
    private final FileUtil fileUtil;
    private final DeviceRepairMapper deviceRepairMapper;
    private final DeviceLedgerMapper deviceLedgerMapper;
    private static final String INSPECTION_RESULT_ABNORMAL = "0";
    private static final String INSPECTION_RESULT_NORMAL = "1";
    private static final int REPAIR_STATUS_PENDING = 0;
    @Override
    public IPage<InspectionTaskDto> selectInspectionTaskList(Page<InspectionTask> page, InspectionTaskDto inspectionTaskDto) {
        LambdaQueryWrapper<InspectionTask> queryWrapper = new LambdaQueryWrapper<>();
@@ -50,14 +64,15 @@
        if (StringUtils.isNotBlank(inspectionTaskDto.getTaskName())) {
            queryWrapper.like(InspectionTask::getTaskName, inspectionTaskDto.getTaskName());
        }
        if (StringUtils.isNotBlank(inspectionTaskDto.getInspectionProject())) {
            queryWrapper.like(InspectionTask::getInspectionProject, inspectionTaskDto.getInspectionProject());
        }
        IPage<InspectionTask> entityPage = inspectionTaskMapper.selectPage(page, queryWrapper);
        //  æ— æ•°æ®æå‰è¿”回
        if (CollectionUtils.isEmpty(entityPage.getRecords())) {
            return new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
        }
        // èŽ·å–id集合
        List<Long> ids = entityPage.getRecords().stream().map(InspectionTask::getId).collect(Collectors.toList());
        //登记人ids
        List<Long> registrantIds = entityPage.getRecords().stream().map(InspectionTask::getRegistrantId).collect(Collectors.toList());
        // æ‰¹é‡æŸ¥è¯¢ç™»è®°äºº
@@ -68,9 +83,6 @@
        } else {
            sysUserMap = new HashMap<>();
        }
        //巡检人ids
        List<String> inspectorIds = entityPage.getRecords().stream().map(InspectionTask::getInspectorId).collect(Collectors.toList());
        //获取所有不重复的用户ID
        Set<Long> allUserIds = entityPage.getRecords().stream()
                .map(InspectionTask::getInspectorId) // èŽ·å–"2,3"这样的字符串
@@ -140,24 +152,230 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int addOrEditInspectionTask(InspectionTaskDto inspectionTaskDto) {
        InspectionTask oldInspectionTask = null;
        if (Objects.nonNull(inspectionTaskDto.getId())) {
            oldInspectionTask = inspectionTaskMapper.selectById(inspectionTaskDto.getId());
            if (oldInspectionTask == null) {
                throw new IllegalArgumentException("巡检任务不存在");
            }
        }
        validateInspectionInput(inspectionTaskDto, oldInspectionTask);
        InspectionTask inspectionTask = new InspectionTask();
        BeanUtils.copyProperties(inspectionTaskDto, inspectionTask);
        inspectionTask.setRegistrantId(SecurityUtils.getLoginUser().getUserId());
        inspectionTask.setRegistrant(SecurityUtils.getLoginUser().getUsername());
        fillAcceptanceInfo(inspectionTask, oldInspectionTask);
        int i;
        if (Objects.isNull(inspectionTaskDto.getId())) {
            i = inspectionTaskMapper.insert(inspectionTask);
        } else {
            i = inspectionTaskMapper.updateById(inspectionTask);
        }
        // ä¿å­˜æ–‡ä»¶
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.INSPECTION_TASK, inspectionTask.getId(), inspectionTaskDto.getCommonFileListDTO());
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.AFTER_FILE, RecordTypeEnum.INSPECTION_TASK, inspectionTask.getId(), inspectionTaskDto.getCommonFileListAfterDTO());
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.BEFORE_FILE, RecordTypeEnum.INSPECTION_TASK, inspectionTask.getId(), inspectionTaskDto.getCommonFileListBeforeDTO());
        if (i <= 0) {
            return i;
        }
        Long linkedRepairId = syncRepairOrderIfAbnormal(inspectionTask, oldInspectionTask);
        if (linkedRepairId != null && !Objects.equals(linkedRepairId, inspectionTask.getDeviceRepairId())) {
            InspectionTask relationUpdate = new InspectionTask();
            relationUpdate.setId(inspectionTask.getId());
            relationUpdate.setDeviceRepairId(linkedRepairId);
            inspectionTaskMapper.updateById(relationUpdate);
            inspectionTask.setDeviceRepairId(linkedRepairId);
        }
        // ä¿å­˜æ–‡ä»¶ï¼ˆå­—段不传则保留历史)
        saveInspectionAttachments(inspectionTask.getId(), inspectionTaskDto);
        return i;
    }
    private void validateInspectionInput(InspectionTaskDto inspectionTaskDto, InspectionTask oldInspectionTask) {
        String inspectionResult = inspectionTaskDto.getInspectionResult();
        if (StringUtils.isBlank(inspectionResult) && oldInspectionTask != null) {
            inspectionResult = oldInspectionTask.getInspectionResult();
        }
        if (StringUtils.isBlank(inspectionResult)) {
            throw new IllegalArgumentException("请选择巡检结果");
        }
        if (!INSPECTION_RESULT_ABNORMAL.equals(inspectionResult) && !INSPECTION_RESULT_NORMAL.equals(inspectionResult)) {
            throw new IllegalArgumentException("巡检结果仅支持:0-异常,1-正常");
        }
        inspectionTaskDto.setInspectionResult(inspectionResult);
        if (!INSPECTION_RESULT_ABNORMAL.equals(inspectionResult)) {
            return;
        }
        String abnormalDescription = inspectionTaskDto.getAbnormalDescription();
        if (StringUtils.isBlank(abnormalDescription) && oldInspectionTask != null) {
            abnormalDescription = oldInspectionTask.getAbnormalDescription();
        }
        if (StringUtils.isBlank(abnormalDescription)) {
            throw new IllegalArgumentException("巡检结果为异常时,异常描述不能为空");
        }
        inspectionTaskDto.setAbnormalDescription(abnormalDescription);
        if (!hasAnyInspectionPhotoAfterSave(inspectionTaskDto, oldInspectionTask)) {
            throw new IllegalArgumentException("巡检结果为异常时,必须上传至少一张照片");
        }
    }
    private boolean hasAnyInspectionPhotoAfterSave(InspectionTaskDto inspectionTaskDto, InspectionTask oldInspectionTask) {
        Long recordId = oldInspectionTask == null ? null : oldInspectionTask.getId();
        return hasApplicationPhotoAfterSave(inspectionTaskDto.getCommonFileListDTO(), ApplicationTypeEnum.FILE, recordId)
                || hasApplicationPhotoAfterSave(inspectionTaskDto.getCommonFileListAfterDTO(), ApplicationTypeEnum.AFTER_FILE, recordId)
                || hasApplicationPhotoAfterSave(inspectionTaskDto.getCommonFileListBeforeDTO(), ApplicationTypeEnum.BEFORE_FILE, recordId);
    }
    private boolean hasApplicationPhotoAfterSave(List<StorageBlobDTO> requestPhotos, ApplicationTypeEnum applicationType, Long recordId) {
        if (requestPhotos != null) {
            return !requestPhotos.isEmpty();
        }
        if (recordId == null) {
            return false;
        }
        return CollectionUtils.isNotEmpty(fileUtil.getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(applicationType, RecordTypeEnum.INSPECTION_TASK, recordId));
    }
    private void fillAcceptanceInfo(InspectionTask inspectionTask, InspectionTask oldInspectionTask) {
        Long acceptanceUserId = inspectionTask.getAcceptanceUserId();
        if (acceptanceUserId == null && oldInspectionTask != null) {
            acceptanceUserId = oldInspectionTask.getAcceptanceUserId();
        }
        if (acceptanceUserId != null) {
            inspectionTask.setAcceptanceUserId(acceptanceUserId);
        }
        String acceptanceName = inspectionTask.getAcceptanceName();
        if (StringUtils.isBlank(acceptanceName) && acceptanceUserId != null) {
            SysUser acceptanceUser = sysUserMapper.selectUserById(acceptanceUserId);
            if (acceptanceUser != null) {
                acceptanceName = acceptanceUser.getNickName();
            }
        }
        if (StringUtils.isBlank(acceptanceName) && oldInspectionTask != null) {
            acceptanceName = oldInspectionTask.getAcceptanceName();
        }
        inspectionTask.setAcceptanceName(acceptanceName);
    }
    private Long syncRepairOrderIfAbnormal(InspectionTask inspectionTask, InspectionTask oldInspectionTask) {
        if (!INSPECTION_RESULT_ABNORMAL.equals(inspectionTask.getInspectionResult())) {
            return inspectionTask.getDeviceRepairId();
        }
        Long linkedRepairId = inspectionTask.getDeviceRepairId();
        if (linkedRepairId == null && oldInspectionTask != null) {
            linkedRepairId = oldInspectionTask.getDeviceRepairId();
        }
        if (linkedRepairId != null) {
            DeviceRepair updateRepair = new DeviceRepair();
            updateRepair.setId(linkedRepairId);
            updateRepair.setRemark(inspectionTask.getAbnormalDescription());
            deviceRepairMapper.updateById(updateRepair);
            return linkedRepairId;
        }
        DeviceRepair deviceRepair = buildDeviceRepair(inspectionTask, oldInspectionTask);
        deviceRepairMapper.insert(deviceRepair);
        return deviceRepair.getId();
    }
    private DeviceRepair buildDeviceRepair(InspectionTask inspectionTask, InspectionTask oldInspectionTask) {
        DeviceRepair deviceRepair = new DeviceRepair();
        Long deviceLedgerId = resolveDeviceLedgerId(inspectionTask, oldInspectionTask);
        DeviceLedger deviceLedger = resolveDeviceLedger(deviceLedgerId);
        deviceRepair.setDeviceLedgerId(deviceLedgerId);
        deviceRepair.setDeviceName(resolveDeviceName(inspectionTask, oldInspectionTask, deviceLedger));
        deviceRepair.setDeviceModel(deviceLedger == null ? null : deviceLedger.getDeviceModel());
        deviceRepair.setRepairName(resolveRepairReporter(inspectionTask, oldInspectionTask));
        deviceRepair.setRepairTime(new Date());
        deviceRepair.setRemark(inspectionTask.getAbnormalDescription());
        deviceRepair.setMachineryCategory(deviceLedger == null ? null : deviceLedger.getType());
        deviceRepair.setStatus(REPAIR_STATUS_PENDING);
        return deviceRepair;
    }
    private Long resolveDeviceLedgerId(InspectionTask inspectionTask, InspectionTask oldInspectionTask) {
        Integer taskId = inspectionTask.getTaskId();
        if (taskId == null && oldInspectionTask != null) {
            taskId = oldInspectionTask.getTaskId();
        }
        if (taskId == null || taskId <= 0) {
            return null;
        }
        return taskId.longValue();
    }
    private DeviceLedger resolveDeviceLedger(Long deviceLedgerId) {
        if (deviceLedgerId == null) {
            return null;
        }
        return deviceLedgerMapper.selectById(deviceLedgerId);
    }
    private String resolveDeviceName(InspectionTask inspectionTask, InspectionTask oldInspectionTask, DeviceLedger deviceLedger) {
        String taskName = inspectionTask.getTaskName();
        if (StringUtils.isBlank(taskName) && oldInspectionTask != null) {
            taskName = oldInspectionTask.getTaskName();
        }
        if (StringUtils.isNotBlank(taskName)) {
            return taskName;
        }
        return deviceLedger == null ? null : deviceLedger.getDeviceName();
    }
    private String resolveRepairReporter(InspectionTask inspectionTask, InspectionTask oldInspectionTask) {
        String reporter = inspectionTask.getInspector();
        if (StringUtils.isBlank(reporter) && oldInspectionTask != null) {
            reporter = oldInspectionTask.getInspector();
        }
        if (StringUtils.isNotBlank(reporter)) {
            return reporter;
        }
        String inspectorNameByUserId = resolveInspectorNameByUserId(inspectionTask.getInspectorId());
        if (StringUtils.isBlank(inspectorNameByUserId) && oldInspectionTask != null) {
            inspectorNameByUserId = resolveInspectorNameByUserId(oldInspectionTask.getInspectorId());
        }
        if (StringUtils.isNotBlank(inspectorNameByUserId)) {
            return inspectorNameByUserId;
        }
        try {
            return SecurityUtils.getUsername();
        } catch (Exception ignored) {
            return "system";
        }
    }
    private String resolveInspectorNameByUserId(String inspectorIds) {
        if (StringUtils.isBlank(inspectorIds)) {
            return null;
        }
        String firstInspectorId = Arrays.stream(inspectorIds.split(","))
                .map(String::trim)
                .filter(StringUtils::isNotBlank)
                .findFirst()
                .orElse(null);
        if (!StringUtils.isNumeric(firstInspectorId)) {
            return null;
        }
        SysUser sysUser = sysUserMapper.selectUserById(Long.parseLong(firstInspectorId));
        return sysUser == null ? null : sysUser.getNickName();
    }
    private void saveInspectionAttachments(Long inspectionTaskId, InspectionTaskDto inspectionTaskDto) {
        saveAttachmentIfPresent(inspectionTaskId, ApplicationTypeEnum.FILE, inspectionTaskDto.getCommonFileListDTO());
        saveAttachmentIfPresent(inspectionTaskId, ApplicationTypeEnum.AFTER_FILE, inspectionTaskDto.getCommonFileListAfterDTO());
        saveAttachmentIfPresent(inspectionTaskId, ApplicationTypeEnum.BEFORE_FILE, inspectionTaskDto.getCommonFileListBeforeDTO());
    }
    private void saveAttachmentIfPresent(Long inspectionTaskId, ApplicationTypeEnum applicationTypeEnum, List<StorageBlobDTO> storageBlobDTOS) {
        if (storageBlobDTOS == null) {
            return;
        }
        fileUtil.saveStorageAttachment(applicationTypeEnum, RecordTypeEnum.INSPECTION_TASK, inspectionTaskId, storageBlobDTOS);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int delByIds(Long[] ids) {
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java
@@ -3,7 +3,7 @@
import com.ruoyi.inspectiontask.mapper.InspectionTaskMapper;
import com.ruoyi.inspectiontask.pojo.InspectionTask;
import com.ruoyi.inspectiontask.pojo.TimingTask;
import lombok.RequiredArgsConstructor;
import com.ruoyi.common.utils.StringUtils;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
@@ -47,6 +47,9 @@
            TimingTask timingTask = tasks.isEmpty() ? null : tasks.get(0);
            if (timingTask == null) {
                throw new JobExecutionException("找不到定时任务: " + taskId);
            }
            if (timingTask.getIsEnabled() != null && timingTask.getIsEnabled() == 0) {
                return;
            }
//            if (!timingTask.isActive()) {
@@ -100,10 +103,15 @@
        // å¤åˆ¶åŸºæœ¬å±žæ€§
        inspectionTask.setTaskName(timingTask.getTaskName());
        inspectionTask.setInspectionProject(timingTask.getInspectionProject());
        inspectionTask.setTaskId(timingTask.getTaskId());
        inspectionTask.setInspectorId(timingTask.getInspectorIds());
        inspectionTask.setInspectionLocation(timingTask.getInspectionLocation());
        inspectionTask.setRemarks("自动生成自定时任务ID: " + timingTask.getId());
        String remarks = "自动生成自定时任务ID: " + timingTask.getId();
        if (StringUtils.isNotBlank(timingTask.getRemarks())) {
            remarks = remarks + ";" + timingTask.getRemarks();
        }
        inspectionTask.setRemarks(remarks);
        inspectionTask.setRegistrantId(timingTask.getRegistrantId());
        inspectionTask.setFrequencyType(timingTask.getFrequencyType());
        inspectionTask.setFrequencyDetail(timingTask.getFrequencyDetail());
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
@@ -35,6 +35,8 @@
    private final TimingTaskMapper timingTaskMapper;
    private final TimingTaskScheduler timingTaskScheduler;
    private final SysUserMapper sysUserMapper;
    private static final int ENABLED = 1;
    private static final int DISABLED = 0;
    @Override
@@ -44,6 +46,12 @@
        LambdaQueryWrapper<TimingTask> queryWrapper = new LambdaQueryWrapper<>();
        if (StringUtils.isNotBlank(timingTask.getTaskName())) {
            queryWrapper.like(TimingTask::getTaskName, timingTask.getTaskName());
        }
        if (StringUtils.isNotBlank(timingTask.getInspectionProject())) {
            queryWrapper.like(TimingTask::getInspectionProject, timingTask.getInspectionProject());
        }
        if (timingTask.getIsEnabled() != null) {
            queryWrapper.eq(TimingTask::getIsEnabled, timingTask.getIsEnabled());
        }
        IPage<TimingTask> taskPage = timingTaskMapper.selectPage(page, queryWrapper);
@@ -115,8 +123,17 @@
    @Override
    @Transactional
    public int addOrEditTimingTask(TimingTaskDto timingTaskDto) throws SchedulerException {
        TimingTask oldTimingTask = null;
        if (Objects.nonNull(timingTaskDto.getId())) {
            oldTimingTask = timingTaskMapper.selectById(timingTaskDto.getId());
            if (oldTimingTask == null) {
                throw new IllegalArgumentException("定时任务不存在");
            }
        }
        TimingTask timingTask = new TimingTask();
        BeanUtils.copyProperties(timingTaskDto, timingTask);
        timingTask.setIsEnabled(resolveEnabledValue(timingTask.getIsEnabled(), oldTimingTask));
        timingTask.setActive(ENABLED == timingTask.getIsEnabled());
        // 1. è§£æžå­—符串为 LocalDate(只包含年月日)
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        LocalDate localDate = LocalDate.now();
@@ -132,13 +149,12 @@
        // è®¾ç½®åˆ›å»ºäººä¿¡æ¯å’Œé»˜è®¤å€¼
        if (Objects.isNull(timingTaskDto.getId())) {
            timingTask.setRegistrationDate(LocalDate.now());
            timingTask.setActive(true);
            // è®¡ç®—首次执行时间
            LocalDateTime firstExecutionTime = calculateFirstExecutionTime(timingTask);
            timingTask.setNextExecutionTime(firstExecutionTime);
            int result = timingTaskMapper.insert(timingTask);
            if (result > 0) {
            if (result > 0 && isEnabled(timingTask.getIsEnabled(), timingTask.isActive())) {
                // æ–°å¢žæˆåŠŸåŽæ·»åŠ åˆ°è°ƒåº¦å™¨
                timingTaskScheduler.scheduleTimingTask(timingTask);
            }
@@ -148,8 +164,17 @@
            int result = timingTaskMapper.updateById(timingTask);
            if (result > 0) {
                boolean oldEnabled = isEnabled(oldTimingTask == null ? null : oldTimingTask.getIsEnabled(), oldTimingTask != null && oldTimingTask.isActive());
                boolean newEnabled = isEnabled(timingTask.getIsEnabled(), timingTask.isActive());
                if (!newEnabled) {
                    timingTaskScheduler.unscheduleTimingTask(timingTask.getId());
                } else if (oldEnabled) {
                // æ›´æ–°æˆåŠŸåŽé‡æ–°è°ƒåº¦ä»»åŠ¡
                timingTaskScheduler.rescheduleTimingTask(timingTask);
                } else {
                    // ä»Žç¦ç”¨æ”¹ä¸ºå¯ç”¨æ—¶é‡æ–°åˆ›å»ºè°ƒåº¦ä»»åŠ¡
                    timingTaskScheduler.scheduleTimingTask(timingTask);
                }
            }
            return result;
        }
@@ -451,6 +476,26 @@
        return days;
    }
    private Integer resolveEnabledValue(Integer requestEnabled, TimingTask oldTimingTask) {
        if (requestEnabled != null) {
            return requestEnabled;
        }
        if (oldTimingTask != null) {
            if (oldTimingTask.getIsEnabled() != null) {
                return oldTimingTask.getIsEnabled();
            }
            return oldTimingTask.isActive() ? ENABLED : DISABLED;
        }
        return ENABLED;
    }
    private boolean isEnabled(Integer enabledValue, boolean activeFallback) {
        if (enabledValue != null) {
            return ENABLED == enabledValue;
        }
        return activeFallback;
    }
    @Override
src/main/java/com/ruoyi/procurementrecord/bean/dto/Details.java
ÎļþÃû´Ó src/main/java/com/ruoyi/procurementrecord/dto/Details.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.procurementrecord.dto;
package com.ruoyi.procurementrecord.bean.dto;
import lombok.Data;
src/main/java/com/ruoyi/procurementrecord/bean/dto/InventoryInformationDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/procurementrecord/dto/InventoryInformationDto.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.procurementrecord.dto;
package com.ruoyi.procurementrecord.bean.dto;
import lombok.Data;
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementAddDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/procurementrecord/dto/ProcurementAddDto.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.procurementrecord.dto;
package com.ruoyi.procurementrecord.bean.dto;
import lombok.Data;
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/procurementrecord/dto/ProcurementDto.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.procurementrecord.dto;
package com.ruoyi.procurementrecord.bean.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementManagementUpdateDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/procurementrecord/dto/ProcurementManagementUpdateDto.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.procurementrecord.dto;
package com.ruoyi.procurementrecord.bean.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementPageDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/procurementrecord/dto/ProcurementPageDto.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.procurementrecord.dto;
package com.ruoyi.procurementrecord.bean.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementPageDtoCopy.java
ÎļþÃû´Ó src/main/java/com/ruoyi/procurementrecord/dto/ProcurementPageDtoCopy.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.procurementrecord.dto;
package com.ruoyi.procurementrecord.bean.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutAdd.java
ÎļþÃû´Ó src/main/java/com/ruoyi/procurementrecord/dto/ProcurementRecordOutAdd.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.procurementrecord.dto;
package com.ruoyi.procurementrecord.bean.dto;
import lombok.Data;
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutPageDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/procurementrecord/dto/ProcurementRecordOutPageDto.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.procurementrecord.dto;
package com.ruoyi.procurementrecord.bean.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementUpdateDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/procurementrecord/dto/ProcurementUpdateDto.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.procurementrecord.dto;
package com.ruoyi.procurementrecord.bean.dto;
import lombok.Data;
src/main/java/com/ruoyi/procurementrecord/bean/dto/ReturnManagementDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/procurementrecord/dto/ReturnManagementDto.java ÐÞ¸Ä
@@ -1,17 +1,9 @@
package com.ruoyi.procurementrecord.dto;
package com.ruoyi.procurementrecord.bean.dto;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.procurementrecord.pojo.ReturnManagement;
import com.ruoyi.procurementrecord.pojo.ReturnSaleProduct;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
/**
src/main/java/com/ruoyi/procurementrecord/bean/dto/ReturnSaleProductDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package com.ruoyi.procurementrecord.bean.dto;
import com.ruoyi.procurementrecord.pojo.ReturnSaleProduct;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ReturnSaleProductDto extends ReturnSaleProduct {
    private String productName;
    private String model;
    private String unit;
    //未退货数量
    private BigDecimal unQuantity;
    //总退货数量
    private BigDecimal totalReturnNum;
    // é€€è´§æ€»ä»·
    private BigDecimal price;
    // é€€è´§æ€»ä»·
    private BigDecimal taxInclusiveUnitPrice;
    @Schema(description = "出库单号")
    private String outboundBatches;
    @Schema(description = "批次号")
    private String batchNo;
    @Schema(description = "发货出库数量")
    private BigDecimal stockOutNum;
}
src/main/java/com/ruoyi/procurementrecord/bean/vo/ShippingInfoVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package com.ruoyi.procurementrecord.bean.vo;
import com.ruoyi.sales.pojo.ShippingInfo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(name = "ShippingInfoVo", description = "营销管理--发货信息")
public class ShippingInfoVo {
    private ShippingInfo shippingInfo;
    @Schema(description = "发货产品列表")
    private List<ShippingProductVo> shippingProductVoList;
}
src/main/java/com/ruoyi/procurementrecord/bean/vo/ShippingProductVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package com.ruoyi.procurementrecord.bean.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(name = "ShippingProductVo", description = "营销管理--发货出库产品列表")
public class ShippingProductVo {
    @Schema(description = "出库单id")
    private Long id;
    @Schema(description = "产品规格id")
    private Long productModelId;
    @Schema(description = "产品大类")
    private String productCategory;
    @Schema(description = "规格型号")
    private String specificationModel;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "出库单号")
    private String outboundBatches;
    @Schema(description = "发货出库数量")
    private BigDecimal stockOutNum;
    @Schema(description = "批次号")
    private String batchNo;
    @Schema(description = "未退货数")
    private BigDecimal unQuantity;
    @Schema(description = "退货总数")
    private BigDecimal totalReturnNum;
    @Schema(description = "含税单价")
    private BigDecimal taxInclusiveUnitPrice;
}
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementExceptionRecordController.java
@@ -1,24 +1,13 @@
package com.ruoyi.procurementrecord.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.AjaxResult;
import com.ruoyi.procurementrecord.dto.*;
import com.ruoyi.procurementrecord.mapper.ProcurementExceptionRecordMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementExceptionRecord;
import com.ruoyi.procurementrecord.service.ProcurementRecordService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * @author :yys
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementRecordController.java
@@ -8,7 +8,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.procurementrecord.dto.*;
import com.ruoyi.procurementrecord.bean.dto.*;
import com.ruoyi.procurementrecord.mapper.CustomStorageMapper;
import com.ruoyi.procurementrecord.pojo.CustomStorage;
import com.ruoyi.procurementrecord.service.ProcurementRecordService;
@@ -16,7 +16,6 @@
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
import org.apache.ibatis.annotations.Delete;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementRecordOutController.java
@@ -7,9 +7,9 @@
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.procurementrecord.dto.ProcurementRecordOutAdd;
import com.ruoyi.procurementrecord.dto.ProcurementRecordOutPageDto;
import com.ruoyi.procurementrecord.dto.ProcurementUpdateDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutAdd;
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementUpdateDto;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.procurementrecord.service.ProcurementRecordOutService;
import io.swagger.v3.oas.annotations.tags.Tag;
src/main/java/com/ruoyi/procurementrecord/controller/ReturnManagementController.java
@@ -4,21 +4,16 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.procurementrecord.pojo.ReturnManagement;
import com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.bean.vo.ShippingInfoVo;
import com.ruoyi.procurementrecord.pojo.ReturnSaleProduct;
import com.ruoyi.procurementrecord.service.ReturnManagementService;
import com.ruoyi.procurementrecord.service.ReturnSaleProductService;
import com.ruoyi.procurementrecord.service.impl.ReturnSaleProductServiceImpl;
import com.ruoyi.sales.dto.SalesLedgerDto;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
@@ -87,10 +82,10 @@
    }
    @GetMapping("/getByShippingId")
    @Operation(summary = "销售退货-根据出库单查询销售订单以及产品信息")
    @Operation(summary = "销售退货-根据发货单查询销售订单以及出库的产品信息")
    public AjaxResult getByShippingId(Long shippingId) {
        SalesLedgerDto salesLedgerDto = returnManagementService.getReturnManagementDtoByShippingIdId(shippingId);
        return success(salesLedgerDto);
        ShippingInfoVo shippingInfoVo = returnManagementService.getReturnManagementDtoByShippingIdId(shippingId);
        return success(shippingInfoVo);
    }
}
src/main/java/com/ruoyi/procurementrecord/dto/ReturnSaleProductDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/procurementrecord/mapper/ProcurementRecordMapper.java
@@ -3,9 +3,9 @@
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.procurementrecord.dto.ProcurementDto;
import com.ruoyi.procurementrecord.dto.ProcurementPageDto;
import com.ruoyi.procurementrecord.dto.ProcurementPageDtoCopy;
import com.ruoyi.procurementrecord.bean.dto.ProcurementDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementPageDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementPageDtoCopy;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
import org.apache.ibatis.annotations.Param;
src/main/java/com/ruoyi/procurementrecord/mapper/ProcurementRecordOutMapper.java
@@ -3,7 +3,7 @@
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.procurementrecord.dto.ProcurementRecordOutPageDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import org.apache.ibatis.annotations.Param;
src/main/java/com/ruoyi/procurementrecord/mapper/ReturnManagementMapper.java
@@ -5,7 +5,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.SalesReturnDto;
import com.ruoyi.account.bean.vo.SalesReturnVo;
import com.ruoyi.procurementrecord.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.pojo.ReturnManagement;
import org.apache.ibatis.annotations.Param;
src/main/java/com/ruoyi/procurementrecord/mapper/ReturnSaleProductMapper.java
@@ -1,6 +1,6 @@
package com.ruoyi.procurementrecord.mapper;
import com.ruoyi.procurementrecord.dto.ReturnSaleProductDto;
import com.ruoyi.procurementrecord.bean.dto.ReturnSaleProductDto;
import com.ruoyi.procurementrecord.pojo.ReturnSaleProduct;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
src/main/java/com/ruoyi/procurementrecord/pojo/ReturnManagement.java
@@ -8,7 +8,6 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
/**
 * @author :yys
@@ -64,10 +63,12 @@
    @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")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @Schema(description = "创建用户")
src/main/java/com/ruoyi/procurementrecord/pojo/ReturnSaleProduct.java
@@ -1,15 +1,12 @@
package com.ruoyi.procurementrecord.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 java.io.Serializable;
import java.math.BigDecimal;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * <p>
@@ -37,8 +34,8 @@
    @Schema(description = "退货单id")
    private Long returnManagementId;
    @Schema(description = "退货产品id")
    private Long returnsalesLedgerProductId;
    @Schema(description = "关联出库单id")
    private Long stockOutRecordId;
    @Schema(description = "退货产品数量")
    private BigDecimal num;
src/main/java/com/ruoyi/procurementrecord/service/ProcurementRecordOutService.java
@@ -3,9 +3,9 @@
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.procurementrecord.dto.ProcurementRecordOutAdd;
import com.ruoyi.procurementrecord.dto.ProcurementRecordOutPageDto;
import com.ruoyi.procurementrecord.dto.ProcurementUpdateDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutAdd;
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementUpdateDto;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import jakarta.servlet.http.HttpServletResponse;
src/main/java/com/ruoyi/procurementrecord/service/ProcurementRecordService.java
@@ -5,7 +5,7 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.dto.*;
import com.ruoyi.procurementrecord.bean.dto.*;
import com.ruoyi.procurementrecord.pojo.CustomStorage;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
src/main/java/com/ruoyi/procurementrecord/service/ReturnManagementService.java
@@ -3,9 +3,9 @@
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.procurementrecord.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.bean.vo.ShippingInfoVo;
import com.ruoyi.procurementrecord.pojo.ReturnManagement;
import com.ruoyi.sales.dto.SalesLedgerDto;
/**
 * @author :yys
@@ -26,7 +26,7 @@
    boolean updateReturnManagementDto(ReturnManagementDto returnManagementDto);
    SalesLedgerDto getReturnManagementDtoByShippingIdId(Long shippingId);
    ShippingInfoVo getReturnManagementDtoByShippingIdId(Long shippingId);
    boolean handle(Long returnManagementId);
src/main/java/com/ruoyi/procurementrecord/service/ReturnSaleProductService.java
@@ -1,6 +1,6 @@
package com.ruoyi.procurementrecord.service;
import com.ruoyi.procurementrecord.dto.ReturnSaleProductDto;
import com.ruoyi.procurementrecord.bean.dto.ReturnSaleProductDto;
import com.ruoyi.procurementrecord.pojo.ReturnSaleProduct;
import com.baomidou.mybatisplus.extension.service.IService;
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordOutServiceImpl.java
@@ -5,9 +5,9 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.procurementrecord.dto.ProcurementRecordOutAdd;
import com.ruoyi.procurementrecord.dto.ProcurementRecordOutPageDto;
import com.ruoyi.procurementrecord.dto.ProcurementUpdateDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutAdd;
import com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto;
import com.ruoyi.procurementrecord.bean.dto.ProcurementUpdateDto;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import com.ruoyi.procurementrecord.service.ProcurementRecordOutService;
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordServiceImpl.java
@@ -12,7 +12,7 @@
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.dto.*;
import com.ruoyi.procurementrecord.bean.dto.*;
import com.ruoyi.procurementrecord.mapper.CustomStorageMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnManagementServiceImpl.java
@@ -10,18 +10,17 @@
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.procurementrecord.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.dto.ReturnSaleProductDto;
import com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.bean.dto.ReturnSaleProductDto;
import com.ruoyi.procurementrecord.bean.vo.ShippingInfoVo;
import com.ruoyi.procurementrecord.bean.vo.ShippingProductVo;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.procurementrecord.pojo.ReturnManagement;
import com.ruoyi.procurementrecord.pojo.ReturnSaleProduct;
import com.ruoyi.procurementrecord.service.ReturnManagementService;
import com.ruoyi.procurementrecord.service.ReturnSaleProductService;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.sales.dto.SalesLedgerDto;
import com.ruoyi.sales.dto.SalesLedgerProductDto;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.service.ShippingInfoService;
import lombok.RequiredArgsConstructor;
@@ -32,7 +31,6 @@
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -76,28 +74,28 @@
    @Override
    public boolean updateReturnManagementDto(ReturnManagementDto returnManagementDto) {
        List<ReturnSaleProduct> returnSaleProducts = new ArrayList<>();
        if (!CollectionUtils.isEmpty(returnManagementDto.getReturnSaleProducts())) {
            returnManagementDto.getReturnSaleProducts().stream().forEach(returnSaleProductDto -> {
                ReturnSaleProduct returnSaleProduct = new ReturnSaleProduct();
                BeanUtils.copyProperties(returnSaleProductDto, returnSaleProduct);
                returnSaleProducts.add(returnSaleProduct);
                if (returnSaleProductDto.getId() == null){
                    returnSaleProduct.setReturnManagementId(returnManagementDto.getId());
                    returnSaleProduct.setStatus(0);
                    returnSaleProductService.save(returnSaleProduct);
                }else returnSaleProductService.updateById(returnSaleProduct);
            });
        }
        returnSaleProductService.updateBatchById(returnSaleProducts);
        return updateById(returnManagementDto);
    }
    @Override
    public SalesLedgerDto getReturnManagementDtoByShippingIdId(Long shippingId) {
    public ShippingInfoVo getReturnManagementDtoByShippingIdId(Long shippingId) {
        ShippingInfo byId = shippingInfoService.getById(shippingId);
        SalesLedger salesLedger = salesLedgerMapper.selectById(byId.getSalesLedgerId());
        SalesLedgerDto salesLedgerDto = new SalesLedgerDto();
        BeanUtils.copyProperties(salesLedger, salesLedgerDto);
        List<SalesLedgerProductDto> salesLedgerProductDtos = shippingInfoService.getReturnManagementDtoById(byId.getId());
        salesLedgerDto.setProductDtoData(salesLedgerProductDtos);
         return salesLedgerDto;
        ShippingInfoVo shippingInfoVo = new ShippingInfoVo();
        shippingInfoVo.setShippingInfo(byId);
        List<ShippingProductVo> shippingProductVos = shippingInfoService.getReturnManagementDtoById(byId.getId());
        shippingInfoVo.setShippingProductVoList(shippingProductVos);
         return shippingInfoVo;
    }
    @Override
@@ -105,6 +103,7 @@
        ReturnManagement byId = this.getById(returnManagementId);
        List<ReturnSaleProductDto> list = returnSaleProductService.listReturnSaleProduct(returnManagementId);
        byId.setStatus(1);
        byId.setSettler(SecurityUtils.getLoginUser().getNickName());
        updateById(byId);
        SalesRefundAmountOrderDto salesRefundAmountOrder = new SalesRefundAmountOrderDto();
        salesRefundAmountOrder.setReturnManagementId(returnManagementId);
@@ -117,11 +116,11 @@
            salesRefundAmountOrder.setRefundedAmount(new BigDecimal(0));
            // æ˜¯å¦æœ‰è´¨é‡é—®é¢˜
            if (returnSaleProduct.getIsQuality() == 1) {
                // æœ‰è´¨é‡é—®é¢˜ï¼Œå…¥ä¸åˆæ ¼åº“
                stockUtils.addUnStock(returnSaleProduct.getProductModelId(),returnSaleProduct.getNum(), StockInQualifiedRecordTypeEnum.RETURN_UNSTOCK_IN.getCode(),returnSaleProduct.getId());
                // æœ‰è´¨é‡é—®é¢˜ï¼Œå…¥ä¸åˆæ ¼åº“(带批次)
                stockUtils.addUnStockWithBatchNo(returnSaleProduct.getProductModelId(),returnSaleProduct.getNum(), StockInQualifiedRecordTypeEnum.RETURN_UNSTOCK_IN.getCode(),returnSaleProduct.getId(),returnSaleProduct.getBatchNo());
            }else{
                // æ— è´¨é‡é—®é¢˜ï¼Œå…¥åˆæ ¼åº“
                stockUtils.addStock(returnSaleProduct.getProductModelId(),returnSaleProduct.getNum(), StockInQualifiedRecordTypeEnum.RETURN_HE_IN.getCode(),returnSaleProduct.getId());
                // æ— è´¨é‡é—®é¢˜ï¼Œå…¥åˆæ ¼åº“(带批次)
                stockUtils.addStockWithBatchNo(returnSaleProduct.getProductModelId(),returnSaleProduct.getNum(), StockInQualifiedRecordTypeEnum.RETURN_HE_IN.getCode(),returnSaleProduct.getId(),returnSaleProduct.getBatchNo());
            }
        }
        salesRefundAmountOrder.setRefundAmount(bigDecimal);
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnSaleProductServiceImpl.java
@@ -1,7 +1,7 @@
package com.ruoyi.procurementrecord.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.procurementrecord.dto.ReturnSaleProductDto;
import com.ruoyi.procurementrecord.bean.dto.ReturnSaleProductDto;
import com.ruoyi.procurementrecord.mapper.ReturnSaleProductMapper;
import com.ruoyi.procurementrecord.pojo.ReturnSaleProduct;
import com.ruoyi.procurementrecord.service.ReturnSaleProductService;
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java
@@ -3,11 +3,11 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.ruoyi.common.enums.ReviewStatusEnum;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.pojo.StockOutRecord;
import com.ruoyi.stock.service.StockInRecordService;
@@ -29,7 +29,6 @@
    private final StockInventoryService stockInventoryService;
    private final StockInRecordService stockInRecordService;
    private final StockOutRecordService stockOutRecordService;
    private final StockInventoryMapper stockInventoryMapper;
    /**
     * ä¸åˆæ ¼å…¥åº“
@@ -45,6 +44,24 @@
        stockUninventoryDto.setRecordType(String.valueOf(recordType));
        stockUninventoryDto.setQualitity(quantity);
        stockUninventoryDto.setProductModelId(productModelId);
        stockUninventoryService.addStockInRecordOnly(stockUninventoryDto);
    }
    /**
     * ä¸åˆæ ¼å…¥åº“带批次号
     *
     * @param productModelId
     * @param quantity
     * @param recordType
     * @param recordId
     */
    public void addUnStockWithBatchNo(Long productModelId, BigDecimal quantity, String recordType, Long recordId, String batchNo) {
        StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
        stockUninventoryDto.setRecordId(recordId);
        stockUninventoryDto.setRecordType(String.valueOf(recordType));
        stockUninventoryDto.setQualitity(quantity);
        stockUninventoryDto.setProductModelId(productModelId);
        stockUninventoryDto.setBatchNo(batchNo);
        stockUninventoryService.addStockInRecordOnly(stockUninventoryDto);
    }
@@ -122,40 +139,38 @@
    public void shipmentStatus(String recordType, Long recordId) {
        LambdaQueryWrapper<StockOutRecord> queryWrapper = new LambdaQueryWrapper<StockOutRecord>().eq(StockOutRecord::getRecordType, recordType)
                .eq(StockOutRecord::getRecordId, recordId);
        StockOutRecord stockOutRecord = stockOutRecordService.getOne(queryWrapper);
        stockOutRecordService.list(queryWrapper).stream().forEach(stockOutRecord -> {
        stockOutRecord.setApprovalStatus(0);
        stockOutRecordService.updateById(stockOutRecord);
        });
    }
    //不合格库存删除
    public void deleteStockInRecord(Long recordId, String recordType) {
        StockInRecord one = stockInRecordService.getOne(new QueryWrapper<StockInRecord>()
                .lambda().eq(StockInRecord::getRecordId, recordId)
                .eq(StockInRecord::getRecordType, recordType));
                .eq(StockInRecord::getRecordType, recordType), false);
        if (ObjectUtils.isNotEmpty(one)) {
            if (ReviewStatusEnum.APPROVED.getCode().equals(one.getApprovalStatus())) {
            stockInRecordService.batchDelete(Collections.singletonList(one.getId()));
            //将库存减回来
            StockInventoryDto stockInventoryDto = new StockInventoryDto();
            stockInventoryDto.setRecordId(recordId);
            stockInventoryDto.setRecordType(recordType);
            stockInventoryDto.setQualitity(one.getStockInNum());
            stockInventoryMapper.updateSubtractStockInventory((stockInventoryDto));
            } else {
                stockInRecordService.removeById(one.getId());
            }
        }
    }
    //删除出库记录
    public void deleteStockOutRecord(Long recordId, String recordType) {
        StockOutRecord one = stockOutRecordService.getOne(new QueryWrapper<StockOutRecord>()
                .lambda().eq(StockOutRecord::getRecordId, recordId)
                .eq(StockOutRecord::getRecordType, recordType));
                .eq(StockOutRecord::getRecordType, recordType), false);
        if (ObjectUtils.isNotEmpty(one)) {
            if (ReviewStatusEnum.APPROVED.getCode().equals(one.getApprovalStatus())) {
            stockOutRecordService.batchDelete(Collections.singletonList(one.getId()));
            //将库存加回来
            StockInventoryDto stockInventoryDto = new StockInventoryDto();
            stockInventoryDto.setRecordId(recordId);
            stockInventoryDto.setRecordType(recordType);
            stockInventoryDto.setQualitity(one.getStockOutNum());
            stockInventoryMapper.updateAddStockInventory((stockInventoryDto));
            } else {
                stockOutRecordService.removeById(one.getId());
            }
        }
    }
src/main/java/com/ruoyi/production/pojo/ProductionAccount.java
@@ -27,12 +27,6 @@
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "销售台账id")
    private Long salesLedgerId;
    @Schema(description = "销售产品规格id")
    private Long salesLedgerProductId;
    @Schema(description = "报工表id")
    private Long productionProductMainId;
src/main/java/com/ruoyi/production/pojo/ProductionProductInput.java
@@ -14,9 +14,6 @@
    @TableId(type = IdType.AUTO)
    private Long id;
    @Schema(description = "报工id")
    private Long productMainId;
    @Schema(description = "生产报工主表id")
    private Long productionProductMainId;
src/main/java/com/ruoyi/production/pojo/ProductionProductOutput.java
@@ -14,9 +14,6 @@
    @TableId(type = IdType.AUTO)
    private Long id;
    @Schema(description = "报工id")
    private Long productMainId;
    @Schema(description = "生产报工主表id")
    private Long productionProductMainId;
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
@@ -1,18 +1,44 @@
package com.ruoyi.production.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.production.bean.dto.ProductionBomStructureDto;
import com.ruoyi.production.bean.vo.ProductionBomStructureVo;
import com.ruoyi.production.mapper.ProductionBomStructureMapper;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.mapper.ProductionOrderBomMapper;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingOperationMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingOperationParamMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.pojo.ProductionBomStructure;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionOrderBom;
import com.ruoyi.production.pojo.ProductionOrderRouting;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperation;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperationParam;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.service.ProductionBomStructureService;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
import com.ruoyi.technology.mapper.TechnologyOperationParamMapper;
import com.ruoyi.technology.mapper.TechnologyParamMapper;
import com.ruoyi.technology.pojo.TechnologyOperation;
import com.ruoyi.technology.pojo.TechnologyOperationParam;
import com.ruoyi.technology.pojo.TechnologyParam;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
 * <p>
@@ -27,6 +53,16 @@
public class ProductionBomStructureServiceImpl extends ServiceImpl<ProductionBomStructureMapper, ProductionBomStructure> implements ProductionBomStructureService {
    private  final ProductionBomStructureMapper productionBomStructureMapper;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderRoutingMapper productionOrderRoutingMapper;
    private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper;
    private final ProductionOrderRoutingOperationParamMapper productionOrderRoutingOperationParamMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final TechnologyOperationParamMapper technologyOperationParamMapper;
    private final TechnologyParamMapper technologyParamMapper;
    /**
     * æ ¹æ®BOM查询并组装结构树。
@@ -136,9 +172,569 @@
        if (!updateList.isEmpty()) {
            this.updateBatchById(updateList);
        }
        syncDemandedQuantityAndTaskPlanQuantity(orderBomId, dto.getProductionOrderId());
        return true;
    }
    private void syncDemandedQuantityAndTaskPlanQuantity(Long orderBomId, Long productionOrderId) {
        if (orderBomId == null) {
            return;
        }
        ProductionOrderBom orderBom = productionOrderBomMapper.selectById(orderBomId);
        if (orderBom == null) {
            return;
        }
        Long currentProductionOrderId = productionOrderId != null ? productionOrderId : orderBom.getProductionOrderId();
        if (currentProductionOrderId == null) {
            return;
        }
        ProductionOrder productionOrder = productionOrderMapper.selectById(currentProductionOrderId);
        if (productionOrder == null) {
            return;
        }
        BigDecimal orderQuantity = defaultDecimal(productionOrder.getQuantity());
        List<ProductionBomStructure> structureList = this.list(
                Wrappers.<ProductionBomStructure>lambdaQuery()
                        .eq(ProductionBomStructure::getProductionOrderBomId, orderBomId)
                        .orderByAsc(ProductionBomStructure::getId));
        //同步需求数量
        syncStructureDemandedQuantity(structureList, orderQuantity);
        Long rootProductModelId = orderBom.getProductModelId() != null ? orderBom.getProductModelId() : productionOrder.getProductModelId();
        //同步生产工艺路线
        syncRoutingOperationsByBom(currentProductionOrderId, productionOrder, orderBom, structureList, rootProductModelId);
        //同步工单
        syncTaskPlanQuantity(
                currentProductionOrderId,
                structureList,
                orderQuantity,
                rootProductModelId);
    }
    private void syncStructureDemandedQuantity(List<ProductionBomStructure> structureList, BigDecimal orderQuantity) {
        if (structureList == null || structureList.isEmpty()) {
            return;
        }
        List<ProductionBomStructure> updateList = new ArrayList<>();
        BigDecimal lastProcessDemandedQuantity = orderQuantity;
        for (ProductionBomStructure structure : structureList) {
            if (structure == null || structure.getId() == null) {
                continue;
            }
            BigDecimal demandedQuantity = lastProcessDemandedQuantity.multiply(defaultDecimal(structure.getUnitQuantity()));
//            if (compareDecimal(structure.getDemandedQuantity(), demandedQuantity) == 0) {
//                continue;
//            }
            ProductionBomStructure update = new ProductionBomStructure();
            update.setId(structure.getId());
            update.setDemandedQuantity(demandedQuantity);
            updateList.add(update);
            structure.setDemandedQuantity(demandedQuantity);
            lastProcessDemandedQuantity = demandedQuantity;
        }
        if (!updateList.isEmpty()) {
            this.updateBatchById(updateList);
        }
    }
    private void syncTaskPlanQuantity(Long productionOrderId,
                                      List<ProductionBomStructure> structureList,
                                      BigDecimal orderQuantity,
                                      Long rootProductModelId) {
        List<ProductionOperationTask> taskList = productionOperationTaskMapper.selectList(
                Wrappers.<ProductionOperationTask>lambdaQuery()
                        .eq(ProductionOperationTask::getProductionOrderId, productionOrderId)
                        .orderByAsc(ProductionOperationTask::getId));
        if (taskList == null || taskList.isEmpty()) {
            return;
        }
        Set<Long> routingOperationIds = taskList.stream()
                .map(ProductionOperationTask::getProductionOrderRoutingOperationId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (routingOperationIds.isEmpty()) {
            return;
        }
        Map<Long, ProductionOrderRoutingOperation> routingOperationMap = productionOrderRoutingOperationMapper
                .selectBatchIds(routingOperationIds)
                .stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionOrderRoutingOperation::getId, item -> item, (left, right) -> left));
        // Keep task plan quantities aligned with the same order BOM snapshot demand used during snapshot creation.
        Map<String, BigDecimal> demandedQuantityMap = buildOperationDemandedQuantityMap(structureList, rootProductModelId);
        for (ProductionOperationTask task : taskList) {
            if (task == null || task.getId() == null || task.getProductionOrderRoutingOperationId() == null) {
                continue;
            }
            ProductionOrderRoutingOperation routingOperation = routingOperationMap.get(task.getProductionOrderRoutingOperationId());
            if (routingOperation == null) {
                continue;
            }
            BigDecimal planQuantity = resolveTaskPlanQuantity(
                    routingOperation,
                    demandedQuantityMap,
                    orderQuantity,
                    rootProductModelId);
            if (compareDecimal(task.getPlanQuantity(), planQuantity) == 0) {
                continue;
            }
            ProductionOperationTask update = new ProductionOperationTask();
            update.setId(task.getId());
            update.setPlanQuantity(planQuantity);
            productionOperationTaskMapper.updateById(update);
        }
    }
    private void syncRoutingOperationsByBom(Long productionOrderId,
                                            ProductionOrder productionOrder,
                                            ProductionOrderBom orderBom,
                                            List<ProductionBomStructure> structureList,
                                            Long rootProductModelId) {
        ProductionOrderRouting orderRouting = getOrCreateOrderRoutingSnapshot(productionOrderId, productionOrder, orderBom, rootProductModelId);
        List<ProductionOrderRoutingOperation> desiredOperationList = buildDesiredRoutingOperationList(structureList, rootProductModelId);
        List<ProductionOrderRoutingOperation> existingOperationList = productionOrderRoutingOperationMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperation>lambdaQuery()
                        .eq(ProductionOrderRoutingOperation::getOrderRoutingId, orderRouting.getId())
                        .eq(ProductionOrderRoutingOperation::getProductionOrderId, productionOrderId)
                        .orderByAsc(ProductionOrderRoutingOperation::getDragSort)
                        .orderByAsc(ProductionOrderRoutingOperation::getId));
        Map<String, Deque<ProductionOrderRoutingOperation>> existingBucketMap = buildExistingRoutingOperationBucketMap(existingOperationList);
        List<ProductionOrderRoutingOperation> finalOperationList = new ArrayList<>();
        for (ProductionOrderRoutingOperation desiredOperation : desiredOperationList) {
            String bucketKey = buildRoutingOperationBucketKey(
                    desiredOperation.getTechnologyOperationId(),
                    desiredOperation.getProductModelId());
            Deque<ProductionOrderRoutingOperation> matchedQueue = existingBucketMap.get(bucketKey);
            ProductionOrderRoutingOperation matchedOperation = matchedQueue == null ? null : matchedQueue.pollFirst();
            if (matchedOperation == null) {
                matchedOperation = insertRoutingOperationSnapshot(orderRouting.getId(), productionOrderId, desiredOperation);
            } else {
                updateRoutingOperationSnapshotIfNecessary(desiredOperation, orderRouting.getId(), productionOrderId, matchedOperation);
            }
            finalOperationList.add(matchedOperation);
        }
        for (Deque<ProductionOrderRoutingOperation> queue : existingBucketMap.values()) {
            while (queue != null && !queue.isEmpty()) {
                removeRoutingOperationSnapshot(queue.pollFirst());
            }
        }
        syncRoutingOperationTasks(productionOrderId, finalOperationList);
    }
    private ProductionOrderRouting getOrCreateOrderRoutingSnapshot(Long productionOrderId,
                                                                   ProductionOrder productionOrder,
                                                                   ProductionOrderBom orderBom,
                                                                   Long rootProductModelId) {
        ProductionOrderRouting orderRouting = productionOrderRoutingMapper.selectOne(
                Wrappers.<ProductionOrderRouting>lambdaQuery()
                        .eq(ProductionOrderRouting::getProductionOrderId, productionOrderId)
                        .orderByDesc(ProductionOrderRouting::getId)
                        .last("limit 1"));
        if (orderRouting == null) {
            orderRouting = new ProductionOrderRouting();
            orderRouting.setProductionOrderId(productionOrderId);
            orderRouting.setProductModelId(rootProductModelId);
            orderRouting.setTechnologyRoutingId(productionOrder == null ? null : productionOrder.getTechnologyRoutingId());
            orderRouting.setBomId(orderBom == null ? null : orderBom.getBomId());
            orderRouting.setOrderBomId(orderBom == null ? null : orderBom.getId());
            productionOrderRoutingMapper.insert(orderRouting);
            return orderRouting;
        }
        ProductionOrderRouting update = new ProductionOrderRouting();
        update.setId(orderRouting.getId());
        boolean changed = false;
        if (!Objects.equals(orderRouting.getProductModelId(), rootProductModelId)) {
            update.setProductModelId(rootProductModelId);
            orderRouting.setProductModelId(rootProductModelId);
            changed = true;
        }
        Long technologyRoutingId = productionOrder == null ? null : productionOrder.getTechnologyRoutingId();
        if (!Objects.equals(orderRouting.getTechnologyRoutingId(), technologyRoutingId)) {
            update.setTechnologyRoutingId(technologyRoutingId);
            orderRouting.setTechnologyRoutingId(technologyRoutingId);
            changed = true;
        }
        Long bomId = orderBom == null ? null : orderBom.getBomId();
        if (!Objects.equals(orderRouting.getBomId(), bomId)) {
            update.setBomId(bomId);
            orderRouting.setBomId(bomId);
            changed = true;
        }
        Long orderBomId = orderBom == null ? null : orderBom.getId();
        if (!Objects.equals(orderRouting.getOrderBomId(), orderBomId)) {
            update.setOrderBomId(orderBomId);
            orderRouting.setOrderBomId(orderBomId);
            changed = true;
        }
        if (changed) {
            productionOrderRoutingMapper.updateById(update);
        }
        return orderRouting;
    }
    private List<ProductionOrderRoutingOperation> buildDesiredRoutingOperationList(List<ProductionBomStructure> structureList,
                                                                                   Long rootProductModelId) {
        if (structureList == null || structureList.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Long, ProductionBomStructure> structureById = structureList.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructure::getId, item -> item, (left, right) -> left));
        Map<String, ProductionBomStructure> uniqueOperationMap = new LinkedHashMap<>();
        for (ProductionBomStructure bomStructure : structureList) {
            if (bomStructure == null || bomStructure.getTechnologyOperationId() == null) {
                continue;
            }
            Long outputProductModelId = resolveOutputProductModelId(resolveOperationOutputNode(bomStructure, structureById), rootProductModelId);
            uniqueOperationMap.putIfAbsent(buildBomOperationDedupKey(bomStructure, outputProductModelId), bomStructure);
        }
        List<ProductionOrderRoutingOperation> desiredOperationList = new ArrayList<>();
        int dragSort = 1;
        for (ProductionBomStructure bomStructure : uniqueOperationMap.values()) {
            Long outputProductModelId = resolveOutputProductModelId(resolveOperationOutputNode(bomStructure, structureById), rootProductModelId);
            TechnologyOperation technologyOperation = getTechnologyOperation(bomStructure.getTechnologyOperationId());
            ProductionOrderRoutingOperation routingOperation = new ProductionOrderRoutingOperation();
            routingOperation.setProductModelId(outputProductModelId);
            routingOperation.setTechnologyOperationId(bomStructure.getTechnologyOperationId());
            routingOperation.setOperationName(technologyOperation == null ? null : technologyOperation.getName());
            routingOperation.setIsQuality(technologyOperation == null ? null : technologyOperation.getIsQuality());
            routingOperation.setIsProduction(technologyOperation == null ? null : technologyOperation.getIsProduction());
            routingOperation.setType(technologyOperation == null ? null : technologyOperation.getType());
            routingOperation.setDragSort(dragSort++);
            desiredOperationList.add(routingOperation);
        }
        return desiredOperationList;
    }
    private Map<String, Deque<ProductionOrderRoutingOperation>> buildExistingRoutingOperationBucketMap(List<ProductionOrderRoutingOperation> existingOperationList) {
        Map<String, Deque<ProductionOrderRoutingOperation>> existingBucketMap = new LinkedHashMap<>();
        if (existingOperationList == null || existingOperationList.isEmpty()) {
            return existingBucketMap;
        }
        for (ProductionOrderRoutingOperation routingOperation : existingOperationList) {
            String bucketKey = buildRoutingOperationBucketKey(
                    routingOperation.getTechnologyOperationId(),
                    routingOperation.getProductModelId());
            existingBucketMap.computeIfAbsent(bucketKey, key -> new ArrayDeque<>()).addLast(routingOperation);
        }
        return existingBucketMap;
    }
    private ProductionOrderRoutingOperation insertRoutingOperationSnapshot(Long orderRoutingId,
                                                                           Long productionOrderId,
                                                                           ProductionOrderRoutingOperation desiredOperation) {
        ProductionOrderRoutingOperation insert = new ProductionOrderRoutingOperation();
        insert.setOrderRoutingId(orderRoutingId);
        insert.setProductionOrderId(productionOrderId);
        insert.setProductModelId(desiredOperation.getProductModelId());
        insert.setTechnologyOperationId(desiredOperation.getTechnologyOperationId());
        insert.setOperationName(desiredOperation.getOperationName());
        insert.setIsQuality(desiredOperation.getIsQuality());
        insert.setIsProduction(desiredOperation.getIsProduction());
        insert.setType(desiredOperation.getType());
        insert.setDragSort(desiredOperation.getDragSort());
        productionOrderRoutingOperationMapper.insert(insert);
        syncRoutingOperationParams(insert.getId(), productionOrderId, insert.getTechnologyOperationId());
        return insert;
    }
    private void updateRoutingOperationSnapshotIfNecessary(ProductionOrderRoutingOperation currentOperation,
                                                           Long orderRoutingId,
                                                           Long productionOrderId,
                                                           ProductionOrderRoutingOperation desiredOperation) {
        if (currentOperation == null || currentOperation.getId() == null) {
            return;
        }
        ProductionOrderRoutingOperation update = new ProductionOrderRoutingOperation();
        update.setId(currentOperation.getId());
        boolean changed = false;
        if (!Objects.equals(currentOperation.getOrderRoutingId(), orderRoutingId)) {
            update.setOrderRoutingId(orderRoutingId);
            currentOperation.setOrderRoutingId(orderRoutingId);
            changed = true;
        }
        if (!Objects.equals(currentOperation.getProductionOrderId(), productionOrderId)) {
            update.setProductionOrderId(productionOrderId);
            currentOperation.setProductionOrderId(productionOrderId);
            changed = true;
        }
        if (!Objects.equals(currentOperation.getProductModelId(), desiredOperation.getProductModelId())) {
            update.setProductModelId(desiredOperation.getProductModelId());
            currentOperation.setProductModelId(desiredOperation.getProductModelId());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getTechnologyOperationId(), desiredOperation.getTechnologyOperationId())) {
            update.setTechnologyOperationId(desiredOperation.getTechnologyOperationId());
            currentOperation.setTechnologyOperationId(desiredOperation.getTechnologyOperationId());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getOperationName(), desiredOperation.getOperationName())) {
            update.setOperationName(desiredOperation.getOperationName());
            currentOperation.setOperationName(desiredOperation.getOperationName());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getIsQuality(), desiredOperation.getIsQuality())) {
            update.setIsQuality(desiredOperation.getIsQuality());
            currentOperation.setIsQuality(desiredOperation.getIsQuality());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getIsProduction(), desiredOperation.getIsProduction())) {
            update.setIsProduction(desiredOperation.getIsProduction());
            currentOperation.setIsProduction(desiredOperation.getIsProduction());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getType(), desiredOperation.getType())) {
            update.setType(desiredOperation.getType());
            currentOperation.setType(desiredOperation.getType());
            changed = true;
        }
        if (!Objects.equals(currentOperation.getDragSort(), desiredOperation.getDragSort())) {
            update.setDragSort(desiredOperation.getDragSort());
            currentOperation.setDragSort(desiredOperation.getDragSort());
            changed = true;
        }
        if (changed) {
            productionOrderRoutingOperationMapper.updateById(update);
        }
    }
    private void removeRoutingOperationSnapshot(ProductionOrderRoutingOperation routingOperation) {
        if (routingOperation == null || routingOperation.getId() == null) {
            return;
        }
        ProductionOperationTask task = productionOperationTaskMapper.selectOne(
                Wrappers.<ProductionOperationTask>lambdaQuery()
                        .eq(ProductionOperationTask::getProductionOrderRoutingOperationId, routingOperation.getId())
                        .last("limit 1"));
        if (task != null) {
            validateTaskCanRemove(task);
            productionOperationTaskMapper.deleteById(task.getId());
        }
        productionOrderRoutingOperationParamMapper.delete(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .eq(ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, routingOperation.getId()));
        productionOrderRoutingOperationMapper.deleteById(routingOperation.getId());
    }
    private void syncRoutingOperationTasks(Long productionOrderId, List<ProductionOrderRoutingOperation> routingOperationList) {
        if (routingOperationList == null || routingOperationList.isEmpty()) {
            return;
        }
        List<Long> routingOperationIdList = routingOperationList.stream()
                .map(ProductionOrderRoutingOperation::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (routingOperationIdList.isEmpty()) {
            return;
        }
        Map<Long, ProductionOperationTask> taskByRoutingOperationId = productionOperationTaskMapper.selectList(
                        Wrappers.<ProductionOperationTask>lambdaQuery()
                                .in(ProductionOperationTask::getProductionOrderRoutingOperationId, routingOperationIdList)
                                .orderByAsc(ProductionOperationTask::getId))
                .stream()
                .filter(item -> item != null && item.getProductionOrderRoutingOperationId() != null)
                .collect(Collectors.toMap(
                        ProductionOperationTask::getProductionOrderRoutingOperationId,
                        item -> item,
                        (left, right) -> left,
                        LinkedHashMap::new));
        for (int i = 0; i < routingOperationList.size(); i++) {
            ProductionOrderRoutingOperation routingOperation = routingOperationList.get(i);
            if (routingOperation == null || routingOperation.getId() == null) {
                continue;
            }
            boolean shouldHaveTask = i == routingOperationList.size() - 1 || Boolean.TRUE.equals(routingOperation.getIsProduction());
            ProductionOperationTask existingTask = taskByRoutingOperationId.get(routingOperation.getId());
            if (shouldHaveTask) {
                if (existingTask == null) {
                    ProductionOperationTask task = new ProductionOperationTask();
                    task.setProductionOrderId(productionOrderId);
                    task.setProductionOrderRoutingOperationId(routingOperation.getId());
                    task.setPlanQuantity(BigDecimal.ZERO);
                    task.setCompleteQuantity(BigDecimal.ZERO);
                    task.setWorkOrderNo(generateNextTaskNo());
                    task.setStatus(2);
                    productionOperationTaskMapper.insert(task);
                }
                continue;
            }
            if (existingTask != null) {
                validateTaskCanRemove(existingTask);
                productionOperationTaskMapper.deleteById(existingTask.getId());
            }
        }
    }
    private void validateTaskCanRemove(ProductionOperationTask task) {
        if (task == null || task.getId() == null) {
            return;
        }
        if (defaultDecimal(task.getCompleteQuantity()).compareTo(BigDecimal.ZERO) > 0) {
            throw new ServiceException("工序已产生报工记录,无法根据 BOM å˜æ›´åˆ é™¤å¯¹åº”工序快照");
        }
        long reportCount = productionProductMainMapper.selectCount(
                Wrappers.<ProductionProductMain>lambdaQuery()
                        .eq(ProductionProductMain::getProductionOperationTaskId, task.getId()));
        if (reportCount > 0) {
            throw new ServiceException("工序已产生报工记录,无法根据 BOM å˜æ›´åˆ é™¤å¯¹åº”工单");
        }
    }
    private void syncRoutingOperationParams(Long routingOperationId, Long productionOrderId, Long technologyOperationId) {
        if (routingOperationId == null || technologyOperationId == null) {
            return;
        }
        List<TechnologyOperationParam> operationParamList = technologyOperationParamMapper.selectList(
                Wrappers.<TechnologyOperationParam>lambdaQuery()
                        .eq(TechnologyOperationParam::getTechnologyOperationId, technologyOperationId)
                        .orderByAsc(TechnologyOperationParam::getId));
        for (TechnologyOperationParam operationParam : operationParamList) {
            TechnologyParam technologyParam = technologyParamMapper.selectById(operationParam.getTechnologyParamId());
            if (technologyParam == null) {
                continue;
            }
            ProductionOrderRoutingOperationParam snapshot = new ProductionOrderRoutingOperationParam();
            snapshot.setProductionOrderId(productionOrderId);
            snapshot.setProductionOrderRoutingOperationId(routingOperationId);
            snapshot.setTechnologyOperationId(operationParam.getTechnologyOperationId());
            snapshot.setTechnologyOperationParamId(operationParam.getId());
            snapshot.setParamId(technologyParam.getId());
            snapshot.setParamCode(technologyParam.getParamCode());
            snapshot.setParamName(technologyParam.getParamName());
            snapshot.setParamType(technologyParam.getParamType());
            snapshot.setParamFormat(technologyParam.getParamFormat());
            snapshot.setUnit(technologyParam.getUnit());
            snapshot.setIsRequired(technologyParam.getIsRequired());
            snapshot.setRemark(technologyParam.getRemark());
            snapshot.setStandardValue(operationParam.getStandardValue());
            productionOrderRoutingOperationParamMapper.insert(snapshot);
        }
    }
    private Map<String, BigDecimal> buildOperationDemandedQuantityMap(List<ProductionBomStructure> structureList,
                                                                      Long rootProductModelId) {
        if (structureList == null || structureList.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, ProductionBomStructure> structureById = structureList.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructure::getId, item -> item, (left, right) -> left));
        Map<String, BigDecimal> demandedQuantityMap = new HashMap<>();
        Set<String> mergedOutputNodeKeySet = new HashSet<>();
        for (ProductionBomStructure bomStructure : structureList) {
            if (bomStructure == null || bomStructure.getTechnologyOperationId() == null) {
                continue;
            }
            // Resolve the output node first, then read the output node demand for the task plan quantity.
            ProductionBomStructure outputNode = resolveOperationOutputNode(bomStructure, structureById);
            Long outputProductModelId = resolveOutputProductModelId(outputNode, rootProductModelId);
            if (outputProductModelId == null) {
                continue;
            }
            String mergedOutputNodeKey = buildOperationOutputNodeKey(
                    bomStructure.getTechnologyOperationId(),
                    outputNode == null ? null : outputNode.getId(),
                    outputProductModelId);
            if (!mergedOutputNodeKeySet.add(mergedOutputNodeKey)) {
                continue;
            }
            // Multiple input rows can point to the same output node, so only count that output demand once.
            String key = buildOperationDemandedQuantityKey(bomStructure.getTechnologyOperationId(), outputProductModelId);
            demandedQuantityMap.merge(key, defaultDecimal(outputNode == null ? null : outputNode.getDemandedQuantity()), BigDecimal::add);
        }
        return demandedQuantityMap;
    }
    private BigDecimal resolveTaskPlanQuantity(ProductionOrderRoutingOperation routingOperation,
                                               Map<String, BigDecimal> demandedQuantityMap,
                                               BigDecimal orderQuantity,
                                               Long rootProductModelId) {
        if (routingOperation == null || demandedQuantityMap == null || demandedQuantityMap.isEmpty()) {
            return orderQuantity;
        }
        Long outputProductModelId = routingOperation.getProductModelId() != null
                ? routingOperation.getProductModelId()
                : rootProductModelId;
        String key = buildOperationDemandedQuantityKey(
                routingOperation.getTechnologyOperationId(),
                outputProductModelId);
        BigDecimal planQuantity = demandedQuantityMap.get(key);
        return planQuantity != null ? planQuantity : orderQuantity;
    }
    private String buildOperationDemandedQuantityKey(Long operationId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId);
    }
    private String buildRoutingOperationBucketKey(Long operationId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId);
    }
    private String buildBomOperationDedupKey(ProductionBomStructure bomStructure, Long outputProductModelId) {
        Long operationId = bomStructure == null ? null : bomStructure.getTechnologyOperationId();
        Long parentId = bomStructure == null ? null : bomStructure.getParentId();
        return operationId + "#" + outputProductModelId + "#" + parentId;
    }
    private String buildOperationOutputNodeKey(Long operationId, Long outputNodeId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputNodeId) + "#" + String.valueOf(outputProductModelId);
    }
    private ProductionBomStructure resolveOperationOutputNode(ProductionBomStructure bomStructure,
                                                              Map<Long, ProductionBomStructure> structureById) {
        if (bomStructure == null) {
            return null;
        }
        // The root node is the first output node; other rows use their direct parent as the current operation output.
        if (bomStructure.getParentId() == null) {
            return bomStructure;
        }
        ProductionBomStructure parent = structureById.get(bomStructure.getParentId());
        return parent != null ? parent : bomStructure;
    }
    private Long resolveOutputProductModelId(ProductionBomStructure outputNode,
                                             Long rootProductModelId) {
        if (outputNode == null) {
            return rootProductModelId;
        }
        return outputNode.getProductModelId() != null ? outputNode.getProductModelId() : rootProductModelId;
    }
    private TechnologyOperation getTechnologyOperation(Long technologyOperationId) {
        if (technologyOperationId == null) {
            return null;
        }
        return technologyOperationMapper.selectById(technologyOperationId);
    }
    private String generateNextTaskNo() {
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        ProductionOperationTask latestTask = productionOperationTaskMapper.selectOne(
                Wrappers.<ProductionOperationTask>lambdaQuery()
                        .likeRight(ProductionOperationTask::getWorkOrderNo, "GD" + datePrefix)
                        .orderByDesc(ProductionOperationTask::getWorkOrderNo)
                        .last("limit 1"));
        int sequenceNumber = 1;
        if (latestTask != null && latestTask.getWorkOrderNo() != null && latestTask.getWorkOrderNo().startsWith("GD" + datePrefix)) {
            try {
                sequenceNumber = Integer.parseInt(latestTask.getWorkOrderNo().substring(("GD" + datePrefix).length())) + 1;
            } catch (NumberFormatException ignored) {
                sequenceNumber = 1;
            }
        }
        return "GD" + String.format("%s%03d", datePrefix, sequenceNumber);
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private int compareDecimal(BigDecimal left, BigDecimal right) {
        return defaultDecimal(left).compareTo(defaultDecimal(right));
    }
    /**
     * å°†æ ‘形结构拍平成列表,便于统一保存。
     */
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
@@ -23,9 +23,11 @@
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingOperationMapper;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperation;
import com.ruoyi.production.service.ProductionOperationTaskService;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
@@ -48,6 +50,7 @@
    private final SysUserMapper sysUserMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper;
    private final FileUtil fileUtil;
@@ -61,6 +64,7 @@
        // åˆ†é¡µæŸ¥è¯¢ç”Ÿäº§å·¥åºä»»åŠ¡
        Page<ProductionOperationTaskVo> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        IPage<ProductionOperationTaskVo> result = baseMapper.pageProductionOperationTask(voPage, dto);
        fillOperationTypes(result.getRecords());
        fillUserNames(result.getRecords());
        return result;
    }
@@ -69,6 +73,7 @@
    public List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto dto) {
        // æŸ¥è¯¢å·¥åºä»»åŠ¡åˆ—è¡¨
        List<ProductionOperationTaskVo> result = BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOperationTaskVo.class);
        fillOperationTypes(result);
        fillUserNames(result);
        return result;
    }
@@ -81,6 +86,7 @@
            return null;
        }
        ProductionOperationTaskVo vo = BeanUtil.copyProperties(item, ProductionOperationTaskVo.class);
        fillOperationTypes(Collections.singletonList(vo));
        if (item.getProductionOrderId() != null) {
            ProductionOrder productionOrder = productionOrderMapper.selectById(item.getProductionOrderId());
            if (productionOrder != null) {
@@ -370,6 +376,38 @@
    @Override
    public List<ProductionOperationTaskVo> getOperation(ProductionOperationTaskDto dto) {
        // æŸ¥è¯¢å·¥åºä»»åŠ¡åˆ—è¡¨
        return baseMapper.getOperation(dto);
        List<ProductionOperationTaskVo> result = baseMapper.getOperation(dto);
        fillOperationTypes(result);
        return result;
    }
    private void fillOperationTypes(List<ProductionOperationTaskVo> voList) {
        // å›žå¡«å·¥åºç±»åž‹ï¼ˆ0 è®¡æ—¶ / 1 è®¡ä»¶ï¼‰
        if (voList == null || voList.isEmpty()) {
            return;
        }
        Set<Long> operationIds = voList.stream()
                .filter(Objects::nonNull)
                .map(ProductionOperationTaskVo::getProductionOrderRoutingOperationId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (operationIds.isEmpty()) {
            return;
        }
        Map<Long, Integer> typeByOperationId = productionOrderRoutingOperationMapper
                .selectBatchIds(new ArrayList<>(operationIds))
                .stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(
                        ProductionOrderRoutingOperation::getId,
                        ProductionOrderRoutingOperation::getType,
                        (left, right) -> left
                ));
        for (ProductionOperationTaskVo vo : voList) {
            if (vo == null || vo.getType() != null || vo.getProductionOrderRoutingOperationId() == null) {
                continue;
            }
            vo.setType(typeByOperationId.get(vo.getProductionOrderRoutingOperationId()));
        }
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -245,6 +245,18 @@
                        .eq(TechnologyRoutingOperation::getTechnologyRoutingId, technologyRouting.getId())
                        .orderByDesc(TechnologyRoutingOperation::getDragSort)
                        .orderByDesc(TechnologyRoutingOperation::getId));
        // Build task plan quantities from order BOM snapshot demand instead of recomputing from technology BOM units.
        Long rootProductModelId = orderBom != null && orderBom.getProductModelId() != null
                ? orderBom.getProductModelId()
                : productionOrder.getProductModelId();
        List<ProductionBomStructure> orderBomStructureList = orderBom == null || orderBom.getId() == null
                ? Collections.emptyList()
                : productionBomStructureMapper.selectList(
                Wrappers.<ProductionBomStructure>lambdaQuery()
                        .eq(ProductionBomStructure::getProductionOrderBomId, orderBom.getId())
                        .orderByAsc(ProductionBomStructure::getId));
        Map<String, BigDecimal> operationDemandedQuantityMap =
                buildOperationDemandedQuantityMap(orderBomStructureList, rootProductModelId);
        Map<Long, String> operationNameMap = technologyOperationMapper.selectBatchIds(
        // éåŽ†å¤„ç†æ•°æ®å¹¶ç»„è£…ç»“æžœ
                        routingOperations.stream()
@@ -278,7 +290,11 @@
                ProductionOperationTask task = new ProductionOperationTask();
                task.setProductionOrderRoutingOperationId(targetOperation.getId());
                task.setProductionOrderId(productionOrder.getId());
                task.setPlanQuantity(defaultDecimal(productionOrder.getQuantity()));
                task.setPlanQuantity(resolveTaskPlanQuantity(
                        sourceOperation,
                        operationDemandedQuantityMap,
                        productionOrder,
                        rootProductModelId));
                task.setCompleteQuantity(BigDecimal.ZERO);
                task.setWorkOrderNo(generateNextTaskNo());
                task.setStatus(2);
@@ -313,6 +329,85 @@
        return syncedParamCount;
    }
    private Map<String, BigDecimal> buildOperationDemandedQuantityMap(List<ProductionBomStructure> bomStructures,
                                                                      Long rootProductModelId) {
        if (bomStructures == null || bomStructures.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, ProductionBomStructure> structureById = bomStructures.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructure::getId, item -> item, (left, right) -> left));
        Map<String, BigDecimal> demandedQuantityMap = new HashMap<>();
        Set<String> mergedOutputNodeKeySet = new HashSet<>();
        for (ProductionBomStructure bomStructure : bomStructures) {
            if (bomStructure == null || bomStructure.getTechnologyOperationId() == null) {
                continue;
            }
            // The BOM row points to the producing operation; task quantity should come from that operation's output node.
            ProductionBomStructure outputNode = resolveOperationOutputNode(bomStructure, structureById);
            Long outputProductModelId = resolveOutputProductModelId(outputNode, rootProductModelId);
            if (outputProductModelId == null) {
                continue;
            }
            String mergedOutputNodeKey = buildOperationOutputNodeKey(
                    bomStructure.getTechnologyOperationId(),
                    outputNode == null ? null : outputNode.getId(),
                    outputProductModelId);
            if (!mergedOutputNodeKeySet.add(mergedOutputNodeKey)) {
                continue;
            }
            // demandedQuantity is already the order-level required output quantity for the current output node.
            BigDecimal demandedQuantity = defaultDecimal(outputNode == null ? null : outputNode.getDemandedQuantity());
            String key = buildOperationDemandedQuantityKey(bomStructure.getTechnologyOperationId(), outputProductModelId);
            demandedQuantityMap.merge(key, demandedQuantity, BigDecimal::add);
        }
        return demandedQuantityMap;
    }
    private BigDecimal resolveTaskPlanQuantity(TechnologyRoutingOperation sourceOperation,
                                               Map<String, BigDecimal> operationDemandedQuantityMap,
                                               ProductionOrder productionOrder,
                                               Long rootProductModelId) {
        if (sourceOperation == null || operationDemandedQuantityMap == null || operationDemandedQuantityMap.isEmpty()) {
            return defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
        }
        Long outputProductModelId = sourceOperation.getProductModelId() != null
                ? sourceOperation.getProductModelId()
                : rootProductModelId;
        String key = buildOperationDemandedQuantityKey(sourceOperation.getTechnologyOperationId(), outputProductModelId);
        BigDecimal planQuantity = operationDemandedQuantityMap.get(key);
        return planQuantity != null ? planQuantity : defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
    }
    private String buildOperationDemandedQuantityKey(Long operationId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId);
    }
    private String buildOperationOutputNodeKey(Long operationId, Long outputNodeId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputNodeId) + "#" + String.valueOf(outputProductModelId);
    }
    private ProductionBomStructure resolveOperationOutputNode(ProductionBomStructure bomStructure,
                                                              Map<Long, ProductionBomStructure> structureById) {
        if (bomStructure == null) {
            return null;
        }
        // The root node is the first output node; child rows use their direct parent as the current operation output.
        if (bomStructure.getParentId() == null) {
            return bomStructure;
        }
        ProductionBomStructure parent = structureById.get(bomStructure.getParentId());
        return parent != null ? parent : bomStructure;
    }
    private Long resolveOutputProductModelId(ProductionBomStructure outputNode,
                                             Long rootProductModelId) {
        if (outputNode == null) {
            return rootProductModelId;
        }
        return outputNode.getProductModelId() != null ? outputNode.getProductModelId() : rootProductModelId;
    }
    private ProductionOrderBom syncProductionOrderBomSnapshot(ProductionOrder productionOrder, TechnologyRouting technologyRouting) {
        // åŒæ­¥è®¢å•BOM快照结构
        if (technologyRouting.getBomId() == null) {
@@ -342,6 +437,7 @@
        productionOrderBomMapper.insert(orderBom);
        Map<Long, Long> idMap = new HashMap<>();
        BigDecimal lastProcessDemandedQuantity = orderQuantity;
        for (TechnologyBomStructure source : structureList) {
            // å­èŠ‚ç‚¹ parentId éœ€è¦æ˜ å°„成新快照节点 id,才能保留原始 BOM å±‚级。
            ProductionBomStructure target = new ProductionBomStructure();
@@ -351,10 +447,11 @@
            target.setProductModelId(source.getProductModelId());
            target.setTechnologyOperationId(source.getOperationId());
            target.setUnitQuantity(source.getUnitQuantity());
            target.setDemandedQuantity(source.getUnitQuantity().multiply(orderQuantity));
            target.setDemandedQuantity(lastProcessDemandedQuantity.multiply(source.getUnitQuantity()));
            target.setUnit(source.getUnit());
            productionBomStructureMapper.insert(target);
            idMap.put(source.getId(), target.getId());
            lastProcessDemandedQuantity = target.getDemandedQuantity();
        }
        return orderBom;
    }
@@ -788,12 +885,10 @@
                if (reportOutput == null) {
                    continue;
                }
                Long reportMainId = reportOutput.getProductionProductMainId() != null
                        ? reportOutput.getProductionProductMainId()
                        : reportOutput.getProductMainId();
                if (reportMainId == null) {
                if (reportOutput.getProductionProductMainId() == null) {
                    continue;
                }
                Long reportMainId = reportOutput.getProductionProductMainId();
                reportOutputMap.computeIfAbsent(reportMainId, k -> new ArrayList<>()).add(reportOutput);
            }
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -26,6 +26,8 @@
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.quality.mapper.*;
import com.ruoyi.quality.pojo.*;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.service.StockInventoryService;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
@@ -77,6 +79,7 @@
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final StockUtils stockUtils;
    private final StockInRecordService stockInRecordService;
    private final StockInventoryService stockInventoryService;
    @Override
@@ -291,7 +294,6 @@
            // å½“前实现按工序成品直接作为投入,后续若接入领料记录可在这里替换来源。
            ProductionProductInput productionProductInput = new ProductionProductInput();
            productionProductInput.setProductionProductMainId(productionProductMain.getId());
            productionProductInput.setProductMainId(productionProductMain.getId());
            productionProductInput.setProductModelId(item.getProductModelId());
            productionProductInput.setInputQuantity(item.getUnitQuantity().multiply(defaultDecimal(dto.getQuantity())));
            productionProductInput.setQuantity(productionProductInput.getInputQuantity());
@@ -300,13 +302,14 @@
        ProductionProductOutput productionProductOutput = new ProductionProductOutput();
        productionProductOutput.setProductionProductMainId(productionProductMain.getId());
        productionProductOutput.setProductMainId(productionProductMain.getId());
        productionProductOutput.setProductModelId(productModel.getId());
        productionProductOutput.setQuantity(defaultDecimal(dto.getQuantity()));
        productionProductOutput.setScrapQty(defaultDecimal(dto.getScrapQty()));
        productionProductOutputMapper.insert(productionProductOutput);
        BigDecimal reportQty = defaultDecimal(productionProductOutput.getQuantity());
        BigDecimal scrapQty = defaultDecimal(productionProductOutput.getScrapQty());
        BigDecimal productQty = reportQty;
        String qualifiedBatchNo = null;
        List<ProductionOrderRoutingOperation> routingOperationList = productionOrderRoutingOperationMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperation>lambdaQuery()
@@ -352,6 +355,11 @@
                stockInventoryDto.setQualitity(productQty);
                stockInventoryDto.setProductModelId(productModel.getId());
                stockInventoryService.addStockInRecordOnly(stockInventoryDto);
                qualifiedBatchNo = resolveLatestStockInBatchNo(
                        productionProductMain.getId(),
                        StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode(),
                        productModel.getId(),
                        "0");
            }
            productionOperationTask.setCompleteQuantity(defaultDecimal(productionOperationTask.getCompleteQuantity()).add(productQty));
@@ -398,9 +406,39 @@
            productionAccount.setSchedulingDate(LocalDateTime.now());
            productionAccountMapper.insert(productionAccount);
        }
        if (scrapQty.compareTo(BigDecimal.ZERO) > 0) {
            stockUtils.addUnStockWithBatchNo(
                    productModel.getId(),
                    scrapQty,
                    StockInQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode(),
                    productionProductMain.getId(),
                    qualifiedBatchNo);
        }
        return true;
    }
    private String resolveLatestStockInBatchNo(Long recordId,
                                               String recordType,
                                               Long productModelId,
                                               String stockType) {
        if (recordId == null || productModelId == null) {
            return null;
        }
        StockInRecord stockInRecord = stockInRecordService.getOne(
                Wrappers.<StockInRecord>lambdaQuery()
                        .eq(StockInRecord::getRecordId, recordId)
                        .eq(StockInRecord::getRecordType, recordType)
                        .eq(StockInRecord::getProductModelId, productModelId)
                        .eq(StockInRecord::getType, stockType)
                        .orderByDesc(StockInRecord::getId)
                        .last("limit 1"),
                false);
        if (stockInRecord == null) {
            throw new ServiceException("未找到对应的入库申请记录");
        }
        return stockInRecord.getBatchNo();
    }
    private void syncOperationParamInputValue(ProductionProductMainDto dto,
                                              Long productionOrderRoutingOperationId,
                                              Long productionProductMainId) {
src/main/java/com/ruoyi/project/system/controller/SysLoginController.java
@@ -73,17 +73,7 @@
    public AjaxResult getInfo()
    {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        SysUser user = userService.selectUserById(loginUser.getUserId());
        if (user == null)
        {
            user = loginUser.getUser();
        }
        else
        {
            loginUser.setUser(user);
            loginUser.setAiEnabled(user.getAiEnabled());
            tokenService.setLoginUser(loginUser);
        }
        SysUser user = loginUser.getUser();
        // èŽ·å–å½“å‰ç™»å½•å…¬å¸
        Long tenantId = loginUser.getTenantId();
        if(null != tenantId){
src/main/java/com/ruoyi/projectManagement/service/impl/PlanServiceImpl.java
@@ -7,6 +7,7 @@
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.service.CustomerFollowUpFileService;
import com.ruoyi.basic.utils.FileUtil;
@@ -53,13 +54,13 @@
    @Transactional(rollbackFor = Exception.class)
    public void savePlan(SavePlanVo savePlanVo) {
        Plan plan = BeanUtil.copyProperties(savePlanVo, Plan.class);
        // é™„件处理
        fileUtil.saveStorageAttachmentByRecordTypeAndRecordId(null, RecordTypeEnum.PLAN, savePlanVo.getId(), savePlanVo.getStorageBlobDTOs());
        if (savePlanVo.getId() == null) {
            planMapper.insert(plan);
        } else {
            planMapper.updateById(plan);
        }
        // é™„件处理
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.PLAN, plan.getId(), savePlanVo.getStorageBlobDTOs());
        planService.savePlanNode(plan.getId(), savePlanVo.getSavePlanNodeList());
    }
src/main/java/com/ruoyi/projectManagement/service/impl/handle/InfoStageHandleService.java
@@ -5,6 +5,7 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.service.CustomerFollowUpFileService;
import com.ruoyi.basic.utils.FileUtil;
@@ -48,18 +49,12 @@
    @Transactional
    public void save(@NotNull SaveInfoStageVo saveInfoStageVo) {
        InfoStage infoStage = BeanUtil.copyProperties(saveInfoStageVo, InfoStage.class);
        // é™„件处理
        String attachmentIds = StrUtil.join(",", Optional.ofNullable(saveInfoStageVo.getAttachmentIds()).orElse(Collections.emptyList()));
        infoStage.setAttachment(attachmentIds);
        fileUtil.saveStorageAttachmentByRecordTypeAndRecordId(null, RecordTypeEnum.INFO_STAGE, infoStage.getProjectManagementInfoId(), saveInfoStageVo.getStorageBlobDTOs());
        if (infoStage.getId() == null) {
            infoStageMapper.insert(infoStage);
        } else {
            infoStageMapper.updateById(infoStage);
        }
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.INFO_STAGE, infoStage.getId(), saveInfoStageVo.getStorageBlobDTOs());
        infoStageHandleService.syncInfoStage(infoStage.getProjectManagementInfoId());
    }
src/main/java/com/ruoyi/purchase/controller/PurchaseReturnOrdersController.java
@@ -8,9 +8,13 @@
import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.purchase.service.PurchaseReturnOrdersService;
import com.ruoyi.purchase.vo.PurchaseStockInProductVo;
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>
@@ -56,5 +60,12 @@
        return AjaxResult.success();
    }
    @GetMapping("/getByPurchaseLedgerId")
    @Operation(summary = "采购退货-根据采购订单id查询采购订单对应的入库产品信息")
    public AjaxResult getByPurchaseLedgerId(Long purchaseLedgerId) {
        List<PurchaseStockInProductVo> purchaseStockInProductVos = purchaseReturnOrdersService.getByPurchaseLedgerId(purchaseLedgerId);
        return AjaxResult.success(purchaseStockInProductVos);
    }
}
src/main/java/com/ruoyi/purchase/dto/PurchaseReturnOrderProductsDto.java
@@ -9,5 +9,7 @@
    private String productName;
    private String model;
    private String unit;
    //批次号
    private String batchNo;
}
src/main/java/com/ruoyi/purchase/dto/SimpleReturnOrderGroupDto.java
@@ -16,6 +16,6 @@
@AllArgsConstructor
@NoArgsConstructor
public class SimpleReturnOrderGroupDto implements Serializable {
    private Long salesLedgerProductId;
    private Long productModelId;
    private BigDecimal sumReturnQuantity;
}
src/main/java/com/ruoyi/purchase/mapper/PurchaseReturnOrdersMapper.java
@@ -1,16 +1,20 @@
package com.ruoyi.purchase.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.account.bean.dto.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.PurchaseReturnVo;
import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.purchase.dto.PurchaseReturnOrderHasAllInfoDto;
import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
import com.ruoyi.purchase.vo.PurchaseReturnOrderProductsDetailVo;
import com.ruoyi.purchase.vo.PurchaseStockInProductVo;
import jakarta.validation.constraints.NotNull;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
@@ -27,4 +31,9 @@
    PurchaseReturnOrderHasAllInfoDto getPurchaseReturnOrderHasAllInfoById(@Param("id") @NotNull Long id);
    IPage<PurchaseReturnVo> listPageAccountPurchaseReturn(Page page, @Param("req") PurchaseReturnDto purchaseReturnDto);
    //根据采购订单id查询采购订单对应的入库产品信息
    List<PurchaseStockInProductVo> getByPurchaseLedgerId(@Param("purchaseLedgerId") Long purchaseLedgerId);
    List<PurchaseReturnOrderProductsDetailVo> getPurchaseReturnOrderProductsDetailById(@Param("id") Long id);
}
src/main/java/com/ruoyi/purchase/pojo/ProductRecord.java
@@ -5,6 +5,7 @@
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@@ -16,7 +17,7 @@
 */
@Data
@TableName("product_record")
public class ProductRecord {
public class ProductRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
src/main/java/com/ruoyi/purchase/pojo/PurchaseReturnOrderProducts.java
@@ -1,16 +1,13 @@
package com.ruoyi.purchase.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 java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
@@ -34,7 +31,7 @@
    @Schema(description = "退货单id")
    private Long purchaseReturnOrderId;
    @Schema(description = "采购产品id")
    @Schema(description = "销售台账产品id")
    private Long salesLedgerProductId;
    @Schema(description = "退货数量")
@@ -54,4 +51,7 @@
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @Schema(description = "关联入库单id")
    private Long stockInRecordId;
}
src/main/java/com/ruoyi/purchase/pojo/SalesLedgerProductTemplate.java
@@ -69,10 +69,10 @@
    private Integer type;
    @Schema(description = "产品id")
    private Integer productId;
    private Long productId;
    @Schema(description = "型号id")
    private Integer productModelId;
    private Long productModelId;
    private String register;
src/main/java/com/ruoyi/purchase/service/PurchaseReturnOrdersService.java
@@ -8,7 +8,10 @@
import com.ruoyi.purchase.vo.PurchaseReturnDetailsVo;
import com.ruoyi.purchase.dto.PurchaseReturnOrderHasAllInfoDto;
import com.ruoyi.purchase.vo.PurchaseStockInProductVo;
import jakarta.validation.constraints.NotNull;
import java.util.List;
/**
 * <p>
@@ -27,4 +30,7 @@
    PurchaseReturnDetailsVo getPurchaseReturnOrderDtoById(@NotNull Long id);
    void deleteById(@NotNull Long id);
    List<PurchaseStockInProductVo> getByPurchaseLedgerId(Long purchaseLedgerId);
}
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java
@@ -1,7 +1,6 @@
package com.ruoyi.purchase.service.impl;
import cn.hutool.core.bean.BeanUtil;
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;
@@ -9,36 +8,32 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.pojo.AccountIncome;
import com.ruoyi.account.service.AccountIncomeService;
import com.ruoyi.common.enums.SaleEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
import com.ruoyi.purchase.dto.PurchaseReturnOrderHasAllInfoDto;
import com.ruoyi.purchase.dto.PurchaseReturnOrderProductsDto;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.mapper.PurchaseReturnOrderProductsMapper;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.purchase.pojo.PurchaseReturnOrderProducts;
import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
import com.ruoyi.purchase.service.PurchaseReturnOrdersService;
import com.ruoyi.purchase.vo.PurchaseReturnDetailsVo;
import com.ruoyi.purchase.vo.PurchaseReturnOrderProductsDetailVo;
import com.ruoyi.purchase.vo.PurchaseStockInProductVo;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.purchase.dto.PurchaseReturnOrderHasAllInfoDto;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.service.ISalesLedgerService;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import com.ruoyi.stock.pojo.StockOutRecord;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
 * <p>
@@ -76,13 +71,13 @@
                purchaseReturnOrderProductsDto.setSalesLedgerProductId(purchaseReturnOrderProductsDto.getSalesLedgerProductId());
                purchaseReturnOrderProductsDto.setPurchaseReturnOrderId(purchaseReturnOrderDto.getId());
                purchaseReturnOrderProductsDto.setReturnQuantity(purchaseReturnOrderProductsDto.getReturnQuantity());
                purchaseReturnOrderProductsDto.setStockInRecordId(purchaseReturnOrderProductsDto.getStockInRecordId());
                // è¿™é‡Œä¸ºæ–°å¢žå› æ­¤id为null
                purchaseReturnOrderProductsDto.setId(null);
                purchaseReturnOrderProductsMapper.insert(purchaseReturnOrderProductsDto);
                //库存需要出库(采购退货)
                PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectById(purchaseReturnOrderDto.getPurchaseLedgerId());
                //库存需要出库(采购退货,带批次号)
                SalesLedgerProduct salesLedgerProduct = salesLedgerProductMapper.selectById(purchaseReturnOrderProductsDto.getSalesLedgerProductId());
                stockUtils.substractStock(salesLedgerProduct.getProductModelId(), purchaseReturnOrderProductsDto.getReturnQuantity(), StockOutQualifiedRecordTypeEnum.PURCHASE_RETURN_STOCK_OUT.getCode(), purchaseReturnOrderDto.getId(), purchaseLedger.getPurchaseContractNumber()+"-"+salesLedgerProduct.getId());
                stockUtils.substractStock(salesLedgerProduct.getProductModelId(), purchaseReturnOrderProductsDto.getReturnQuantity(), StockOutQualifiedRecordTypeEnum.PURCHASE_RETURN_STOCK_OUT.getCode(), purchaseReturnOrderProductsDto.getId(), purchaseReturnOrderProductsDto.getBatchNo());
            }
        }else {
            throw new RuntimeException("请选择退货商品");
@@ -107,25 +102,12 @@
    @Override
    public PurchaseReturnDetailsVo getPurchaseReturnOrderDtoById(Long id) {
        //查主体
        PurchaseReturnOrderHasAllInfoDto purchaseReturnOrders = purchaseReturnOrdersMapper.getPurchaseReturnOrderHasAllInfoById(id);
        PurchaseReturnDetailsVo purchaseReturnOrderDto = BeanUtil.copyProperties(purchaseReturnOrders, PurchaseReturnDetailsVo.class);
        // æŸ¥è¯¢å‡ºä»–具体对应的退货
        LambdaQueryWrapper<PurchaseReturnOrderProducts> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(PurchaseReturnOrderProducts::getPurchaseReturnOrderId, purchaseReturnOrders.getId());
        List<PurchaseReturnOrderProducts> purchaseReturnOrderProducts = purchaseReturnOrderProductsMapper.selectList(queryWrapper);
        List<PurchaseReturnDetailsVo.PurchaseReturnOrderProductsDetailVo> purchaseReturnOrderProductsDetailVos = BeanUtil.copyToList(purchaseReturnOrderProducts, PurchaseReturnDetailsVo.PurchaseReturnOrderProductsDetailVo.class);
        // æŸ¥è¯¢å‡ºå¯¹åº”的商品信息
        List<Long> productIds = purchaseReturnOrderProductsDetailVos.stream().map(PurchaseReturnDetailsVo.PurchaseReturnOrderProductsDetailVo::getSalesLedgerProductId).distinct().filter(Objects::nonNull).collect(Collectors.toList());
        List<SalesLedgerProduct> salesLedgerProducts = salesLedgerService.getSalesLedgerProductListByIds(productIds, SaleEnum.PURCHASE);
        Map<Long, SalesLedgerProduct> productmap = salesLedgerProducts.stream().collect(Collectors.toMap(SalesLedgerProduct::getId, product -> product));
        purchaseReturnOrderProductsDetailVos.forEach(purchaseReturnOrderProductsDetailVo -> {
            purchaseReturnOrderProductsDetailVo.setSalesLedgerProduct(productmap.get(purchaseReturnOrderProductsDetailVo.getSalesLedgerProductId()));
        });
        //查明细
        List<PurchaseReturnOrderProductsDetailVo> purchaseReturnOrderProductsDetailVos = purchaseReturnOrdersMapper.getPurchaseReturnOrderProductsDetailById(id);
        purchaseReturnOrderDto.setPurchaseReturnOrderProductsDetailVoList(purchaseReturnOrderProductsDetailVos);
        return purchaseReturnOrderDto;
    }
@@ -133,13 +115,14 @@
    @Transactional
    public void deleteById(Long id) {
        purchaseReturnOrdersMapper.deleteById(id);
        List<PurchaseReturnOrderProducts> purchaseReturnOrderProducts = purchaseReturnOrderProductsMapper.selectList(Wrappers.<PurchaseReturnOrderProducts>lambdaQuery().eq(PurchaseReturnOrderProducts::getPurchaseReturnOrderId, id));
        LambdaUpdateWrapper<PurchaseReturnOrderProducts> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(PurchaseReturnOrderProducts::getPurchaseReturnOrderId, id);
        purchaseReturnOrderProductsMapper.delete(updateWrapper);
        //(采购退货的数据需要删掉)
        stockOutRecordMapper.delete(Wrappers.<StockOutRecord>lambdaQuery()
                .eq(StockOutRecord::getRecordType,StockOutQualifiedRecordTypeEnum.PURCHASE_RETURN_STOCK_OUT.getCode())
                .eq(StockOutRecord::getRecordId, id));
        purchaseReturnOrderProducts.stream().forEach(purchaseReturnOrderProducts1 -> {
            stockUtils.deleteStockOutRecord(purchaseReturnOrderProducts1.getId(),StockOutQualifiedRecordTypeEnum.PURCHASE_RETURN_STOCK_OUT.getCode());
        });
        // è´¢åŠ¡
        LambdaUpdateWrapper<AccountIncome> updateWrapperAccountIncome = new LambdaUpdateWrapper<>();
        updateWrapperAccountIncome.eq(AccountIncome::getBusinessId, id);
@@ -147,4 +130,9 @@
        updateWrapperAccountIncome.eq(AccountIncome::getIncomeType, 4);
        accountIncomeService.remove(updateWrapperAccountIncome);
    }
    @Override
    public List<PurchaseStockInProductVo> getByPurchaseLedgerId(Long purchaseLedgerId) {
        return purchaseReturnOrdersMapper.getByPurchaseLedgerId(purchaseLedgerId);
    }
}
src/main/java/com/ruoyi/purchase/vo/PurchaseReturnDetailsVo.java
@@ -1,14 +1,12 @@
package com.ruoyi.purchase.vo;
import com.ruoyi.purchase.dto.PurchaseReturnOrderHasAllInfoDto;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -24,15 +22,4 @@
    private List<PurchaseReturnOrderProductsDetailVo> purchaseReturnOrderProductsDetailVoList;
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class PurchaseReturnOrderProductsDetailVo implements Serializable {
        private Long id;
        private BigDecimal returnQuantity;
        private Long salesLedgerProductId;
        private Long purchaseReturnOrderId;
        private SalesLedgerProduct salesLedgerProduct;
    }
}
src/main/java/com/ruoyi/purchase/vo/PurchaseReturnOrderProductsDetailVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package com.ruoyi.purchase.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PurchaseReturnOrderProductsDetailVo  {
    @Schema(description = "退货明细id")
    private Long id;
    @Schema(description = "销售台账的产品id")
    private Long salesLedgerProductId;
    @Schema(description = "产品规格id")
    private Long productModelId;
    @Schema(description = "产品大类")
    private String productCategory;
    @Schema(description = "规格型号")
    private String specificationModel;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "入库单号")
    private String inboundBatches;
    @Schema(description = "入库数量")
    private BigDecimal stockInNum;
    @Schema(description = "批次号")
    private String batchNo;
    @Schema(description = "未退货数")
    private BigDecimal unQuantity;
    @Schema(description = "已退货数量")
    private BigDecimal totalReturnNum;
    @Schema(description = "含税单价")
    private BigDecimal taxInclusiveUnitPrice;
    @Schema(description = "退货数量")
    private BigDecimal returnQuantity;
    @Schema(description = "退货单id")
    private Long purchaseReturnOrderId;
    @Schema(description = "是否质检")
    private Boolean isChecked;
}
src/main/java/com/ruoyi/purchase/vo/PurchaseStockInProductVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package com.ruoyi.purchase.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(name = "PurchaseStockInProductVo", description = "采购管理--采购订单下入库产品列表")
public class PurchaseStockInProductVo {
    @Schema(description = "入库单id")
    private Long id;
    @Schema(description = "销售台账的产品id")
    private Long salesLedgerProductId;
    @Schema(description = "产品规格id")
    private Long productModelId;
    @Schema(description = "产品大类")
    private String productCategory;
    @Schema(description = "规格型号")
    private String specificationModel;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "入库单号")
    private String inboundBatches;
    @Schema(description = "入库数量")
    private BigDecimal stockInNum;
    @Schema(description = "批次号")
    private String batchNo;
    @Schema(description = "可退货数")
    private BigDecimal unQuantity;
    @Schema(description = "退货总数")
    private BigDecimal totalReturnNum;
    @Schema(description = "含税单价")
    private BigDecimal taxInclusiveUnitPrice;
    @Schema(description = "是否质检")
    private Boolean isChecked;
}
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -96,9 +96,17 @@
    /**
     * æ•°é‡
     */
    @Excel(name = "数量")
    @Excel(name = "总数量")
    private BigDecimal quantity;
    @Excel(name = "合格数量")
    @TableField("qualified_quantity")
    private BigDecimal qualifiedQuantity;
    @Excel(name = "不合格数量")
    @TableField("unqualified_quantity")
    private BigDecimal unqualifiedQuantity;
    /**
     * æ£€æµ‹å•位
     */
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -1,6 +1,7 @@
package com.ruoyi.quality.service.impl;
import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -20,6 +21,8 @@
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.pojo.QualityUnqualified;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.quality.service.IQualityInspectParamService;
import com.ruoyi.quality.service.IQualityInspectService;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
@@ -33,7 +36,9 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
@@ -45,6 +50,7 @@
    private final StockUtils stockUtils;
    private final StockInventoryService stockInventoryService;
    private final StockInRecordService stockInRecordService;
    private QualityInspectMapper qualityInspectMapper;
    private IQualityInspectParamService qualityInspectParamService;
@@ -88,18 +94,10 @@
        if (ObjectUtils.isNull(qualityInspect.getCheckResult())) {
            throw new RuntimeException("请先判断是否合格");
        }
        /*判断不合格*/
        if (qualityInspect.getCheckResult().equals("不合格")) {
            QualityUnqualified qualityUnqualified = new QualityUnqualified();
            BeanUtils.copyProperties(qualityInspect, qualityUnqualified);
            qualityUnqualified.setInspectState(0);//待处理
            qualityUnqualified.setProductModelId(qualityInspect.getProductModelId());
            List<QualityInspectParam> inspectParams = qualityInspectParamService.list(Wrappers.<QualityInspectParam>lambdaQuery().eq(QualityInspectParam::getInspectId, inspect.getId()));
            String text = inspectParams.stream().map(QualityInspectParam::getParameterItem).collect(Collectors.joining(","));
            qualityUnqualified.setDefectivePhenomena(text + "这些指标中存在不合格");//不合格现象
            qualityUnqualified.setInspectId(qualityInspect.getId());
            qualityUnqualifiedMapper.insert(qualityUnqualified);
        } else {
        // åŒºåˆ†åˆæ ¼æ•°é‡ä»¥åŠä¸åˆæ ¼å¤„理进行对应的处理
        Assert.isTrue(qualityInspect.getQuantity().compareTo(qualityInspect.getQualifiedQuantity().add(qualityInspect.getUnqualifiedQuantity())) == 0,"请检查合格数量和不合格数量,需要合格数量+不合格数量与总数保持一致");
        if(qualityInspect.getQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0){
            //合格直接入库
            // stockUtils.addStock(qualityInspect.getProductModelId(), qualityInspect.getQuantity(), StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId());
            //仅添加入库记录
@@ -111,13 +109,66 @@
            }
            stockInventoryDto.setRecordId(qualityInspect.getId());
            stockInventoryDto.setProductModelId(qualityInspect.getProductModelId());
            stockInventoryDto.setQualitity(qualityInspect.getQuantity());
            stockInventoryDto.setQualitity(qualityInspect.getQualifiedQuantity());
            stockInventoryDto.setBatchNo(resolveProductionBatchNo(
                    qualityInspect.getProductMainId(),
                    qualityInspect.getId(),
                    qualityInspect.getProductModelId()));
            stockInventoryService.addStockInRecordOnly(stockInventoryDto);
        }
        if(qualityInspect.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) > 0){
            QualityUnqualified qualityUnqualified = new QualityUnqualified();
            BeanUtils.copyProperties(qualityInspect, qualityUnqualified);
            qualityUnqualified.setInspectState(0);//待处理
            qualityUnqualified.setQuantity(qualityInspect.getUnqualifiedQuantity());
            List<QualityInspectParam> inspectParams = qualityInspectParamService.list(Wrappers.<QualityInspectParam>lambdaQuery().eq(QualityInspectParam::getInspectId, inspect.getId()));
            String text = inspectParams.stream().map(QualityInspectParam::getParameterItem).collect(Collectors.joining(","));
            qualityUnqualified.setDefectivePhenomena(text + "这些指标中存在不合格");//不合格现象
            qualityUnqualified.setInspectId(qualityInspect.getId());
            qualityUnqualifiedMapper.insert(qualityUnqualified);
        }
        qualityInspect.setInspectState(1);//已提交
        return qualityInspectMapper.updateById(qualityInspect);
    }
    private String resolveProductionBatchNo(Long productionProductMainId,
                                            Long qualityInspectId,
                                            Long productModelId) {
        if (productModelId == null) {
            return null;
        }
        if (productionProductMainId != null) {
            StockInRecord productionRecord = stockInRecordService.getOne(
                    Wrappers.<StockInRecord>lambdaQuery()
                            .eq(StockInRecord::getRecordId, productionProductMainId)
                            .eq(StockInRecord::getProductModelId, productModelId)
                            .in(StockInRecord::getRecordType, Arrays.asList(
                                    StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode(),
                                    StockInQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode()))
                            .isNotNull(StockInRecord::getBatchNo)
                            .orderByDesc(StockInRecord::getId)
                            .last("limit 1"),
                    false);
            if (productionRecord != null) {
                return productionRecord.getBatchNo();
            }
        }
        if (qualityInspectId == null) {
            return null;
        }
        StockInRecord inspectRecord = stockInRecordService.getOne(
                Wrappers.<StockInRecord>lambdaQuery()
                        .eq(StockInRecord::getRecordId, qualityInspectId)
                        .eq(StockInRecord::getProductModelId, productModelId)
                        .eq(StockInRecord::getRecordType, StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode())
                        .isNotNull(StockInRecord::getBatchNo)
                        .orderByDesc(StockInRecord::getId)
                        .last("limit 1"),
                false);
        return inspectRecord == null ? null : inspectRecord.getBatchNo();
    }
    /*生成检验报告*/
    @Override
    public void down(HttpServletResponse response, QualityInspect qualityInspect) {
src/main/java/com/ruoyi/quality/service/impl/QualityUnqualifiedServiceImpl.java
@@ -23,6 +23,8 @@
import com.ruoyi.quality.mapper.QualityUnqualifiedMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityUnqualified;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.quality.service.IQualityInspectService;
import com.ruoyi.quality.service.IQualityUnqualifiedService;
import com.ruoyi.stock.service.StockUninventoryService;
@@ -33,6 +35,7 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -50,6 +53,7 @@
    private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final StockUninventoryService stockUninventoryService;
    private final StockInRecordService stockInRecordService;
    @Override
    public IPage<QualityUnqualified> qualityUnqualifiedListPage(Page page, QualityUnqualified qualityUnqualified) {
@@ -67,6 +71,8 @@
    public int deal(QualityUnqualified qualityUnqualified) {
        QualityUnqualified unqualified = qualityUnqualifiedMapper.selectById(qualityUnqualified.getId());
        QualityInspect qualityInspect = qualityInspectService.getById(unqualified.getInspectId());
        String batchNo = qualityInspect == null ? null
                : resolveProductionBatchNo(qualityInspect.getProductMainId(), qualityInspect.getId(), qualityInspect.getProductModelId());
        if (ObjectUtils.isNotNull(qualityInspect) && qualityInspect.getInspectType() != 0) {
            switch (qualityUnqualified.getDealResult()) {
                case "返修":
@@ -80,12 +86,12 @@
                    }
                    break;
                case "报废":
                    stockUtils.addUnStock(qualityInspect.getProductModelId(), unqualified.getQuantity(),
                            StockInQualifiedRecordTypeEnum.DEFECTIVE_SCRAP.getCode(), unqualified.getId());
                    stockUtils.addUnStockWithBatchNo(qualityInspect.getProductModelId(), unqualified.getQuantity(),
                            StockInQualifiedRecordTypeEnum.DEFECTIVE_SCRAP.getCode(), unqualified.getId(), batchNo);
                    break;
                case "让步放行":
                    stockUtils.addStock(qualityInspect.getProductModelId(), unqualified.getQuantity(),
                            StockInQualifiedRecordTypeEnum.DEFECTIVE_PASS.getCode(), unqualified.getId());
                    stockUtils.addStockWithBatchNo(qualityInspect.getProductModelId(), unqualified.getQuantity(),
                            StockInQualifiedRecordTypeEnum.DEFECTIVE_PASS.getCode(), unqualified.getId(), batchNo);
                    break;
                default:
                    break;
@@ -219,4 +225,41 @@
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private String resolveProductionBatchNo(Long productionProductMainId,
                                            Long qualityInspectId,
                                            Long productModelId) {
        if (productModelId == null) {
            return null;
        }
        if (productionProductMainId != null) {
            StockInRecord productionRecord = stockInRecordService.getOne(
                    Wrappers.<StockInRecord>lambdaQuery()
                            .eq(StockInRecord::getRecordId, productionProductMainId)
                            .eq(StockInRecord::getProductModelId, productModelId)
                            .in(StockInRecord::getRecordType, Arrays.asList(
                                    StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode(),
                                    StockInQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode()))
                            .isNotNull(StockInRecord::getBatchNo)
                            .orderByDesc(StockInRecord::getId)
                            .last("limit 1"),
                    false);
            if (productionRecord != null) {
                return productionRecord.getBatchNo();
            }
        }
        if (qualityInspectId == null) {
            return null;
        }
        StockInRecord inspectRecord = stockInRecordService.getOne(
                Wrappers.<StockInRecord>lambdaQuery()
                        .eq(StockInRecord::getRecordId, qualityInspectId)
                        .eq(StockInRecord::getProductModelId, productModelId)
                        .eq(StockInRecord::getRecordType, StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode())
                        .isNotNull(StockInRecord::getBatchNo)
                        .orderByDesc(StockInRecord::getId)
                        .last("limit 1"),
                false);
        return inspectRecord == null ? null : inspectRecord.getBatchNo();
    }
}
src/main/java/com/ruoyi/sales/controller/SalesLedgerProductController.java
@@ -71,9 +71,9 @@
        if (CollUtil.isEmpty(list)) {
            return AjaxResult.success(list);
        }
        List<Long> productIds = list.stream().map(SalesLedgerProduct::getId).collect(Collectors.toList());
        List<Long> productIds = list.stream().map(SalesLedgerProduct::getProductModelId).collect(Collectors.toList());
        List<SimpleReturnOrderGroupDto> groupListByProductIds = purchaseReturnOrderProductsMapper.getReturnOrderGroupListByProductIds(productIds);
        Map<Long, BigDecimal> returnOrderGroupDtoMap = groupListByProductIds.stream().collect(Collectors.toMap(SimpleReturnOrderGroupDto::getSalesLedgerProductId, item -> item.getSumReturnQuantity()));
        Map<Long, BigDecimal> returnOrderGroupDtoMap = groupListByProductIds.stream().collect(Collectors.toMap(SimpleReturnOrderGroupDto::getProductModelId, item -> item.getSumReturnQuantity()));
        list.forEach(item -> {
            if (item.getFutureTickets().compareTo(BigDecimal.ZERO) == 0) {
@@ -90,7 +90,7 @@
                }
            }
            // ç»Ÿè®¡é€€è´§æ•°é‡
            BigDecimal returnQuality = returnOrderGroupDtoMap.getOrDefault(item.getId(), BigDecimal.ZERO);
            BigDecimal returnQuality = returnOrderGroupDtoMap.getOrDefault(item.getProductModelId(), BigDecimal.ZERO);
            item.setReturnQuality(returnQuality);
            item.setAvailableQuality(item.getQuantity().subtract(returnQuality));
        });
src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java
@@ -116,7 +116,7 @@
    @GetMapping("/getByCustomerName")
    @Operation(summary = "通过客户名称查询")
    @Operation(summary = "通过客户名称查询关联的发货单号")
    public AjaxResult getByCustomerName(String customerName) {
        return AjaxResult.success(shippingInfoService.getShippingInfoByCustomerName(customerName));
    }
src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java
@@ -8,6 +8,7 @@
import com.ruoyi.sales.pojo.ShippingProductDetail;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -40,6 +41,11 @@
    private List<Long> batchNo;
    private List<ShippingProductDetail> batchNoDetailList;
    //关联的出库单号
    private String outboundBatches;
    //发货数量
    private BigDecimal totalQuantity;
}
src/main/java/com/ruoyi/sales/mapper/ShippingInfoMapper.java
@@ -3,7 +3,7 @@
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.sales.dto.SalesLedgerProductDto;
import com.ruoyi.procurementrecord.bean.vo.ShippingProductVo;
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.pojo.ShippingInfo;
import org.apache.ibatis.annotations.Param;
@@ -19,7 +19,7 @@
    List<ShippingInfo> listAll();
    List<SalesLedgerProductDto> getReturnManagementDtoById(@Param("shippingId")Long shippingId);
    List<ShippingProductVo> getReturnManagementDtoById(@Param("shippingId")Long shippingId);
    List<ShippingInfo> getShippingInfoByCustomerName(String customerName);
}
src/main/java/com/ruoyi/sales/pojo/SalesQuotationProduct.java
@@ -14,7 +14,12 @@
    private Long id;
    @Schema(description = "销售报价单id")
    private Long salesQuotationId;
    @Schema(description = "产品Id")
    @TableField(value = "product_id")
    private Long productId;
    @Schema(description = "产品规格Id")
    @TableField(value = "product_model_id")
    private Long productModelId;
    @Schema(description = "商品名称")
    private String product;
    @Schema(description = "商品规格")
src/main/java/com/ruoyi/sales/service/ShippingInfoService.java
@@ -3,7 +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.sales.dto.SalesLedgerProductDto;
import com.ruoyi.procurementrecord.bean.vo.ShippingProductVo;
import com.ruoyi.sales.dto.ShippingApproveDto;
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.dto.ShippingProductDetailDto;
@@ -22,7 +22,7 @@
    boolean delete(List<Long> ids);
    List<SalesLedgerProductDto> getReturnManagementDtoById( Long shippingId);
    List<ShippingProductVo> getReturnManagementDtoById(Long shippingId);
    List<ShippingInfo> getShippingInfoByCustomerName(String customerName);
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -9,10 +9,13 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.service.AccountIncomeService;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.SaleEnum;
import com.ruoyi.common.exception.base.BaseException;
@@ -102,6 +105,8 @@
    private final ProductionProductInputMapper productionProductInputMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final RedisTemplate<String, String> redisTemplate;
    private final FileUtil fileUtil;
    @Autowired
    private SysDeptMapper sysDeptMapper;
    @Value("${file.upload-dir}")
@@ -142,14 +147,14 @@
        List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(productWrapper);
        if (type.equals(SaleEnum.PURCHASE)) {
            // æŸ¥è¯¢é€€è´§ä¿¡æ¯
            List<Long> productIds = salesLedgerProducts.stream().map(SalesLedgerProduct::getId).collect(Collectors.toList());
            List<Long> productIds = salesLedgerProducts.stream().map(SalesLedgerProduct::getProductModelId).collect(Collectors.toList());
            List<SimpleReturnOrderGroupDto> groupListByProductIds = new ArrayList<>();
            if(CollectionUtils.isNotEmpty(productIds)){
                groupListByProductIds = purchaseReturnOrderProductsMapper.getReturnOrderGroupListByProductIds(productIds);
            }
            Map<Long, BigDecimal> returnOrderGroupDtoMap = groupListByProductIds.stream().collect(Collectors.toMap(SimpleReturnOrderGroupDto::getSalesLedgerProductId, SimpleReturnOrderGroupDto::getSumReturnQuantity));
            Map<Long, BigDecimal> returnOrderGroupDtoMap = groupListByProductIds.stream().collect(Collectors.toMap(SimpleReturnOrderGroupDto::getProductModelId, SimpleReturnOrderGroupDto::getSumReturnQuantity));
            salesLedgerProducts.forEach(item -> {
                BigDecimal returnQuality = returnOrderGroupDtoMap.getOrDefault(item.getId(), BigDecimal.ZERO);
                BigDecimal returnQuality = returnOrderGroupDtoMap.getOrDefault(item.getProductModelId(), BigDecimal.ZERO);
                item.setReturnQuality(returnQuality);
                item.setAvailableQuality(item.getQuantity().subtract(returnQuality));
            });
@@ -582,7 +587,6 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int addOrUpdateSalesLedger(SalesLedgerDto salesLedgerDto) {
        try {
            // 1. æ ¡éªŒå®¢æˆ·ä¿¡æ¯
            Customer customer = customerMapper.selectById(salesLedgerDto.getCustomerId());
            if (customer == null) {
@@ -602,6 +606,7 @@
                salesLedgerMapper.updateById(salesLedger);
            }
            // 4. å¤„理子表数据
            List<SalesLedgerProduct> productList = salesLedgerDto.getProductData();
            if (productList != null && !productList.isEmpty()) {
@@ -615,92 +620,11 @@
                );
            }
            // 5. è¿ç§»ä¸´æ—¶æ–‡ä»¶åˆ°æ­£å¼ç›®å½•
            if (salesLedgerDto.getTempFileIds() != null && !salesLedgerDto.getTempFileIds().isEmpty()) {
                migrateTempFilesToFormal(salesLedger.getId(), salesLedgerDto.getTempFileIds());
            }
        // 5. ä¿å­˜é”€å”®å°è´¦é™„ä»¶
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.SALES_LEDGER, salesLedger.getId(), salesLedgerDto.getStorageBlobDTOs());
            return 1;
        } catch (IOException e) {
            throw new BaseException("文件迁移失败: " + e.getMessage());
        }
    }
    /**
     * å°†ä¸´æ—¶æ–‡ä»¶è¿ç§»åˆ°æ­£å¼ç›®å½•
     *
     * @param businessId  ä¸šåŠ¡ID(销售台账ID)
     * @param tempFileIds ä¸´æ—¶æ–‡ä»¶ID列表
     * @throws IOException æ–‡ä»¶æ“ä½œå¼‚常
     */
    private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
        if (CollectionUtils.isEmpty(tempFileIds)) {
            return;
        }
        // æž„建正式目录路径(按业务类型和日期分组)
        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
        Path formalDirPath = Paths.get(formalDir);
        // ç¡®ä¿æ­£å¼ç›®å½•存在(递归创建)
        if (!Files.exists(formalDirPath)) {
            Files.createDirectories(formalDirPath);
        }
        for (String tempFileId : tempFileIds) {
            // æŸ¥è¯¢ä¸´æ—¶æ–‡ä»¶è®°å½•
            TempFile tempFile = tempFileMapper.selectById(tempFileId);
            if (tempFile == null) {
                log.warn("临时文件不存在,跳过处理: {}", tempFileId);
                continue;
            }
            // æž„建正式文件名(包含业务ID和时间戳,避免冲突)
            String originalFilename = tempFile.getOriginalName();
            String fileExtension = FilenameUtils.getExtension(originalFilename);
            String formalFilename = businessId + "_" +
                    System.currentTimeMillis() + "_" +
                    UUID.randomUUID().toString().substring(0, 8) +
                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
            Path formalFilePath = formalDirPath.resolve(formalFilename);
            try {
                // æ‰§è¡Œæ–‡ä»¶è¿ç§»ï¼ˆä½¿ç”¨åŽŸå­æ“ä½œç¡®ä¿å®‰å…¨æ€§ï¼‰
//                Files.move(
//                        Paths.get(tempFile.getTempPath()),
//                        formalFilePath,
//                        StandardCopyOption.REPLACE_EXISTING,
//                        StandardCopyOption.ATOMIC_MOVE
//                );
                // åŽŸå­ç§»åŠ¨å¤±è´¥ï¼Œä½¿ç”¨å¤åˆ¶+删除
                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
                // æ›´æ–°æ–‡ä»¶è®°å½•(关联到业务ID)
                CommonFile fileRecord = new CommonFile();
                fileRecord.setCommonId(businessId);
                fileRecord.setName(originalFilename);
                fileRecord.setUrl(formalFilePath.toString());
                fileRecord.setCreateTime(LocalDateTime.now());
                //销售
                fileRecord.setType(FileNameType.SALE.getValue());
                commonFileMapper.insert(fileRecord);
                // åˆ é™¤ä¸´æ—¶æ–‡ä»¶è®°å½•
                tempFileMapper.deleteById(tempFile);
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
            } catch (IOException e) {
                log.error("文件迁移失败: {}", tempFile.getTempPath(), e);
                // å¯é€‰æ‹©å›žæ»šäº‹åŠ¡æˆ–è®°å½•å¤±è´¥æ–‡ä»¶
                throw new IOException("文件迁移异常", e);
            }
        }
    }
    // æ–‡ä»¶è¿ç§»æ–¹æ³•
    @Override
    public void handleSalesLedgerProducts(Long salesLedgerId, List<SalesLedgerProduct> products, SaleEnum type) {
src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java
@@ -13,6 +13,7 @@
import com.ruoyi.approve.bean.vo.ApproveProcessVO;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.common.enums.IsDeleteEnum;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
@@ -138,6 +139,7 @@
        // åˆ é™¤æŠ¥ä»·å®¡æ‰¹
        ApproveProcess one = approveProcessService.getOne(new LambdaQueryWrapper<ApproveProcess>()
                .eq(ApproveProcess::getApproveType, 6)
                .eq(ApproveProcess::getApproveDelete, IsDeleteEnum.NOT_DELETED)
                .eq(ApproveProcess::getApproveReason, salesQuotation.getQuotationNo()));
        if(one != null){
            approveProcessService.delByIds(Collections.singletonList(one.getId()));
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -11,8 +11,8 @@
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.procurementrecord.bean.vo.ShippingProductVo;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.sales.dto.SalesLedgerProductDto;
import com.ruoyi.sales.dto.ShippingApproveDto;
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.dto.ShippingProductDetailDto;
@@ -95,12 +95,6 @@
        if (CollectionUtils.isEmpty(shippingInfos)) return false;
        // åˆ é™¤é™„ä»¶
        commonFileService.deleteByBusinessIds(ids, FileNameType.SHIP.getValue());
        // æ‰£å·²å‘货库存
        for (ShippingInfo shippingInfo : shippingInfos) {
            if ("已发货".equals(shippingInfo.getStatus())) {
                stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
            }
        }
        // åˆ é™¤å‘货审批
        if (CollectionUtils.isNotEmpty(shippingInfos)) {
            for (ShippingInfo shippingInfo : shippingInfos) {
@@ -110,6 +104,8 @@
                    List<Long> list = one.stream().map(ApproveProcess::getId).toList();
                    approveProcessService.delByIds(list);
                }
                // æ‰£å·²å‘货库存
                stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
            }
        }
        //删除发货明细
@@ -119,7 +115,7 @@
    }
    @Override
    public List<SalesLedgerProductDto> getReturnManagementDtoById(Long shippingId) {
    public List<ShippingProductVo> getReturnManagementDtoById(Long shippingId) {
        return shippingInfoMapper.getReturnManagementDtoById(shippingId);
    }
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java
@@ -51,6 +51,19 @@
        return R.ok(stockInventoryDtoIPage);
    }
    /**
     * æŸ¥è¯¢å¯¹åº”批号和数量
     * @param page
     * @param stockInventoryDto
     * @return
     */
    @GetMapping("/getBatchNoQty")
    @Operation(summary = "查询对应批号和数量")
    public R getBatchNoQty(Page page, StockInventoryDto stockInventoryDto) {
        IPage<StockInventoryDto> stockInventoryDtoIPage = stockInventoryService.getBatchNoQty(page, stockInventoryDto);
        return R.ok(stockInventoryDtoIPage);
    }
    @PostMapping("/addstockInventory")
    @Operation(summary = "新增库存")
    public R addstockInventory(@RequestBody StockInventoryDto stockInventoryDto) {
@@ -85,7 +98,7 @@
    }
    @PostMapping("importStockInventory")
    @PostMapping("/importStockInventory")
    @Operation(summary = "导入库存")
    public R importStockInventory(MultipartFile file) {
        return stockInventoryService.importStockInventory(file);
@@ -105,13 +118,13 @@
        stockInventoryService.exportStockInventory(response, stockInventoryDto);
    }
    @GetMapping("stockInventoryPage")
    @GetMapping("/stockInventoryPage")
    @Operation(summary = "库存报表查询")
    public R stockInventoryPage(Page page, StockInventoryDto stockInventoryDto) {
        return R.ok(stockInventoryService.stockInventoryPage(stockInventoryDto,page));
    }
    @GetMapping("stockInAndOutRecord")
    @GetMapping("/stockInAndOutRecord")
    @Operation(summary = "统计各个产品的入库和出库记录")
    public R stockInAndOutRecord(StockInventoryDto stockInventoryDto,Page page) {
        return R.ok(stockInventoryService.stockInAndOutRecord(stockInventoryDto,page));
@@ -128,7 +141,6 @@
    public R thawStock(@RequestBody StockInventoryDto stockInventoryDto) {
        return R.ok(stockInventoryService.thawStock(stockInventoryDto));
    }
    @GetMapping("/getByModelId")
    @Operation(summary = "根据产品规格ID获取入库记录")
src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java
@@ -76,4 +76,7 @@
    @Schema(description = "不合格库存ID")
    private Long unQualifiedId;
    @Schema(description = "产品id")
    private Long productId;
}
src/main/java/com/ruoyi/stock/execl/StockInRecordExportData.java
@@ -18,11 +18,13 @@
    private String model;
    @Excel(name = "单位")
    private String unit;
    @Excel(name = "批号")
    private String batchNo;
    @Excel(name = "入库来源")
    private String recordType;
    @Excel(name = "入库数量")
    private String stockInNum;
    @Excel(name = "入库时间")
    @Excel(name = "入库时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
src/main/java/com/ruoyi/stock/execl/StockInventoryExportData.java
@@ -19,6 +19,10 @@
    @Excel(name = "单位")
    private String unit;
    @Excel(name = "批号")
    private String batchNo;
    @Excel(name = "合格库存数量")
    private BigDecimal qualifiedQuantity;
src/main/java/com/ruoyi/stock/execl/StockOutRecordExportData.java
@@ -17,11 +17,13 @@
    private String model;
    @Excel(name = "单位")
    private String unit;
    @Excel(name = "批号")
    private String batchNo;
    @Excel(name = "出库来源")
    private String recordType;
    @Excel(name = "出库数量")
    private String stockInNum;
    @Excel(name = "出库时间")
    private String stockOutNum;
    @Excel(name = "出库时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
src/main/java/com/ruoyi/stock/execl/StockUnInventoryExportData.java
@@ -19,6 +19,8 @@
    @Excel(name = "单位")
    private String unit;
    @Excel(name = "批号")
    private String batchNo;
    @Excel(name = "库存数量")
    private BigDecimal qualitity;
src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java
@@ -56,4 +56,6 @@
    List<StockInventory> listSelectableBatchNoByProductModelIds(@Param("productModelIds") List<Long> productModelIds);
    List<StockInventory> getByModelId(@Param("productModelId") Long productModelId);
    IPage<StockInventoryDto> getBatchNoQty(Page page, @Param("ew") StockInventoryDto stockInventoryDto);
}
src/main/java/com/ruoyi/stock/service/StockInventoryService.java
@@ -47,4 +47,6 @@
    Boolean thawStock(StockInventoryDto stockInventoryDto);
    List<StockInventory> getByModelId(Long modelId);
    IPage<StockInventoryDto> getBatchNoQty(Page page, StockInventoryDto stockInventoryDto);
}
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java
@@ -120,7 +120,7 @@
    public void exportStockInRecord(HttpServletResponse response, StockInRecordDto stockInRecordDto) {
        List<StockInRecordExportData> list = stockInRecordMapper.listStockInRecordExportData(stockInRecordDto);
        for (StockInRecordExportData stockInRecordExportData : list) {
            if (stockInRecordExportData.getType().equals("0")) {
            if (!stockInRecordExportData.getType().equals("0")) {
                stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockOutQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
            }else {
                stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockInQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -29,7 +29,7 @@
import com.ruoyi.stock.service.StockOutRecordService;
import com.ruoyi.stock.service.StockUninventoryService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@@ -51,15 +51,16 @@
 * @since 2026-01-21 04:16:36
 */
@Service
@AllArgsConstructor
@RequiredArgsConstructor
public class StockInventoryServiceImpl extends ServiceImpl<StockInventoryMapper, StockInventory> implements StockInventoryService {
    private  StockInventoryMapper stockInventoryMapper;
    private StockInRecordService stockInRecordService;
    private StockOutRecordService stockOutRecordService;
    private StockUninventoryService stockUninventoryService;
    private SalesLedgerProductMapper salesLedgerProductMapper;
    private ProductModelMapper productModelMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final StockInRecordService stockInRecordService;
    private final StockOutRecordService stockOutRecordService;
    private final StockUninventoryService stockUninventoryService;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final ProductModelMapper productModelMapper;
    @Override
    public IPage<StockInventoryDto> pagestockInventory(Page page, StockInventoryDto stockInventoryDto) {
        return stockInventoryMapper.pagestockInventory(page, stockInventoryDto);
@@ -336,7 +337,7 @@
                        }
                        stockInventoryDto.setProductModelId(matchedProduct.getProductModelId());
                        this.addstockInventory(stockInventoryDto);
                        this.addStockInRecordOnly(stockInventoryDto);
                        successCount++;
                    }
@@ -433,4 +434,9 @@
    public List<StockInventory> getByModelId(Long modelId) {
        return stockInventoryMapper.getByModelId(modelId);
    }
    @Override
    public IPage<StockInventoryDto> getBatchNoQty(Page page, StockInventoryDto stockInventoryDto) {
        return stockInventoryMapper.getBatchNoQty(page, stockInventoryDto);
    }
}
src/main/java/com/ruoyi/stock/service/impl/StockUninventoryServiceImpl.java
@@ -4,8 +4,12 @@
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
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.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
@@ -14,21 +18,22 @@
import com.ruoyi.stock.dto.StockOutRecordDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
import com.ruoyi.stock.execl.StockUnInventoryExportData;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.mapper.StockUninventoryMapper;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.stock.pojo.StockUninventory;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.stock.service.StockOutRecordService;
import com.ruoyi.stock.service.StockUninventoryService;
import lombok.AllArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
@@ -47,6 +52,8 @@
    private final StockUninventoryMapper stockUninventoryMapper;
    private final StockOutRecordService stockOutRecordService;
    private final StockInRecordService stockInRecordService;
    private final ProductModelMapper productModelMapper;
    private final StockInventoryMapper stockInventoryMapper;
    @Override
    public IPage<StockUninventoryDto> pageStockUninventory(Page page, StockUninventoryDto stockUninventoryDto) {
@@ -120,7 +127,11 @@
        stockInRecordDto.setRecordId(stockUninventoryDto.getRecordId());
        stockInRecordDto.setRecordType(stockUninventoryDto.getRecordType());
        stockInRecordDto.setStockInNum(stockUninventoryDto.getQualitity());
        stockInRecordDto.setBatchNo(stockUninventoryDto.getBatchNo());
        String batchNo = StringUtils.trim(stockUninventoryDto.getBatchNo());
        if (StringUtils.isEmpty(batchNo)) {
            batchNo = generateAutoBatchNo(stockUninventoryDto.getProductModelId());
        }
        stockInRecordDto.setBatchNo(batchNo);
        stockInRecordDto.setProductModelId(stockUninventoryDto.getProductModelId());
        stockInRecordDto.setType("1");
        stockInRecordDto.setRemark(stockUninventoryDto.getRemark());
@@ -200,4 +211,80 @@
        stockUninventory.setLockedQuantity(stockUninventory.getLockedQuantity().subtract(stockInventoryDto.getLockedQuantity()));
        return this.updateById(stockUninventory);
    }
    //规则生成:20260424-产品编号-001
    private String generateAutoBatchNo(Long productModelId) {
        if (productModelId == null) {
            throw new ServiceException("产品规格ID不能为空");
        }
        ProductModel productModel = productModelMapper.selectById(productModelId);
        if (productModel == null) {
            throw new ServiceException("产品规格不存在,ID=" + productModelId);
        }
        String productCode = StringUtils.trim(productModel.getProductCode());
        if (StringUtils.isEmpty(productCode)) {
            throw new ServiceException("产品规格未维护产品编码,ID=" + productModelId);
        }
        String dateText = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
        String prefix = dateText + "-" + productCode + "-";
        int maxSequence = resolveMaxSequence(prefix);
        int sequence = maxSequence + 1;
        while (sequence < 1_000_000) {
            String batchNo = prefix + String.format("%03d", sequence);
            if (!isBatchNoExists(batchNo)) {
                return batchNo;
            }
            sequence++;
        }
        throw new ServiceException("批号序号超出范围,请检查批号数据");
    }
    private int resolveMaxSequence(String prefix) {
        int maxSequence = 0;
        List<StockInventory> stockInventoryList = stockInventoryMapper.selectList(
                Wrappers.<StockInventory>lambdaQuery()
                        .select(StockInventory::getBatchNo)
                        .likeRight(StockInventory::getBatchNo, prefix));
        for (StockInventory stockInventory : stockInventoryList) {
            maxSequence = Math.max(maxSequence, parseSequence(stockInventory.getBatchNo(), prefix));
        }
        List<StockInRecord> stockInRecordList = stockInRecordService.list(
                Wrappers.<StockInRecord>lambdaQuery()
                        .select(StockInRecord::getBatchNo)
                        .likeRight(StockInRecord::getBatchNo, prefix));
        for (StockInRecord stockInRecord : stockInRecordList) {
            maxSequence = Math.max(maxSequence, parseSequence(stockInRecord.getBatchNo(), prefix));
        }
        return maxSequence;
    }
    private int parseSequence(String batchNo, String prefix) {
        if (StringUtils.isEmpty(batchNo) || StringUtils.isEmpty(prefix) || !batchNo.startsWith(prefix)) {
            return 0;
        }
        String sequenceText = batchNo.substring(prefix.length());
        if (StringUtils.isEmpty(sequenceText) || !sequenceText.matches("\\d+")) {
            return 0;
        }
        try {
            return Integer.parseInt(sequenceText);
        } catch (NumberFormatException ignored) {
            return 0;
        }
    }
    private boolean isBatchNoExists(String batchNo) {
        if (StringUtils.isEmpty(batchNo)) {
            return false;
        }
        Long inventoryCount = stockInventoryMapper.selectCount(
                Wrappers.<StockInventory>lambdaQuery().eq(StockInventory::getBatchNo, batchNo));
        if (inventoryCount != null && inventoryCount > 0) {
            return true;
        }
        return stockInRecordService.count(
                Wrappers.<StockInRecord>lambdaQuery().eq(StockInRecord::getBatchNo, batchNo)) > 0;
    }
}
src/main/java/com/ruoyi/technology/bean/vo/TechnologyBomStructureVo.java
@@ -23,5 +23,8 @@
    @Schema(description = "规格型号")
    private String model;
    @Schema(description = "产品编码")
    private String productCode;
    private List<TechnologyBomStructureVo> children;
}
src/main/java/com/ruoyi/technology/controller/TechnologyBomController.java
@@ -76,7 +76,7 @@
    @PreAuthorize("@ss.hasPermi('product:bom:export')")
    @Operation(summary = "导出BOM文件")
    @Log(title = "导出BOM文件", businessType = BusinessType.EXPORT)
    public void exportBom(HttpServletResponse response, @RequestParam Integer bomId) {
    public void exportBom(HttpServletResponse response, @RequestParam Long bomId) {
        technologyBomService.exportBom(response, bomId);
    }
src/main/java/com/ruoyi/technology/service/TechnologyBomService.java
@@ -26,7 +26,7 @@
    R uploadBom(MultipartFile file);
    void exportBom(HttpServletResponse response, Integer bomId);
    void exportBom(HttpServletResponse response, Long bomId);
    R copy(TechnologyBom technologyBom);
}
src/main/java/com/ruoyi/technology/service/impl/TechnologyBomServiceImpl.java
@@ -15,8 +15,7 @@
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.bean.dto.BomImportDto;
import com.ruoyi.production.bean.dto.ProductStructureDto;
import com.ruoyi.technology.bean.dto.BomImportDto;
import com.ruoyi.technology.bean.dto.TechnologyBomDto;
import com.ruoyi.technology.bean.dto.TechnologyBomStructureDto;
import com.ruoyi.technology.bean.vo.TechnologyBomStructureVo;
@@ -263,70 +262,72 @@
    @Override
    public void exportBom(HttpServletResponse response, Integer bomId) {
    public void exportBom(HttpServletResponse response, Long bomId) {
        if (bomId == null) {
            throw new ServiceException("BOM ID不能为空");
        }
        List<TechnologyBomStructureVo> treeData = technologyBomStructureService.listByBomId(bomId);
        if (treeData == null || treeData.isEmpty()) {
            return;
        }
//        List<ProductStructureDto> treeData = productStructureService.listBybomId(bomId);
//        if (treeData == null || treeData.isEmpty()) {
//            return;
//        }
//
//        //  å°†æ ‘形结构扁平化 ä½¿ç”¨ BFS算法 å¯¼å‡º,按层级顺序
//        List<BomImportDto> exportList = new ArrayList<>();
//
//        // Map<ID, Node> idMap ç”¨äºŽæŸ¥æ‰¾çˆ¶èŠ‚ç‚¹
//        Map<Long, ProductStructureDto> idMap = new HashMap<>();
//        populateMap(treeData, idMap);
//
//        //  treeData çš„第一个是根节点
//        for (ProductStructureDto root : treeData) {
//            //  æ·»åŠ æ ¹èŠ‚ç‚¹
//            BomImportDto rootRow = new BomImportDto();
//            rootRow.setParentName(root.getProductName());
//            rootRow.setParentSpec(root.getModel());
//            rootRow.setUnitQty(root.getUnitQuantity());
//            rootRow.setRemark("");
//            exportList.add(rootRow);
//
//            //  BFS éåކ-队列
//            Queue<ProductStructureDto> queue = new LinkedList<>();
//            if (root.getChildren() != null) {
//                queue.addAll(root.getChildren());
//            }
//
//            while (!queue.isEmpty()) {
//                ProductStructureDto child = queue.poll();
//
//                // æŸ¥æ‰¾çˆ¶èŠ‚ç‚¹
//                ProductStructureDto parent = idMap.get(child.getParentId());
//                if (parent == null) {
//                    // é™¤äº†æœ€å¤–层节点,其他节点的父类肯定是不会为空的
//                    continue;
//                }
//
//                BomImportDto row = new BomImportDto();
//                // çˆ¶ç±»ä¿¡æ¯
//                row.setParentName(parent.getProductName());
//                row.setParentSpec(parent.getModel());
//                // å­ç±»ä¿¡æ¯
//                row.setChildName(child.getProductName());
//                row.setChildSpec(child.getModel());
//                row.setUnitQty(child.getUnitQuantity());
//                row.setProcess(child.getProcessName());
//
//                exportList.add(row);
//
//                //  å°†å­èŠ‚ç‚¹çš„å­èŠ‚ç‚¹åŠ å…¥é˜Ÿåˆ—-下一层
//                if (child.getChildren() != null && !child.getChildren().isEmpty()) {
//                    queue.addAll(child.getChildren());
//                }
//            }
//        }
        //  å°†æ ‘形结构扁平化 ä½¿ç”¨ BFS算法 å¯¼å‡º,按层级顺序
        List<BomImportDto> exportList = new ArrayList<>();
        // Map<ID, Node> idMap ç”¨äºŽæŸ¥æ‰¾çˆ¶èŠ‚ç‚¹
        Map<Long, TechnologyBomStructureVo> idMap = new HashMap<>();
        populateMap(treeData, idMap);
        //  treeData çš„第一个是根节点
        for (TechnologyBomStructureVo root : treeData) {
            //  æ·»åŠ æ ¹èŠ‚ç‚¹
            BomImportDto rootRow = new BomImportDto();
            rootRow.setParentName(root.getProductName());
            rootRow.setParentSpec(root.getModel());
            rootRow.setUnitQty(root.getUnitQuantity());
            rootRow.setParentCode(root.getProductCode());
            rootRow.setRemark("");
            exportList.add(rootRow);
            //  BFS éåކ-队列
            Queue<TechnologyBomStructureVo> queue = new LinkedList<>();
            if (root.getChildren() != null) {
                queue.addAll(root.getChildren());
            }
            while (!queue.isEmpty()) {
                TechnologyBomStructureVo child = queue.poll();
                // æŸ¥æ‰¾çˆ¶èŠ‚ç‚¹
                TechnologyBomStructureVo parent = idMap.get(child.getParentId());
                if (parent == null) {
                    // é™¤äº†æœ€å¤–层节点,其他节点的父类肯定是不会为空的
                    continue;
                }
                BomImportDto row = new BomImportDto();
                // çˆ¶ç±»ä¿¡æ¯
                row.setParentName(parent.getProductName());
                row.setParentSpec(parent.getModel());
                // å­ç±»ä¿¡æ¯
                row.setChildName(child.getProductName());
                row.setChildSpec(child.getModel());
                row.setUnitQty(child.getUnitQuantity());
                row.setProcess(child.getOperationName());
                row.setChildCode(child.getProductCode());
                exportList.add(row);
                //  å°†å­èŠ‚ç‚¹çš„å­èŠ‚ç‚¹åŠ å…¥é˜Ÿåˆ—-下一层
                if (child.getChildren() != null && !child.getChildren().isEmpty()) {
                    queue.addAll(child.getChildren());
                }
            }
        }
        ExcelUtil<BomImportDto> util = new ExcelUtil<>(BomImportDto.class);
//        util.exportExcel(response, exportList, "BOM结构导出");
        util.exportExcel(response, exportList, "BOM结构导出");
    }
    @Override
@@ -408,11 +409,11 @@
        return s.replaceAll("[\\u00A0\\u3000]", "").trim();
    }
    private void populateMap(List<ProductStructureDto> nodes, Map<Long, ProductStructureDto> map) {
    private void populateMap(List<TechnologyBomStructureVo> nodes, Map<Long, TechnologyBomStructureVo> map) {
        if (nodes == null || nodes.isEmpty()) {
            return;
        }
        for (ProductStructureDto node : nodes) {
        for (TechnologyBomStructureVo node : nodes) {
            map.put(node.getId(), node);
            populateMap(node.getChildren(), map);
        }
src/main/java/com/ruoyi/warehouse/mapper/DocumentationMapper.java
@@ -2,7 +2,6 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.procurementrecord.dto.ProcurementRecordOutPageDto;
import com.ruoyi.warehouse.dto.DocumentationDto;
import com.ruoyi.warehouse.pojo.Documentation;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
src/main/resources/application-dev.yml
@@ -28,7 +28,7 @@
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口,默认为8080
  port: 7005
  port: 7006
  servlet:
    # åº”用的访问路径
    context-path: /
@@ -260,7 +260,7 @@
  upload-dir: D:/ruoyi/prod/uploads # æ­£å¼ç›®å½• åŽæœŸåˆ é™¤
  path: D:/ruoyi/prod/uploads # ä¸Šä¼ ç›®å½•
  urlPrefix: /common # é“¾æŽ¥å‰ç¼€
  domain: http://127.0.0.1:7005 # åŸŸåå‰ç¼€
  domain: http://127.0.0.1:7006 # åŸŸåå‰ç¼€
  expired: 120 # è¿‡æœŸæ—¶é—´(单位:分钟)
  useLimit: 10 # ä½¿ç”¨æ¬¡æ•°
  compress: true # æ˜¯å¦åŽ‹ç¼©
src/main/resources/logback.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- æ—¥å¿—存放路径 -->
    <property name="log.path" value="/home/ruoyi/logs" />
    <property name="log.path" value="./logs" />
    <!-- æ—¥å¿—输出格式 -->
    <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
src/main/resources/manufacturing-agent-prompt.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
你是企业制造智能助手,覆盖生产现场、计划、工单、设备、质量、物料、异常处理七个域。
工作规则:
1. ç”¨æˆ·æå‡ºâ€œæŸ¥ã€é—®ã€é¢„警、分析”需求时,优先调用工具拿结构化结果,不要臆造业务数据。
2. ç”¨æˆ·æå‡ºâ€œåŠžâ€éœ€æ±‚æ—¶ï¼Œä¼˜å…ˆè¾“å‡ºåŠžç†å»ºè®®åŠ¨ä½œå¡ï¼ˆæŽ¥å£ã€å¿…å¡«å­—æ®µã€ç¤ºä¾‹ï¼‰ï¼Œæ˜Žç¡®éœ€è¦å‰ç«¯äºŒæ¬¡ç¡®è®¤ã€‚
3. å·¥å…·è¿”回 JSON æ—¶ï¼Œç›´æŽ¥è¾“出原始 JSON å­—符串,不要额外包裹 Markdown,不要在前后加解释文字。
4. å›žç­”必须使用中文;若用户问题缺少时间范围、关键字等条件,可先给默认口径并提示可补充条件。
5. è‹¥æ— æ³•从工具结果得到结论,明确说明缺少的筛选条件或业务字段。
src/main/resources/mapper/account/AccountSubjectMapper.xml
@@ -5,6 +5,7 @@
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.account.pojo.AccountSubject">
        <id column="id" property="id" />
        <result column="parent_id" property="parentId" />
        <result column="subject_code" property="subjectCode" />
        <result column="subject_name" property="subjectName" />
        <result column="subject_type" property="subjectType" />
@@ -18,4 +19,13 @@
        <result column="dept_id" property="deptId" />
    </resultMap>
    <select id="countReferencedBySubjectCodes" resultType="java.lang.Long">
        SELECT COUNT(1)
        FROM fin_voucher_entry
        WHERE subject_code IN
        <foreach collection="subjectCodes" item="item" open="(" separator="," close=")">
            #{item}
        </foreach>
    </select>
</mapper>
src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
<?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.account.mapper.financial.FinVoucherEntryMapper">
    <resultMap id="BaseResultMap" type="com.ruoyi.account.pojo.financial.FinVoucherEntry">
        <id column="id" property="id"/>
        <result column="voucher_id" property="voucherId"/>
        <result column="row_no" property="rowNo"/>
        <result column="subject_code" property="subjectCode"/>
        <result column="subject_name" property="subjectName"/>
        <result column="summary" property="summary"/>
        <result column="debit" property="debit"/>
        <result column="credit" property="credit"/>
        <result column="auxiliary_type" property="auxiliaryType"/>
        <result column="auxiliary_id" property="auxiliaryId"/>
        <result column="auxiliary_name" property="auxiliaryName"/>
        <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="listPostedEntries" resultType="com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo">
        SELECT
            v.voucher_date AS voucherDate,
            v.voucher_no AS voucherNo,
            CASE
                WHEN e.summary IS NOT NULL AND e.summary != '' THEN e.summary
                ELSE v.summary
            END AS summary,
            e.debit AS debit,
            e.credit AS credit,
            e.row_no AS rowNo
        FROM fin_voucher_entry e
        INNER JOIN fin_voucher v ON e.voucher_id = v.id
        WHERE v.status = 'posted'
          AND (e.subject_code = #{subjectCode} OR e.subject_code LIKE CONCAT(#{subjectCode}, '%'))
          AND v.voucher_date <![CDATA[>=]]> #{startDate}
          AND v.voucher_date <![CDATA[<=]]> #{endDate}
        <if test="auxiliaryType != null and auxiliaryType != ''">
          AND e.auxiliary_type = #{auxiliaryType}
        </if>
        <if test="auxiliaryId != null and auxiliaryId != ''">
          AND e.auxiliary_id = #{auxiliaryId}
        </if>
        ORDER BY v.voucher_date ASC, v.id ASC, e.row_no ASC, e.id ASC
    </select>
    <select id="listPostedEntriesBefore" resultType="com.ruoyi.account.bean.vo.financial.FinLedgerEntryRecordVo">
        SELECT
            v.voucher_date AS voucherDate,
            v.voucher_no AS voucherNo,
            CASE
                WHEN e.summary IS NOT NULL AND e.summary != '' THEN e.summary
                ELSE v.summary
            END AS summary,
            e.debit AS debit,
            e.credit AS credit,
            e.row_no AS rowNo
        FROM fin_voucher_entry e
        INNER JOIN fin_voucher v ON e.voucher_id = v.id
        WHERE v.status = 'posted'
          AND (e.subject_code = #{subjectCode} OR e.subject_code LIKE CONCAT(#{subjectCode}, '%'))
          AND v.voucher_date <![CDATA[<]]> #{beforeDate}
        <if test="auxiliaryType != null and auxiliaryType != ''">
          AND e.auxiliary_type = #{auxiliaryType}
        </if>
        <if test="auxiliaryId != null and auxiliaryId != ''">
          AND e.auxiliary_id = #{auxiliaryId}
        </if>
    </select>
</mapper>
src/main/resources/mapper/basic/CustomerMapper.xml
@@ -26,6 +26,7 @@
        from customer c
        left join sys_user u on c.usage_user = u.user_id
        <where>
            and c.usage_status = 1
            <if test="c.customerName != null and c.customerName != ''">
                and customer_name like concat('%', #{c.customerName}, '%')
            </if>
src/main/resources/mapper/collaborativeApproval/SealApplicationManagementMapper.xml
@@ -4,13 +4,27 @@
    <select id="listPage" resultType="com.ruoyi.collaborativeApproval.dto.SealApplicationManagementDTO">
        select sam.*, su.user_name as create_user_name, d.dept_name as department,
        su1.nick_name as approveUserName
        from seal_application_management sam
        left join sys_user su on sam.create_user = su.user_id
        left join sys_user su1 on sam.approve_user_id = su1.user_id
        left join sys_user_dept sud on su.user_id = sud.user_id
        left join sys_dept d on sud.dept_id = d.dept_id
        SELECT
        sam.*,
        su.user_name AS create_user_name,
        GROUP_CONCAT(DISTINCT d.dept_name ORDER BY d.dept_id SEPARATOR ',') AS department,
        su1.nick_name AS approveUserName
        FROM seal_application_management sam
        LEFT JOIN sys_user su
        ON sam.create_user = su.user_id
        LEFT JOIN sys_user su1
        ON sam.approve_user_id = su1.user_id
        LEFT JOIN sys_user_dept sud
        ON su.user_id = sud.user_id
        LEFT JOIN sys_dept d
        ON sud.dept_id = d.dept_id
        <where>
            <if test="ew.applicationNum != null and ew.applicationNum != ''">
                and sam.application_num like concat('%',#{ew.applicationNum},'%')
@@ -22,5 +36,6 @@
                and sam.status = #{ew.status}
            </if>
        </where>
        GROUP BY sam.id
    </select>
</mapper>
src/main/resources/mapper/device/DeviceRepairMapper.xml
@@ -14,6 +14,9 @@
                dr.maintenance_name,
                dr.maintenance_time,
                dr.maintenance_result,
                 dr.acceptance_name,
                 dr.acceptance_time,
                 dr.acceptance_remark,
                dr.status,
                dr.create_time,
                dr.update_time,
@@ -60,6 +63,9 @@
               dr.maintenance_name,
               dr.maintenance_time,
               dr.maintenance_result,
               dr.acceptance_name,
               dr.acceptance_time,
               dr.acceptance_remark,
               dr.status,
               dr.create_time,
               dr.update_time,
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml
@@ -15,7 +15,9 @@
        next_date,
        record_date,
        CASE
        WHEN most_date &gt;=  DATE_FORMAT(now(),'%Y-%m-%d') THEN 1
        WHEN most_date IS NOT NULL
        AND valid IS NOT NULL
        AND DATE_ADD(most_date, INTERVAL valid DAY) &gt;= CURDATE() THEN 1
        ELSE 2
        END AS status,
        create_user,
@@ -40,10 +42,16 @@
            <if test="req.status != null">
                <choose>
                    <when test="req.status == 1">
                        AND most_date &gt;=  DATE_FORMAT(now(),'%Y-%m-%d')
                        AND most_date IS NOT NULL
                        AND valid IS NOT NULL
                        AND DATE_ADD(most_date, INTERVAL valid DAY) &gt;= CURDATE()
                    </when>
                    <when test="req.status == 2">
                        AND most_date &lt;  DATE_FORMAT(now(),'%Y-%m-%d')
                        AND (
                        most_date IS NULL
                        OR valid IS NULL
                        OR DATE_ADD(most_date, INTERVAL valid DAY) &lt; CURDATE()
                        )
                    </when>
                </choose>
            </if>
src/main/resources/mapper/procurementrecord/ProcurementRecordMapper.xml
@@ -2,7 +2,7 @@
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper">
    <select id="listProcurementBySalesLedgerId" resultType="com.ruoyi.procurementrecord.dto.ProcurementDto">
    <select id="listProcurementBySalesLedgerId" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementDto">
        select
            t1.supplier_name,
            t2.product_category,
@@ -28,7 +28,7 @@
        </if>
        group by t2.id
    </select>
    <select id="listPage" resultType="com.ruoyi.procurementrecord.dto.ProcurementPageDto">
    <select id="listPage" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementPageDto">
        select
        t3.supplier_name,
        t3.purchase_contract_number,
@@ -68,7 +68,7 @@
        </where>
        order by t1.create_time desc
    </select>
    <select id="list" resultType="com.ruoyi.procurementrecord.dto.ProcurementPageDto">
    <select id="list" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementPageDto">
        select
            t3.supplier_name,
            t3.purchase_contract_number,
@@ -92,7 +92,7 @@
                  left join purchase_ledger t3 on t3.id = t2.sales_ledger_id
                where t1.type = 1
    </select>
    <select id="listOne" resultType="com.ruoyi.procurementrecord.dto.ProcurementPageDto">
    <select id="listOne" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementPageDto">
        select
            t3.customer_contract_no,
            t3.sales_contract_no,
@@ -117,7 +117,7 @@
            left join sales_ledger t3 on t3.id = t2.sales_ledger_id
        where t1.type = 2
    </select>
    <select id="listPageCopy" resultType="com.ruoyi.procurementrecord.dto.ProcurementPageDtoCopy">
    <select id="listPageCopy" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementPageDtoCopy">
        select
        t3.supplier_name,
        t3.purchase_contract_number,
@@ -175,7 +175,7 @@
        group by t3.supplier_name,t2.product_category,t2.specification_model,t1.unit_price
        order by t1.create_time desc
    </select>
    <select id="listCopy" resultType="com.ruoyi.procurementrecord.dto.ProcurementPageDtoCopy">
    <select id="listCopy" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementPageDtoCopy">
        select
            t3.supplier_name,
            t3.purchase_contract_number,
@@ -203,7 +203,7 @@
        where t1.type = 1
        group by t3.supplier_name,t2.product_category,t2.specification_model
    </select>
    <select id="listCopyOne" resultType="com.ruoyi.procurementrecord.dto.ProcurementPageDtoCopy">
    <select id="listCopyOne" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementPageDtoCopy">
        select
            t3.customer_contract_no,
            t3.sales_contract_no,
@@ -232,7 +232,7 @@
        where t1.type = 2
        group by t3.customer_name,t2.product_category,t2.specification_model
    </select>
    <select id="listPageByProduction" resultType="com.ruoyi.procurementrecord.dto.ProcurementPageDto">
    <select id="listPageByProduction" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementPageDto">
        select
        t3.customer_contract_no,
        t3.sales_contract_no,
@@ -274,7 +274,7 @@
        </where>
        order by t1.create_time desc
    </select>
    <select id="listPageCopyByProduction" resultType="com.ruoyi.procurementrecord.dto.ProcurementPageDtoCopy">
    <select id="listPageCopyByProduction" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementPageDtoCopy">
        select
        t3.customer_contract_no,
        t3.sales_contract_no,
@@ -336,7 +336,7 @@
        group by t2.product_category,t2.specification_model,t1.unit_price
        order by t1.create_time desc
    </select>
    <select id="listPagePRS" resultType="com.ruoyi.procurementrecord.dto.ProcurementPageDtoCopy">
    <select id="listPagePRS" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementPageDtoCopy">
        select *
        from  procurement_record_storage t1
        left join sales_ledger_product t2 on t2.id = t1.sales_ledger_product_id
@@ -358,7 +358,7 @@
        from procurement_record_storage
        where product_model_id = #{productModelId}
    </select>
    <select id="listPageByProductProduction" resultType="com.ruoyi.procurementrecord.dto.ProcurementPageDto">
    <select id="listPageByProductProduction" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementPageDto">
        select
        t1.*,
        t1.inbound_num as inboundNum0,
src/main/resources/mapper/procurementrecord/ProcurementRecordOutMapper.xml
@@ -2,7 +2,7 @@
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper">
    <select id="listPage" resultType="com.ruoyi.procurementrecord.dto.ProcurementRecordOutPageDto">
    <select id="listPage" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto">
        select
        t3.supplier_name,
        t2.product_category,
@@ -38,7 +38,7 @@
        </where>
        order by t1.create_time desc
    </select>
    <select id="list" resultType="com.ruoyi.procurementrecord.dto.ProcurementRecordOutPageDto">
    <select id="list" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto">
        select t3.supplier_name,
               t2.product_category,
               t1.id,
@@ -58,7 +58,7 @@
        where t1.type = 1
    </select>
    <select id="listOne" resultType="com.ruoyi.procurementrecord.dto.ProcurementRecordOutPageDto">
    <select id="listOne" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto">
        select t3.customer_contract_no,
               t3.sales_contract_no,
               t3.customer_name,
@@ -80,7 +80,7 @@
        where t1.type = 2
    </select>
    <select id="listTwo" resultType="com.ruoyi.procurementrecord.dto.ProcurementRecordOutPageDto">
    <select id="listTwo" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto">
        select t1.supplier_name,
               t1.product_category,
               t1.id,
@@ -97,7 +97,7 @@
        from procurement_record_out t1
        where t1.type = 3
    </select>
    <select id="listPageByProduct" resultType="com.ruoyi.procurementrecord.dto.ProcurementRecordOutPageDto">
    <select id="listPageByProduct" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto">
        select
        t3.customer_contract_no,
        t3.sales_contract_no,
@@ -134,7 +134,7 @@
        </where>
        order by t1.create_time desc
    </select>
    <select id="listPageByCustom" resultType="com.ruoyi.procurementrecord.dto.ProcurementRecordOutPageDto">
    <select id="listPageByCustom" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto">
        select
        t2.supplier_name,
        t2.product_category,
@@ -180,7 +180,7 @@
        order by id desc
        limit 1
    </select>
    <select id="listPageBySemiProduct" resultType="com.ruoyi.procurementrecord.dto.ProcurementRecordOutPageDto">
    <select id="listPageBySemiProduct" resultType="com.ruoyi.procurementrecord.bean.dto.ProcurementRecordOutPageDto">
        select
        t1.id,
        t1.code,
src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml
@@ -2,7 +2,7 @@
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.procurementrecord.mapper.ReturnManagementMapper">
    <select id="listPage" resultType="com.ruoyi.procurementrecord.dto.ReturnManagementDto">
    <select id="listPage" resultType="com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto">
        select rm.*,
               c.customer_name,
               si.shipping_no,
@@ -40,7 +40,7 @@
            </if>
        </where>
    </select>
    <select id="getReturnManagementDtoById" resultType="com.ruoyi.procurementrecord.dto.ReturnManagementDto">
    <select id="getReturnManagementDtoById" resultType="com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto">
     select rm.*,
               c.customer_name,
               si.shipping_no,
@@ -53,7 +53,7 @@
                 left join sales_ledger sl on si.sales_ledger_id = sl.id
        where rm.id = #{id}
    </select>
    <select id="listPageBySalesReturn" resultType="com.ruoyi.account.bean.vo.SalesReturnVo">
    <select id="listPageAccountSalesReturn" resultType="com.ruoyi.account.bean.vo.SalesReturnVo">
         select rm.id,
                rm.return_no,
                c.customer_name,
@@ -75,7 +75,7 @@
                and c.customer_name like concat('%',#{req.customerName},'%')
            </if>
            <if test="req.startDate != null and req.endDate != null">
                AND DATE_FORMAT(rm.make_time, '%Y-%m-%d') BETWEEN #{startDate} AND #{endDate}
                AND DATE_FORMAT(rm.make_time, '%Y-%m-%d') BETWEEN #{req.startDate} AND #{req.endDate}
            </if>
         order by rm.id DESC
    </select>
src/main/resources/mapper/procurementrecord/ReturnSaleProductMapper.xml
@@ -6,35 +6,51 @@
    <resultMap id="BaseResultMap" type="com.ruoyi.procurementrecord.pojo.ReturnSaleProduct">
        <id column="id" property="id" />
        <result column="return_management_id" property="returnManagementId" />
        <result column="return_sales_ledger_product_id" property="returnsalesLedgerProductId" />
        <result column="stock_out_record_id" property="stockOutRecordId" />
        <result column="num" property="num" />
        <result column="status" property="status" />
    </resultMap>
    <select id="listReturnSaleProductDto" resultType="com.ruoyi.procurementrecord.dto.ReturnSaleProductDto">
        SELECT p.product_name                                         as product_name,
    <select id="listReturnSaleProductDto" resultType="com.ruoyi.procurementrecord.bean.dto.ReturnSaleProductDto">
        SELECT distinct
            p.product_name                                         as product_name,
               pm.model                                     as model,
               pm.unit                                      as unit,
               rsp.*,
               GREATEST(slp.quantity - COALESCE(rs.total_return_num, 0), 0) AS un_quantity,
            sor.outbound_batches,
            sor.stock_out_num,
            sor.batch_no,
               GREATEST(sor.stock_out_num - COALESCE(rs1.total_return_num1, 0), 0) AS un_quantity,
               COALESCE(rs.total_return_num, 0)                             AS total_return_num
        FROM return_sale_product rsp
                 LEFT JOIN return_management rm ON rm.id = rsp.return_management_id
                 LEFT JOIN shipping_info si ON si.id = rm.shipping_id
                 LEFT JOIN shipping_product_detail spd ON spd.shipping_info_id = si.id
                 LEFT JOIN stock_out_record sor ON rsp.stock_out_record_id = sor.id and sor.record_type = '13'
                 LEFT JOIN sales_ledger_product slp ON si.sales_ledger_product_id = slp.id and slp.type = 1
                left join product_model pm on slp.product_model_id = pm.id
                  LEFT JOIN product p on pm.product_id = p.id
                 LEFT JOIN (SELECT return_sales_ledger_product_id,
                 LEFT JOIN (SELECT stock_out_record_id,
                                   SUM(num) AS total_return_num
                            FROM return_sale_product
                            WHERE 1 = 1 and return_management_id != #{returnManagementId}
                            GROUP BY return_sales_ledger_product_id) rs ON rs.return_sales_ledger_product_id = slp.id
                            WHERE 1 = 1 and return_management_id = #{returnManagementId}
                            GROUP BY stock_out_record_id) rs ON rs.stock_out_record_id = sor.id
                 LEFT JOIN (SELECT stock_out_record_id,
                                   SUM(num) AS total_return_num1
                            FROM return_sale_product
                            WHERE 1 = 1
                            GROUP BY stock_out_record_id) rs1 ON rs1.stock_out_record_id = sor.id
        where rm.id =#{returnManagementId}
    </select>
    <select id="listReturnSaleProduct" resultType="com.ruoyi.procurementrecord.dto.ReturnSaleProductDto">
        select rsp.*,slp.tax_inclusive_unit_price ,slp.tax_inclusive_total_price*rsp.num as price
    <select id="listReturnSaleProduct" resultType="com.ruoyi.procurementrecord.bean.dto.ReturnSaleProductDto">
        select rsp.*,
               sor.batch_no,
               slp.tax_inclusive_unit_price ,
               slp.tax_inclusive_total_price*rsp.num as price
        from return_sale_product rsp
                 left join sales_ledger_product slp on slp.id = rsp.return_sales_ledger_product_id
                 LEFT JOIN return_management rm ON rm.id = rsp.return_management_id
                 LEFT JOIN shipping_info si ON si.id = rm.shipping_id
                 LEFT JOIN sales_ledger_product slp ON si.sales_ledger_product_id = slp.id and slp.type = 1
                 LEFT JOIN stock_out_record sor ON rsp.stock_out_record_id = sor.id
        where rsp.return_management_id = #{returnManagementId}
    </select>
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -159,6 +159,7 @@
    <select id="getOperation" resultType="com.ruoyi.production.bean.vo.ProductionOperationTaskVo">
        select poro.operation_name as operationName,
               max(poro.type) as type,
               count(pot.id) as productionTaskCount,
               sum(ifnull(pot.plan_quantity, 0)) as planQuantity,
               sum(ifnull(pot.complete_quantity, 0)) as completeQuantity,
src/main/resources/mapper/purchase/PurchaseReturnOrderProductsMapper.xml
@@ -13,7 +13,9 @@
    </resultMap>
    <select id="getReturnOrderGroupListByProductIds" resultType="com.ruoyi.purchase.dto.SimpleReturnOrderGroupDto"
            parameterType="java.util.List">
        select t1.sales_ledger_product_id,sum(t1.return_quantity) as sum_return_quantity from purchase_return_order_products as t1
        select t1.sales_ledger_product_id,
               sum(t1.return_quantity) as sum_return_quantity
        from purchase_return_order_products as t1
        inner join purchase_return_orders as t2 on t1.purchase_return_order_id = t2.id
        WHERE t1.sales_ledger_product_id IN
        <foreach item="id" collection="productIds" separator="," open="(" close=")">
src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml
@@ -57,16 +57,22 @@
            resultType="com.ruoyi.account.bean.vo.PurchaseReturnVo">
         select pro.id,
                pro.no returnNo,
                t.inboundBatches,
                sm.supplier_name,
                pro.prepared_at,
                pro.total_amount,
                CASE pro.return_type WHEN 0 THEN '退货退款' WHEN 1 THEN '拒收' END AS returnType,
                pl.purchase_contract_number
        from purchase_return_orders pro
        left join
            (select prop.purchase_return_order_id,
                    GROUP_CONCAT(sir.inbound_batches SEPARATOR ',') AS inboundBatches
            from purchase_return_order_products prop
            left join stock_in_record sir on prop.stock_in_record_id = sir.id
            GROUP BY prop.purchase_return_order_id) t on t.purchase_return_order_id = pro.id
                 left join supplier_manage sm on pro.supplier_id = sm.id
                 left join purchase_ledger pl on pro.purchase_ledger_id = pl.id
        where rm.status=1
        where 1=1
            <if test="req.returnNo != null and req.returnNo != ''">
                and pro.no like concat('%',#{req.returnNo},'%')
            </if>
@@ -74,8 +80,74 @@
                and sm.supplier_name like concat('%',#{req.supplierName},'%')
            </if>
            <if test="req.startDate != null and req.endDate != null">
                AND DATE_FORMAT(pro.prepared_at, '%Y-%m-%d') BETWEEN #{startDate} AND #{endDate}
                AND DATE_FORMAT(pro.prepared_at, '%Y-%m-%d') BETWEEN #{req.startDate} AND #{req.endDate}
            </if>
         order by pro.id DESC
    </select>
    <select id="getByPurchaseLedgerId" resultType="com.ruoyi.purchase.vo.PurchaseStockInProductVo">
         SELECT
            sir.id,
            sir.product_model_id,
            slp.id salesLedgerProductId,
            slp.product_category,
            slp.specification_model,
            slp.unit,
            slp.is_checked,
            sir.inbound_batches,
            sir.stock_in_num,
            sir.batch_no,
            slp.tax_inclusive_unit_price,
            GREATEST(sir.stock_in_num - COALESCE(rs.total_return_num, 0), 0) AS un_quantity,
            COALESCE(rs.total_return_num, 0) AS total_return_num
            FROM stock_in_record sir
            LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
            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
            LEFT JOIN (
                SELECT
                    stock_in_record_id,
                    SUM(return_quantity) AS total_return_num
                FROM purchase_return_order_products prop
                         left join purchase_return_orders pro on pro.id = prop.purchase_return_order_id
                WHERE 1=1
                GROUP BY stock_in_record_id
            ) rs ON rs.stock_in_record_id = sir.id
        WHERE sir.approval_status = 1 AND slp.type = 2
        AND sir.record_type IN ('7','10')
         and pl.id = #{purchaseLedgerId}
    </select>
    <select id="getPurchaseReturnOrderProductsDetailById"
            resultType="com.ruoyi.purchase.vo.PurchaseReturnOrderProductsDetailVo">
    select prop.id,
           prop.sales_ledger_product_id,
           slp.product_model_id,
           slp.product_category,
           slp.specification_model,
           slp.is_checked,
           slp.unit,
           sir.inbound_batches,
           sir.stock_in_num,
           sir.batch_no,
           slp.tax_inclusive_unit_price,
           prop.return_quantity,
           prop.purchase_return_order_id,
           GREATEST(sir.stock_in_num - COALESCE(rs1.total_return_num1, 0), 0) AS un_quantity,
           COALESCE(rs.total_return_num, 0)                             AS total_return_num
    from purchase_return_order_products prop
    left join purchase_return_orders pro on prop.purchase_return_order_id = pro.id
    LEFT JOIN stock_in_record sir ON prop.stock_in_record_id = sir.id and sir.record_type in ('7','10')
    LEFT JOIN sales_ledger_product slp ON prop.sales_ledger_product_id = slp.id  and slp.type = 2
    LEFT JOIN (SELECT stock_in_record_id,
                      SUM(return_quantity) AS total_return_num
               FROM purchase_return_order_products
               WHERE 1 = 1 and purchase_return_order_id = #{id}
               GROUP BY stock_in_record_id) rs ON rs.stock_in_record_id = sir.id
    LEFT JOIN (SELECT stock_in_record_id,
                      SUM(return_quantity) AS total_return_num1
               FROM purchase_return_order_products
               WHERE 1 = 1 and purchase_return_order_id = #{id}
               GROUP BY stock_in_record_id) rs1 ON rs1.stock_in_record_id = sir.id
    where pro.id = #{id}
    </select>
</mapper>
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml
@@ -48,7 +48,8 @@
        END as has_sufficient_stock,
        (IFNULL(T1.quantity, 0) - IFNULL(t3.shipped_quantity, 0)) as no_quantity,
        CASE
        WHEN (IFNULL(T1.quantity, 0) - IFNULL(t3.shipped_quantity, 0)) > 0 THEN '待发货'
         WHEN IFNULL(t3.shipped_quantity, 0) = 0 THEN '待发货'
         WHEN (IFNULL(T1.quantity, 0) - IFNULL(t3.shipped_quantity, 0)) > 0 THEN '部分发货'
        ELSE '已发货'
        END as shippingStatus
        FROM
@@ -62,6 +63,7 @@
        SELECT sales_ledger_product_id, IFNULL(SUM(spd.quantity), 0) as shipped_quantity
        FROM shipping_info si
        LEFT JOIN shipping_product_detail spd ON si.id = spd.shipping_info_id
        where si.status != '审核拒绝'
        GROUP BY sales_ledger_product_id
        ) t3 ON t3.sales_ledger_product_id = T1.id
        left join product_model pm ON T1.product_model_id = pm.id
src/main/resources/mapper/sales/SalesQuotationMapper.xml
src/main/resources/mapper/sales/ShippingInfoMapper.xml
@@ -22,12 +22,19 @@
        pm.model as specification_model,
        pm.unit,
        p.product_name,
        sl.customer_name
        sl.customer_name,
        spd.totalQuantity,
        sor.outboundBatches
        FROM shipping_info s
        LEFT JOIN (select shipping_info_id,sum(quantity) totalQuantity from shipping_product_detail GROUP BY shipping_info_id) spd ON spd.shipping_info_id = s.id
        LEFT JOIN sales_ledger sl ON s.sales_ledger_id = sl.id
        LEFT JOIN sales_ledger_product slp ON s.sales_ledger_product_id = slp.id and slp.type = 1
        left join product_model pm on slp.product_model_id = pm.id
        left join product p on pm.product_id = p.id
        left join (select record_id,GROUP_CONCAT(outbound_batches SEPARATOR ',') AS outboundBatches
                   from stock_out_record
                   where record_type='13'and approval_status=1
                   group by record_id)sor on sor.record_id= s.id
        WHERE 1=1
        <if test="req.salesContractNo != null and req.salesContractNo != ''">
            AND sl.sales_contract_no LIKE CONCAT('%',#{req.salesContractNo},'%')
@@ -59,33 +66,43 @@
        FROM shipping_info s
                 LEFT JOIN sales_ledger sl ON s.sales_ledger_id = sl.id
    </select>
    <select id="getReturnManagementDtoById" resultType="com.ruoyi.sales.dto.SalesLedgerProductDto">
        SELECT
        slp.*,
        si.shipping_no,
        GREATEST(slp.quantity - COALESCE(rs.total_return_num, 0), 0) AS un_quantity,
    <select id="getReturnManagementDtoById" resultType="com.ruoyi.procurementrecord.bean.vo.ShippingProductVo">
        SELECT distinct
            sor.id,
            slp.product_category,
            slp.specification_model,
            slp.unit,
            slp.product_model_id,
            sor.outbound_batches,
            sor.stock_out_num,
            sor.batch_no,
            slp.tax_inclusive_unit_price,
            GREATEST(sor.stock_out_num - COALESCE(rs.total_return_num, 0), 0) AS un_quantity,
        COALESCE(rs.total_return_num, 0) AS total_return_num
        FROM shipping_info si
        LEFT JOIN shipping_product_detail spd ON spd.shipping_info_id = si.id
        LEFT JOIN stock_out_record sor ON sor.record_id = si.id and sor.record_type = '13'
        LEFT JOIN sales_ledger_product slp ON si.sales_ledger_product_id = slp.id and slp.type = 1
        LEFT JOIN (
        SELECT
        return_sales_ledger_product_id,
        stock_out_record_id,
        SUM(num) AS total_return_num
        FROM return_sale_product rsp
        left join return_management rm on rm.id = rsp.return_management_id
        left join shipping_info si on si.id = rm.shipping_id
        WHERE 1=1
        GROUP BY return_sales_ledger_product_id
        ) rs ON rs.return_sales_ledger_product_id = slp.id
        GROUP BY stock_out_record_id
        ) rs ON rs.stock_out_record_id = sor.id
        <where>
            <if test="shippingId != null">
                si.id = #{shippingId}
            </if>
        </where>
        order by sor.id
    </select>
    <select id="getShippingInfoByCustomerName" resultType="com.ruoyi.sales.pojo.ShippingInfo">
        select * from shipping_info si
        left join sales_ledger sl on si.sales_ledger_id = sl.id
        where si.status = '已发货' and sl.customer_name = #{customerName}
        where  sl.customer_name = #{customerName}
    </select>
</mapper>
src/main/resources/mapper/stock/StockInRecordMapper.xml
@@ -78,7 +78,7 @@
            DATE(sir.create_time) AS inboundDate,
            p.product_name,
            pm.model as specification_model,
            sor.stock_in_num * slp.tax_inclusive_unit_price AS InboundAmount,
            sir.stock_in_num * slp.tax_inclusive_unit_price AS InboundAmount,
            pl.purchase_contract_number
            FROM stock_in_record sir
            -- 10 ç±»åž‹æ‰å…³è”质检表
@@ -87,11 +87,11 @@
            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 slp.type = 2 AND pl.id = slp.product_id
            LEFT JOIN product_model pm ON slp.product_model_id = pm.id
            LEFT JOIN sales_ledger_product slp ON pl.id = slp.product_id
            LEFT JOIN product_model pm ON sir.product_model_id = pm.id
            LEFT JOIN product p ON pm.product_id = p.id
            -- æ¡ä»¶
        WHERE sir.approval_status = 1
        WHERE sir.approval_status = 1 AND slp.type = 2
        AND sir.record_type IN ('7','10')
        <if test="req.inboundBatches != null and req.inboundBatches != ''">
            AND sir.inbound_batches LIKE CONCAT('%',#{req.inboundBatches},'%')
@@ -100,7 +100,7 @@
            AND pl.supplier_name LIKE CONCAT('%',#{req.supplierName},'%')
        </if>
        <if test="req.startDate != null and req.endDate != null">
            AND DATE(sir.create_time) BETWEEN #{startDate} AND #{endDate}
            AND DATE(sir.create_time) BETWEEN #{req.startDate} AND #{req.endDate}
        </if>
        order by sir.id DESC
    </select>
src/main/resources/mapper/stock/StockInventoryMapper.xml
@@ -99,6 +99,138 @@
        INNER JOIN product_tree pt ON p.parent_id = pt.id
        )
        select
        GROUP_CONCAT(DISTINCT batch_no ORDER BY batch_no SEPARATOR ',') as batch_no,
        MAX(qualifiedId) as qualifiedId,
        MAX(unQualifiedId) as unQualifiedId,
        SUM(qualifiedQuantity) as qualifiedQuantity,
        SUM(unQualifiedQuantity) as unQualifiedQuantity,
        SUM(qualifiedLockedQuantity) as qualifiedLockedQuantity,
        SUM(unQualifiedLockedQuantity) as unQualifiedLockedQuantity,
        SUM(qualifiedQuantity - qualifiedLockedQuantity - IFNULL(qualifiedPendingOut, 0)) as qualifiedUnLockedQuantity,
        SUM(unQualifiedQuantity - unQualifiedLockedQuantity - IFNULL(unQualifiedPendingOut, 0)) as unQualifiedUnLockedQuantity,
        SUM(IFNULL(qualifiedPendingOut, 0)) as qualifiedPendingOutQuantity,
        SUM(IFNULL(unQualifiedPendingOut, 0)) as unQualifiedPendingOutQuantity,
        product_model_id,
        MAX(create_time) as create_time,
        MAX(update_time) as update_time,
        MAX(warn_num) as warn_num,
        MAX(version) as version,
        model,
        MAX(remark) as remark,
        unit,
        product_name,
        product_id,
        'combined' as stockType
        from (
        select
        si.batch_no,
        si.id as qualifiedId,
        null as unQualifiedId,
        si.qualitity as qualifiedQuantity,
        0 as unQualifiedQuantity,
        COALESCE(si.locked_quantity, 0) as locked_quantity,
        COALESCE(si.locked_quantity, 0) as qualifiedLockedQuantity,
        0 as unQualifiedLockedQuantity,
        si.product_model_id,
        si.create_time,
        si.update_time,
        COALESCE(si.warn_num, 0) as warn_num,
        si.version,
        (si.qualitity - COALESCE(si.locked_quantity, 0)) as un_locked_quantity,
        pm.model,
        si.remark,
        pm.unit,
        p.product_name,
        p.id as product_id,
        (
        select IFNULL(SUM(sor.stock_out_num), 0)
        from stock_out_record sor
        where sor.product_model_id = si.product_model_id
        and (
        (si.batch_no is null and sor.batch_no is null)
        or si.batch_no = sor.batch_no
        )
        and sor.type = '0'
        and sor.approval_status = 0
        ) as qualifiedPendingOut,
        0 as unQualifiedPendingOut
        from stock_inventory si
        left join product_model pm on si.product_model_id = pm.id
        left join product p on pm.product_id = p.id
        union all
        select
        su.batch_no,
        null as qualifiedId,
        su.id as unQualifiedId,
        0 as qualifiedQuantity,
        su.qualitity as unQualifiedQuantity,
        COALESCE(su.locked_quantity, 0) as locked_quantity,
        0 as qualifiedLockedQuantity,
        COALESCE(su.locked_quantity, 0) as unQualifiedLockedQuantity,
        su.product_model_id,
        su.create_time,
        su.update_time,
        0 as warn_num,
        su.version,
        (su.qualitity - COALESCE(su.locked_quantity, 0)) as un_locked_quantity,
        pm.model,
        su.remark,
        pm.unit,
        p.product_name,
        p.id as product_id,
        0 as qualifiedPendingOut,
        (
        select IFNULL(SUM(sor.stock_out_num), 0)
        from stock_out_record sor
        where sor.product_model_id = su.product_model_id
        and (
        (su.batch_no is null and sor.batch_no is null)
        or su.batch_no = sor.batch_no
        )
        and sor.type = '1'
        and sor.approval_status = 0
        ) as unQualifiedPendingOut
        from stock_uninventory su
        left join product_model pm on su.product_model_id = pm.id
        left join product p on pm.product_id = p.id
        ) as combined
        <where>
            <if test="ew.productName != null and ew.productName !=''">
                and combined.product_name in (
                select distinct p.product_name
                from product p
                left join product_model pm on p.id = pm.product_id
                where p.product_name like concat('%',#{ew.productName},'%')
                or pm.model like concat('%',#{ew.productName},'%')
                )
            </if>
            <if test="ew.topParentProductId != null and ew.topParentProductId > 0">
                and combined.product_id in (select id from product_tree)
            </if>
        </where>
        group by
        product_model_id,
        model,
        unit,
        product_name,
        product_id
    </select>
    <select id="listStockInventoryExportData" resultType="com.ruoyi.stock.execl.StockInventoryExportData">
        WITH RECURSIVE product_tree AS (
        SELECT id
        FROM product
        WHERE id = #{ew.topParentProductId}
        UNION ALL
        SELECT p.id
        FROM product p
        INNER JOIN product_tree pt ON p.parent_id = pt.id
        )
        select
            batch_no,
            MAX(qualifiedId) as qualifiedId,
            MAX(unQualifiedId) as unQualifiedId,
@@ -204,84 +336,6 @@
            </if>
        </where>
        group by batch_no, product_model_id, model, unit, product_name, product_id
    </select>
    <select id="listStockInventoryExportData" resultType="com.ruoyi.stock.execl.StockInventoryExportData">
        WITH RECURSIVE product_tree AS (
        SELECT id
        FROM product
        WHERE id = #{ew.topParentProductId}
        UNION ALL
        SELECT p.id
        FROM product p
        INNER JOIN product_tree pt ON p.parent_id = pt.id
        )
        select
            SUM(qualifiedQuantity) as qualifiedQuantity,
            SUM(unQualifiedQuantity) as unQualifiedQuantity,
            SUM(qualifiedLockedQuantity) as qualifiedLockedQuantity,
            SUM(unQualifiedLockedQuantity) as unQualifiedLockedQuantity,
            model,
            unit,
            product_name,
            MAX(warn_num) as warn_num,
            MAX(remark) as remark,
            MAX(update_time) as update_time
        from (
            select
            si.qualitity as qualifiedQuantity,
            0 as unQualifiedQuantity,
            COALESCE(si.locked_quantity, 0) as qualifiedLockedQuantity,
            0 as unQualifiedLockedQuantity,
            si.product_model_id,
            si.create_time,
            si.update_time,
            COALESCE(si.warn_num, 0) as warn_num,
            si.remark,
            pm.model,
            pm.unit,
            p.product_name,
            p.id as product_id
            from stock_inventory si
            left join product_model pm on si.product_model_id = pm.id
            left join product p on pm.product_id = p.id
            union all
            select
            0 as qualifiedQuantity,
            su.qualitity as unQualifiedQuantity,
            0 as qualifiedLockedQuantity,
            COALESCE(su.locked_quantity, 0) as unQualifiedLockedQuantity,
            su.product_model_id,
            su.create_time,
            su.update_time,
            0 as warn_num,
            su.remark,
            pm.model,
            pm.unit,
            p.product_name,
            p.id as product_id
            from stock_uninventory su
            left join product_model pm on su.product_model_id = pm.id
            left join product p on pm.product_id = p.id
        ) as combined
        <where>
            <if test="ew.productName != null and ew.productName !=''">
                and combined.product_name in (
                select distinct p.product_name
                from product p
                left join product_model pm on p.id = pm.product_id
                where p.product_name like concat('%',#{ew.productName},'%') or pm.model like concat('%',#{ew.productName},'%')
                )
            </if>
            <if test="ew.topParentProductId != null and ew.topParentProductId > 0">
                and combined.product_id in (select id from product_tree)
            </if>
        </where>
        group by product_model_id, model, unit, product_name
    </select>
    <select id="stockInventoryPage" resultType="com.ruoyi.stock.dto.StockInRecordDto">
        select sir.*,si.qualitity as current_stock,
@@ -471,6 +525,141 @@
                    group by spd.stock_inventory_id
                 ) as sd on sd.stock_inventory_id = si.id
        where si.product_model_id = #{productModelId}
        and si.qualitity > IFNULL(sd.qualitity, 0)
    </select>
    <select id="getBatchNoQty" resultType="com.ruoyi.stock.dto.StockInventoryDto">
        select
        batch_no,
        MAX(qualifiedId) as qualifiedId,
        MAX(unQualifiedId) as unQualifiedId,
        SUM(qualifiedQuantity) as qualifiedQuantity,
        SUM(unQualifiedQuantity) as unQualifiedQuantity,
        SUM(qualifiedLockedQuantity) as qualifiedLockedQuantity,
        SUM(unQualifiedLockedQuantity) as unQualifiedLockedQuantity,
        SUM(IFNULL(qualifiedPendingOut, 0)) as qualifiedPendingOutQuantity,
        SUM(IFNULL(unQualifiedPendingOut, 0)) as unQualifiedPendingOutQuantity,
        SUM(qualifiedQuantity - qualifiedLockedQuantity - IFNULL(qualifiedPendingOut, 0)) as qualifiedUnLockedQuantity,
        SUM(unQualifiedQuantity - unQualifiedLockedQuantity - IFNULL(unQualifiedPendingOut, 0)) as unQualifiedUnLockedQuantity,
        product_model_id,
        model,
        unit,
        product_name,
        product_id,
        MAX(create_time) as create_time,
        MAX(update_time) as update_time,
        MAX(warn_num) as warn_num,
        MAX(version) as version,
        MAX(remark) as remark,
        'combined' as stockType
        from (
        select
        si.batch_no,
        si.id as qualifiedId,
        null as unQualifiedId,
        si.qualitity as qualifiedQuantity,
        0 as unQualifiedQuantity,
        COALESCE(si.locked_quantity, 0) as qualifiedLockedQuantity,
        0 as unQualifiedLockedQuantity,
        si.product_model_id,
        pm.model,
        pm.unit,
        p.product_name,
        p.id as product_id,
        si.create_time,
        si.update_time,
        COALESCE(si.warn_num, 0) as warn_num,
        si.version,
        si.remark,
        (
        select IFNULL(SUM(sor.stock_out_num), 0)
        from stock_out_record sor
        where sor.product_model_id = si.product_model_id
        and (
        (si.batch_no is null and sor.batch_no is null)
        or si.batch_no = sor.batch_no
        )
        and sor.type = '0'
        and sor.approval_status = 0
        ) as qualifiedPendingOut,
        0 as unQualifiedPendingOut
        from stock_inventory si
        left join product_model pm on si.product_model_id = pm.id
        left join product p on pm.product_id = p.id
        union all
        select
        su.batch_no,
        null as qualifiedId,
        su.id as unQualifiedId,
        0 as qualifiedQuantity,
        su.qualitity as unQualifiedQuantity,
        0 as qualifiedLockedQuantity,
        COALESCE(su.locked_quantity, 0) as unQualifiedLockedQuantity,
        su.product_model_id,
        pm.model,
        pm.unit,
        p.product_name,
        p.id as product_id,
        su.create_time,
        su.update_time,
        0 as warn_num,
        su.version,
        su.remark,
        0 as qualifiedPendingOut,
        (
        select IFNULL(SUM(sor.stock_out_num), 0)
        from stock_out_record sor
        where sor.product_model_id = su.product_model_id
        and (
        (su.batch_no is null and sor.batch_no is null)
        or su.batch_no = sor.batch_no
        )
        and sor.type = '1'
        and sor.approval_status = 0
        ) as unQualifiedPendingOut
        from stock_uninventory su
        left join product_model pm on su.product_model_id = pm.id
        left join product p on pm.product_id = p.id
        ) as combined
        <where>
            <if test="ew.productModelId != null and ew.productModelId > 0">
                and combined.product_model_id = #{ew.productModelId}
            </if>
            <if test="ew.productId != null and ew.productId > 0">
                and combined.product_id = #{ew.productId}
            </if>
        </where>
        group by
        batch_no,
        product_model_id,
        model,
        unit,
        product_name,
        product_id
        order by
        batch_no
    </select>
</mapper>
src/main/resources/mapper/stock/StockOutRecordMapper.xml
@@ -91,7 +91,7 @@
        sor.id,
        sor.outbound_batches,
        sl.customer_name,
        s.shipping_date,
        sor.create_time as shippingDate,
        p.product_name,
        pm.model as specification_model,
        sor.stock_out_num * slp.tax_inclusive_unit_price as outboundAmount,
@@ -103,7 +103,7 @@
        LEFT JOIN sales_ledger_product slp ON s.sales_ledger_product_id = slp.id and slp.type = 1
        left join product_model pm on slp.product_model_id = pm.id
        left join product p on pm.product_id = p.id
        WHERE s.status='已发货' and sor.record_type='13'
        WHERE sor.record_type='13' and sor.approval_status=1
        <if test="req.outboundBatches != null and req.outboundBatches != ''">
            AND sor.outbound_batches LIKE CONCAT('%',#{req.outboundBatches},'%')
        </if>
@@ -111,7 +111,7 @@
            AND sl.customer_name LIKE CONCAT('%',#{req.customerName},'%')
        </if>
        <if test="req.startDate != null and req.endDate != null">
            AND s.shipping_date BETWEEN #{startDate} AND #{endDate}
            AND s.shipping_date BETWEEN #{req.startDate} AND #{req.endDate}
        </if>
        order by sor.id DESC
    </select>
src/main/resources/mapper/technology/TechnologyBomStructureMapper.xml
@@ -21,7 +21,8 @@
               p.product_name as productName,
               pm.product_id as productId,
               pm.model,
               top1.name as operationName
               top1.name as operationName,
               pm.product_code as productCode
        from technology_bom_structure tbs
        left join product_model pm on tbs.product_model_id = pm.id
        left join product p on pm.product_id = p.id