gongchunyi
2026-05-16 c5aac1b321d3840a56ccc41108d42b389d68681e
Merge branch 'dev_New_pro' into dev_山西_晋和园_pro

Integrate dev_New_pro from merge-base 837e2759; resolve application-jhy.yml port/domain for JHY profile.

Co-authored-by: Cursor <cursoragent@cursor.com>
已添加53个文件
已重命名11个文件
已修改134个文件
已删除23个文件
8606 ■■■■ 文件已修改
.gitignore 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260511_设备保养定时任务设备项目字段前端联调说明.md 111 ●●●●● 补丁 | 查看 | 原始文档 | 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/前端联调文档-设备报修保养财务模块改造.md 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/PurchaseInboundDto.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/PurchaseReturnDto.java 29 ●●●●● 补丁 | 查看 | 原始文档 | 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/PurchaseInboundVo.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/PurchaseReturnVo.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/SalesOutboundVo.java 6 ●●●● 补丁 | 查看 | 原始文档 | 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/AccounPurchaseController.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountSalesController.java 12 ●●●● 补丁 | 查看 | 原始文档 | 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/AccountPurchaseService.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountSalesService.java 4 ●●●● 补丁 | 查看 | 原始文档 | 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/AccountPurchaseServiceImpl.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/AccountSalesServiceImpl.java 16 ●●●● 补丁 | 查看 | 原始文档 | 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/approve/bean/vo/ApproveProcessVO.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/CustomerController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/ProductModelDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/StorageBlobVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java 5 ●●●● 补丁 | 查看 | 原始文档 | 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/CustomerServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/utils/FileUtil.java 11 ●●●●● 补丁 | 查看 | 原始文档 | 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/common/enums/StockInQualifiedRecordTypeEnum.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockInUnQualifiedRecordTypeEnum.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockOutQualifiedRecordTypeEnum.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockOutUnQualifiedRecordTypeEnum.java 21 ●●●●● 补丁 | 查看 | 原始文档 | 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 13 ●●●●● 补丁 | 查看 | 原始文档 | 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 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/controller/OaProjectController.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/controller/OaProjectPhaseController.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/controller/OaProjectPhaseTaskController.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/dto/OaProjectDto.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/dto/OaProjectPhaseDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/mapper/OaProjectMapper.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/mapper/OaProjectPhaseMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/mapper/OaProjectPhaseTaskMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/pojo/OaProject.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/pojo/OaProjectPhase.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/pojo/OaProjectPhaseTask.java 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/service/OaProjectPhaseService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/service/OaProjectPhaseTaskService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/service/OaProjectService.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/service/impl/OaProjectPhaseServiceImpl.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/service/impl/OaProjectPhaseTaskServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/oA/service/impl/OaProjectServiceImpl.java 67 ●●●●● 补丁 | 查看 | 原始文档 | 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 4 ●●●● 补丁 | 查看 | 原始文档 | 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 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnSaleProductServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java 78 ●●●●● 补丁 | 查看 | 原始文档 | 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 595 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysLoginController.java 42 ●●●●● 补丁 | 查看 | 原始文档 | 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 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/ProductRecord.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseReturnOrderProducts.java 20 ●●●● 补丁 | 查看 | 原始文档 | 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 42 ●●●●● 补丁 | 查看 | 原始文档 | 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/service/impl/QualityInspectServiceImpl.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityUnqualifiedServiceImpl.java 54 ●●●● 补丁 | 查看 | 原始文档 | 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 8 ●●●● 补丁 | 查看 | 原始文档 | 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 164 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockUninventoryController.java 12 ●●●● 补丁 | 查看 | 原始文档 | 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/StockInRecordMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/mapper/StockOutRecordMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/pojo/StockOutRecord.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/StockInventoryService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | 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/logback.xml 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountSubjectMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/SalesRefundAmountOrderMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/SealApplicationManagementMapper.xml 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceRepairMapper.xml 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/oA/OaProjectMapper.xml 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/oA/OaProjectPhaseMapper.xml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/oA/OaProjectPhaseTaskMapper.xml 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ProcurementRecordMapper.xml 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ProcurementRecordOutMapper.xml 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ReturnSaleProductMapper.xml 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseReturnOrderProductsMapper.xml 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesQuotationMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ShippingInfoMapper.xml 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInRecordMapper.xml 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 371 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockOutRecordMapper.xml 29 ●●●●● 补丁 | 查看 | 原始文档 | 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
@@ -43,4 +43,4 @@
!*/build/*.java
!*/build/*.html
!*/build/*.xml
!*/build/*.xml
doc/20260511_É豸±£Ñø¶¨Ê±ÈÎÎñÉ豸ÏîÄ¿×Ö¶Îǰ¶ËÁªµ÷˵Ã÷.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
# è®¾å¤‡ä¿å…»å®šæ—¶ä»»åŠ¡è®¾å¤‡é¡¹ç›®å­—æ®µå‰ç«¯è”è°ƒè¯´æ˜Ž
> æ›´æ–°æ—¶é—´ï¼š2026-05-11
> é€‚用范围:设备保养定时任务(`maintenance_task`)与设备保养记录(`device_maintenance`)
## 1. å˜æ›´æ¦‚è¿°
本次后端变更:
1. `MaintenanceTask` æ–°å¢žå­—段 `machineryCategory`(设备项目)。
2. å®šæ—¶ä»»åŠ¡æ‰§è¡Œç”Ÿæˆè®¾å¤‡ä¿å…»è®°å½•æ—¶ï¼Œä¼šæŠŠ `MaintenanceTask.machineryCategory` åŒæ­¥å†™å…¥ `DeviceMaintenance.machineryCategory`。
对应代码位置:
- `src/main/java/com/ruoyi/device/pojo/MaintenanceTask.java`
- `src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java`
## 2. æ•°æ®åº“变更
需要先执行数据库变更(否则新增/查询该字段会异常):
```sql
ALTER TABLE `maintenance_task`
  ADD COLUMN `machinery_category` VARCHAR(100) NULL COMMENT '设备项目' AFTER `device_model`;
```
## 3. å­—段约定
| å­—段 | å«ä¹‰ | ç±»åž‹ | å»ºè®® |
| --- | --- | --- | --- |
| machineryCategory | è®¾å¤‡é¡¹ç›® | string | å‰ç«¯æ–°å¢ž/编辑定时任务时传值 |
说明:
- åŽç«¯å½“前未对该字段做强制非空校验,但业务上建议前端作为必填处理。
- åŽ†å² `maintenance_task` æ—§æ•°æ®è‹¥è¯¥å­—段为空,则基于旧任务生成的保养记录也会为空。
## 4. æŽ¥å£è”è°ƒ
### 4.1 æ–°å¢žå®šæ—¶ä»»åŠ¡
```http
POST /deviceMaintenanceTask/add
Content-Type: application/json
```
请求示例:
```json
{
  "taskName": "空压机A-周保养",
  "taskId": 1001,
  "deviceModel": "GA75",
  "machineryCategory": "动力设备",
  "frequencyType": "WEEKLY",
  "frequencyDetail": "MON,09:00",
  "registrantId": 1,
  "registrationDate": "2026-05-11",
  "remarks": "联调样例"
}
```
### 4.2 ä¿®æ”¹å®šæ—¶ä»»åŠ¡
```http
POST /deviceMaintenanceTask/update
Content-Type: application/json
```
请求示例(含 `id`):
```json
{
  "id": 12,
  "machineryCategory": "动力设备"
}
```
### 4.3 å®šæ—¶ä»»åŠ¡åˆ—è¡¨
```http
GET /deviceMaintenanceTask/listPage?pageNum=1&pageSize=10
```
返回记录中会包含 `machineryCategory` å­—段。
### 4.4 è®¾å¤‡ä¿å…»è®°å½•列表/详情
```http
GET /device/maintenance/page?pageNum=1&pageSize=10
GET /device/maintenance/{id}
```
返回中 `machineryCategory` æ¥æºäºŽ `device_maintenance.machinery_category`。
对于“由定时任务自动生成”的记录,该值会继承生成时对应任务的 `machineryCategory`。
## 5. å‰ç«¯æ”¹é€ ç‚¹
1. å®šæ—¶ä»»åŠ¡æ–°å¢ž/编辑表单增加“设备项目(machineryCategory)”输入项。
2. æäº¤ `/deviceMaintenanceTask/add`、`/deviceMaintenanceTask/update` æ—¶æºå¸¦ `machineryCategory`。
3. å®šæ—¶ä»»åŠ¡åˆ—è¡¨å¢žåŠ â€œè®¾å¤‡é¡¹ç›®â€åˆ—ï¼ˆå¦‚é¡µé¢æœ‰è¯¥åˆ—è¡¨ï¼‰ã€‚
4. è®¾å¤‡ä¿å…»è®°å½•列表/详情增加“设备项目”展示,空值显示 `--`。
## 6. è”调验收清单
1. æ–°å¢žä¸€ä¸ªå¸¦ `machineryCategory` çš„定时任务,保存成功。
2. æŸ¥è¯¢ `/deviceMaintenanceTask/listPage`,确认该任务返回 `machineryCategory`。
3. ç­‰å¾…一次定时触发(或将频率调到临近时间),生成保养记录。
4. æŸ¥è¯¢ `/device/maintenance/page` æˆ–详情接口,确认新记录的 `machineryCategory` ä¸Žä»»åŠ¡ä¸€è‡´ã€‚
5. ä¿®æ”¹ä»»åŠ¡ `machineryCategory` åŽå†æ¬¡è§¦å‘,确认“新生成记录”使用新值(历史记录不回写)。
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/ǰ¶ËÁªµ÷Îĵµ-É豸±¨ÐÞ±£Ñø²ÆÎñÄ£¿é¸ÄÔì.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
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package com.ruoyi.account.bean.dto;
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;
@Data
@Schema(name = "PurchaseInboundDto", description = "财务管理--采购入库台账(传参)")
public class PurchaseInboundDto {
    @Schema(description = "入库单号")
    private String inboundBatches;
    @Schema(description = "供应商")
    private String supplierName;
    @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
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package com.ruoyi.account.bean.dto;
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;
@Data
@Schema(name = "PurchaseReturnDto", description = "财务管理--采购退货台账(传参)")
public class PurchaseReturnDto {
    @Schema(description = "退货单号")
    private String returnNo;
    @Schema(description = "供应商")
    private String supplierName;
    @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/PurchaseInboundVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package com.ruoyi.account.bean.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Schema(name = "PurchaseInboundVo", description = "财务管理--采购入库台账(返回)")
@ExcelIgnoreUnannotated
public class PurchaseInboundVo {
    @Schema(description = "入库单id")
    private Long id;
    @Schema(description = "入库单号")
    @Excel(name = "入库单号")
    private String inboundBatches;
    @Schema(description = "供应商")
    @Excel(name = "供应商")
    private String supplierName;
    @Schema(description = "入库日期")
    @Excel(name = "入库日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date InboundDate;
    @Schema(description = "产品名称")
    @Excel(name = "产品名称")
    private String productName;
    @Schema(description = "产品规格")
    @Excel(name = "产品规格")
    private String  specificationModel;
    @Schema(description = "金额")
    @Excel(name = "金额")
    private BigDecimal InboundAmount;
    @Schema(description = "采购订单号")
    @Excel(name = "采购订单号")
    private String purchaseContractNumber;
}
src/main/java/com/ruoyi/account/bean/vo/PurchaseReturnVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package com.ruoyi.account.bean.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@Schema(name = "PurchaseReturnVo", description = "财务管理--采购退货台账(返回)")
@ExcelIgnoreUnannotated
public class PurchaseReturnVo {
    @Schema(description = "退货单id")
    private Long id;
    @Excel(name = "退货单号")
    @Schema(description = "退货单号")
    private String returnNo;
    @Schema(description = "供应商")
    @Excel(name = "供应商")
    private String supplierName;
    @Schema(description = "关联入库单号")
    @Excel(name = "关联入库单号")
    private String inboundBatches;
    @Schema(description = "退货日期")
    @Excel(name = "退货日期")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime preparedAt;
    @Schema(description = "退款总额")
    @Excel(name = "退款总额")
    private BigDecimal totalAmount;
    @Schema(description = "退货方式")
    @Excel(name = "退货方式")
    private String returnType;
    @Schema(description = "采购订单号")
    @Excel(name = "采购订单号")
    private String purchaseContractNumber;
}
src/main/java/com/ruoyi/account/bean/vo/SalesOutboundVo.java
@@ -38,9 +38,9 @@
    @Excel(name = "产品规格")
    private String  specificationModel;
    @Schema(description = "出库数量")
    @Excel(name = "出库数量")
    private BigDecimal stockOutNum;
    @Schema(description = "金额")
    @Excel(name = "金额")
    private BigDecimal outboundAmount;
    @Schema(description = "发货编号")
    @Excel(name = "发货编号")
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/AccounPurchaseController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.PurchaseReturnVo;
import com.ruoyi.account.service.AccountPurchaseService;
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 jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†çš„é‡‡è´­éƒ¨åˆ† å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-07 04:45:30
 */
@RestController
@RequestMapping("/accountPurchase")
@RequiredArgsConstructor
@Tag(name = "财务管理的采购部分")
public class AccounPurchaseController {
    private final AccountPurchaseService accountPurchaseService;
    @GetMapping("/listPageAccountPurchase")
    @Log(title = "采购入库台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--采购入库台账")
    public R<IPage<PurchaseInboundVo>> listPageAccountPurchase(Page page, PurchaseInboundDto purchaseInboundDto) {
        IPage<PurchaseInboundVo> listPage = accountPurchaseService.listPageAccountPurchase(page,purchaseInboundDto);
        return R.ok(listPage);
    }
    @PostMapping("/exportAccountPurchaseInbound")
    @Operation(summary = "导出采购入库文件")
    @Log(title = "导出采购入库文件", businessType = BusinessType.EXPORT)
    public void exportAccountPurchaseInbound(HttpServletResponse response, PurchaseInboundDto purchaseInboundDto) {
        accountPurchaseService.exportAccountPurchaseInbound(response,purchaseInboundDto);
    }
    @GetMapping("/listPageAccountPurchaseReturn")
    @Log(title = "采购退货台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--采购退货台账")
    public R<IPage<PurchaseReturnVo>> listPageAccountPurchaseReturn(Page page, PurchaseReturnDto purchaseReturnDto) {
        IPage<PurchaseReturnVo> listPage = accountPurchaseService.listPageAccountPurchaseReturn(page,purchaseReturnDto);
        return R.ok(listPage);
    }
    @PostMapping("/exportAccountPurchaseReturn")
    @Operation(summary = "导出采购退货文件")
    @Log(title = "导出采购退货文件", businessType = BusinessType.EXPORT)
    public void exportAccountPurchaseReturn(HttpServletResponse response,PurchaseReturnDto purchaseReturnDto) {
        accountPurchaseService.exportAccountPurchaseReturn(response,purchaseReturnDto);
    }
}
src/main/java/com/ruoyi/account/controller/AccountSalesController.java
@@ -35,11 +35,11 @@
    private final AccountSalesService accountSalesService;
    @GetMapping("/listPageByOutbound")
    @GetMapping("/listPageAccountSales")
    @Log(title = "销售出库台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--销售出库台账")
    public R<IPage<SalesOutboundVo>> listPageByOutbound(Page page, SalesOutboundDto salesOutboundDto) {
        IPage<SalesOutboundVo> listPage = accountSalesService.listPageByOutbound(page,salesOutboundDto);
    public R<IPage<SalesOutboundVo>> listPageAccountSales(Page page, SalesOutboundDto salesOutboundDto) {
        IPage<SalesOutboundVo> listPage = accountSalesService.listPageAccountSales(page,salesOutboundDto);
        return R.ok(listPage);
    }
@@ -50,11 +50,11 @@
        accountSalesService.exportAccountSalesOutbound(response,salesOutboundDto);
    }
    @GetMapping("/listPageByReturn")
    @GetMapping("/listPageAccountSalesReturn")
    @Log(title = "销售退货台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--销售退货台账")
    public R<IPage<SalesReturnVo>> listPageBySalesReturn(Page page, SalesReturnDto salesReturnDto) {
        IPage<SalesReturnVo> listPage = accountSalesService.listPageBySalesReturn(page,salesReturnDto);
    public R<IPage<SalesReturnVo>> listPageAccountSalesReturn(Page page, SalesReturnDto salesReturnDto) {
        IPage<SalesReturnVo> listPage = accountSalesService.listPageAccountSalesReturn(page,salesReturnDto);
        return R.ok(listPage);
    }
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/AccountPurchaseService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package com.ruoyi.account.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.PurchaseReturnVo;
import jakarta.servlet.http.HttpServletResponse;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†çš„é”€å”®éƒ¨åˆ† æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-07 04:45:30
 */
public interface AccountPurchaseService {
    IPage<PurchaseInboundVo> listPageAccountPurchase(Page page, PurchaseInboundDto purchaseInboundDto);
    void exportAccountPurchaseInbound(HttpServletResponse response, PurchaseInboundDto purchaseInboundDto);
    IPage<PurchaseReturnVo> listPageAccountPurchaseReturn(Page page, PurchaseReturnDto purchaseReturnDto);
    void exportAccountPurchaseReturn(HttpServletResponse response, PurchaseReturnDto purchaseReturnDto);
}
src/main/java/com/ruoyi/account/service/AccountSalesService.java
@@ -18,11 +18,11 @@
 */
public interface AccountSalesService  {
    IPage<SalesOutboundVo> listPageByOutbound(Page page, SalesOutboundDto salesOutboundDto);
    IPage<SalesOutboundVo> listPageAccountSales(Page page, SalesOutboundDto salesOutboundDto);
    void exportAccountSalesOutbound(HttpServletResponse response, SalesOutboundDto salesOutboundDto);
    IPage<SalesReturnVo> listPageBySalesReturn(Page page, SalesReturnDto salesReturnDto);
    IPage<SalesReturnVo> listPageAccountSalesReturn(Page page, SalesReturnDto salesReturnDto);
    void exportAccountSalesReturn(HttpServletResponse response, SalesReturnDto salesReturnDto);
}
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/AccountPurchaseServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.PurchaseReturnVo;
import com.ruoyi.account.service.AccountPurchaseService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†çš„é”€å”®éƒ¨åˆ† æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-07 04:45:30
 */
@Service
@RequiredArgsConstructor
public class AccountPurchaseServiceImpl implements AccountPurchaseService {
    private final StockInRecordMapper stockInRecordMapper;
    private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
    @Override
    public IPage<PurchaseInboundVo> listPageAccountPurchase(Page page, PurchaseInboundDto purchaseInboundDto) {
        return stockInRecordMapper.listPageAccountPurchase(page,purchaseInboundDto);
    }
    @Override
    public void exportAccountPurchaseInbound(HttpServletResponse response, PurchaseInboundDto purchaseInboundDto) {
        List<PurchaseInboundVo> list = stockInRecordMapper.listPageAccountPurchase(new Page(1,-1),purchaseInboundDto).getRecords();
        ExcelUtil<PurchaseInboundVo> util = new ExcelUtil<>(PurchaseInboundVo.class);
        util.exportExcel(response, list , "采购入库");
    }
    @Override
    public IPage<PurchaseReturnVo> listPageAccountPurchaseReturn(Page page, PurchaseReturnDto purchaseReturnDto) {
        return purchaseReturnOrdersMapper.listPageAccountPurchaseReturn(page,purchaseReturnDto);
    }
    @Override
    public void exportAccountPurchaseReturn(HttpServletResponse response, PurchaseReturnDto purchaseReturnDto) {
        List<PurchaseReturnVo> list = purchaseReturnOrdersMapper.listPageAccountPurchaseReturn(new Page(1,-1),purchaseReturnDto).getRecords();
        ExcelUtil<PurchaseReturnVo> util = new ExcelUtil<>(PurchaseReturnVo.class);
        util.exportExcel(response, list , "采购退货");
    }
}
src/main/java/com/ruoyi/account/service/impl/AccountSalesServiceImpl.java
@@ -9,7 +9,7 @@
import com.ruoyi.account.service.AccountSalesService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -28,30 +28,30 @@
@RequiredArgsConstructor
public class AccountSalesServiceImpl  implements AccountSalesService {
    private final ShippingInfoMapper shippingInfoMapper;
    private final StockOutRecordMapper stockOutRecordMapper;
    private final ReturnManagementMapper returnManagementMapper;
    @Override
    public IPage<SalesOutboundVo> listPageByOutbound(Page page, SalesOutboundDto salesOutboundDto) {
        return shippingInfoMapper.listPageByOutbound(page,salesOutboundDto);
    public IPage<SalesOutboundVo> listPageAccountSales(Page page, SalesOutboundDto salesOutboundDto) {
        return stockOutRecordMapper.listPageAccountSales(page,salesOutboundDto);
    }
    @Override
    public void exportAccountSalesOutbound(HttpServletResponse response, SalesOutboundDto salesOutboundDto) {
        List<SalesOutboundVo> list = shippingInfoMapper.listPageByOutbound(new Page(1,-1),salesOutboundDto).getRecords();
        List<SalesOutboundVo> list = stockOutRecordMapper.listPageAccountSales(new Page(1,-1),salesOutboundDto).getRecords();
        ExcelUtil<SalesOutboundVo> util = new ExcelUtil<>(SalesOutboundVo.class);
        util.exportExcel(response, list , "销售出库");
    }
    @Override
    public IPage<SalesReturnVo> listPageBySalesReturn(Page page, SalesReturnDto salesReturnDto) {
        return returnManagementMapper.listPageBySalesReturn(page,salesReturnDto);
    public IPage<SalesReturnVo> listPageAccountSalesReturn(Page page, SalesReturnDto salesReturnDto) {
        return returnManagementMapper.listPageAccountSalesReturn(page,salesReturnDto);
    }
    @Override
    public void exportAccountSalesReturn(HttpServletResponse response, SalesReturnDto salesReturnDto) {
        List<SalesReturnVo> list = returnManagementMapper.listPageBySalesReturn(new Page(1,-1),salesReturnDto).getRecords();
        List<SalesReturnVo> list = returnManagementMapper.listPageAccountSalesReturn(new Page(1,-1),salesReturnDto).getRecords();
        ExcelUtil<SalesReturnVo> util = new ExcelUtil<>(SalesReturnVo.class);
        util.exportExcel(response, list , "销售退货");
    }
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/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
@@ -15,6 +15,7 @@
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.procurementrecord.utils.StockUtils;
@@ -31,14 +32,8 @@
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
import com.ruoyi.sales.mapper.CommonFileMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.mapper.SalesQuotationMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.SalesQuotation;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.mapper.*;
import com.ruoyi.sales.pojo.*;
import com.ruoyi.sales.service.impl.CommonFileServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -64,6 +59,7 @@
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesQuotationMapper salesQuotationMapper;
    private final ShippingInfoMapper shippingInfoMapper;
    private final ShippingProductDetailMapper shippingProductDetailMapper;
    private final CommonFileServiceImpl commonFileService;
    private final StockUtils stockUtils;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
@@ -183,7 +179,7 @@
                            addQualityInspect(purchaseLedger, salesLedgerProduct);
                        } else {
                            //直接入库
                            stockUtils.addStockWithBatchNo(salesLedgerProduct.getProductModelId(), salesLedgerProduct.getQuantity(), StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(), purchaseLedger.getId(),purchaseLedger.getPurchaseContractNumber()+"-"+salesLedgerProduct.getId());
                            stockUtils.addStockWithBatchNo(salesLedgerProduct.getProductModelId(), salesLedgerProduct.getQuantity(), StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(), purchaseLedger.getId(), purchaseLedger.getPurchaseContractNumber() + "-" + salesLedgerProduct.getId());
                        }
                    }
                } else if (status.equals(3)) {
@@ -220,14 +216,18 @@
            if (shippingInfo != null) {
                if (status.equals(2)) {
                    shippingInfo.setStatus("审核通过");
                    shippingInfo.setShippingDate(new Date());
                    //更改出库审核状态(待确认改成待审核)
                    stockUtils.shipmentStatus(StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), shippingInfo.getId());
                } else if (status.equals(3)) {
                    //删除原本(待确认)的出库审核状态
                    stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
                    shippingInfo.setStatus("审核拒绝");
                } else if (status.equals(1)) {
                    shippingInfo.setStatus("审核中");
                }
                shippingInfoMapper.updateById(shippingInfo);
            }
            //库存扣减
        }
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVE_NODE, approveNode.getId(), approveNode.getStorageBlobDTOS());
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/controller/CustomerController.java
@@ -148,8 +148,8 @@
     * ç§æµ·å®¢æˆ·æµå›žå…¬æµ·
     */
    @Log(title = "客户档案", businessType = BusinessType.OTHER)
    @PostMapping("/back")
    public R back(Long id) {
    @PostMapping("/back/{id}")
    public R back(@PathVariable("id") Long id) {
        return R.ok(customerService.back(id));
    }
}
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/dto/StorageBlobVO.java
@@ -10,6 +10,10 @@
     */
    private String previewURL;
    private String url;
    private String name;
    /**
     * ä¸‹è½½åœ°å€
     */
src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java
@@ -132,10 +132,6 @@
    INBOUND_MANAGEMENT("inbound_management"),
    // Office Supplies
    OFFICE_SUPPLIES("office_supplies"),
    // OA
    OA_PROJECT_PHASE_TASK("oa_project_phase_task"),
    OA_PROJECT("oa_project"),
    OA_PROJECT_PHASE("oa_project_phase"),
    // Measuring Instrument Ledger
    SPARE_PARTS("spare_parts"),
    MEASURING_INSTRUMENT_LEDGER_RECORD("measuring_instrument_ledger_record"),
@@ -208,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/CustomerServiceImpl.java
@@ -375,7 +375,7 @@
        //将客户的type改为1 ä¸”直接分配给当前用户
        Customer customer = customerMapper.selectById(id);
        customer.setType(1);
        customer.setIsAssigned(1);
        customer.setIsAssigned(0);
        return this.updateById(customer);
    }
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("父节点不存在,无法添加子产品");
                }
            // æ£€æŸ¥çˆ¶èŠ‚ç‚¹æ˜¯å¦å­˜åœ¨ï¼ˆå¯é€‰ï¼Œæ ¹æ®ä¸šåŠ¡éœ€æ±‚ï¼‰
            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/basic/utils/FileUtil.java
@@ -98,8 +98,11 @@
        }
        // åˆ é™¤æ—§é™„件信息
        if (application == null) {
        if (application == null || application.trim().isEmpty()) {
            for (StorageBlobDTO storageBlobDTO : storageBlobDTOS) {
                if (storageBlobDTO.getApplication() == null || storageBlobDTO.getApplication().trim().isEmpty()) {
                    throw new RuntimeException("文件用途不能为空");
                }
                deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.getByType(storageBlobDTO.getApplication()), recordType, recordId);
            }
        } else {
@@ -344,6 +347,8 @@
            StorageBlobVO storageBlobVO = new StorageBlobVO();
            BeanUtils.copyProperties(storageBlob, storageBlobVO);
            storageBlobVO.setPreviewURL(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setUrl(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setName(storageBlob.getOriginalFilename());
            storageBlobVO.setDownloadURL(buildSignedDownloadUrl(storageBlobVO));
            storageBlobVO.setStorageAttachmentId(blobIdToAttachmentIdMap.get(storageBlob.getId()));
            storageBlobDTOS.add(storageBlobVO);
@@ -392,6 +397,8 @@
            StorageBlobVO storageBlobVO = new StorageBlobVO();
            BeanUtils.copyProperties(storageBlob, storageBlobVO);
            storageBlobVO.setPreviewURL(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setUrl(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setName(storageBlob.getOriginalFilename());
            storageBlobVO.setDownloadURL(buildSignedDownloadUrl(storageBlobVO));
            storageBlobVO.setStorageAttachmentId(blobIdToAttachmentIdMap.get(storageBlob.getId()));
            storageBlobDTOS.add(storageBlobVO);
@@ -417,6 +424,8 @@
            StorageBlobVO storageBlobVO = new StorageBlobVO();
            BeanUtils.copyProperties(storageBlob, storageBlobVO);
            storageBlobVO.setPreviewURL(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setUrl(buildSignedPreviewUrl(storageBlobVO));
            storageBlobVO.setName(storageBlob.getOriginalFilename());
            storageBlobVO.setDownloadURL(buildSignedDownloadUrl(storageBlobVO));
            storageBlobDTOS.add(storageBlobVO);
        }
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/common/enums/StockInQualifiedRecordTypeEnum.java
@@ -2,29 +2,23 @@
import lombok.Getter;
//入库枚举
@Getter
public enum StockInQualifiedRecordTypeEnum implements BaseEnum<String> {
    CUSTOMIZATION_STOCK_IN("0", "合格自定义入库"),
    CUSTOMIZATION_STOCK_OUT("1", "合格自定义出库"),
    PRODUCTION_REPORT_STOCK_IN("2", "生产报工-入库"),
    PRODUCTION_REPORT_STOCK_OUT("3", "生产报工-出库"),
    DEFECTIVE_SCRAP("4", "不合格处理-报废"),
    PRODUCTION_SCRAP("5", "生产报工-报废"),
    QUALITYINSPECT_STOCK_IN("6", "质检-合格入库"),
    PURCHASE_STOCK_IN("7", "采购-入库"),
    SALE_STOCK_OUT("8", "销售-出库"),
    CUSTOMIZATION_UNSTOCK_IN("9", "不合格自定义入库"),
    CUSTOMIZATION_UNSTOCK_OUT("10", "不合格自定义出库"),
    CUSTOMIZATION_UNSTOCK_OUT("10", "采购-质检-合格入库"),
    DEFECTIVE_PASS("11", "不合格-让步放行"),
    QUALITYINSPECT_UNSTOCK_IN("12", "质检-不合格入库"),
    SALE_SHIP_STOCK_OUT("13", "销售-发货出库"),
    RETURN_HE_IN("14", "销售退货-合格入库"),
    RETURN_UNSTOCK_IN("15", "销售退货-不合格入库"),
    PICK_RETURN_IN("20", "领料退料-合格入库"),
    PURCHASE_RETURN_STOCK_OUT("21", "采购退货"),
    FEED_RETURN_IN("22", "生产退料-合格入库");
src/main/java/com/ruoyi/common/enums/StockInUnQualifiedRecordTypeEnum.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/common/enums/StockOutQualifiedRecordTypeEnum.java
@@ -9,6 +9,7 @@
    PRODUCTION_REPORT_STOCK_OUT("3", "生产报工-出库"),
    SALE_STOCK_OUT("8", "销售-出库"),
    PURCHASE_RETURN_STOCK_OUT("9", "采购退货"),
    CUSTOMIZATION_UNSTOCK_OUT("10", "不合格自定义出库"),
    SALE_SHIP_STOCK_OUT("13", "销售-发货出库"),
    PICK_STOCK_OUT("14", "生产领料出库"),
    FEED_STOCK_OUT("15", "生产补料出库");
src/main/java/com/ruoyi/common/enums/StockOutUnQualifiedRecordTypeEnum.java
ÎļþÒÑɾ³ý
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
@@ -4,6 +4,7 @@
import com.ruoyi.device.pojo.MaintenanceTask;
import lombok.RequiredArgsConstructor;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@@ -18,14 +19,15 @@
import java.util.Set;
@Component
@DisallowConcurrentExecution // ç¦æ­¢å¹¶å‘执行同一个Job
@RequiredArgsConstructor
@DisallowConcurrentExecution
public class MaintenanceTaskJob implements Job, Serializable {
    private static final long serialVersionUID = 1L; // å¿…须定义序列化ID
    private static final long serialVersionUID = 1L;
    private final DeviceMaintenanceServiceImpl deviceMaintenanceService;
    @Autowired
    private DeviceMaintenanceServiceImpl deviceMaintenanceService;
    private final JdbcTemplate jdbcTemplate;
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
@@ -91,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,8 +3,9 @@
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;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@@ -20,12 +21,13 @@
@Component
@DisallowConcurrentExecution // ç¦æ­¢å¹¶å‘执行同一个Job
@RequiredArgsConstructor
public class TimingTaskJob implements Job, Serializable {
    private static final long serialVersionUID = 1L; // å¿…须定义序列化ID
    private final InspectionTaskMapper inspectionTaskMapper;
    private final JdbcTemplate jdbcTemplate;
    @Autowired
    private InspectionTaskMapper inspectionTaskMapper;
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
@@ -46,7 +48,10 @@
            if (timingTask == null) {
                throw new JobExecutionException("找不到定时任务: " + taskId);
            }
            if (timingTask.getIsEnabled() != null && timingTask.getIsEnabled() == 0) {
                return;
            }
//            if (!timingTask.isActive()) {
//                throw new JobExecutionException("定时任务已禁用: " + taskId);
//            }
@@ -98,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) {
                // æ›´æ–°æˆåŠŸåŽé‡æ–°è°ƒåº¦ä»»åŠ¡
                timingTaskScheduler.rescheduleTimingTask(timingTask);
                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/oA/controller/OaProjectController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/controller/OaProjectPhaseController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/controller/OaProjectPhaseTaskController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/dto/OaProjectDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/dto/OaProjectPhaseDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/mapper/OaProjectMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/mapper/OaProjectPhaseMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/mapper/OaProjectPhaseTaskMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/pojo/OaProject.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/pojo/OaProjectPhase.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/pojo/OaProjectPhaseTask.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/service/OaProjectPhaseService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/service/OaProjectPhaseTaskService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/service/OaProjectService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/service/impl/OaProjectPhaseServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/service/impl/OaProjectPhaseTaskServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/oA/service/impl/OaProjectServiceImpl.java
ÎļþÒÑɾ³ý
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;
@@ -25,5 +25,5 @@
    ReturnManagementDto getReturnManagementDtoById(Long id);
    IPage<SalesReturnVo> listPageBySalesReturn(Page page, @Param("req") SalesReturnDto salesReturnDto);
    IPage<SalesReturnVo> listPageAccountSalesReturn(Page page, @Param("req") SalesReturnDto salesReturnDto);
}
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
@@ -8,21 +8,19 @@
import com.ruoyi.account.pojo.AccountExpense;
import com.ruoyi.account.service.SalesRefundAmountOrderService;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
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;
@@ -33,7 +31,6 @@
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -77,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
@@ -106,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);
@@ -118,11 +116,11 @@
            salesRefundAmountOrder.setRefundedAmount(new BigDecimal(0));
            // æ˜¯å¦æœ‰è´¨é‡é—®é¢˜
            if (returnSaleProduct.getIsQuality() == 1) {
                // æœ‰è´¨é‡é—®é¢˜ï¼Œå…¥ä¸åˆæ ¼åº“
                stockUtils.addUnStock(returnSaleProduct.getProductModelId(),returnSaleProduct.getNum(), StockInUnQualifiedRecordTypeEnum.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
@@ -1,12 +1,13 @@
package com.ruoyi.procurementrecord.utils;
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;
@@ -28,7 +29,6 @@
    private final StockInventoryService stockInventoryService;
    private final StockInRecordService stockInRecordService;
    private final StockOutRecordService stockOutRecordService;
    private final StockInventoryMapper stockInventoryMapper;
    /**
     * ä¸åˆæ ¼å…¥åº“
@@ -44,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);
    }
@@ -66,8 +84,6 @@
    /**
     * åˆæ ¼å…¥åº“
     * @param productModelId
     * @param quantity
     * @param recordType
     * @param recordId
     */
@@ -105,15 +121,6 @@
     * @param recordType
     * @param recordId
     */
    public void substractStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId) {
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setRecordId(recordId);
        stockInventoryDto.setRecordType(String.valueOf(recordType));
        stockInventoryDto.setQualitity(quantity);
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryService.subtractStockInventory(stockInventoryDto);
    }
    public void substractStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId, String batchNo) {
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setRecordId(recordId);
@@ -121,38 +128,49 @@
        stockInventoryDto.setQualitity(quantity);
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setBatchNo(batchNo);
        stockInventoryService.subtractStockInventory(stockInventoryDto);
        stockInventoryService.addStockOutRecordOnly(stockInventoryDto);
    }
    /**
     * å‘货审批状态更改
     * @param recordType
     * @param recordId
     */
    public void shipmentStatus(String recordType, Long recordId) {
        LambdaQueryWrapper<StockOutRecord> queryWrapper = new LambdaQueryWrapper<StockOutRecord>().eq(StockOutRecord::getRecordType, recordType)
                .eq(StockOutRecord::getRecordId, recordId);
        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)) {
            stockInRecordService.batchDelete(Collections.singletonList(one.getId()));
            //将库存减回来
            StockInventoryDto stockInventoryDto = new StockInventoryDto();
            stockInventoryDto.setRecordId(recordId);
            stockInventoryDto.setRecordType(recordType);
            stockInventoryDto.setQualitity(one.getStockInNum());
            stockInventoryMapper.updateSubtractStockInventory((stockInventoryDto));
            if (ReviewStatusEnum.APPROVED.getCode().equals(one.getApprovalStatus())) {
                stockInRecordService.batchDelete(Collections.singletonList(one.getId()));
            } 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)) {
            stockOutRecordService.batchDelete(Collections.singletonList(one.getId()));
            //将库存加回来
            StockInventoryDto stockInventoryDto = new StockInventoryDto();
            stockInventoryDto.setRecordId(recordId);
            stockInventoryDto.setRecordType(recordType);
            stockInventoryDto.setQualitity(one.getStockOutNum());
            stockInventoryMapper.updateAddStockInventory((stockInventoryDto));
            if (ReviewStatusEnum.APPROVED.getCode().equals(one.getApprovalStatus())) {
                stockOutRecordService.batchDelete(Collections.singletonList(one.getId()));
            } 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>
@@ -26,7 +52,17 @@
@RequiredArgsConstructor()
public class ProductionBomStructureServiceImpl extends ServiceImpl<ProductionBomStructureMapper, ProductionBomStructure> implements ProductionBomStructureService {
    private  final ProductionBomStructureMapper productionBomStructureMapper;
    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,566 @@
        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<>();
        for (ProductionBomStructure structure : structureList) {
            if (structure == null || structure.getId() == null) {
                continue;
            }
            BigDecimal demandedQuantity = defaultDecimal(structure.getUnitQuantity()).multiply(orderQuantity);
            if (compareDecimal(structure.getDemandedQuantity(), demandedQuantity) == 0) {
                continue;
            }
            ProductionBomStructure update = new ProductionBomStructure();
            update.setId(structure.getId());
            update.setDemandedQuantity(demandedQuantity);
            updateList.add(update);
            structure.setDemandedQuantity(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(matchedOperation, orderRouting.getId(), productionOrderId, desiredOperation);
            }
            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/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);
@@ -311,6 +327,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) {
@@ -788,12 +883,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
@@ -12,7 +12,6 @@
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.bean.BeanUtils;
@@ -27,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;
@@ -78,6 +79,7 @@
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final StockUtils stockUtils;
    private final StockInRecordService stockInRecordService;
    private final StockInventoryService stockInventoryService;
    @Override
@@ -292,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());
@@ -301,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()
@@ -353,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));
@@ -391,8 +398,6 @@
            }
            ProductionAccount productionAccount = new ProductionAccount();
            productionAccount.setProductionProductMainId(productionProductMain.getId());
//            productionAccount.setSalesLedgerId(productionOrder.getSalesLedgerId());
//            productionAccount.setSalesLedgerProductId(productionOrder.getSalesLedgerProductId() == null ? null : productionOrder.getSalesLedgerProductId().longValue());
            productionAccount.setSchedulingUserId(user == null ? null : user.getUserId());
            productionAccount.setSchedulingUserName(user == null ? dto.getUserName() : user.getNickName());
            productionAccount.setFinishedNum(productQty);
@@ -401,11 +406,37 @@
            productionAccount.setSchedulingDate(LocalDateTime.now());
            productionAccountMapper.insert(productionAccount);
        }
//        if (defaultDecimal(dto.getScrapQty()).compareTo(BigDecimal.ZERO) > 0) {
//            stockUtils.addUnStock(productModel.getId(), dto.getScrapQty(),
//                    StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode(), productionProductMain.getId());
//        }
        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,
@@ -605,7 +636,7 @@
        productionOrderRoutingOperationParamMapper.delete(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .eq(ProductionOrderRoutingOperationParam::getProductionProductMainId, productionProductMain.getId()));
        stockUtils.deleteStockInRecord(productionProductMain.getId(), StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode());
        stockUtils.deleteStockInRecord(productionProductMain.getId(), StockInQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode());
        stockUtils.deleteStockInRecord(productionProductMain.getId(), StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode());
        stockUtils.deleteStockOutRecord(productionProductMain.getId(), StockOutQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_OUT.getCode());
        productionProductMainMapper.deleteById(productionProductMain.getId());
src/main/java/com/ruoyi/project/system/controller/SysLoginController.java
@@ -12,10 +12,10 @@
import com.ruoyi.project.system.domain.SysMenu;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.domain.vo.SysUserDeptVo;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.service.ISysMenuService;
import com.ruoyi.project.system.service.ISysUserDeptService;
import com.ruoyi.project.system.service.ISysUserService;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.service.ISysMenuService;
import com.ruoyi.project.system.service.ISysUserDeptService;
import com.ruoyi.project.system.service.ISysUserService;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;
@@ -41,11 +41,11 @@
{
    private SysLoginService loginService;
    private ISysMenuService menuService;
    private SysPermissionService permissionService;
    private TokenService tokenService;
    private ISysUserDeptService userDeptService;
    private ISysUserService userService;
    private SysDeptMapper sysDeptMapper;
    private SysPermissionService permissionService;
    private TokenService tokenService;
    private ISysUserDeptService userDeptService;
    private ISysUserService userService;
    private SysDeptMapper sysDeptMapper;
    /**
     * ç™»å½•方法
@@ -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){
@@ -102,12 +92,12 @@
            loginUser.setPermissions(permissions);
            tokenService.refreshToken(loginUser);
        }
        AjaxResult ajax = AjaxResult.success();
        ajax.put("user", user);
        ajax.put("aiEnabled", loginUser.getAiEnabled());
        ajax.put("roles", roles);
        ajax.put("permissions", permissions);
        return ajax;
        AjaxResult ajax = AjaxResult.success();
        ajax.put("user", user);
        ajax.put("aiEnabled", loginUser.getAiEnabled());
        ajax.put("roles", roles);
        ajax.put("permissions", permissions);
        return ajax;
    }
    /**
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("", 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("", 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,14 +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>
@@ -23,4 +29,11 @@
    IPage<PurchaseReturnOrderHasAllInfoDto> listPage(Page page, @Param("params") PurchaseReturnOrderDto purchaseReturnOrder);
    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,20 +1,17 @@
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>
 *
 *
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
@@ -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,24 +8,24 @@
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;
@@ -36,8 +35,6 @@
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -76,13 +73,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 +104,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 +117,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));
                .in(StockOutRecord::getRecordId, purchaseReturnOrderProducts.stream().map(PurchaseReturnOrderProducts::getId).collect(Collectors.toList())));
        // è´¢åŠ¡
        LambdaUpdateWrapper<AccountIncome> updateWrapperAccountIncome = new LambdaUpdateWrapper<>();
        updateWrapperAccountIncome.eq(AccountIncome::getBusinessId, id);
@@ -147,4 +132,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/service/impl/QualityInspectServiceImpl.java
@@ -20,6 +20,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;
@@ -34,6 +36,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
@@ -45,6 +48,7 @@
    private final StockUtils stockUtils;
    private final StockInventoryService stockInventoryService;
    private final StockInRecordService stockInRecordService;
    private QualityInspectMapper qualityInspectMapper;
    private IQualityInspectParamService qualityInspectParamService;
@@ -103,16 +107,61 @@
            // stockUtils.addStock(qualityInspect.getProductModelId(), qualityInspect.getQuantity(), StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId());
            //仅添加入库记录
            StockInventoryDto stockInventoryDto = new StockInventoryDto();
            //如果是采购质检合格入库选用CUSTOMIZATION_UNSTOCK_OUT,其余合格入库选用QUALITYINSPECT_STOCK_IN
            stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode()));
            if (ObjectUtils.isNotEmpty(qualityInspect.getPurchaseLedgerId())){
                stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_OUT.getCode()));
            }
            stockInventoryDto.setRecordId(qualityInspect.getId());
            stockInventoryDto.setProductModelId(qualityInspect.getProductModelId());
            stockInventoryDto.setQualitity(qualityInspect.getQuantity());
            stockInventoryDto.setBatchNo(resolveProductionBatchNo(
                    qualityInspect.getProductMainId(),
                    qualityInspect.getId(),
                    qualityInspect.getProductModelId()));
            stockInventoryService.addStockInRecordOnly(stockInventoryDto);
        }
        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
@@ -6,7 +6,6 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
@@ -24,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;
@@ -34,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;
@@ -51,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) {
@@ -68,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 "返修":
@@ -81,12 +86,12 @@
                    }
                    break;
                case "报废":
                    stockUtils.addUnStock(qualityInspect.getProductModelId(), unqualified.getQuantity(),
                            StockInUnQualifiedRecordTypeEnum.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;
@@ -96,7 +101,7 @@
            switch (qualityUnqualified.getDealResult()) {
                case "报废":
                    stockUtils.addUnStock(modelId, unqualified.getQuantity(),
                            StockInUnQualifiedRecordTypeEnum.DEFECTIVE_SCRAP.getCode(), unqualified.getId());
                            StockInQualifiedRecordTypeEnum.DEFECTIVE_SCRAP.getCode(), unqualified.getId());
                    break;
                case "让步放行":
                    stockUtils.addStock(modelId, unqualified.getQuantity(),
@@ -220,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,9 +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.account.bean.dto.SalesOutboundDto;
import com.ruoyi.account.bean.vo.SalesOutboundVo;
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;
@@ -21,9 +19,7 @@
    List<ShippingInfo> listAll();
    List<SalesLedgerProductDto> getReturnManagementDtoById(@Param("shippingId")Long shippingId);
    List<ShippingProductVo> getReturnManagementDtoById(@Param("shippingId")Long shippingId);
    List<ShippingInfo> getShippingInfoByCustomerName(String customerName);
    IPage<SalesOutboundVo> listPageByOutbound(Page page, @Param("req") SalesOutboundDto salesOutboundDto);
}
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,125 +587,44 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int addOrUpdateSalesLedger(SalesLedgerDto salesLedgerDto) {
        try {
            // 1. æ ¡éªŒå®¢æˆ·ä¿¡æ¯
            Customer customer = customerMapper.selectById(salesLedgerDto.getCustomerId());
            if (customer == null) {
                throw new BaseException("客户不存在");
            }
            // 2. DTO转Entity
            SalesLedger salesLedger = convertToEntity(salesLedgerDto);
            salesLedger.setCustomerName(customer.getCustomerName());
            salesLedger.setTenantId(customer.getTenantId());
            // 3. æ–°å¢žæˆ–更新主表
            if (salesLedger.getId() == null) {
                String contractNo = generateSalesContractNo();
                salesLedger.setSalesContractNo(contractNo);
                salesLedgerMapper.insert(salesLedger);
            } else {
                salesLedgerMapper.updateById(salesLedger);
            }
            // 4. å¤„理子表数据
            List<SalesLedgerProduct> productList = salesLedgerDto.getProductData();
            if (productList != null && !productList.isEmpty()) {
                handleSalesLedgerProducts(salesLedger.getId(), productList, EnumUtil.fromCode(SaleEnum.class, salesLedgerDto.getType()));
                updateMainContractAmount(
                        salesLedger.getId(),
                        productList,
                        SalesLedgerProduct::getTaxInclusiveTotalPrice,
                        salesLedgerMapper,
                        SalesLedger.class
                );
            }
            // 5. è¿ç§»ä¸´æ—¶æ–‡ä»¶åˆ°æ­£å¼ç›®å½•
            if (salesLedgerDto.getTempFileIds() != null && !salesLedgerDto.getTempFileIds().isEmpty()) {
                migrateTempFilesToFormal(salesLedger.getId(), salesLedgerDto.getTempFileIds());
            }
            return 1;
        } catch (IOException e) {
            throw new BaseException("文件迁移失败: " + e.getMessage());
        // 1. æ ¡éªŒå®¢æˆ·ä¿¡æ¯
        Customer customer = customerMapper.selectById(salesLedgerDto.getCustomerId());
        if (customer == null) {
            throw new BaseException("客户不存在");
        }
        // 2. DTO转Entity
        SalesLedger salesLedger = convertToEntity(salesLedgerDto);
        salesLedger.setCustomerName(customer.getCustomerName());
        salesLedger.setTenantId(customer.getTenantId());
        // 3. æ–°å¢žæˆ–更新主表
        if (salesLedger.getId() == null) {
            String contractNo = generateSalesContractNo();
            salesLedger.setSalesContractNo(contractNo);
            salesLedgerMapper.insert(salesLedger);
        } else {
            salesLedgerMapper.updateById(salesLedger);
        }
        // 4. å¤„理子表数据
        List<SalesLedgerProduct> productList = salesLedgerDto.getProductData();
        if (productList != null && !productList.isEmpty()) {
            handleSalesLedgerProducts(salesLedger.getId(), productList, EnumUtil.fromCode(SaleEnum.class, salesLedgerDto.getType()));
            updateMainContractAmount(
                    salesLedger.getId(),
                    productList,
                    SalesLedgerProduct::getTaxInclusiveTotalPrice,
                    salesLedgerMapper,
                    SalesLedger.class
            );
        }
        // 5. ä¿å­˜é”€å”®å°è´¦é™„ä»¶
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.SALES_LEDGER, salesLedger.getId(), salesLedgerDto.getStorageBlobDTOs());
        return 1;
    }
    /**
     * å°†ä¸´æ—¶æ–‡ä»¶è¿ç§»åˆ°æ­£å¼ç›®å½•
     *
     * @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;
@@ -55,7 +55,7 @@
    @Override
    public IPage<ShippingInfoDto> listPage(Page page, ShippingInfo req) {
        IPage<ShippingInfoDto> listPage = shippingInfoMapper.listPage(page, req);
        listPage.getRecords().forEach(item ->{
        listPage.getRecords().forEach(item -> {
            item.setStorageBlobVOs(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.IMAGE, RecordTypeEnum.SHIPPING_INFO, item.getId()));
        });
        return listPage;
@@ -68,7 +68,7 @@
            throw new RuntimeException("发货信息不存在");
        }
        //扣减库存
        if(!"已发货".equals(byId.getStatus())){
        if (!"已发货".equals(byId.getStatus())) {
            List<ShippingProductDetail> shippingProductDetails = shippingProductDetailMapper.selectList(new LambdaQueryWrapper<ShippingProductDetail>().eq(ShippingProductDetail::getShippingInfoId, req.getId()));
            if (CollectionUtils.isEmpty(shippingProductDetails)) {
                throw new RuntimeException("发货信息不存在");
@@ -85,31 +85,27 @@
        boolean update = this.updateById(byId);
        // ä¿å­˜æ–‡ä»¶
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.IMAGE, RecordTypeEnum.SHIPPING_INFO, req.getId(), req.getStorageBlobDTOs());
        return update ;
        return update;
    }
    @Override
    public boolean delete(List<Long> ids) {
        List<ShippingInfo> shippingInfos = shippingInfoMapper.selectList(new LambdaQueryWrapper<ShippingInfo>()
                .in(ShippingInfo::getId, ids));
        if(CollectionUtils.isEmpty(shippingInfos)) return false;
        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){
        if (CollectionUtils.isNotEmpty(shippingInfos)) {
            for (ShippingInfo shippingInfo : shippingInfos) {
                List<ApproveProcess> one = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
                        .like(ApproveProcess::getApproveReason, shippingInfo.getShippingNo()));
                if(one != null){
                if (one != null) {
                    List<Long> list = one.stream().map(ApproveProcess::getId).toList();
                    approveProcessService.delByIds(list);
                }
                // æ‰£å·²å‘货库存
                stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
            }
        }
        //删除发货明细
@@ -119,8 +115,8 @@
    }
    @Override
    public List<SalesLedgerProductDto> getReturnManagementDtoById(Long shippingId) {
        return shippingInfoMapper.getReturnManagementDtoById(shippingId );
    public List<ShippingProductVo> getReturnManagementDtoById(Long shippingId) {
        return shippingInfoMapper.getReturnManagementDtoById(shippingId);
    }
@@ -131,9 +127,14 @@
    @Override
    public boolean add(ShippingInfoDto req) {
        this.save( req);
        this.save(req);
        req.getBatchNoDetailList().forEach(item -> item.setShippingInfoId(req.getId()));
        shippingProductDetailMapper.insert(req.getBatchNoDetailList());
        for (ShippingProductDetail shippingProductDetail : req.getBatchNoDetailList()) {
            stockUtils.substractStock(shippingProductDetail.getProductModelId(), shippingProductDetail.getQuantity(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), req.getId(), shippingProductDetail.getBatchNo());
        }
        // ä¿å­˜æ–‡ä»¶
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.IMAGE, RecordTypeEnum.SHIPPING_INFO, req.getId(), req.getStorageBlobDTOs());
        return true;
    }
@@ -147,7 +148,7 @@
        ShippingApproveDto shippingApproveDto = new ShippingApproveDto();
        ShippingInfo shippingInfo = new ShippingInfo();
        shippingInfo.setShippingNo(shippingNo);
        shippingApproveDto.setShippingInfo(shippingInfoMapper.listPage(new Page(1, -1),shippingInfo).getRecords().get(0));
        shippingApproveDto.setShippingInfo(shippingInfoMapper.listPage(new Page(1, -1), shippingInfo).getRecords().get(0));
        List<ShippingProductDetailDto> dateilByShippingNo = shippingProductDetailMapper.getDateilByShippingNo(shippingNo);
        shippingApproveDto.setShippingProductDetailDtoList(dateilByShippingNo);
        return shippingApproveDto;
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/controller/StockUninventoryController.java
@@ -2,8 +2,8 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutUnQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
@@ -37,7 +37,7 @@
    @PostMapping("/addstockUninventory")
    @Operation(summary = "新增库存")
    public R addstockUninventory(@RequestBody StockUninventoryDto stockUninventoryDto) {
        stockUninventoryDto.setRecordType(String.valueOf(StockInUnQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_IN.getCode()));
        stockUninventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_IN.getCode()));
        stockUninventoryDto.setRecordId(0L);
        return R.ok(stockUninventoryService.addStockUninventory(stockUninventoryDto));
    }
@@ -46,7 +46,7 @@
    @PostMapping("/subtractstockUninventory")
    @Operation(summary = "扣减库存")
    public R subtractstockUninventory(@RequestBody StockUninventoryDto stockUninventoryDto) {
        stockUninventoryDto.setRecordType(String.valueOf(StockOutUnQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_OUT.getCode()));
        stockUninventoryDto.setRecordType(String.valueOf(StockOutQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_OUT.getCode()));
        stockUninventoryDto.setRecordId(0L);
        return R.ok(stockUninventoryService.subtractStockUninventory(stockUninventoryDto));
    }
@@ -54,7 +54,7 @@
    @PostMapping("/addStockInRecordOnly")
    @Operation(summary = "新增入库记录(仅创建记录,不调整库存)")
    public R addStockInRecordOnly(@RequestBody StockUninventoryDto stockUninventoryDto) {
        stockUninventoryDto.setRecordType(String.valueOf(StockInUnQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_IN.getCode()));
        stockUninventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_IN.getCode()));
        stockUninventoryDto.setRecordId(0L);
        return R.ok(stockUninventoryService.addStockInRecordOnly(stockUninventoryDto));
    }
@@ -62,7 +62,7 @@
    @PostMapping("/addStockOutRecordOnly")
    @Operation(summary = "新增出库记录(仅创建记录,不调整库存)")
    public R addStockOutRecordOnly(@RequestBody StockUninventoryDto stockUninventoryDto) {
        stockUninventoryDto.setRecordType(String.valueOf(StockOutUnQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_OUT.getCode()));
        stockUninventoryDto.setRecordType(String.valueOf(StockOutQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_OUT.getCode()));
        stockUninventoryDto.setRecordId(0L);
        return R.ok(stockUninventoryService.addStockOutRecordOnly(stockUninventoryDto));
    }
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/StockInRecordMapper.java
@@ -3,6 +3,8 @@
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.PurchaseInboundDto;
import com.ruoyi.account.bean.vo.PurchaseInboundVo;
import com.ruoyi.stock.dto.StockInRecordDto;
import com.ruoyi.stock.execl.StockInRecordExportData;
import com.ruoyi.stock.pojo.StockInRecord;
@@ -14,4 +16,6 @@
    IPage<StockInRecordDto> listPage(Page page, @Param("params") StockInRecordDto stockInRecordDto);
    List<StockInRecordExportData> listStockInRecordExportData(@Param("params") StockInRecordDto stockInRecordDto);
    IPage<PurchaseInboundVo> listPageAccountPurchase(Page page, @Param("req") PurchaseInboundDto purchaseInboundDto);
}
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/mapper/StockOutRecordMapper.java
@@ -3,6 +3,8 @@
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.SalesOutboundDto;
import com.ruoyi.account.bean.vo.SalesOutboundVo;
import com.ruoyi.stock.dto.StockOutRecordDto;
import com.ruoyi.stock.execl.StockOutRecordExportData;
import com.ruoyi.stock.pojo.StockOutRecord;
@@ -25,4 +27,5 @@
    List<StockOutRecordExportData> listStockOutRecordExportData(@Param("params") StockOutRecordDto stockOutRecordDto);
    IPage<SalesOutboundVo> listPageAccountSales(Page page, @Param("req") SalesOutboundDto salesOutboundDto);
}
src/main/java/com/ruoyi/stock/pojo/StockOutRecord.java
@@ -75,7 +75,7 @@
    @Schema(description = "类型  0合格入库 1不合格入库")
    private String type;
    @Schema(description = "审批状态  0-待审批 1-通过 2-驳回", implementation = ReviewStatusEnum.class)
    @Schema(description = "审批状态  0-待审批 1-通过 2-驳回 3-销售出库待确认", implementation = ReviewStatusEnum.class)
    private Integer approvalStatus;
    @TableField(fill = FieldFill.INSERT)
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
@@ -5,8 +5,8 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.ReviewStatusEnum;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.EnumUtil;
import com.ruoyi.common.utils.OrderUtils;
@@ -24,12 +24,12 @@
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.stock.pojo.StockUninventory;
import com.ruoyi.stock.service.StockInRecordService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
@Service
@@ -120,10 +120,10 @@
    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(StockInUnQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
                stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockInQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
            }
        }
        ExcelUtil<StockInRecordExportData> util = new ExcelUtil<>(StockInRecordExportData.class);
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -10,7 +10,6 @@
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
@@ -30,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;
@@ -52,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);
@@ -106,8 +106,8 @@
            newStockInventory.setLockedQuantity(stockInventoryDto.getLockedQuantity());
            newStockInventory.setWarnNum(stockInventoryDto.getWarnNum());
            stockInventoryMapper.insert(newStockInventory);
        }else {
             stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
        } else {
            stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
        }
        return true;
    }
@@ -117,7 +117,7 @@
    @Transactional(rollbackFor = Exception.class)
    public Boolean subtractStockInventory(StockInventoryDto stockInventoryDto) {
        LambdaQueryWrapper<StockInventory> eq = new QueryWrapper<StockInventory>().lambda()
            .eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId());
                .eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId());
        if (StringUtils.isEmpty(stockInventoryDto.getBatchNo())) {
            eq.isNull(StockInventory::getBatchNo);
            stockInventoryDto.setBatchNo(null);
@@ -337,7 +337,7 @@
                        }
                        stockInventoryDto.setProductModelId(matchedProduct.getProductModelId());
                        this.addstockInventory(stockInventoryDto);
                        this.addStockInRecordOnly(stockInventoryDto);
                        successCount++;
                    }
@@ -345,7 +345,7 @@
                    if (dto.getUnQualifiedQuantity() != null && dto.getUnQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0) {
                        StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
                        stockUninventoryDto.setRecordId(0L);
                        stockUninventoryDto.setRecordType(StockInUnQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_IN.getCode());
                        stockUninventoryDto.setRecordType(StockInQualifiedRecordTypeEnum.CUSTOMIZATION_UNSTOCK_IN.getCode());
                        stockUninventoryDto.setQualitity(dto.getUnQualifiedQuantity());
                        stockUninventoryDto.setRemark(dto.getRemark());
@@ -393,28 +393,28 @@
        List<StockInventoryExportData> list = stockInventoryMapper.listStockInventoryExportData(stockInventoryDto);
        ExcelUtil<StockInventoryExportData> util = new ExcelUtil<>(StockInventoryExportData.class);
        util.exportExcel(response,list, "库存信息");
        util.exportExcel(response, list, "库存信息");
    }
    @Override
    public IPage<StockInRecordDto> stockInventoryPage(StockInventoryDto stockInventoryDto, Page page) {
        return stockInventoryMapper.stockInventoryPage(stockInventoryDto,page);
        return stockInventoryMapper.stockInventoryPage(stockInventoryDto, page);
    }
    @Override
    public IPage<StockInventoryDto> stockInAndOutRecord(StockInventoryDto stockInventoryDto, Page page) {
        return stockInventoryMapper.stockInAndOutRecord(stockInventoryDto,page);
        return stockInventoryMapper.stockInAndOutRecord(stockInventoryDto, page);
    }
    @Override
    public Boolean frozenStock(StockInventoryDto stockInventoryDto) {
        StockInventory stockInventory = stockInventoryMapper.selectById(stockInventoryDto.getId());
        if (stockInventory.getQualitity().compareTo(stockInventoryDto.getLockedQuantity())<0) {
        if (stockInventory.getQualitity().compareTo(stockInventoryDto.getLockedQuantity()) < 0) {
            throw new RuntimeException("冻结数量不能超过库存数量");
        }
        if (ObjectUtils.isEmpty(stockInventory.getLockedQuantity())) {
            stockInventory.setLockedQuantity(stockInventoryDto.getLockedQuantity());
        }else {
        } else {
            stockInventory.setLockedQuantity(stockInventory.getLockedQuantity().add(stockInventoryDto.getLockedQuantity()));
        }
        return this.updateById(stockInventory);
@@ -423,7 +423,7 @@
    @Override
    public Boolean thawStock(StockInventoryDto stockInventoryDto) {
        StockInventory stockInventory = stockInventoryMapper.selectById(stockInventoryDto.getId());
        if (stockInventory.getLockedQuantity().compareTo(stockInventoryDto.getLockedQuantity())<0) {
        if (stockInventory.getLockedQuantity().compareTo(stockInventoryDto.getLockedQuantity()) < 0) {
            throw new RuntimeException("解冻数量不能超过冻结数量");
        }
        stockInventory.setLockedQuantity(stockInventory.getLockedQuantity().subtract(stockInventoryDto.getLockedQuantity()));
@@ -434,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/StockOutRecordServiceImpl.java
@@ -5,8 +5,8 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.ReviewStatusEnum;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.EnumUtil;
import com.ruoyi.common.utils.OrderUtils;
@@ -20,19 +20,16 @@
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import com.ruoyi.stock.mapper.StockUninventoryMapper;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.stock.pojo.StockOutRecord;
import com.ruoyi.stock.pojo.StockUninventory;
import com.ruoyi.stock.service.StockOutRecordService;
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 org.springframework.util.CollectionUtils;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
@@ -59,8 +56,9 @@
    public int add(StockOutRecordDto stockOutRecordDto) {
        String no = OrderUtils.countTodayByCreateTime(stockOutRecordMapper, "CK","outbound_batches");
        stockOutRecordDto.setOutboundBatches(no);
        StockInRecord stockInRecord = new StockInRecord();
        BeanUtils.copyProperties(stockOutRecordDto, stockInRecord);
        if (StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode().equals(stockOutRecordDto.getRecordType())){
            stockOutRecordDto.setApprovalStatus(3);
        }
        return stockOutRecordMapper.insert(stockOutRecordDto);
    }
@@ -129,7 +127,7 @@
            if (stockInRecordExportData.getType().equals("0")) {
                stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockOutQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
            }else {
                stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockInUnQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
                stockInRecordExportData.setRecordType(EnumUtil.fromCode(StockInQualifiedRecordTypeEnum.class, Integer.parseInt(stockInRecordExportData.getRecordType())).getValue());
            }
        }
        ExcelUtil<StockOutRecordExportData> util = new ExcelUtil<>(StockOutRecordExportData.class);
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/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" />
@@ -11,7 +11,7 @@
            <pattern>${log.pattern}</pattern>
        </encoder>
    </appender>
    <!-- ç³»ç»Ÿæ—¥å¿—输出 -->
    <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-info.log</file>
@@ -34,7 +34,7 @@
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-error.log</file>
        <!-- å¾ªçŽ¯æ”¿ç­–ï¼šåŸºäºŽæ—¶é—´åˆ›å»ºæ—¥å¿—æ–‡ä»¶ -->
@@ -56,7 +56,7 @@
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- ç”¨æˆ·è®¿é—®æ—¥å¿—输出  -->
    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-user.log</file>
@@ -70,7 +70,7 @@
            <pattern>${log.pattern}</pattern>
        </encoder>
    </appender>
    <!-- ç³»ç»Ÿæ¨¡å—日志级别控制  -->
    <logger name="com.ruoyi" level="info" />
    <!-- Spring日志级别控制  -->
@@ -79,15 +79,15 @@
    <root level="info">
        <appender-ref ref="console" />
    </root>
    <!--系统操作日志-->
    <root level="info">
        <appender-ref ref="file_info" />
        <appender-ref ref="file_error" />
    </root>
    <!--系统用户操作日志-->
    <logger name="sys-user" level="info">
        <appender-ref ref="sys-user"/>
    </logger>
</configuration>
</configuration>
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/SalesRefundAmountOrderMapper.xml
@@ -18,9 +18,9 @@
    <select id="pageSalesRefundAmountOrderDto" resultType="com.ruoyi.account.bean.dto.SalesRefundAmountOrderDto">
        select sl.sales_contract_no,
        sl.customer_contract_no,
        slp.specification_model,
        slp.product_category as product_name,
        slp.unit,
        pm.model as specification_model,
        p.product_name ,
        pm.unit,
        sl.customer_name,
        rm.return_no as return_management_no,
        srao.*
@@ -28,6 +28,8 @@
        left join return_management rm on srao.return_management_id = rm.id
        left join return_sale_product rs on rm.id = rs.return_management_id
        left join sales_ledger_product slp on rs.return_sales_ledger_product_id = slp.id
        left join product_model pm on slp.product_model_id = pm.id
            left join product p on pm.product_id = p.id
        left join sales_ledger sl on slp.sales_ledger_id = sl.id
        <where>
            <if test="ew.salesContractNo != null and ew.salesContractNo !=''">
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>
@@ -107,4 +108,4 @@
            </if>
        </where>
    </select>
</mapper>
</mapper>
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
@@ -11,10 +11,13 @@
                dr.repair_time,
                dr.repair_name,
                dr.remark,
                dr.maintenance_name,
                dr.maintenance_time,
                dr.maintenance_result,
                dr.status,
                 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,
                dr.create_user,
@@ -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/oA/OaProjectMapper.xml
ÎļþÒÑɾ³ý
src/main/resources/mapper/oA/OaProjectPhaseMapper.xml
ÎļþÒÑɾ³ý
src/main/resources/mapper/oA/OaProjectPhaseTaskMapper.xml
ÎļþÒÑɾ³ý
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,
@@ -380,4 +380,4 @@
        </where>
        order by t1.create_time desc
    </select>
</mapper>
</mapper>
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,
@@ -206,4 +206,4 @@
        order by t1.create_time desc
    </select>
</mapper>
</mapper>
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,33 +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 slp.product_category                                         as product_name,
               slp.specification_model                                      as model,
               slp.unit                                      as unit,
    <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 (SELECT return_sales_ledger_product_id,
                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 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/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
@@ -53,4 +53,101 @@
        <include refid="getPurchaseReturnOrderHasAllInfoFormAndColumn"/>
        where pro.id = #{id}
    </select>
    <select id="listPageAccountPurchaseReturn"
            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 1=1
            <if test="req.returnNo != null and req.returnNo != ''">
                and pro.no like concat('%',#{req.returnNo},'%')
            </if>
            <if test="req.supplierName != null and req.supplierName != ''">
                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 #{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
@@ -6,14 +6,50 @@
    <select id="selectSalesLedgerProductList" resultType="com.ruoyi.sales.pojo.SalesLedgerProduct">
        SELECT
        T1.*,
        T1.id,
        T1.sales_ledger_id,
        T1.warn_num,
        T1.speculative_trading_name,
        T1.quantity,
        T1.min_stock,
        T1.tax_rate,
        T1.tax_inclusive_unit_price,
        T1.tax_inclusive_total_price,
        T1.tax_exclusive_total_price,
        T1.invoice_type,
        T1.type,
        T1.tickets_num,
        T1.tickets_amount,
        T1.future_tickets,
        T1.future_tickets_amount,
        T1.invoice_num,
        T1.no_invoice_num,
        T1.invoice_amount,
        T1.no_invoice_amount,
        T1.product_id,
        T1.product_model_id,
        T1.register,
        T1.register_date,
        T1.approve_status,
        T1.pending_invoice_total,
        T1.invoice_total,
        T1.pending_tickets_total,
        T1.tickets_total,
        T1.is_checked,
        T1.is_production,
        T1.create_user,
        T1.dept_id,
        p.product_name as product_category,
        pm.model as specification_model,
        pm.unit as unit,
        CASE
        WHEN (IFNULL(t2.qualitity, 0) - IFNULL(t2.locked_quantity, 0)) >0 THEN 1
        ELSE 0
        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
@@ -27,8 +63,11 @@
        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
        left join product p ON pm.product_id = p.id
        <where>
            <if test="salesLedgerProduct.salesLedgerId != null">
                AND T1.sales_ledger_id = #{salesLedgerProduct.salesLedgerId}
src/main/resources/mapper/sales/SalesQuotationMapper.xml
@@ -20,4 +20,4 @@
            AND t1.status = #{salesQuotationDto.status}
        </if>
    </select>
</mapper>
</mapper>
src/main/resources/mapper/sales/ShippingInfoMapper.xml
@@ -19,14 +19,22 @@
        s.update_user,
        s.tenant_id,
        sl.sales_contract_no,
        slp.specification_model,
        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},'%')
@@ -58,62 +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,
        COALESCE(rs.total_return_num, 0) AS total_return_num
    <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}
    </select>
    <select id="listPageByOutbound" resultType="com.ruoyi.account.bean.vo.SalesOutboundVo">
         SELECT
        sor.id,
        sor.outbound_batches,
        sl.customer_name,
        s.shipping_date,
        p.product_name,
        slp.specification_model,
        slp.stock_out_num,
        s.shipping_no,
        sl.sales_contract_no
        FROM shipping_info s
        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 stock_out_record sor on sor.record_id = s.id and sor.record_type='13'
        WHERE s.status='已发货'
        <if test="req.outboundBatches != null and req.outboundBatches != ''">
            AND sor.outbound_batches LIKE CONCAT('%',#{req.outboundBatches},'%')
        </if>
        <if test="req.customerName != null and req.customerName != ''">
            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}
        </if>
        order by sor.id DESC
        where  sl.customer_name = #{customerName}
    </select>
</mapper>
src/main/resources/mapper/stock/StockInRecordMapper.xml
@@ -70,4 +70,38 @@
        </where>
        order by sir.id desc
    </select>
    <select id="listPageAccountPurchase" resultType="com.ruoyi.account.bean.vo.PurchaseInboundVo">
        SELECT
            sir.id,
            sir.inbound_batches,
            pl.supplier_name,
            DATE(sir.create_time) AS inboundDate,
            p.product_name,
            pm.model as specification_model,
            sir.stock_in_num * slp.tax_inclusive_unit_price AS InboundAmount,
            pl.purchase_contract_number
            FROM stock_in_record sir
            -- 10 ç±»åž‹æ‰å…³è”质检表
            LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
            -- åŠ¨æ€å…³è”é‡‡è´­ï¼ˆè‡ªåŠ¨é€‚é… 7 å’Œ 10)
            LEFT JOIN purchase_ledger pl
            ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
            -- äº§å“å…³è”不动
            LEFT JOIN sales_ledger_product slp ON pl.id = slp.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 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},'%')
        </if>
        <if test="req.supplierName != null and req.supplierName != ''">
            AND pl.supplier_name LIKE CONCAT('%',#{req.supplierName},'%')
        </if>
        <if test="req.startDate != null and req.endDate != null">
            AND DATE(sir.create_time) BETWEEN #{req.startDate} AND #{req.endDate}
        </if>
        order by sir.id DESC
    </select>
</mapper>
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,
@@ -436,7 +490,7 @@
        WHERE sor.product_model_id = #{productModelId}
          AND (sor.batch_no = #{batchNo} OR (#{batchNo} IS NULL AND sor.batch_no IS NULL))
          AND sor.type = #{type}
          AND sor.approval_status = 0
          AND sor.approval_status IN (0, 3)
    </select>
    <select id="listSelectableBatchNoByProductModelIds" resultType="com.ruoyi.stock.pojo.StockInventory">
@@ -453,12 +507,159 @@
        order by si.product_model_id, si.batch_no
    </select>
    <select id="getByModelId" resultType="com.ruoyi.stock.pojo.StockInventory">
        select spd.id, spd.batch_no, spd.locked_quantity, (spd.qualitity - IFNULL(sd.qualitity, 0)) as qualitity
        from stock_inventory spd
                 left join (select stock_inventory_id, sum(quantity) as qualitity
                            from shipping_product_detail
                            group by stock_inventory_id) as sd on sd.stock_inventory_id = spd.id
        where product_model_id = #{productModelId}
        select si.id, si.batch_no, si.locked_quantity, (si.qualitity - IFNULL(sd.qualitity, 0)) as qualitity
        from stock_inventory si
                 left join (
                    select spd.stock_inventory_id, sum(spd.quantity) as qualitity
                    from shipping_product_detail spd
                    where exists (
                        select 1
                        from stock_out_record sor
                        where sor.record_id = spd.shipping_info_id
                          and sor.record_type = '13'
                          and sor.type = '0'
                          and sor.approval_status in (0, 3)
                          and sor.product_model_id = spd.product_model_id
                          and (sor.batch_no = spd.batch_no or (sor.batch_no is null and spd.batch_no is null))
                    )
                    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
@@ -86,4 +86,33 @@
        order by sor.id desc
    </select>
    <select id="listPageAccountSales" resultType="com.ruoyi.account.bean.vo.SalesOutboundVo">
    SELECT
        sor.id,
        sor.outbound_batches,
        sl.customer_name,
        sor.create_time as shippingDate,
        p.product_name,
        pm.model as specification_model,
        sor.stock_out_num * slp.tax_inclusive_unit_price as outboundAmount,
        s.shipping_no,
        sl.sales_contract_no
        FROM stock_out_record sor
        left join shipping_info s on sor.record_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
        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>
        <if test="req.customerName != null and req.customerName != ''">
            AND sl.customer_name LIKE CONCAT('%',#{req.customerName},'%')
        </if>
        <if test="req.startDate != null and req.endDate != null">
            AND s.shipping_date BETWEEN #{req.startDate} AND #{req.endDate}
        </if>
        order by sor.id DESC
    </select>
</mapper>
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