2026-07-08 92eb695ac1254708feb6e740432fc5da9e1ad42b
Merge branch 'dev_New_pro' into dev_山西_黎城康森商砼
已添加335个文件
已修改358个文件
已删除130个文件
72636 ■■■■ 文件已修改
.gitignore 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260508_采购多文件分析附件存储与历史回显联调说明.md 149 ●●●●● 补丁 | 查看 | 原始文档 | 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/20260516_制造智能助手前端联调文档.md 258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260518_销售助手前端联调文档.md 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260520_首页生产看板前端联调文档.md 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260520_首页生产看板性能优化前端变更文档.md 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260521_采购智能体优化前端变更文档.md 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260521_首页HomeController接口升级前端变更文档.md 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_StockInRecord列表源单号前端联调文档.md 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_StockInRecord列表源单号前端联调文档_276补充.md 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_财务助手提问优化前端变更文档.md 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_财务升级AI模块前端变更联调文档.md 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_采购台账入库状态_销售产品入库审核状态前端联调文档.md 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_首页财务接口升级前端变更文档.md 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260523_ 协同审批新增出差时间和结束时间.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260615_add_plan_person_and_executor_to_routing_operation.sql 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/financial-ai-front-integration.md 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/sql/20260609_knowledge_base_vector.sql 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/前端联调文档-设备报修保养财务模块改造.md 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/知识库RAG功能实现文档.md 1034 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mock_data_check.md 183 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mock_data_generate.md 243 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/routing_operation_plan_person.md 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/product-inventory-management-new-pro.sql 5814 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/CodeGenerator.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/AccountReportDto.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/DeviceTypeDetail.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/DeviceTypeDistributionVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/SalesReceiptReturnDto.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/SalesRefundAmountOrderDto.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/StatementAccountDto.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectDto.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectImportDto.java 36 ●●●●● 补丁 | 查看 | 原始文档 | 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/dto/purchase/AccountPaymentApplicationDto.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/purchase/AccountPurchaseInvoiceDto.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/purchase/AccountPurchasePaymentDto.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseInboundDto.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseReturnDto.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/sales/AccountInvoiceApplicationDto.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/sales/AccountSalesCollectionDto.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/sales/AccountSalesInvoiceDto.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/sales/SalesOutboundDto.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/sales/SalesReturnDto.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/AccountReportVo.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/StatementAccountVo.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/financial/AccountSubjectVo.java 21 ●●●●● 补丁 | 查看 | 原始文档 | 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/bean/vo/purchase/AccountPaymentApplicationVo.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/purchase/AccountPurchaseInvoiceVo.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/purchase/AccountPurchasePaymentVo.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseInboundVo.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseReturnVo.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/sales/AccountInvoiceApplicationVo.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/sales/AccountSalesCollectionVo.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/sales/AccountSalesInvoiceVo.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/sales/SalesOutboundVo.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/sales/SalesReturnVo.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountExpenseController.java 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountFileController.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountIncomeController.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountStatementController.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountingController.java 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/BorrowInfoController.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/SalesReceiptReturnController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/SalesRefundAmountOrderController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/financial/AccountSubjectController.java 70 ●●●●● 补丁 | 查看 | 原始文档 | 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/controller/purchase/AccounPurchaseController.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/purchase/AccountPaymentApplicationController.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/purchase/AccountPurchaseInvoiceController.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/purchase/AccountPurchasePaymentController.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/sales/AccountInvoiceApplicationController.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/sales/AccountSalesCollectionController.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/sales/AccountSalesController.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/sales/AccountSalesInvoiceController.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/dto/AccountDto.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/dto/AccountDto2.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/dto/AccountDto3.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/dto/DeviceTypeDetail.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/dto/DeviceTypeDistributionVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/dto/ReportDateDto.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/dto/SalesReceiptReturnDto.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/dto/SalesRefundAmountOrderDto.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/AccountExpenseMapper.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/AccountFileMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/AccountIncomeMapper.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/AccountStatementDetailsMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/AccountStatementMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/BorrowInfoMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/SalesRefundAmountOrderMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/financial/AccountSubjectMapper.java 23 ●●●●● 补丁 | 查看 | 原始文档 | 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/mapper/purchase/AccountPaymentApplicationMapper.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/purchase/AccountPurchaseInvoiceMapper.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/purchase/AccountPurchasePaymentMapper.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/sales/AccountInvoiceApplicationMapper.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/sales/AccountSalesCollectionMapper.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/sales/AccountSalesInvoiceMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/AccountExpense.java 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/AccountFile.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/AccountIncome.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/AccountStatement.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/AccountStatementDetails.java 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/BorrowInfo.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/financial/AccountSubject.java 117 ●●●●● 补丁 | 查看 | 原始文档 | 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/pojo/purchase/AccountPaymentApplication.java 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/purchase/AccountPurchaseInvoice.java 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/purchase/AccountPurchasePayment.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/sales/AccountInvoiceApplication.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/sales/AccountSalesCollection.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/sales/AccountSalesInvoice.java 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountExpenseService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountFileService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountIncomeService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountStatementDetailsService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountStatementService.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountingService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/BorrowInfoService.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/SalesReceiptReturnService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/SalesRefundAmountOrderService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/financial/AccountPurchaseService.java 27 ●●●●● 补丁 | 查看 | 原始文档 | 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/AccountExpenseServiceImpl.java 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/AccountFileServiceImpl.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/AccountIncomeServiceImpl.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/AccountStatementDetailsServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/AccountStatementServiceImpl.java 298 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/AccountingServiceImpl.java 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/BorrowInfoServiceImpl.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/SalesReceiptReturnServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/SalesRefundAmountOrderServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java 397 ●●●●● 补丁 | 查看 | 原始文档 | 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/account/service/impl/purchase/AccountPaymentApplicationServiceImpl.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchaseInvoiceServiceImpl.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchasePaymentServiceImpl.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchaseServiceImpl.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/sales/AccountInvoiceApplicationServiceImpl.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesCollectionServiceImpl.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesInvoiceServiceImpl.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesServiceImpl.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/purchase/AccountPaymentApplicationService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/purchase/AccountPurchaseInvoiceService.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/purchase/AccountPurchasePaymentService.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/purchase/AccountSubjectService.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/sales/AccountInvoiceApplicationService.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/sales/AccountSalesCollectionService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/sales/AccountSalesInvoiceService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/sales/AccountSalesService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/aftersalesservice/pojo/AfterSalesService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/aftersalesservice/service/impl/AfterSalesServiceServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ApproveTodoAgent.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java 199 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/FinancialAgent.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/FinancialIntentExecutor.java 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/KnowledgeChatAgent.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/PurchaseAgent.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java 249 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/SalesAgent.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/SalesIntentExecutor.java 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/FinancialAgentConfig.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/SalesAgentConfig.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/FinancialAiController.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/KnowledgeChatController.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java 855 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/SalesAiController.java 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/XiaozhiController.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/dto/KnowledgeChatRequest.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/schedule/AiSessionCleanupTask.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/service/PurchaseAiService.java 1069 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/service/impl/KnowledgeRagServiceImpl.java 342 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java 2311 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java 1035 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java 311 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java 1633 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalInstanceDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateDto.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeApproverDto.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/FinReimbursementDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalInstanceVo.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeApproverVo.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeVo.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateVo.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApproveGetAndUpdateVo.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApproveProcessVO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/FinReimbursementVo.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceController.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceNodeController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalRecordController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTaskController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateController.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeApproverController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementController.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementDetailController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementTravelController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java 140 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/dto/KnowledgeBaseVectorVO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceNodeMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalRecordMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTaskMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeApproverMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementDetailMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementTravelMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/KnowledgeBaseVectorMapper.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalInstance.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalInstanceNode.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalRecord.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTask.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplate.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNode.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNodeApprover.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApproveProcess.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursement.java 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursementDetail.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursementTravel.java 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/KnowledgeBaseVector.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalInstanceNodeService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalInstanceService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalRecordService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTaskService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeApproverService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeService.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTemplateService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementDetailService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementService.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementTravelService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/KnowledgeBaseVectorService.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceNodeServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java 876 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalRecordServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTaskServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeApproverServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeServiceImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateServiceImpl.java 308 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveBusinessStatusService.java 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java 123 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementDetailServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java 574 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementTravelServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/KnowledgeBaseVectorServiceImpl.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/utils/ApproveProcessConfigNodeUtils.java 363 ●●●●● 补丁 | 查看 | 原始文档 | 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/ProductModelExportDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/StorageBlobVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/CustomerMapper.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/ProductModelMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/SupplierManageMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/ProductModel.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/ICustomerService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/ISupplierService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/SupplierServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/utils/FileUtil.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/vo/CustomerVo.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsController.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeDeptController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeUserController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/SealApplicationManagementController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/dto/EnterpriseNewsDto.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/dto/SealApplicationManagementDTO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeDeptMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeUserMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNews.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeDept.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeUser.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeDeptService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeUserService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsService.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeDeptServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeUserServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsServiceImpl.java 409 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/MeetingServiceImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/SealApplicationManagementServiceImpl.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/vo/EnterpriseNewsVo.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/vo/SearchMeetingUseVo.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/ApprovalStatusEnum.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/EnterpriseNewsStatusEnum.java 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/SalesQuotationStatusEnum.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/ShippingStatusEnum.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockInUnQualifiedRecordTypeEnum.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockOutQualifiedRecordTypeEnum.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockOutUnQualifiedRecordTypeEnum.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/TypeEnums.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/utils/DateUtils.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/utils/OrderUtils.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/customervisits/service/impl/CustomerVisitsServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/controller/DeviceLedgerController.java 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/controller/DeviceRepairController.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/execl/DeviceRepairExeclDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/mapper/DeviceLedgerMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/DeviceRepair.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/MaintenanceTask.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/IDeviceLedgerService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/IDeviceRepairService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceLedgerServiceImpl.java 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskServiceImpl.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/framework/config/ApplicationConfig.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/framework/security/LoginUser.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/controller/HomeController.java 588 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/dto/StatisticsReceivablePayableDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java 676 ●●●● 补丁 | 查看 | 原始文档 | 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 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/controller/DataCheckController.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/dto/DataCheckRequest.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/dto/DataGenerateRequest.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/prompt/MockDataPrompt.java 242 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/service/DataCheckService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/service/DataGenerateService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/service/impl/DataCheckServiceImpl.java 282 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/service/impl/DataGenerateServiceImpl.java 1745 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/vo/DataCheckResult.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/mock/vo/DataGenerateResult.java 37 ●●●●● 补丁 | 查看 | 原始文档 | 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 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/InventoryInformationDto.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementAddDto.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementDto.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementManagementUpdateDto.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementPageDto.java 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementPageDtoCopy.java 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutAdd.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutPageDto.java 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementUpdateDto.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ReturnManagementDto.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ReturnSaleProductDto.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/vo/ShippingInfoVo.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/vo/ShippingProductVo.java 46 ●●●●● 补丁 | 查看 | 原始文档 | 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 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/Details.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/InventoryInformationDto.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/ProcurementAddDto.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/ProcurementDto.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/ProcurementManagementUpdateDto.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/ProcurementPageDto.java 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/ProcurementPageDtoCopy.java 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/ProcurementRecordOutAdd.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/ProcurementRecordOutPageDto.java 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/ProcurementUpdateDto.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/dto/ReturnManagementDto.java 47 ●●●●● 补丁 | 查看 | 原始文档 | 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 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/mapper/ReturnSaleProductMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/pojo/CustomStorage.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/pojo/ReturnManagement.java 14 ●●●●● 补丁 | 查看 | 原始文档 | 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 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnManagementServiceImpl.java 108 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnSaleProductServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java 129 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionAccountDto.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionPlanDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionPlanImportDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionProductMainDto.java 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionAccountVo.java 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderWorkOrderDetailVo.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionPlanController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/enums/ProductOrderStatusEnum.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionAccount.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrder.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrderRoutingOperation.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionProductInput.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionProductMain.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionProductOutput.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionPlanService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionAccountServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java 751 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickRecordServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java 424 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationParamServiceImpl.java 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 264 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java 317 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductInputServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductOutputServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/SalesLedgerProductionAccountingServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/util/TaskPlanQuantityUtil.java 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/common/CaptchaController.java 196 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/monitor/controller/CacheController.java 222 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/monitor/controller/ServerController.java 54 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/monitor/controller/SysJobController.java 372 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/monitor/controller/SysJobLogController.java 186 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/monitor/controller/SysLogininforController.java 140 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/monitor/controller/SysOperlogController.java 140 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/monitor/controller/SysUserOnlineController.java 166 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysConfigController.java 254 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysDeptController.java 268 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysDictDataController.java 240 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysDictTypeController.java 264 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysLoginController.java 284 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysMenuController.java 270 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysNoticeController.java 192 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysPostController.java 266 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysProfileController.java 266 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysRegisterController.java 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysRoleController.java 510 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysUserController.java 581 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/domain/SysUser.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/mapper/SysUserDeptMapper.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/mapper/SysUserMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/ISysUserService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/SysNoticeServiceImpl.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/SysUserServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/tool/gen/controller/GenController.java 508 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/controller/RolesController.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/pojo/Roles.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/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/AccountingReportController.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/InvoicePurchaseController.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/PaymentRegistrationController.java 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/PurchaseLedgerController.java 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/PurchaseReturnOrdersController.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/TicketRegistrationController.java 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/InvoicePurchaseDto.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/InvoicePurchaseReportDto.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/PaymentRegistrationDto.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/ProductRecordDto.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/PurchaseReturnOrderHasAllInfoDto.java 30 ●●●●● 补丁 | 查看 | 原始文档 | 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/dto/TicketRegistrationDto.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/VatDto.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/InvoicePurchaseMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/PaymentRegistrationMapper.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/ProductRecordMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/PurchaseLedgerMapper.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/PurchaseReturnOrdersMapper.java 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/TicketRegistrationMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/InvoicePurchase.java 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PaymentRegistration.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/ProductRecord.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseReturnOrderProducts.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseReturnOrders.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/SalesLedgerProductTemplate.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/TicketRegistration.java 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/IInvoicePurchaseService.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/IPaymentRegistrationService.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/IProductRecordService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/IPurchaseLedgerService.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/ITicketRegistrationService.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/PurchaseReportService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/PurchaseReturnOrdersService.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PaymentRegistrationServiceImpl.java 564 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/ProductRecordServiceImpl.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java 775 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReportServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java 94 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java 464 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/PurchaseReportVo.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/PurchaseReturnDetailsVo.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/PurchaseReturnOrderProductsDetailVo.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/PurchaseReturnOrderVo.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/PurchaseStockInProductVo.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java 47 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityInspectParamController.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityReportController.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityTestStandardBindingController.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityTestStandardController.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityTestStandardParamController.java 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityUnqualifiedController.java 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityUnqualified.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/IQualityInspectService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityUnqualifiedServiceImpl.java 54 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/utils/QualityInspectHelper.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/InvoiceLedgerController.java 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/InvoiceRegistrationController.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/MetricStatisticsController.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/PaymentShippingController.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/ReceiptPaymentController.java 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/SalesLedgerProductController.java 91 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/ShipmentApprovalController.java 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/InvoiceLedgerDto.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/InvoiceRegistrationDto.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/InvoiceRegistrationProductDto.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/ReceiptPaymentDto.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/ReceiptPaymentExeclDto.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/ReceiptPaymentRecordDto.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesLedgerImportDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesLedgerProductImportDto.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesQuotationDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/ShippingApproveDto.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/excel/InvoiceLedgerExcelDto.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/excel/InvoiceRegisAndProductExcelDto.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/InvoiceLedgerFileMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/InvoiceLedgerMapper.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/InvoiceRegistrationMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/InvoiceRegistrationProductMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/ReceiptPaymentMapper.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/SalesLedgerProductMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/ShipmentApprovalMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/ShippingInfoMapper.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/ShippingProductDetailMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/InvoiceLedger.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/InvoiceLedgerFile.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/InvoiceRegistration.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/InvoiceRegistrationProduct.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/PaymentShipping.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/ReceiptPayment.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java 105 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesQuotation.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesQuotationProduct.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/ShipmentApproval.java 355 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ISalesLedgerProductService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/InvoiceLedgerService.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/InvoiceRegistrationService.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ReceiptPaymentService.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ShipmentApprovalService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ShippingInfoService.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/InvoiceLedgerServiceImpl.java 509 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/InvoiceRegistrationServiceImpl.java 228 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ReceiptPaymentServiceImpl.java 349 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java 116 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 398 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java 142 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShipmentApprovalServiceImpl.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 111 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/PersonalAttendanceLocationConfigController.java 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/PersonalShiftController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/StaffOnJobController.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/StaffSalaryMainController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/dto/StaffLeaveDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/dto/StaffOnJobDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/dto/StaffSalaryCalculateDto.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/excel/StaffSalaryExportData.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/init/DataInitializer.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/mapper/SchemeApplicableStaffMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/mapper/StaffOnJobMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/mapper/StaffSalaryMainMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/pojo/StaffOnJob.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/pojo/StaffSalaryDetail.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/IStaffOnJobService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/StaffSalaryMainService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/PersonalAttendanceRecordsServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/StaffLeaveServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInRecordController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockOutRecordController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockUninventoryController.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockOutRecordDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | 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/StockInRecord.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/pojo/StockOutRecord.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/StockInRecordService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/StockInventoryService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/StockOutRecordService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockUninventoryServiceImpl.java 113 ●●●● 补丁 | 查看 | 原始文档 | 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/controller/TechnologyOperationController.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyOperationParamController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/pojo/TechnologyOperation.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/pojo/TechnologyParam.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/pojo/TechnologyRouting.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/pojo/TechnologyRoutingOperation.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/TechnologyBomService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/TechnologyOperationService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyBomServiceImpl.java 137 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyOperationParamServiceImpl.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyOperationServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyParamServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/warehouse/controller/DocumentationFileController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/warehouse/mapper/DocumentationFileMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/warehouse/mapper/DocumentationMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/warehouse/service/DocumentationFileService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-btw.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-ckgm.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-hqjc.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-hthb.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-jfhy.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-khyy.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-lsy.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-myhb-pro.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-nhxcl.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-qfsw.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-qlmc-pro.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-rfsy.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-xhks.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-xlgs.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-yss.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-zdjc.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/approve-todo-agent-prompt.txt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/financial-agent-prompt.txt 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/logback.xml 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/manufacturing-agent-prompt.txt 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountExpenseMapper.xml 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountFileMapper.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountIncomeMapper.xml 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/AccountStatementMapper.xml 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/BorrowInfoMapper.xml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/SalesRefundAmountOrderMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/financial/AccountSubjectMapper.xml 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPaymentApplicationMapper.xml 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPurchaseInvoiceMapper.xml 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPurchasePaymentMapper.xml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountInvoiceApplicationMapper.xml 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountSalesCollectionMapper.xml 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountSalesInvoiceMapper.xml 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/aftersalesservice/AfterSalesNearExpiryMapper.xml 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/aftersalesservice/AfterSalesServiceMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalInstanceMapper.xml 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalInstanceNodeMapper.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalRecordMapper.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTaskMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTemplateMapper.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTemplateNodeApproverMapper.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTemplateNodeMapper.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApproveProcessMapper.xml 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementDetailMapper.xml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementMapper.xml 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementTravelMapper.xml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/KnowledgeBaseMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/ProductModelMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/SupplierManageMapper.xml 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsMapper.xml 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeDeptMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeUserMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/NoticeMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/RulesRegulationsManagementMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/SealApplicationManagementMapper.xml 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceLedgerMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceMaintenanceMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceRepairMapper.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerRecordMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/SparePartsMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/SparePartsRequisitionRecordMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | 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 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ReturnSaleProductMapper.xml 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionAccountMapper.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderMapper.xml 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderRoutingOperationMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionPlanMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionProductMainMapper.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/InvoicePurchaseMapper.xml 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PaymentRegistrationMapper.xml 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/ProductRecordMapper.xml 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml 250 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseReturnOrderProductsMapper.xml 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityInspectMapper.xml 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityTestStandardMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/InvoiceLedgerMapper.xml 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/InvoiceRegistrationMapper.xml 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/InvoiceRegistrationProductMapper.xml 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ReceiptPaymentMapper.xml 454 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerMapper.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesQuotationMapper.xml 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ShipmentApprovalMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ShippingInfoMapper.xml 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ShippingProductDetailMapper.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/PersonalAttendanceRecordsMapper.xml 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/PersonalShiftMapper.xml 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/SchemeApplicableStaffMapper.xml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/StaffLeaveMapper.xml 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/StaffOnJobMapper.xml 112 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/StaffSalaryMainMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInRecordMapper.xml 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 390 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockOutRecordMapper.xml 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysDeptMapper.xml 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysDictTypeMapper.xml 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysNoticeMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysPostMapper.xml 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysRoleMapper.xml 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysUserMapper.xml 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/technology/TechnologyBomStructureMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/technology/TechnologyOperationMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/technology/TechnologyOperationParamMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/technology/TechnologyRoutingOperationMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/DocumentationBorrowManagementMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/DocumentationMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/DocumentationReturnManagementMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/WarehouseGoodsShelvesRowcolMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/purchase-agent-prompt.txt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sales-agent-prompt.txt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/销售台账导入模板.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -4,12 +4,14 @@
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
claude.md
target/
!.mvn/wrapper/maven-wrapper.jar
######################################################################
# IDE
# Claude Code
.claude/
### STS ###
.apt_generated
@@ -43,4 +45,4 @@
!*/build/*.java
!*/build/*.html
!*/build/*.xml
!*/build/*.xml
doc/20260508_²É¹º¶àÎļþ·ÖÎö¸½¼þ´æ´¢ÓëÀúÊ·»ØÏÔÁªµ÷˵Ã÷.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,149 @@
# é‡‡è´­å¤šæ–‡ä»¶åˆ†æžé™„件存储与历史回显联调说明
> æ›´æ–°æ—¶é—´ï¼š2026-05-08
> é€‚用范围:采购智能体多文件分析 + åŽ†å²ä¼šè¯é™„ä»¶å›žæ˜¾
## 1. å˜æ›´èƒŒæ™¯
后端已补齐以下能力:
1. `POST /purchase-ai/analyze-files` ä¸Šä¼ æ—¶å…ˆå­˜é™„件到服务器(公共访问)。
2. æŒ‰æ–‡ä»¶ç±»åž‹è¿”回可访问路径:
   - å›¾ç‰‡ / PDF:优先预览路径
   - å…¶å®ƒæ–‡ä»¶ï¼šä¼˜å…ˆä¸‹è½½è·¯å¾„
3. ç”¨æˆ·æé—®ä¸Žé™„件路径分开存入 Mongo。
4. åŽ†å²æ¶ˆæ¯æŽ¥å£å¯æŒ‰â€œæ¶ˆæ¯ç»´åº¦â€å›žä¼ é™„ä»¶è·¯å¾„ï¼Œå‰ç«¯å¯ç›´æŽ¥å›žæ˜¾é™„ä»¶å¡ç‰‡ã€‚
---
## 2. æŽ¥å£è¡Œä¸º
### 2.1 å¤šæ–‡ä»¶åˆ†æžæŽ¥å£
```http
POST /purchase-ai/analyze-files
Content-Type: multipart/form-data
```
请求参数:
- `files`: `MultipartFile[]`(必填)
- `message`: `string`(可选)
- `memoryId`: `string`(可选)
后端处理流程:
1. è°ƒç”¨ `StorageBlobService.upload(files, true)` ä¸Šä¼ å¹¶ä¿å­˜é™„件。
2. ç”Ÿæˆé™„件访问路径(预览 / ä¸‹è½½ï¼‰ã€‚
3. å°†â€œæœ¬æ¬¡æé—® + æœ¬æ¬¡é™„件路径列表”写入 Mongo。
4. ç»§ç»­æ‰§è¡ŒåŽŸæœ‰æ–‡ä»¶è§£æžå’Œ AI åˆ†æžæµç¨‹ã€‚
可能错误(流式文本):
- `文件上传失败`
- `会话文件信息保存失败`
### 2.2 åŽ†å²æ¶ˆæ¯æŽ¥å£ï¼ˆå‰ç«¯é‡ç‚¹ï¼‰
```http
GET /purchase-ai/history/messages/{memoryId}
```
消息对象新增可选字段 `filePaths`(仅用户消息可能有值):
```json
{
  "role": "user | assistant | system | tool | unknown",
  "content": "消息文本",
  "filePaths": [
    "/common/preview/xxx?publicKey=...",
    "/common/download/yyy?publicKey=..."
  ]
}
```
说明:
1. `filePaths` å¯èƒ½ç¼ºå¤±æˆ–为空(老会话 / æ™®é€šå¯¹è¯ï¼‰ã€‚
2. å•条用户消息可能包含多个附件路径。
3. è¯¥å­—段已按消息维度对齐,可直接用于历史回显。
---
## 3. å‰ç«¯æ”¹é€ å»ºè®®ï¼ˆæŒ‰å½“前实现落地)
### 3.1 åŽ†å²å“åº”æ¨¡åž‹
建议在历史消息原始结构中保留 `filePaths`:
```ts
type AiHistoryMessage = {
  role: string;
  content: string;
  filePaths?: string[];
};
```
### 3.2 UI æ¶ˆæ¯æ¨¡åž‹æ˜ å°„
若前端页面用的是 `localUploadFiles` æ¸²æŸ“附件卡片(如 `AIChatSidebar` å½“前实现),历史消息需做一次映射:
```ts
type LocalUploadFileItem = {
  previewId: string;
  name: string;
  size: number;
  type: string;
  isImage: boolean;
  previewUrl: string;
  rawFile: null;
};
```
映射规则建议:
1. ä»… `role === 'user'` ä¸” `filePaths?.length > 0` æ—¶ç”Ÿæˆ `localUploadFiles`。
2. `previewId` ç”¨ `${memoryId}-${messageIndex}-${fileIndex}` ç”Ÿæˆç¨³å®šå€¼ã€‚
3. `name` å¯ä»Ž URL è·¯å¾„解析;解析失败用 `file-{n}`。
4. `isImage` å¯æŒ‰æ‰©å±•名判断(`png/jpg/jpeg/gif/webp/bmp/svg`)。
5. å›¾ç‰‡çš„ `previewUrl` ç›´æŽ¥ä½¿ç”¨è·¯å¾„;非图片可置空并走图标展示。
### 3.3 æ¶ˆæ¯æ¸²æŸ“
对于用户消息:
1. æ­£å¸¸æ¸²æŸ“ `content`。
2. è‹¥å­˜åœ¨ `localUploadFiles`(或直接使用 `filePaths`),在消息下方渲染附件列表/卡片。
3. é“¾æŽ¥ç›´æŽ¥ä½¿ç”¨åŽç«¯è¿”回路径,不再拼接或二次改写。
> åŽç«¯å·²å®Œæˆâ€œé¢„览/下载路径”选择,前端只负责展示与打开。
### 3.4 å…¼å®¹è¦æ±‚
- `filePaths` ç¼ºå¤±ï¼šä¸æŠ¥é”™ï¼Œä¸æ¸²æŸ“附件区域。
- è€ä¼šè¯ï¼šç»§ç»­æŒ‰ `role/content` å±•示,不影响历史记录查看。
- å¤šé™„件:保持顺序展示,避免打乱用户上传顺序。
---
## 4. Mongo å­—段说明(后端已实现)
`chat_messages` æ–‡æ¡£æ–°å¢ž/使用字段:
- `analyzeUserQuestions: string[]`
- `analyzeFilePaths: string[]`(兼容旧字段)
- `analyzeFilePathGroups: string[][]`(推荐,按提问分组)
历史消息回传时,后端优先读取 `analyzeFilePathGroups`,并兼容 `analyzeFilePaths`。
---
## 5. è”调验收清单
1. æ–°å»ºä¼šè¯ï¼Œä¸Šä¼  1 å¼ å›¾ç‰‡ + 1 ä¸ª Excel,发送分析请求。
2. è°ƒç”¨ `GET /purchase-ai/history/messages/{memoryId}`,确认用户消息含 `filePaths` ä¸”数量正确。
3. åˆ·æ–°é¡µé¢åŽé‡æ–°è¿›å…¥åŒä¸€ä¼šè¯ï¼Œç¡®è®¤é™„件卡片可回显。
4. åˆ†åˆ«éªŒè¯ï¼š
   - å›¾ç‰‡/PDF å¯é¢„览访问
   - å…¶å®ƒæ–‡ä»¶å¯ä¸‹è½½è®¿é—®
5. éªŒè¯è€ä¼šè¯ï¼ˆæ—  `filePaths`)可正常展示,页面不报错。
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/20260516_ÖÆÔìÖÇÄÜÖúÊÖǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,258 @@
# åˆ¶é€ æ™ºèƒ½åŠ©æ‰‹å‰ç«¯è”è°ƒæ–‡æ¡£ï¼ˆ`manufacturing-ai`)
> æ›´æ–°æ—¥æœŸï¼š2026-05-16
> é€‚用模块:生产现场、计划、工单、设备、质量、物料、异常处理
> èƒ½åŠ›èŒƒå›´ï¼šæŸ¥ã€é—®ã€åŠžã€é¢„è­¦ã€åˆ†æž
## 1. æŽ¥å£æ€»è§ˆ
1. æµå¼å¯¹è¯ï¼š`POST /manufacturing-ai/chat`
2. ä¼šè¯åˆ—表:`GET /manufacturing-ai/history/sessions`
3. ä¼šè¯æ¶ˆæ¯ï¼š`GET /manufacturing-ai/history/messages/{memoryId}`
4. åˆ é™¤ä¼šè¯ï¼š`DELETE /manufacturing-ai/history/{memoryId}`
说明:
- `/chat` ä¸º **SSE/流式文本** è¿”回(`text/stream;charset=utf-8`)。
- å‘½ä¸­â€œæŸ¥/预警/分析/办”工具时,流式最终内容是 **JSON å­—符串**(不是 `AjaxResult`)。
- æœªå‘½ä¸­å·¥å…·æ—¶ï¼Œè¿”回普通自然语言文本。
## 2. é‰´æƒä¸Žè¯·æ±‚头
- ç»Ÿä¸€ä½¿ç”¨ç³»ç»Ÿç™»å½•态(`Authorization` ä¸ŽçŽ°æœ‰æŽ¥å£ä¸€è‡´ï¼‰ã€‚
- `POST /manufacturing-ai/chat` è¯·æ±‚头:`Content-Type: application/json`。
## 3. å¯¹è¯æŽ¥å£
### 3.1 è¯·æ±‚
```http
POST /manufacturing-ai/chat
Content-Type: application/json
```
```json
{
  "memoryId": "mfg-ai-001",
  "message": "查设备西门子变频器的维修情况"
}
```
字段说明:
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
| --- | --- | --- | --- |
| memoryId | string | æ˜¯ | ä¼šè¯ ID,前端生成并复用 |
| message | string | æ˜¯ | ç”¨æˆ·è¾“å…¥ |
### 3.2 è¿”回(流式)
```http
Content-Type: text/stream;charset=utf-8
```
前端处理建议:
1. æŒ‰æµæ‹¼æŽ¥å®Œæ•´æ–‡æœ¬ã€‚
2. å°è¯• `JSON.parse(fullText)`:
   - æˆåŠŸï¼šæŒ‰ç»“æž„åŒ–ç»“æžœæ¸²æŸ“ã€‚
   - å¤±è´¥ï¼šæŒ‰æ™®é€šèŠå¤©æ–‡æœ¬æ¸²æŸ“。
## 4. ç»“构化响应协议
### 4.1 é€šç”¨ç»“æž„
```json
{
  "success": true,
  "type": "manufacturing_device_repair_list",
  "description": "已返回设备维修记录。",
  "summary": {},
  "data": {},
  "charts": {}
}
```
### 4.2 `type` æžšä¸¾
| type | åœºæ™¯ |
| --- | --- |
| manufacturing_site_snapshot | ç”Ÿäº§çŽ°åœºæ¦‚è§ˆ |
| manufacturing_plan_list | ç”Ÿäº§è®¡åˆ’查询 |
| manufacturing_workorder_list | å·¥å•查询 |
| manufacturing_device_list | è®¾å¤‡å°è´¦æŸ¥è¯¢ |
| manufacturing_device_repair_list | è®¾å¤‡ç»´ä¿®è®°å½•查询 |
| manufacturing_quality_list | è´¨é‡æŸ¥è¯¢ |
| manufacturing_material_list | ç‰©æ–™åº“存查询 |
| manufacturing_exception_list | å¼‚常处理查询 |
| manufacturing_warning | é¢„警看板 |
| manufacturing_analysis | ç»è¥åˆ†æž |
| manufacturing_action_plan | åŠžç†å»ºè®®ï¼ˆåŠ¨ä½œå¡ï¼‰ |
## 5. â€œæŸ¥â€èƒ½åŠ›è”è°ƒè¦ç‚¹
### 5.1 è®¾å¤‡ç›¸å…³è·¯ç”±è§„则(关键)
- å½“用户输入包含 `ç»´ä¿®/报修/检修/维护`,设备域会返回 `manufacturing_device_repair_list`(查 `device_repair`)。
- æœªåŒ…含以上词时,返回 `manufacturing_device_list`(查设备台账)。
示例:
- `查设备A-01` -> `manufacturing_device_list`
- `查设备A-01维修情况` -> `manufacturing_device_repair_list`
### 5.2 ç»´ä¿®è®°å½•时间过滤规则(关键)
- ç”¨æˆ·æ˜Žç¡®å¸¦æ—¶é—´æ¡ä»¶ï¼ˆå¦‚“本月/上周/近7天/2026-05-01 åˆ° 2026-05-16”)才按时间过滤维修记录。
- æœªå¸¦æ—¶é—´æ¡ä»¶æ—¶ï¼Œä¸é»˜è®¤æŒ‰è¿‘ 30 å¤©æˆªæ–­ï¼Œé¿å…åŽ†å²ç»´ä¿®è®°å½•è¢«è¯¯è¿‡æ»¤ã€‚
### 5.3 å…³é”®è¯å¤„理规则(设备/维修)
- ç³»ç»Ÿä¼šæ¸…洗噪音词:`查询/查看/请/设备/维修情况/记录/信息` ç­‰ã€‚
- åŒæ—¶ä¼šé€šè¿‡è®¾å¤‡å°è´¦åŒ¹é… `deviceLedgerId` å…œåº•,再回查维修记录,降低“有数据但查不到”的概率。
### 5.4 åˆ—表结果约定
- åˆ—表数据统一在 `data.items`
- ç»Ÿè®¡æ‘˜è¦åœ¨ `summary`
常用字段:
| type | å¸¸ç”¨å­—段 |
| --- | --- |
| manufacturing_plan_list | `mpsNo`, `requiredDate`, `status` |
| manufacturing_workorder_list | `workOrderNo`, `planStartTime`, `planEndTime`, `status` |
| manufacturing_device_list | `deviceName`, `deviceModel`, `pendingRepairCount` |
| manufacturing_device_repair_list | `deviceName`, `deviceModel`, `repairTime`, `repairName`, `maintenanceName`, `status`, `createTime` |
## 6. â€œé¢„警”联调要点
- `type = manufacturing_warning`
- é¢„警明细在 `data.items`,每项包含:
  - `level`:`high` / `medium`
  - `title`
  - `count`
  - `detail`
状态口径:
- è®¾å¤‡â€œå¾…维修”统计按 `status = 0` è®¡ç®—(不再把其他状态计入待维修)。
## 7. â€œåˆ†æžâ€è”调要点
- `type = manufacturing_analysis`
- å…³é”®æŒ‡æ ‡åœ¨ `summary`
- æŒ‡æ ‡å¡åœ¨ `data.coreMetrics`
- å›¾è¡¨é…ç½®åœ¨ `charts`:
  - `charts.domainBarOption`
  - `charts.qualityPieOption`
图表配置可直接给 ECharts ä½¿ç”¨ã€‚
## 8. â€œåŠžâ€èƒ½åŠ›è”è°ƒè¦ç‚¹
当前“办”为 **办理建议模式**(AI è¾“出动作卡,前端确认后调用目标业务接口)。
- `type = manufacturing_action_plan`
- åŠ¨ä½œå¡æ•°ç»„ï¼š`data.actionCards`
动作卡字段:
| å­—段 | è¯´æ˜Ž |
| --- | --- |
| code | åŠ¨ä½œç¼–ç  |
| name | åŠ¨ä½œåç§° |
| method | è¯·æ±‚方法 |
| targetApi | ç›®æ ‡ä¸šåŠ¡æŽ¥å£ |
| requiredFields | å¿…填字段 |
| examplePayload | ç¤ºä¾‹å‚æ•° |
| description | è¯´æ˜Ž |
内置动作示例:
1. `POST /productionOperationTask/assign`
2. `POST /device/repair`
3. `POST /quality/qualityUnqualified/deal`
4. `POST /stockInventory/addstockInventory`
5. `POST /procurementExceptionRecord/add`
## 9. ä¼šè¯ç®¡ç†æŽ¥å£
### 9.1 ä¼šè¯åˆ—表
```http
GET /manufacturing-ai/history/sessions
```
`AjaxResult.data` å­—段:
- `memoryId`
- `title`
- `lastMessage`
- `messageCount`
- `lastChatTime`
### 9.2 ä¼šè¯æ¶ˆæ¯
```http
GET /manufacturing-ai/history/messages/{memoryId}
```
`AjaxResult.data` å­—段:
- `role`:`user` / `assistant` / `system` / `tool`
- `content`
- `filePaths`
### 9.3 åˆ é™¤ä¼šè¯
```http
DELETE /manufacturing-ai/history/{memoryId}
```
返回标准 `AjaxResult`。
## 10. é”™è¯¯ä¸Žè¾¹ç•Œ
`/chat` å¸¸è§è¿”回文本:
- `memoryId不能为空`
- `message不能为空`
建议前端发送前先做必填校验。
## 11. å‰ç«¯è”调流程建议
1. ç™»å½•后创建并复用 `memoryId`。
2. è°ƒç”¨ `/manufacturing-ai/chat`,按 SSE æ‹¼æŽ¥å®Œæ•´æ–‡æœ¬ã€‚
3. å…ˆå°è¯• JSON è§£æžï¼š
   - æˆåŠŸï¼šæŒ‰ `type` è·¯ç”±åˆ°å¯¹åº” UI(列表/预警/分析/动作卡)。
   - å¤±è´¥ï¼šæŒ‰æ™®é€šèŠå¤©æ¶ˆæ¯å±•示。
4. â€œåŠžâ€åœºæ™¯ç”±ç”¨æˆ·ç¡®è®¤åŠ¨ä½œå¡åŽï¼Œå‰ç«¯è°ƒç”¨ `targetApi` å®Œæˆä¸šåŠ¡æäº¤ã€‚
5. é€šè¿‡åŽ†å²æŽ¥å£åšä¼šè¯å›žæ˜¾ä¸Žåˆ é™¤ã€‚
## 12. å‰ç«¯é›†æˆçº¦æŸï¼ˆæœ¬æ¬¡è¡¥å……)
### 12.1 æ™ºèƒ½ä½“新增与弹窗同步规则(强制)
1. å½“ `src/views/aiIndustrialBrain/index.vue` æ–°å¢žæ™ºèƒ½ä½“(`agents`)逻辑时,必须同步确认弹窗助手可用性。
2. å¼¹çª—助手统一由 `src/components/AIChatSidebar/assistants/index.js` çš„ `assistantRegistry` æ³¨å†Œã€‚
3. æ–°å¢žæ™ºèƒ½ä½“çš„ `key` è‹¥è¦åœ¨å¼¹çª—中可用,必须在 `assistantRegistry` ä¸­æä¾›åŒåé…ç½®ã€‚
4. æœªåœ¨ `assistantRegistry` æ³¨å†Œçš„æ™ºèƒ½ä½“,弹窗显示为 `pending`(开发中)态。
### 12.2 ç”Ÿäº§åŠ©æ‰‹æŽ¥å…¥çº¦å®š
1. ç”Ÿäº§åŠ©æ‰‹é…ç½®ä½äºŽ `src/components/AIChatSidebar/assistants/productionAssistant.js`,`apiBase = /manufacturing-ai`。
2. AI å·¥ä¸šå¤§è„‘中生产智能体进入弹窗后,默认使用 `production` åŠ©æ‰‹ã€‚
3. å…¨å±€å³ä¾§å¯¹è¯æ¡†åŠ©æ‰‹åˆ‡æ¢åˆ—è¡¨å·²åŒ…å«ï¼š
   - `general`(待办助理)
   - `purchase`(采购助理)
   - `production`(生产助理)
### 12.3 å­—段中文化展示规则
1. é¢å‘业务用户的字段名、标签、必填提示不直接展示英文 key。
2. `requiredFields`、`missingFields` æç¤ºéœ€è½¬æ¢ä¸ºä¸­æ–‡è·¯å¾„标签(示例:`缺少必填字段:工单号、计划结束时间`)。
3. ç»“构化列表列名、摘要指标、动作卡字段优先显示中文;英文 key ä»…用于接口通信与调试。
## 13. æœ¬æ¬¡æ›´æ–°è®°å½•(2026-05-16)
1. æ–°å¢žè®¾å¤‡ç»´ä¿®è®°å½•返回类型:`manufacturing_device_repair_list`。
2. ä¿®æ­£è®¾å¤‡åŸŸæ„å›¾åˆ†æµï¼š`ç»´ä¿®/报修/检修/维护` èµ°ç»´ä¿®è®°å½•,不再误走设备列表。
3. ä¿®æ­£ç»´ä¿®è®°å½•时间过滤:仅在用户明确时间条件时生效。
4. ä¿®æ­£å¾…维修统计口径:按 `status = 0` ç»Ÿè®¡ã€‚
5. æ–°å¢ž AI å·¥ä¸šå¤§è„‘智能体与弹窗同步维护规则:新增智能体必须同步注册弹窗助手。
6. ç”Ÿäº§åŠ©æ‰‹å·²æŽ¥å…¥å·¥ä¸šå¤§è„‘å¼¹çª—ä¸Žå…¨å±€å³ä¾§å¯¹è¯æ¡†åŠ©æ‰‹åˆ‡æ¢ã€‚
7. å¢žåŠ å­—æ®µä¸­æ–‡åŒ–å±•ç¤ºçº¦æŸï¼šé¿å…è‹±æ–‡å­—æ®µå¯¹ä¸šåŠ¡ç”¨æˆ·ç›´å‡ºã€‚
doc/20260518_ÏúÊÛÖúÊÖǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,188 @@
# é”€å”®åŠ©æ‰‹å‰ç«¯è”è°ƒæ–‡æ¡£ï¼ˆ`/sales-ai`)
> æ›´æ–°æ—¶é—´ï¼š2026-05-18
> é€‚用模块:客户档案(私海/公海)、销售报价、销售台账、销售退货、客户往来、发货台账、指标统计
> é‡ç‚¹èƒ½åŠ›ï¼šå®¢æˆ·æµå¤±é£Žé™©åˆ†æžã€å›žæ¬¾ä¸ŽæŠ¥ä»·ç­–ç•¥å»ºè®®
## 1. æŽ¥å£æ€»è§ˆ
1. æµå¼å¯¹è¯ï¼š`POST /sales-ai/chat`
2. ä¼šè¯åˆ—表:`GET /sales-ai/history/sessions`
3. ä¼šè¯æ¶ˆæ¯ï¼š`GET /sales-ai/history/messages/{memoryId}`
4. åˆ é™¤ä¼šè¯ï¼š`DELETE /sales-ai/history/{memoryId}`
说明:
- `/chat` è¿”回 `text/stream;charset=utf-8`(SSE æ–‡æœ¬æµï¼‰ã€‚
- å‘½ä¸­å·¥å…·æ—¶ï¼Œæœ€ç»ˆå†…容为 **JSON å­—符串**(非 `AjaxResult`)。
- æœªå‘½ä¸­å·¥å…·æ—¶ï¼Œè¿”回普通中文文本。
## 2. å¯¹è¯æŽ¥å£
### 2.1 è¯·æ±‚
```http
POST /sales-ai/chat
Content-Type: application/json
```
```json
{
  "memoryId": "sales-ai-001",
  "message": "帮我做客户流失风险分析,近90天,前10条"
}
```
字段说明:
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
| --- | --- | --- | --- |
| `memoryId` | string | æ˜¯ | ä¼šè¯ ID,前端生成并复用 |
| `message` | string | æ˜¯ | ç”¨æˆ·è¾“å…¥ |
### 2.2 è¿”回处理
前端建议流程:
1. å…ˆæŒ‰æµæ‹¼æŽ¥å®Œæ•´æ–‡æœ¬ `fullText`。
2. å°è¯• `JSON.parse(fullText)`:
   - æˆåŠŸï¼šæŒ‰ `type` è·¯ç”±åˆ°ç»“构化组件。
   - å¤±è´¥ï¼šæŒ‰æ™®é€šèŠå¤©æ–‡æœ¬å±•示。
## 3. ç»“构化响应协议
### 3.1 é€šç”¨ç»“æž„
```json
{
  "success": true,
  "type": "sales_dashboard",
  "description": "已返回销售指标统计",
  "summary": {},
  "data": {},
  "charts": {}
}
```
### 3.2 `type` æžšä¸¾
| type | åœºæ™¯ |
| --- | --- |
| `sales_customer_profile_list` | å®¢æˆ·æ¡£æ¡ˆï¼ˆç§æµ·/公海) |
| `sales_quotation_list` | é”€å”®æŠ¥ä»· |
| `sales_ledger_list` | é”€å”®å°è´¦ |
| `sales_return_list` | é”€å”®é€€è´§ |
| `sales_customer_interaction_list` | å®¢æˆ·å¾€æ¥ï¼ˆå›žæ¬¾ï¼‰ |
| `sales_shipping_list` | å‘货台账 |
| `sales_dashboard` | æŒ‡æ ‡ç»Ÿè®¡ |
| `sales_customer_churn_risk` | å®¢æˆ·æµå¤±é£Žé™©åˆ†æž |
| `sales_collection_quote_strategy` | å›žæ¬¾ä¸ŽæŠ¥ä»·ç­–略建议 |
## 4. èœå•能力映射(对应营销管理)
1. å®¢æˆ·æ¡£æ¡ˆï¼ˆç§æµ·ï¼‰ï¼šç¤ºä¾‹æé—® `查询私海客户档案前10条`
2. å®¢æˆ·æ¡£æ¡ˆï¼ˆå…¬æµ·ï¼‰ï¼šç¤ºä¾‹æé—® `查询公海客户档案`
3. é”€å”®æŠ¥ä»·ï¼šç¤ºä¾‹æé—® `查询本月销售报价`
4. é”€å”®å°è´¦ï¼šç¤ºä¾‹æé—® `查询本月销售台账`
5. é”€å”®é€€è´§ï¼šç¤ºä¾‹æé—® `查询近30天销售退货`
6. å®¢æˆ·å¾€æ¥ï¼šç¤ºä¾‹æé—® `查询近30天客户回款往来`
7. å‘货台账:示例提问 `查询本月发货台账`
8. æŒ‡æ ‡ç»Ÿè®¡ï¼šç¤ºä¾‹æé—® `查看销售指标统计`
## 5. é‡ç‚¹èƒ½åŠ›è”è°ƒ
### 5.1 å®¢æˆ·æµå¤±é£Žé™©åˆ†æžï¼ˆ`sales_customer_churn_risk`)
数据位置:
- åˆ—表:`data.items`
- æ±‡æ€»ï¼š`summary.highRiskCount / mediumRiskCount / lowRiskCount`
- å›¾è¡¨ï¼š`charts.riskLevelPieOption`、`charts.riskScoreBarOption`
单项常用字段:
- `customerName`
- `riskLevel`(`high`/`medium`/`low`)
- `riskScore`(0-100)
- `pendingAmount`
- `pendingRate`
- `daysSinceLastOrder`
- `riskReasons`(字符串数组)
### 5.2 å›žæ¬¾ä¸ŽæŠ¥ä»·ç­–略建议(`sales_collection_quote_strategy`)
数据位置:
- ç­–略卡:`data.items`
- æ±‡æ€»ï¼š`summary.highPriorityCount / mediumPriorityCount / lowPriorityCount`
- å›¾è¡¨ï¼š`charts.pendingAmountBarOption`、`charts.priorityPieOption`
单项常用字段:
- `customerName`
- `priority`(`high`/`medium`/`low`)
- `pendingAmount`
- `quoteConversionRate`
- `collectionStrategy`
- `quotationStrategy`
- `nextAction`
## 6. æŒ‡æ ‡ç»Ÿè®¡è”调(`sales_dashboard`)
关键字段:
- `summary.contractAmountTotal`
- `summary.receivedAmountTotal`
- `summary.pendingAmountTotal`
- `summary.shipRate`
图表字段(可直接给 ECharts):
- `charts.amountBarOption`
- `charts.shippingPieOption`
- `charts.customerTopBarOption`
- `charts.contractTrendLineOption`
附加数据:
- `data.topCustomers`
- `data.contractTrend`
## 7. ä¼šè¯åŽ†å²æŽ¥å£
### 7.1 ä¼šè¯åˆ—表
```http
GET /sales-ai/history/sessions
```
返回 `AjaxResult.data` å­—段:
- `memoryId`
- `title`
- `lastMessage`
- `messageCount`
- `lastChatTime`
### 7.2 ä¼šè¯æ¶ˆæ¯
```http
GET /sales-ai/history/messages/{memoryId}
```
返回 `AjaxResult.data` å­—段:
- `role`:`user` / `assistant` / `system` / `tool`
- `content`
- `filePaths`(当前销售助手未使用文件分析,可忽略)
### 7.3 åˆ é™¤ä¼šè¯
```http
DELETE /sales-ai/history/{memoryId}
```
返回标准 `AjaxResult`。
## 8. å‰ç«¯æŽ¥å…¥çº¦æŸ
1. æ–°å¢žåŠ©æ‰‹é…ç½®æ—¶ï¼Œ`assistantRegistry` å¿…须注册 `sales`(或你方约定 key),并指向 `apiBase = /sales-ai`。
2. ç»“构化渲染必须基于 `type` åˆ†å‘,不要仅靠关键词。
3. èŠå¤©æ¸²æŸ“需保留“文本兜底”,避免 JSON è§£æžå¤±è´¥æ—¶é¡µé¢ç©ºç™½ã€‚
4. ä¸šåŠ¡å±•ç¤ºå­—æ®µå»ºè®®ä¸­æ–‡åŒ–ï¼Œä¸ç›´æŽ¥å±•ç¤ºè‹±æ–‡å­—æ®µ key。
## 9. è”调验收清单
1. èƒ½æ­£å¸¸æµå¼æŽ¥æ”¶ `/sales-ai/chat` å“åº”并拼接文本。
2. èƒ½æŒ‰ `type` æ­£ç¡®æ¸²æŸ“ 9 ç±»ç»“构化结果。
3. èƒ½æ­£ç¡®å±•示“客户流失风险分析”和“回款与报价策略建议”两个重点场景。
4. ä¼šè¯åˆ—表、会话消息、删除会话全链路可用。
5. `memoryId` å¤ç”¨åŽå¯å›žçœ‹åŽ†å²ï¼Œä¸ä¼šä¸²ä¼šè¯ã€‚
doc/20260520_Ê×Ò³Éú²ú¿´°åǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,160 @@
# é¦–页生产看板前端联调文档
更新时间:2026-05-20
模块:`/home`(首页)
## 1. æŽ¥å£æ¸…单
1. `GET /home/productionOverview`:生产总览
2. `GET /home/productionRealtimeBoard`:生产实时看板
3. `GET /home/productionOrderProgress`:生产订单进度
4. `GET /home/todayProductionPlan`:今日生产计划
所有接口统一返回 `AjaxResult`:
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {}
}
```
## 2. ç”Ÿäº§æ€»è§ˆ
### 2.1 è¯·æ±‚
```http
GET /home/productionOverview
```
### 2.2 è¿”回 `data`
```json
{
  "totalOutput": 1280.00,
  "totalScrap": 25.00,
  "yieldRate": 98.08
}
```
字段说明:
- `totalOutput`:累计产出(件,合格数)
- `totalScrap`:累计报废(件)
- `yieldRate`:良率(0-100,前端展示时可拼接 `%`)
## 3. ç”Ÿäº§å®žæ—¶çœ‹æ¿
### 3.1 è¯·æ±‚
```http
GET /home/productionRealtimeBoard
```
### 3.2 è¿”回 `data`
```json
{
  "deviceOee": {
    "value": 74.00,
    "compareYesterday": 2.50
  },
  "orderAchievementRate": {
    "value": 81.30,
    "compareYesterday": -1.20
  },
  "defectRate": {
    "value": 1.40,
    "compareYesterday": 0.30
  }
}
```
字段说明:
- `value`:当日指标值(0-100)
- `compareYesterday`:较昨日变化值(可正可负;前端按正负决定箭头方向和颜色)
## 4. ç”Ÿäº§è®¢å•进度
### 4.1 è¯·æ±‚
```http
GET /home/productionOrderProgress?tab=all&pageNum=1&pageSize=10
```
参数:
- `tab`:`all` / `inProgress` / `completed` / `paused`
- `pageNum`:页码(默认 `1`)
- `pageSize`:每页条数(默认 `10`,最大 `50`)
### 4.2 è¿”回 `data`
```json
{
  "tab": "all",
  "total": 24,
  "pageNum": 1,
  "pageSize": 10,
  "inProgressCount": 6,
  "completedCount": 12,
  "pausedCount": 2,
  "records": [
    {
      "orderNo": "MO-20260518-001",
      "productName": "智能控制器",
      "plannedQuantity": 1000.00,
      "completedQuantity": 860.00,
      "completionRate": 86.00,
      "dueDate": "2026-05-20",
      "status": 2,
      "statusLabel": "进行中"
    }
  ]
}
```
字段说明:
- `completionRate`:完成率(0-100)
- `status`:后端状态码(`1`待开始,`2`进行中,`3`已完成,`4`已暂停)
- `statusLabel`:状态中文展示值
## 5. ä»Šæ—¥ç”Ÿäº§è®¡åˆ’
### 5.1 è¯·æ±‚
```http
GET /home/todayProductionPlan?limit=4
```
参数:
- `limit`:返回条数(默认 `4`,最大 `20`)
### 5.2 è¿”回 `data`
```json
{
  "total": 9,
  "records": [
    {
      "orderNo": "MO-20260518-004",
      "productName": "结构件A",
      "plannedQuantity": 1200.00,
      "dueDate": "2026-05-15",
      "status": 2,
      "statusLabel": "进行中"
    }
  ]
}
```
## 6. å‰ç«¯å±•示约定
- ç™¾åˆ†æ¯”字段统一是数值(如 `74.00`),前端自行拼接 `%`。
- æ‰€æœ‰æ•°å€¼ä¿ç•™ä¸¤ä½å°æ•°ã€‚
- `dueDate` å¯èƒ½ä¸º `null`,前端需兜底展示(如 `--`)。
- `compareYesterday` æ­£è´Ÿéƒ½å¯èƒ½å‡ºçŽ°ï¼Œå»ºè®®æŒ‰ `>0` ä¸Šå‡ã€`<0` ä¸‹é™ã€`=0` æŒå¹³å¤„理。
doc/20260520_Ê×Ò³Éú²ú¿´°åÐÔÄÜÓÅ»¯Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
# é¦–页生产看板性能优化前端变更文档
更新时间:2026-05-20
适用页面:首页
涉及区块:
1. ç”Ÿäº§è®¢å•进度
2. ä»Šæ—¥ç”Ÿäº§è®¡åˆ’
## 1. æœ¬æ¬¡ä¼˜åŒ–目标
针对大数据量场景(订单数量多、生产历史长)优化查询性能,降低首页接口响应时间和内存占用。
## 2. æ¶‰åŠæŽ¥å£
1. `GET /home/productionOrderProgress`
2. `GET /home/todayProductionPlan`
## 3. å‰ç«¯æ˜¯å¦éœ€è¦æ”¹ä»£ç 
结论:**无强制改动,接口入参与返回结构保持兼容**。
你现有页面可以直接联调,不需要改字段映射。
## 4. æŽ¥å£è¯´æ˜Žï¼ˆä¿æŒä¸å˜ï¼‰
### 4.1 ç”Ÿäº§è®¢å•进度
请求:
```http
GET /home/productionOrderProgress?tab=all&pageNum=1&pageSize=10
```
参数:
- `tab`:`all` / `inProgress` / `completed` / `paused`
- `pageNum`:页码,默认 `1`
- `pageSize`:每页条数,默认 `10`,最大 `50`
返回 `data`(结构不变):
```json
{
  "tab": "all",
  "total": 1200,
  "pageNum": 1,
  "pageSize": 10,
  "inProgressCount": 180,
  "completedCount": 900,
  "pausedCount": 20,
  "records": [
    {
      "orderNo": "MO-20260518-001",
      "productName": "智能控制器",
      "plannedQuantity": 1000.00,
      "completedQuantity": 860.00,
      "completionRate": 86.00,
      "dueDate": "2026-05-20",
      "status": 2,
      "statusLabel": "进行中"
    }
  ]
}
```
### 4.2 ä»Šæ—¥ç”Ÿäº§è®¡åˆ’
请求:
```http
GET /home/todayProductionPlan?limit=4
```
参数:
- `limit`:返回条数,默认 `4`,最大 `20`
返回 `data`(结构不变):
```json
{
  "total": 230,
  "records": [
    {
      "orderNo": "MO-20260518-004",
      "productName": "结构件A",
      "plannedQuantity": 1200.00,
      "dueDate": "2026-05-15",
      "status": 2,
      "statusLabel": "进行中"
    }
  ]
}
```
## 5. åŽç«¯ä¼˜åŒ–点(供前端知悉)
1. è®¢å•进度与今日计划改为轻量 SQL,仅查询首页必需字段。
2. åŽ»æŽ‰äº†é¦–é¡µæŸ¥è¯¢è·¯å¾„ä¸­ä¸å¿…è¦çš„å¤§å…³è”ã€å›¾ç‰‡å¡«å……å’Œå¯¹è±¡è£…é…ã€‚
3. çŠ¶æ€ç»Ÿè®¡æ”¹ä¸ºæ•°æ®åº“èšåˆè®¡æ•°ï¼Œä¸å†é€æ¡æ‹‰å–è®¡ç®—ã€‚
4. åˆ†é¡µä¸Žæ¡æ•°ä¸Šé™ä¿ç•™ï¼ˆ`pageSize <= 50`, `limit <= 20`)。
## 6. å‰ç«¯å»ºè®®
1. åˆ‡æ¢ `tab` æ—¶ä¿ç•™çŽ°æœ‰è°ƒç”¨æ–¹å¼å³å¯ã€‚
2. `dueDate` å¯èƒ½ä¸ºç©ºï¼Œç»§ç»­æŒ‰ `--` å…œåº•展示。
3. ç™¾åˆ†æ¯”字段仍为数值,前端继续追加 `%` å±•示。
doc/20260521_²É¹ºÖÇÄÜÌåÓÅ»¯Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
# é‡‡è´­æ™ºèƒ½ä½“优化前端变更文档
## 1. å˜æ›´èƒŒæ™¯
本次针对采购智能体做了对齐优化(参考销售/审批/制造智能体):
1. æå‡ `quickPrompts` å‘½ä¸­ç¨³å®šæ€§ã€‚
2. å¢žå¼ºç›¸å¯¹æ—¶é—´è¯†åˆ«ï¼ˆä»Šå¤©/昨天/本周/上周/本月/上月/今年/去年/近N天等)。
3. å¢žåŠ ä¸šåŠ¡æ„å›¾æœªè¯†åˆ«æ—¶çš„ç»“æž„åŒ–å…œåº•å“åº”ï¼Œé¿å…ç¼–é€ æ•°æ®ã€‚
4. è¡¥å……待付款查询的汇总字段,便于前端直接渲染统计卡片。
## 2. æŽ¥å£å½±å“æ¦‚览
| æŽ¥å£ | æ–¹æ³• | æ˜¯å¦æ”¹è·¯å¾„ | æ˜¯å¦æ”¹å…¥å‚ | æ˜¯å¦æ”¹è¿”回结构 |
| --- | --- | --- | --- | --- |
| `/purchase-ai/chat` | POST(SSE) | å¦ | å¦ | æ˜¯ï¼ˆæ–°å¢žå…œåº• JSON ç±»åž‹ï¼‰ |
| `/purchase-ai/analyze-files` | POST(SSE) | å¦ | å¦ | å¦ï¼ˆä»…内部提示词增强) |
## 3. æ–°å¢žå…œåº•响应(重点)
当用户明显在问采购业务,但条件不充分且未命中可执行意图时,`/purchase-ai/chat` ä¼šç›´æŽ¥è¿”回结构化 JSON(而不是自由文本):
```json
{
  "success": false,
  "type": "purchase_intent_not_recognized",
  "description": "未识别到可执行的采购查询条件。为保证结果准确,当前不会推测或编造数据,请补充明确时间范围、供应商、采购合同号或物料后再查询。",
  "summary": {},
  "data": {
    "quickPrompts": [
      "本月采购金额排名前十的物料有哪些?",
      "哪些采购订单还未入库?",
      "最近7天供应商到货异常有哪些?",
      "帮我统计待付款采购单!",
      "列出本月采购退货情况"
    ]
  },
  "charts": {}
}
```
前端处理建议:
1. å½“ `type === "purchase_intent_not_recognized"` æ—¶ï¼Œå±•示 `description`。
2. è¯»å– `data.quickPrompts` ä½œä¸ºå¿«æ·æé—®æŒ‰é’®ï¼ˆå¯ç›´æŽ¥å›žå¡«è¾“入框)。
## 4. å¾…付款返回新增汇总字段
接口类型:`type = "purchase_pending_payment_list"`
位置:`summary`
新增字段:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
| --- | --- | --- |
| pendingOrderCount | number | å¾…付款订单数 |
| totalContractAmount | number | å¾…付款订单合同总额 |
| totalPaidAmount | number | å·²ä»˜æ¬¾æ€»é¢ |
| totalPendingAmount | number | å¾…付款总额 |
说明:原有字段仍保留(兼容),本次为增量字段,不破坏现有渲染。
## 5. æ—¶é—´å£å¾„优化
采购智能体现在统一按中国时区动态日期换算相对时间,支持:
- ä»Šå¤©ã€æ˜¨å¤©
- æœ¬å‘¨ã€ä¸Šå‘¨
- æœ¬æœˆã€ä¸Šæœˆ
- ä»Šå¹´ã€å޻年
- è¿‘N天/周/月/年、近半年、近半个月
前端无需改传参,但展示时间范围时请以后端返回 `summary.startDate/endDate/timeRange` ä¸ºå‡†ã€‚
## 6. å‰ç«¯è”调检查清单
1. `chat` æµå¼ç»“果拼接后,优先按 JSON è§£æžã€‚
2. è¦†ç›–新类型 `purchase_intent_not_recognized` çš„ UI å¤„理。
3. å¾…付款页面读取并展示 `summary.totalPendingAmount` ç­‰æ–°å¢žå­—段。
4. éªŒè¯ä»¥ä¸‹å¿«æ·é—®é¢˜å¯ç¨³å®šè¿”回结构化结果:
   - æœ¬æœˆé‡‡è´­é‡‘额排名前十的物料有哪些?
   - å“ªäº›é‡‡è´­è®¢å•还未入库?
   - æœ€è¿‘7天供应商到货异常有哪些?
   - å¸®æˆ‘统计待付款采购单!
   - åˆ—出本月采购退货情况
doc/20260521_Ê×Ò³HomeController½Ó¿ÚÉý¼¶Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
# é¦–页 HomeController æŽ¥å£å‡çº§å‰ç«¯å˜æ›´æ–‡æ¡£
更新时间:2026-05-21
适用模块:首页(`/home`)
## 1. å˜æ›´æ¦‚览
本次为 **兼容式升级**,旧调用方式仍可用。
重点是给生产看板接口增加更明确的筛选参数,便于前端按日期和状态查询。
涉及接口:
1. `GET /home/productionOrderProgress`
2. `GET /home/todayProductionPlan`
## 2. å‚数变更
### 2.1 ç”Ÿäº§è®¢å•进度 `GET /home/productionOrderProgress`
旧参数(仍兼容):
- `tab`:`all` / `inProgress` / `completed` / `paused`
- `pageNum`:默认 `1`
- `pageSize`:默认 `10`,最大 `50`
新增参数:
- `status`(可选):状态筛选,优先级高于 `tab`
  å¯é€‰å€¼ï¼š`all` / `waiting` / `inProgress` / `completed` / `paused` / `1` / `2` / `3` / `4`
- `bizDate`(可选):业务日期筛选,格式 `yyyy-MM-dd`(按订单创建时间过滤)
参数优先级:
1. å¦‚果传了 `status`,后端优先按 `status` è§£æžï¼›
2. æœªä¼  `status` æ—¶ï¼Œæ²¿ç”¨åŽŸæœ‰ `tab` è¡Œä¸ºï¼›
3. `status` æˆ– `bizDate` æ ¼å¼é”™è¯¯æ—¶è¿”回失败信息。
请求示例:
```http
GET /home/productionOrderProgress?status=completed&bizDate=2026-05-20&pageNum=1&pageSize=10
```
### 2.2 ä»Šæ—¥ç”Ÿäº§è®¡åˆ’ `GET /home/todayProductionPlan`
旧参数(仍兼容):
- `limit`:默认 `4`,最大 `20`
新增参数:
- `planDate`(可选):计划日期筛选,格式 `yyyy-MM-dd`(按 `plan_complete_time` è¿‡æ»¤ï¼‰
请求示例:
```http
GET /home/todayProductionPlan?limit=6&planDate=2026-05-21
```
## 3. è¿”回结构变更
### 3.1 `productionOrderProgress` è¿”回新增字段
新增:
- `status`:标准化状态回显(`all` / `waiting` / `inProgress` / `completed` / `paused`)
- `bizDate`:日期筛选回显(未传时为 `null`)
- `waitingCount`:待开始订单数量
兼容保留:
- `tab` å­—段继续返回(老页面无需改动可继续使用)
返回示例:
```json
{
  "tab": "completed",
  "status": "completed",
  "bizDate": "2026-05-20",
  "total": 24,
  "pageNum": 1,
  "pageSize": 10,
  "waitingCount": 3,
  "inProgressCount": 6,
  "completedCount": 12,
  "pausedCount": 2,
  "records": []
}
```
### 3.2 `todayProductionPlan` è¿”回新增字段
新增:
- `planDate`:日期筛选回显(未传时为 `null`)
返回示例:
```json
{
  "planDate": "2026-05-21",
  "total": 9,
  "records": []
}
```
## 4. å‰ç«¯æ”¹é€ å»ºè®®
1. æ–°é¡µé¢å»ºè®®ä¼˜å…ˆä¼  `status`,逐步替代 `tab`。
2. éœ€è¦æŒ‰æ—¥æœŸå¤ç›˜çœ‹æ¿æ—¶ï¼Œä½¿ç”¨ `bizDate` / `planDate`。
3. è€é¡µé¢å¯ä¸æ”¹ï¼Œç»§ç»­æ²¿ç”¨åŽŸå‚æ•°ä¹Ÿèƒ½æ­£å¸¸è”è°ƒã€‚
doc/20260522_StockInRecordÁбíÔ´µ¥ºÅǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
# StockInRecord åˆ—表源单号前端联调文档
更新时间:2026-05-22
适用接口:`GET /stockInRecord/listPage`
## 1. å˜æ›´è¯´æ˜Ž
本次对入库管理列表接口增加返回字段 `sourceOrderNo`(源单号),用于原材料场景展示采购来源单号。
生效条件:
- å½“请求参数 `topParentProductId = 278` æ—¶ï¼ŒåŽç«¯è¿”回 `sourceOrderNo`。
- å…¶ä»– `topParentProductId` åœºæ™¯ä¸‹ï¼Œè¯¥å­—段返回 `null`(或不展示)。
## 2. å­—段定义
新增字段:
- `sourceOrderNo`:`string`,源单号(采购合同号)。
## 3. å–值规则
仅在 `topParentProductId = 278` æ—¶æŒ‰â€œæ¥æºâ€è®¡ç®—:
1. æ¥æº = `采购-入库`(`recordType = 7`)
   - å…ˆæŒ‰ `recordId` æŸ¥é‡‡è´­äº§å“è¡¨ `sales_ledger_product`(`type=2`);
   - å†é€šè¿‡ `sales_ledger_product.sales_ledger_id` æŸ¥é‡‡è´­å°è´¦è¡¨ `purchase_ledger`;
   - è¿”回 `purchase_ledger.purchase_contract_number` ä½œä¸º `sourceOrderNo`。
   - å…¼å®¹å…œåº•:若未命中采购产品链路,则按 `recordId` ç›´æŽ¥æŸ¥ `purchase_ledger.id` å–单号。
2. æ¥æº = `采购-质检-合格入库`(`recordType = 10`)
   - å…ˆæŒ‰ `recordId` æŸ¥è´¨æ£€è¡¨ `quality_inspect`;
   - å†é€šè¿‡ `quality_inspect.purchase_ledger_id` æŸ¥é‡‡è´­å°è´¦è¡¨ `purchase_ledger`;
   - è¿”回 `purchase_ledger.purchase_contract_number` ä½œä¸º `sourceOrderNo`。
## 4. è¿”回示例
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "id": 1024,
        "recordType": "7",
        "productName": "铜排",
        "model": "T2-30x3",
        "sourceOrderNo": "CG-2026-00128"
      },
      {
        "id": 1025,
        "recordType": "10",
        "productName": "铜排",
        "model": "T2-30x3",
        "sourceOrderNo": "CG-2026-00131"
      }
    ],
    "total": 2
  }
}
```
## 5. å‰ç«¯è”调建议
1. åˆ—表列新增“源单号”,读取字段 `sourceOrderNo`。
2. å»ºè®®ä»…在 `topParentProductId = 278` çš„页面/筛选条件下展示该列。
3. å½“ `sourceOrderNo` ä¸ºç©ºæ—¶å±•示 `--`,避免空白。
## 6. å›žå½’清单
1. `topParentProductId=278` + `recordType=7`:应返回采购合同号。
2. `topParentProductId=278` + `recordType=10`:应返回采购合同号(经质检链路)。
3. `topParentProductId!=278`:`sourceOrderNo` åº”为 `null` æˆ–前端不展示。
4. åŽŸæœ‰å­—æ®µï¼ˆ`productName/model/unit/createBy` ç­‰ï¼‰ä¸å—影响。
doc/20260522_StockInRecordÁбíÔ´µ¥ºÅǰ¶ËÁªµ÷Îĵµ_276²¹³ä.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
# StockInRecord åˆ—表源单号前端联调文档(`topParentProductId=276`补充)
更新时间:2026-05-22
适用接口:`GET /stockInRecord/listPage`
## 1. å˜æ›´è¯´æ˜Ž
在已有 `sourceOrderNo` åŸºç¡€ä¸Šï¼Œæ–°å¢ž `topParentProductId = 276` çš„æºå•号溯源逻辑:
- æ ¹æ®â€œæ¥æºï¼ˆrecordType)+ recordId”溯源;
- ä¼˜å…ˆè¿”回销售单号(销售合同号);
- è‹¥é”€å”®å•号为空,则回退返回生产订单号;
- ä¸è€ƒè™‘自定义入库(`recordType=0/9`)。
## 2. è¿”回字段
字段无新增,继续使用:
- `sourceOrderNo`:`string`,源单号。
## 3. 276 åœºæ™¯å–值规则
请求参数满足 `topParentProductId = 276` æ—¶ï¼š
1. `recordType = 14/15`(销售退货-合格/不合格入库)
   - `stock_in_record.record_id -> return_sale_product.id -> return_management.shipping_id -> shipping_info.sales_ledger_id -> sales_ledger.sales_contract_no`
   - è¿”回销售合同号。
2. `recordType = 2/5`(生产报工-入库/报废)
   - `stock_in_record.record_id -> production_product_main.id -> production_operation_task.production_order_id -> production_order`
   - å…ˆå–该生产订单关联销售合同号(由生产计划关联销售台账聚合);
   - é”€å”®åˆåŒå·ä¸ºç©ºæ—¶ï¼Œè¿”回 `production_order.nps_no`。
3. `recordType = 6`(质检-合格入库)
   - `stock_in_record.record_id -> quality_inspect.id -> quality_inspect.product_main_id -> production_product_main -> production_operation_task -> production_order`
   - å…ˆå–销售合同号,空则回退 `production_order.nps_no`。
4. `recordType = 4/11`(不合格处理-报废/让步放行)
   - `stock_in_record.record_id -> quality_unqualified.id -> quality_unqualified.inspect_id -> quality_inspect -> production_product_main -> production_operation_task -> production_order`
   - å…ˆå–销售合同号,空则回退 `production_order.nps_no`。
5. `recordType = 20/22`(领料退料/生产退料-合格入库)
   - `stock_in_record.record_id -> production_order_pick.id -> production_order`
   - å…ˆå–销售合同号,空则回退 `production_order.nps_no`。
6. `recordType = 0/9`(自定义入库)
   - ä¸å‚与溯源,`sourceOrderNo = null`。
## 4. å…¶ä»–场景说明
- `topParentProductId = 278` çš„采购链路源单号逻辑保持不变。
- å…¶ä»– `topParentProductId` ä¸è§¦å‘本次 276 è§„则,`sourceOrderNo` ä¸ºç©ºã€‚
## 5. å‰ç«¯è”调建议
1. åœ¨ `topParentProductId=276` çš„列表场景展示“源单号”列,读取 `sourceOrderNo`。
2. å»ºè®®ç©ºå€¼ç»Ÿä¸€å±•示 `--`。
3. ä¸éœ€è¦æ–°å¢žè¯·æ±‚参数,沿用现有 `/stockInRecord/listPage`。
## 6. å›žå½’清单
1. `topParentProductId=276` + `recordType=14/15`:应返回销售合同号。
2. `topParentProductId=276` + `recordType=2/5/6/4/11/20/22`:优先销售合同号,缺失时返回生产订单号。
3. `topParentProductId=276` + `recordType=0/9`:`sourceOrderNo` ä¸ºç©ºã€‚
4. `topParentProductId=278`:仍按采购链路返回采购合同号。
doc/20260522_²ÆÎñÖúÊÖÌáÎÊÓÅ»¯Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
# è´¢åŠ¡åŠ©æ‰‹æé—®ä¼˜åŒ–å‰ç«¯å˜æ›´æ–‡æ¡£
更新时间:2026-05-22
适用模块:财务智能助手(`/financial-ai`)
## 1. èƒŒæ™¯
当前首页财务助手快捷提问为:
1. `生成本周经营周报`
2. `为什么利润下降`
3. `哪个客户最赚钱`
问题点:
- ç¬¬ 3 æ¡é—®æ³•在部分场景下意图命中不稳定,容易走普通文本回答,导致图表链接以原始 Markdown æ–‡æœ¬å±•示(如 `![...](https://local/generate_chart?options=...)`)。
- å¿«æ·æé—®ç¼ºå°‘时间范围和分析目标,结果稳定性与可解释性较弱。
## 2. å‰ç«¯å¿«æ·æé—®æ–‡æ¡ˆä¼˜åŒ–(必改)
建议将默认三条快捷提问调整为:
1. `生成本周经营周报(利润与现金流)`
2. `分析本月利润下降原因`
3. `近30天哪个客户利润贡献最高`
说明:
- ä¸‰æ¡é—®æ³•均带时间范围或分析目标,后端命中更稳定。
- ç¬¬ 3 æ¡ä¸Žâ€œæœ€èµšé’±å®¢æˆ·â€è¯­ä¹‰ä¸€è‡´ï¼Œä½†â€œåˆ©æ¶¦è´¡çŒ®æœ€é«˜â€æ›´æ˜Žç¡®ï¼Œé€‚合直接驱动利润分析结果页。
## 3. ä¸ŽåŽç«¯èƒ½åŠ›æ˜ å°„
| å¿«æ·æé—® | é¢„期命中能力 | é¢„期 `type` |
| --- | --- | --- |
| ç”Ÿæˆæœ¬å‘¨ç»è¥å‘¨æŠ¥ï¼ˆåˆ©æ¶¦ä¸ŽçŽ°é‡‘æµï¼‰ | ç»è¥æŠ¥å‘Šç”Ÿæˆ | `financial_operation_report` |
| åˆ†æžæœ¬æœˆåˆ©æ¶¦ä¸‹é™åŽŸå›  | è®¢å•利润分析 | `financial_order_profit_analysis` |
| è¿‘30天哪个客户利润贡献最高 | è®¢å•利润分析 | `financial_order_profit_analysis` |
后端已同步增强“最赚钱客户/客户利润最高/利润贡献最高”等同义问法识别,前端按以上文案改造后可直接联调。
## 4. èŠå¤©å†…容渲染兜底(建议改)
针对聊天返回文本中出现的图表 Markdown é“¾æŽ¥ï¼ˆ`https://local/generate_chart?options=...`),建议前端增加兜底处理:
1. è¯†åˆ« Markdown å›¾ç‰‡è¯­æ³•中的 `local/generate_chart` é“¾æŽ¥ã€‚
2. è§£æž `options` å‚数并转换为 ECharts `option` åŽæ¸²æŸ“图表组件。
3. è§£æžå¤±è´¥æ—¶ä¸å±•示原始长链接文本,展示统一空态提示。
## 5. è”调回归清单
1. ç‚¹å‡»å¿«æ·æé—® `生成本周经营周报(利润与现金流)`
   - æ ¡éªŒè¿”回 `type=financial_operation_report`,并正常渲染摘要/建议/图表。
2. ç‚¹å‡»å¿«æ·æé—® `分析本月利润下降原因`
   - æ ¡éªŒè¿”回 `type=financial_order_profit_analysis`,并展示亏损订单与客户利润排行。
3. ç‚¹å‡»å¿«æ·æé—® `近30天哪个客户利润贡献最高`
   - æ ¡éªŒè¿”回 `type=financial_order_profit_analysis`,`summary.topCustomerByProfit` æœ‰å€¼ã€‚
4. æ‰‹å·¥è¾“å…¥ `哪个客户最赚钱`、`哪个客户利润最高`
   - æ ¡éªŒä»å‘½ä¸­ `financial_order_profit_analysis`,不再出现原始图表 Markdown é“¾æŽ¥ç›´å‡ºã€‚
doc/20260522_²ÆÎñÉý¼¶AIÄ£¿éǰ¶Ë±ä¸üÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,150 @@
# è´¢åŠ¡æ¨¡å—å‡çº§åŽ AI æ¨¡å—前端变更联调文档(采购/销售/生产/待办)
更新日期:2026-05-22
适用范围:`/sales-ai`、`/purchase-ai`、`/manufacturing-ai`、`/xiaozhi`(审批待办)
## 1. å˜æ›´æ€»è§ˆ
| æ¨¡å— | å¯¹å¤–接口 | æ˜¯å¦éœ€è¦å‰ç«¯æ”¹é€  | ç»“论 |
| --- | --- | --- | --- |
| é”€å”® AI | `POST /sales-ai/chat` | æ˜¯ | è´¢åŠ¡å£å¾„åˆ‡æ¢åˆ°æ–°æ”¶æ¬¾æ¨¡åž‹ï¼Œéƒ¨åˆ† `type` çš„字段语义变化 |
| é‡‡è´­ AI | `POST /purchase-ai/chat` | æ˜¯ | ä»˜æ¬¾/发票/待付款计算切换到新财务链路,统计值从占位改为真实值 |
| ç”Ÿäº§ AI | `POST /manufacturing-ai/chat` | å¦ | å·²æ ¸æŸ¥ï¼Œæ— æ—§è´¢åŠ¡é€»è¾‘ä¾èµ–ï¼Œæ— å­—æ®µå˜æ›´ |
| å¾…办 AI | `POST /xiaozhi/chat` | å¦ | å·²æ ¸æŸ¥ï¼Œæ— æ—§è´¢åŠ¡é€»è¾‘ä¾èµ–ï¼Œæ— å­—æ®µå˜æ›´ |
## 2. é”€å”® AI å˜æ›´ï¼ˆ`/sales-ai/chat`)
### 2.1 `type = sales_return_list`(销售退款/回款记录)
当前返回数据来源统一为新财务表 `account_sales_collection`,不再走旧收款退货逻辑。
`data.items[]` å…³é”®å­—段:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
| --- | --- | --- |
| id | number | æ”¶æ¬¾è®°å½•ID |
| refundId | string | æ˜ å°„ `collectionNumber`,前端可继续作为“退款/回款单号”展示 |
| collectionNumber | string | æ”¶æ¬¾å•号 |
| paymentMethod | string | æ”¶æ¬¾æ–¹å¼ |
| actualAmount | number | æ”¶æ¬¾é‡‘额(与 `collectionAmount` åŒå€¼ï¼‰ |
| collectionAmount | number | æ”¶æ¬¾é‡‘额(推荐主展示字段) |
| customerId | number | å®¢æˆ·ID |
| remark | string | å¤‡æ³¨ |
| createTime | string | æ”¶æ¬¾æ—¥æœŸï¼ˆyyyy-MM-dd) |
`summary` å¢žé‡å…³æ³¨ï¼š
- `returnAmount`:时间范围内金额汇总(按 `collectionAmount` ç»Ÿè®¡ï¼‰
### 2.2 `type = sales_customer_interaction_list`(客户往来)
当前返回基于新链路:
`account_sales_collection.stock_out_record_ids -> stock_out_record(record_type=13) -> shipping_info -> sales_ledger`
返回约定:
- æ— æ•°æ®æ—¶ï¼š`description = "no_customer_interactions"`
- æœ‰æ•°æ®æ—¶ï¼š`description = "ok"`
`summary` å…³é”®å­—段:
- `totalReceiptAmount`
- `customerCount`
`data.items[]` å…³é”®å­—段:
- `salesLedgerId`
- `salesContractNo`
- `customerName`
- `projectName`
- `receiptPaymentDate`
- `receiptPaymentAmount`
- `receiptPaymentType`
- `collectionNumber`
- `registrant`
- `remark`
### 2.3 `type = sales_ledger_list`(销售台账)
字段结构不变,但金额口径已切换:
- `receivedAmount` ç”±æ–°æ”¶æ¬¾æ¨¡åž‹æ±‡æ€»å¾—到;
- `pendingAmount = max(0, invoicedAmount - receivedAmount)`;
- è‹¥æ”¶æ¬¾è®°å½•未显式关联台账,则按客户维度兜底归集。
前端改造建议:
- ä¸æ”¹å­—段名;
- é‡ç‚¹å›žå½’“已收金额/待回款金额”是否与财务台账一致。
## 3. é‡‡è´­ AI å˜æ›´ï¼ˆ`/purchase-ai/chat`)
### 3.1 `type = purchase_stats`(采购统计)
以下字段已从占位值改为真实统计值:
- `summary.paymentCount`
- `summary.invoiceCount`
- `summary.paymentAmount`
- `summary.invoiceAmount`
发票金额口径:
- ä¼˜å…ˆ `taxInclusivePrice`
- è‹¥ä¸ºç©º/0,则使用 `taxExclusivelPrice + taxPrice`
### 3.2 `type = purchase_pending_payment_list`(待付款采购单)
核心计算已切换到新财务链路:
`account_purchase_payment -> account_payment_application -> stock_in_record -> (purchase_ledger / quality_inspect) -> purchase_ledger_id`
映射规则:
1. `stock_in_record.record_type = 7`:`record_id` ç›´æŽ¥è§†ä¸º `purchase_ledger_id`
2. `stock_in_record.record_type = 10`:通过 `quality_inspect.id = record_id` å– `quality_inspect.purchase_ledger_id`
金额字段口径:
- `paidAmount`:新链路累计已付款金额
- `pendingAmount = contractAmount - paidAmount`(<=0 çš„记录不返回)
`summary` å…³é”®å­—段(均为真实值):
- `pendingOrderCount`
- `totalContractAmount`
- `totalPaidAmount`
- `totalPendingAmount`
### 3.3 æ•°æ®æ¸…洗修复
已修复 `record_type` å¸¦ç©ºæ ¼å¯¼è‡´çš„æ˜ å°„丢失问题(后端统一 `trim()` åŽå†åˆ¤æ–­ `7/10`)。
## 4. ç”Ÿäº§ AI / å¾…办 AI æ ¸æŸ¥ç»“论
已核查以下模块代码,未发现旧财务逻辑耦合点:
- `ManufacturingAgentTools`(生产)
- `ApproveTodoTools`(待办审批)
结论:
- å¯¹å¤– `type` ä¸Žå­—段结构无变更;
- å‰ç«¯æ— éœ€åšå…¼å®¹æ”¹é€ ï¼Œä»…需做一次回归验证。
## 5. å‰ç«¯è”调要点
1. `/sales-ai/chat`、`/purchase-ai/chat` ç»§ç»­æŒ‰ SSE æ–‡æœ¬æµæ‹¼æŽ¥åŽåš JSON è§£æžã€‚
2. æŒ‰ `type` è·¯ç”±æ¸²æŸ“,不要仅依赖 `description` æ–‡æ¡ˆã€‚
3. `sales_customer_interaction_list` éœ€å…¼å®¹ `description` æžšä¸¾ï¼š`ok` / `no_customer_interactions`。
4. `sales_return_list` é‡‘额展示统一用 `collectionAmount`(`actualAmount` ä¿ç•™å…¼å®¹ï¼‰ã€‚
5. `purchase_pending_payment_list` çš„æ±‡æ€»å¡ç‰‡è¯·ç›´æŽ¥è¯»å– `summary.totalPendingAmount` ç­‰å­—段,不再前端二次估算。
## 6. å›žå½’清单(建议)
### é”€å”®
1. æé—®ï¼šâ€œè¿‘30天哪个订单回款最少”
   - æ ¡éªŒ `sales_ledger_list` çš„ `receivedAmount/pendingAmount`。
2. æé—®ï¼šâ€œæŸ¥è¯¢æœ¬æœˆé”€å”®é€€æ¬¾â€
   - æ ¡éªŒ `sales_return_list` çš„ `collectionNumber/collectionAmount/returnAmount`。
3. æé—®ï¼šâ€œæŸ¥è¯¢æœ¬æœˆå®¢æˆ·å¾€æ¥â€
   - æ ¡éªŒ `sales_customer_interaction_list` çš„ `totalReceiptAmount/customerCount`。
### é‡‡è´­
1. æé—®ï¼šâ€œç»Ÿè®¡æœ¬æœˆé‡‡è´­æ•°æ®â€
   - æ ¡éªŒ `purchase_stats` çš„ `paymentCount/invoiceCount/paymentAmount/invoiceAmount` éžå›ºå®š0。
2. æé—®ï¼šâ€œåˆ—出待付款采购单”
   - æ ¡éªŒ `purchase_pending_payment_list` çš„ `paidAmount/pendingAmount` ä¸Žè´¢åŠ¡å®žé™…ä¸€è‡´ã€‚
### ç”Ÿäº§/待办
1. ç”Ÿäº§æé—®ï¼šâ€œæŸ¥è¯¢æœ¬å‘¨è®¾å¤‡ç»´ä¿®è®°å½•”
2. å¾…办提问:“查询我的待审批列表”
   - æ ¡éªŒè¿”回结构与升级前一致(无字段破坏)。
doc/20260522_²É¹ºÌ¨ÕËÈë¿â״̬_ÏúÊÛ²úÆ·Èë¿âÉóºË״̬ǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
# é‡‡è´­å…¥åº“状态前端联调文档
更新时间:2026-05-22
适用版本:本次后端变更后
## 1. å˜æ›´èŒƒå›´
1. `GET /purchaseLedger/listPage`
   - æ–°å¢žæŸ¥è¯¢æ¡ä»¶ï¼š`stockInStatus`(入库状态)
   - æ–°å¢žè¿”回字段:`stockInStatus`(入库状态)
2. `GET /salesLedgerProduct/list`
   - æ–°å¢žè¿”回字段:`stockInApprovalStatus`(每个产品的入库审核状态)
---
## 2. å…¥åº“状态枚举(两接口一致)
- `待入库`
- `入库中`
- `完全入库`
说明:前端筛选值请直接使用以上中文枚举值。
---
## 3. æŽ¥å£ä¸€ï¼š`GET /purchaseLedger/listPage`
### 3.1 æ–°å¢žè¯·æ±‚参数
- `stockInStatus`:`string`,可选
  å¯ä¼ å€¼ï¼š`待入库` / `入库中` / `完全入库`
### 3.2 æ–°å¢žè¿”回字段
- `stockInStatus`:`string`,采购台账维度入库状态
### 3.3 çŠ¶æ€è®¡ç®—è§„åˆ™ï¼ˆé‡‡è´­å°è´¦ç»´åº¦ï¼‰
以该采购台账下 `sales_ledger_product.type = 2` çš„采购产品为计算范围:
1. å…¨éƒ¨äº§å“éƒ½è¾¾åˆ°â€œå®Œå…¨å…¥åº“” => å°è´¦çŠ¶æ€ `完全入库`
2. è‡³å°‘有一个产品存在“审核通过入库”,但未全部完全入库 => å°è´¦çŠ¶æ€ `入库中`
3. æ²¡æœ‰ä»»ä½•产品存在“审核通过入库” => å°è´¦çŠ¶æ€ `待入库`
“审核通过入库”统计口径:`stock_in_record.approval_status = 1`,并按以下来源溯源:
- `record_type = 7`(采购-入库):按采购台账+产品关联统计
- `record_type = 10`(采购-质检-合格入库):通过 `quality_inspect` å›žæº¯åˆ°é‡‡è´­å°è´¦+产品统计
### 3.4 è¿”回示例(节选)
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "id": 1201,
        "purchaseContractNumber": "CG20260522001",
        "supplierName": "XX供应商",
        "stockInStatus": "入库中"
      }
    ],
    "total": 1
  }
}
```
---
## 4. æŽ¥å£äºŒï¼š`GET /salesLedgerProduct/list`
### 4.1 æ–°å¢žè¿”回字段
- `stockInApprovalStatus`:`string`,当前产品行的入库审核状态
### 4.2 çŠ¶æ€è®¡ç®—è§„åˆ™ï¼ˆäº§å“ç»´åº¦ï¼‰
仅当产品 `type = 2`(采购产品)时计算并返回:
1. å®¡æ ¸é€šè¿‡å…¥åº“数量 `<= 0` => `待入库`
2. å®¡æ ¸é€šè¿‡å…¥åº“数量 `>= äº§å“é‡‡è´­æ•°é‡` => `完全入库`
3. å…¶ä»–情况 => `入库中`
其中“审核通过入库数量”统计同样基于:
- `stock_in_record.approval_status = 1`
- æ¥æº `record_type = 7 / 10` çš„æº¯æºå…³è”逻辑
`type != 2` çš„产品,该字段返回 `null`。
### 4.3 è¿”回示例(节选)
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": [
    {
      "id": 5566,
      "type": 2,
      "productCategory": "铜材",
      "specificationModel": "T2-30x3",
      "quantity": 100,
      "stockInApprovalStatus": "待入库"
    }
  ]
}
```
---
## 5. å‰ç«¯æ”¹é€ å»ºè®®
1. é‡‡è´­å°è´¦åˆ—表新增“入库状态”筛选项,值固定:`待入库/入库中/完全入库`。
2. é‡‡è´­å°è´¦åˆ—表新增“入库状态”列,展示 `stockInStatus`。
3. é‡‡è´­äº§å“åˆ—表新增“入库审核状态”列,展示 `stockInApprovalStatus`。
4. ç©ºå€¼ï¼ˆå¦‚ `type != 2`)建议展示为 `--`。
---
## 6. è”调检查清单
1. `/purchaseLedger/listPage` ä¸ä¼  `stockInStatus`:应正常返回全部数据,并带 `stockInStatus`。
2. `/purchaseLedger/listPage?stockInStatus=待入库`:仅返回待入库台账。
3. `/purchaseLedger/listPage?stockInStatus=入库中`:仅返回入库中台账。
4. `/purchaseLedger/listPage?stockInStatus=完全入库`:仅返回完全入库台账。
5. `/salesLedgerProduct/list` åœ¨é‡‡è´­äº§å“ï¼ˆ`type=2`)下返回 `stockInApprovalStatus`。
6. `/salesLedgerProduct/list` åœ¨éžé‡‡è´­äº§å“ï¼ˆ`type!=2`)下 `stockInApprovalStatus` ä¸º `null`。
doc/20260522_Ê×Ò³²ÆÎñ½Ó¿ÚÉý¼¶Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
# é¦–页财务接口升级前端变更文档
更新时间:2026-05-22
适用模块:首页(`/home`)
## 1. å˜æ›´æ¦‚览
本次为 **兼容式升级**,接口 URL、请求参数、返回字段保持不变。
主要变更是首页财务数据从占位/半成品逻辑,切换为按财务真实数据口径计算。
涉及接口:
1. `GET /home/statisticsReceivablePayable`
2. `GET /home/monthlyIncome`
3. `GET /home/monthlyExpenditure`
## 2. å‚数说明(无新增)
### 2.1 `GET /home/statisticsReceivablePayable`
- `type`:`1` æœ¬å‘¨ï¼Œ`2` æœ¬æœˆï¼Œ`3` æœ¬å­£åº¦ï¼ˆé»˜è®¤ `1`)
### 2.2 `GET /home/monthlyIncome`
- æ— å‚æ•°
### 2.3 `GET /home/monthlyExpenditure`
- æ— å‚æ•°
## 3. è¿”回字段口径变更
### 3.1 åº”收应付统计 `statisticsReceivablePayable`
返回字段不变:
- `receivableMoney`
- `payableMoney`
- `advanceMoney`
- `prepayMoney`
新口径:
- `receivableMoney = max(销售合同金额合计 - æ”¶æ¬¾é‡‘额合计, 0)`
- `payableMoney = max(采购合同金额合计 - ä»˜æ¬¾é‡‘额合计, 0)`
- `advanceMoney = æ”¶æ¬¾é‡‘额合计`
- `prepayMoney = ä»˜æ¬¾é‡‘额合计`
以上金额均按 `type` å¯¹åº”时间范围统计,保留两位小数。
返回示例:
```json
{
  "receivableMoney": 128000.00,
  "payableMoney": 76000.00,
  "advanceMoney": 42000.00,
  "prepayMoney": 31000.00
}
```
### 3.2 æœˆåº¦æ”¶å…¥ `monthlyIncome`
返回字段不变:
- `monthlyIncome`
- `collectionRate`
- `overdueNum`
- `overdueRate`
新口径:
- `monthlyIncome`:当月收款合计
- `collectionRate`:`当月收款合计 / å½“月销售合同金额合计 * 100`
- `overdueNum`:历史应收对账单(`account_statement.account_type=1`)中,早于当月且 `closing_balance > 0` çš„æ•°é‡
- `overdueRate`:`overdueNum / åŽ†å²åº”æ”¶å¯¹è´¦å•æ€»æ•° * 100`
返回示例:
```json
{
  "monthlyIncome": 89500.00,
  "collectionRate": "62.80",
  "overdueNum": 4,
  "overdueRate": "18.18"
}
```
### 3.3 æœˆåº¦æ”¯å‡º `monthlyExpenditure`
返回字段不变:
- `monthlyExpenditure`
- `paymentRate`
- `grossProfit`
- `profitMarginRate`
新口径:
- `monthlyExpenditure`:当月付款合计
- `paymentRate`:`当月付款合计 / å½“月采购合同金额合计 * 100`
- `grossProfit`:`当月收款合计 - å½“月付款合计`
- `profitMarginRate`:`grossProfit / å½“月收款合计 * 100`
返回示例:
```json
{
  "monthlyExpenditure": 73400.00,
  "paymentRate": "57.34",
  "grossProfit": 16100.00,
  "profitMarginRate": "17.99"
}
```
## 4. å‰ç«¯è”调说明
1. å‰ç«¯å­—段映射无需调整,可直接沿用现有解析逻辑。
2. ç™¾åˆ†æ¯”字段仍为不带 `%` çš„字符串,前端如需展示 `%` è¯·ç»§ç»­å‰ç«¯æ‹¼æŽ¥ã€‚
3. æœ¬æ¬¡åŽç«¯è¿”回值由真实财务数据驱动,建议重点回归卡片汇总与趋势图的数值联动。
doc/20260523_ ЭͬÉóÅúÐÂÔö³ö²îʱ¼äºÍ½áÊøÊ±¼ä.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
ALTER TABLE approve_process
    ADD COLUMN start_date_time datetime DEFAULT NULL COMMENT '出差开始时间',
    ADD COLUMN end_date_time   datetime DEFAULT NULL COMMENT '出差结束时间';
doc/20260615_add_plan_person_and_executor_to_routing_operation.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
# ç”Ÿäº§å·¥åºæ–°å¢žè®¡åˆ’人员和计划执行人员字段
ALTER TABLE technology_routing_operation
    ADD plan_person bigint NULL COMMENT '计划人员ID',
    ADD executor bigint NULL COMMENT '计划执行人员ID';
ALTER TABLE production_order_routing_operation
    ADD plan_person bigint NULL COMMENT '计划人员ID',
    ADD executor bigint NULL COMMENT '计划执行人员ID';
doc/financial-ai-front-integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,192 @@
# è´¢åŠ¡æ™ºèƒ½ä½“å‰ç«¯è”è°ƒæ–‡æ¡£
## 1. æ¨¡å—说明
财务智能体后端已新增统一入口 `financial-ai`,用于业财一体化分析,覆盖:
- æ™ºèƒ½æˆæœ¬æ ¸ç®—
- è®¢å•利润分析
- åº“存资金分析
- åº”收应付与现金流预测
- ç»è¥å¼‚常预警
- AI ç»è¥é©¾é©¶èˆ±
- æ—¥æŠ¥/周报自动生成
- è´¢åŠ¡çŸ¥è¯†æ£€ç´¢ï¼ˆè½»é‡ RAG ä¸Šä¸‹æ–‡ï¼‰
接口采用 **SSE æµå¼è¾“出**,工具命中时返回结构化 JSON å­—符串。
## 2. æŽ¥å£æ¸…单
### 2.1 å¯¹è¯æŽ¥å£ï¼ˆSSE)
- `POST /financial-ai/chat`
- `Content-Type: application/json`
- `Accept: text/stream;charset=utf-8`
请求体:
```json
{
  "memoryId": "finance-uuid-001",
  "message": "查询近30天亏损订单"
}
```
字段说明:
- `memoryId`:会话唯一标识(前端生成 UUID,单会话复用)
- `message`:自然语言问题
---
### 2.2 ä¼šè¯åˆ—表
- `GET /financial-ai/history/sessions`
---
### 2.3 ä¼šè¯æ¶ˆæ¯
- `GET /financial-ai/history/messages/{memoryId}`
---
### 2.4 åˆ é™¤ä¼šè¯
- `DELETE /financial-ai/history/{memoryId}`
## 3. SSE è¿”回处理规范
### 3.1 è¿”回形态
- æ™®é€šé—®ç­”:流式文本片段
- å·¥å…·å‘½ä¸­ï¼šå®Œæ•´ JSON å­—符串(通常一次性输出,也可能分片)
前端建议处理流程:
1. å°† SSE åˆ†ç‰‡æŒ‰é¡ºåºæ‹¼æŽ¥æˆ `rawText`
2. å¯¹ `rawText` å°è¯• `JSON.parse`
3. è‹¥å¯è§£æžï¼ŒæŒ‰ `type` åˆ†å‘渲染图表/表格
4. è‹¥ä¸å¯è§£æžï¼ŒæŒ‰æ™®é€šæ–‡æœ¬å±•示
### 3.2 ç»“构化 JSON é€šç”¨æ ¼å¼
```json
{
  "success": true,
  "type": "financial_order_profit_analysis",
  "description": "已完成订单利润分析",
  "summary": {},
  "data": {},
  "charts": {}
}
```
字段说明:
- `type`:结果类型(前端渲染分发键)
- `summary`:头部指标
- `data`:表格明细/建议列表
- `charts`:ECharts `option` æ•°æ®
## 4. type ä¸Žå‰ç«¯é¡µé¢æ˜ å°„
建议按 `type` å»ºç«‹æ¸²æŸ“策略:
- `financial_cost_accounting`:成本核算页
- `financial_order_profit_analysis`:订单利润页
- `financial_inventory_capital_analysis`:库存资金页
- `financial_cashflow_forecast`:现金流页
- `financial_business_anomaly_warning`:风险预警页
- `financial_business_cockpit`:经营驾驶舱
- `financial_operation_report`:日报周报页
- `financial_rag_knowledge`:知识检索/口径说明卡片
## 5. å…³é”®æ•°æ®å­—段(联调重点)
### 5.1 æˆæœ¬/利润类
- `data.orders[]`:
  - `salesContractNo`
  - `customerName`
  - `revenue`
  - `materialCost`
  - `laborCost`
  - `depreciationCost`
  - `scrapCost`
  - `totalCost`
  - `profit`
  - `profitRate`
  - `riskLevel`
  - `reasons`
  - `suggestion`
### 5.2 åº“存资金类
- `data.items[]`:
  - `productName`
  - `model`
  - `quantity`
  - `inventoryValue`
  - `stagnantDays`
  - `overstock`
  - `riskLevel`
### 5.3 çŽ°é‡‘æµç±»
- `data.actualMonthly[]` / `data.forecastMonthly[]`:
  - `month`
  - `income`
  - `expense`
  - `netFlow`
- `data.receivableRiskTop[]` / `data.payablePressureTop[]`
### 5.4 å¼‚常预警类
- `data.items[]`:
  - `riskLevel`
  - `type`
  - `message`
  - `detail`
### 5.5 æŠ¥å‘Šç±»
- `data.headline`
- `data.conclusions[]`
- `data.riskSuggestions[]`
- `data.orderProfitTop[]`
## 6. å›¾è¡¨è”调规范
`charts` å†…字段均为 ECharts `option`,可直接喂给图表组件。
常见字段:
- æŸ±çŠ¶å›¾ï¼š`orderProfitBarOption` / `processCostBarOption` / `inventoryValueTopOption`
- é¥¼å›¾ï¼š`costCompositionPieOption` / `inventoryAgingPieOption` / `anomalyLevelPieOption`
- è¶‹åŠ¿å›¾ï¼š`cashFlowTrendOption`
- ä»ªè¡¨ç›˜ï¼š`fundGapGaugeOption` / `inventoryTurnoverGauge`
## 7. æŽ¨èå‰ç«¯é—®å¥ï¼ˆå›žå½’测试)
1. `查看本月经营驾驶舱`
2. `查询近30天亏损订单`
3. `分析近30天库存资金占用`
4. `预测未来3个月现金流`
5. `生成本周经营周报`
6. `为什么利润下降`
7. `哪个客户最赚钱`
8. `哪个工序成本最高`
## 8. å¼‚常与兜底处理
- `memoryId` ä¸ºç©ºï¼šè¿”回文本 `memoryId不能为空`
- `message` ä¸ºç©ºï¼šè¿”回文本 `message不能为空`
- æ— æ•°æ®åœºæ™¯ï¼š`success=true` ä¸” `data.items=[]`,前端按空态展示
- éž JSON æµè¿”回:按普通聊天文本展示
## 9. è”调建议
1. å…ˆåš `type` åˆ†å‘器(保证所有结构化结果可落地)
2. å†åšæ‘˜è¦å¡ç‰‡ï¼ˆ`summary`)+ è¡¨æ ¼ï¼ˆ`data`)+ å›¾è¡¨ï¼ˆ`charts`)
3. æœ€åŽè¡¥ä¼šè¯åŽ†å²ä¸Žåˆ é™¤èƒ½åŠ›ï¼Œå½¢æˆå®Œæ•´å¯¹è¯é—­çŽ¯
doc/sql/20260609_knowledge_base_vector.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
-- çŸ¥è¯†åº“向量检索功能数据库变更
-- æ‰§è¡Œå‰è¯·ç¡®ä¿ knowledge_base è¡¨å·²å­˜åœ¨
-- 1. knowledge_base è¡¨å¢žåŠ å­—æ®µ
ALTER TABLE knowledge_base
ADD COLUMN IF NOT EXISTS file_count INT DEFAULT 0 COMMENT '文件数量',
ADD COLUMN IF NOT EXISTS total_chunk_count INT DEFAULT 0 COMMENT '总切片数量',
ADD COLUMN IF NOT EXISTS description VARCHAR(500) COMMENT '知识库描述';
-- 2. åˆ›å»ºçŸ¥è¯†åº“文件向量记录表
CREATE TABLE IF NOT EXISTS knowledge_base_vector (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    knowledge_base_id BIGINT NOT NULL COMMENT '关联知识库ID',
    storage_blob_id BIGINT NOT NULL COMMENT '关联文件blob ID',
    file_name VARCHAR(255) NOT NULL COMMENT '文件名称',
    file_type VARCHAR(50) NOT NULL COMMENT '文件类型(docx/pdf/xlsx/txt等)',
    vector_status TINYINT DEFAULT 0 COMMENT '向量化状态: 0-待处理, 1-处理中, 2-已完成, 3-失败',
    vector_error VARCHAR(500) COMMENT '向量化失败原因',
    chunk_count INT DEFAULT 0 COMMENT '切片数量',
    namespace VARCHAR(100) COMMENT '向量命名空间',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    create_user INT COMMENT '创建人',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    update_user INT COMMENT '更新人',
    tenant_id BIGINT COMMENT '租户ID',
    dept_id BIGINT COMMENT '部门ID',
    INDEX idx_knowledge_base_id (knowledge_base_id),
    INDEX idx_storage_blob_id (storage_blob_id),
    INDEX idx_vector_status (vector_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库文件向量记录表';
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,相关接口会出现字段不存在异常。
doc/֪ʶ¿âRAG¹¦ÄÜʵÏÖÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1034 @@
# çŸ¥è¯†åº“RAG向量检索功能实现文档
## ä¸€ã€åŠŸèƒ½æ¦‚è¿°
基于 RAG(Retrieval-Augmented Generation)技术实现知识库问答功能,支持:
- çŸ¥è¯†åº“管理(CRUD)
- æ–‡ä»¶ä¸Šä¼ ä¸Žå‘量化处理
- åŸºäºŽå‘量检索的智能问答
- å¤šç§æ–‡ä»¶æ ¼å¼æ”¯æŒï¼ˆtxt、md、docx、xlsx、xls、pdf)
## äºŒã€æŠ€æœ¯æž¶æž„
### 2.1 æŠ€æœ¯æ ˆ
| ç»„ä»¶ | æŠ€æœ¯ |
|------|------|
| å‘量数据库 | Pinecone |
| Embedding模型 | é˜¿é‡Œäº‘ DashScope text-embedding-v3 |
| LLM | é˜¿é‡Œäº‘通义千问 qwen-max |
| æ¡†æž¶ | langchain4j |
| ORM | MyBatis-Plus |
### 2.2 æž¶æž„图
```
┌─────────────────────────────────────────────────────────────┐
│                        å‰ç«¯åº”用                              â”‚
└─────────────────────────────────────────────────────────────┘
                              â”‚
                              â–¼
┌─────────────────────────────────────────────────────────────┐
│                     Controller Layer                         â”‚
│  â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”  â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”   â”‚
│  â”‚ KnowledgeBaseCtrl   â”‚  â”‚ KnowledgeChatController     â”‚   â”‚
│  â”‚ (知识库管理)         â”‚  â”‚ (知识库问答)                 â”‚   â”‚
│  â””─────────────────────┘  â””─────────────────────────────┘   â”‚
└─────────────────────────────────────────────────────────────┘
                              â”‚
                              â–¼
┌─────────────────────────────────────────────────────────────┐
│                      Service Layer                           â”‚
│  â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”  â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”   â”‚
│  â”‚KnowledgeBaseService â”‚  â”‚ KnowledgeRagService         â”‚   â”‚
│  â”‚ (知识库CRUD)         â”‚  â”‚ (向量化/检索)                â”‚   â”‚
│  â””─────────────────────┘  â””─────────────────────────────┘   â”‚
└─────────────────────────────────────────────────────────────┘
                              â”‚
                              â–¼
┌─────────────────────────────────────────────────────────────┐
│                      AI Layer                                â”‚
│  â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”  â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”   â”‚
│  â”‚ KnowledgeChatAgent  â”‚  â”‚ EmbeddingStore (Pinecone)   â”‚   â”‚
│  â”‚ (问答Agent)          â”‚  â”‚ (向量存储)                   â”‚   â”‚
│  â””─────────────────────┘  â””─────────────────────────────┘   â”‚
└─────────────────────────────────────────────────────────────┘
```
---
## ä¸‰ã€åŽç«¯å®žçް
### 3.1 æ•°æ®åº“设计
#### 3.1.1 çŸ¥è¯†åº“表(knowledge_base)
```sql
CREATE TABLE knowledge_base (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) COMMENT '知识标题',
    type VARCHAR(50) COMMENT '知识类型',
    scenario VARCHAR(255) COMMENT '适用场景',
    efficiency VARCHAR(20) COMMENT '解决效率',
    problem TEXT COMMENT '问题描述',
    solution TEXT COMMENT '解决方案',
    key_points TEXT COMMENT '关键要点',
    creator VARCHAR(100) COMMENT '创建人',
    usage_count INT DEFAULT 0 COMMENT '使用次数',
    file_count INT DEFAULT 0 COMMENT '文件数量',
    total_chunk_count INT DEFAULT 0 COMMENT '总切片数量',
    description VARCHAR(500) COMMENT '知识库描述',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    create_user INT,
    update_user INT,
    tenant_id BIGINT,
    dept_id BIGINT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库表';
```
#### 3.1.2 çŸ¥è¯†åº“向量记录表(knowledge_base_vector)
```sql
CREATE TABLE knowledge_base_vector (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    knowledge_base_id BIGINT NOT NULL COMMENT '关联知识库ID',
    storage_blob_id BIGINT NOT NULL COMMENT '关联文件blob ID',
    file_name VARCHAR(255) NOT NULL COMMENT '文件名称',
    file_type VARCHAR(50) NOT NULL COMMENT '文件类型',
    vector_status TINYINT DEFAULT 0 COMMENT '向量化状态: 0-待处理, 1-处理中, 2-已完成, 3-失败',
    vector_error VARCHAR(500) COMMENT '向量化失败原因',
    chunk_count INT DEFAULT 0 COMMENT '切片数量',
    namespace VARCHAR(100) COMMENT '向量命名空间',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    create_user INT,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    update_user INT,
    tenant_id BIGINT,
    dept_id BIGINT,
    INDEX idx_knowledge_base_id (knowledge_base_id),
    INDEX idx_storage_blob_id (storage_blob_id),
    INDEX idx_vector_status (vector_status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库文件向量记录表';
```
### 3.2 Maven依赖
```xml
<!-- langchain4j BOM -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-bom</artifactId>
            <version>1.0.0-beta3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <!-- langchain4j æ ¸å¿ƒ -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-spring-boot-starter</artifactId>
    </dependency>
    <!-- Pinecone å‘量数据库 -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-pinecone</artifactId>
    </dependency>
    <!-- é˜¿é‡Œäº‘ DashScope -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
    </dependency>
</dependencies>
```
### 3.3 é…ç½®æ–‡ä»¶ï¼ˆapplication.yml)
```yaml
# Pinecone å‘量数据库配置
pinecone:
  api-key: your-pinecone-api-key
  index: your-index-name
  namespace: knowledge-base
# langchain4j é…ç½®
langchain4j:
  community:
    dashscope:
      streaming-chat-model:
        api-key: your-dashscope-api-key
        model-name: "qwen-max"
      embedding-model:
        api-key: your-dashscope-api-key
        model-name: "text-embedding-v3"
```
### 3.4 æ ¸å¿ƒä»£ç å®žçް
#### 3.4.1 å®žä½“ç±»
**KnowledgeBase.java**
```java
@Data
@TableName("knowledge_base")
public class KnowledgeBase implements Serializable {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String title;
    private String type;
    private String scenario;
    private String efficiency;
    private String problem;
    private String solution;
    private String keyPoints;
    private String creator;
    private Integer usageCount;
    private Integer fileCount;
    private Integer totalChunkCount;
    private String description;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
```
**KnowledgeBaseVector.java**
```java
@Data
@TableName("knowledge_base_vector")
public class KnowledgeBaseVector implements Serializable {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long knowledgeBaseId;
    private Long storageBlobId;
    private String fileName;
    private String fileType;
    private Integer vectorStatus;
    private String vectorError;
    private Integer chunkCount;
    private String namespace;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    // å‘量化状态常量
    public static final int STATUS_PENDING = 0;
    public static final int STATUS_PROCESSING = 1;
    public static final int STATUS_COMPLETED = 2;
    public static final int STATUS_FAILED = 3;
}
```
#### 3.4.2 EmbeddingStore配置
**EmbeddingStoreConfig.java**
```java
@Configuration
public class EmbeddingStoreConfig {
    @Value("${pinecone.api-key}")
    private String pineconeApiKey;
    @Value("${pinecone.index}")
    private String indexName;
    @Value("${pinecone.namespace}")
    private String namespace;
    @Bean
    public Pinecone pinecone() {
        return new Pinecone.Builder(pineconeApiKey).build();
    }
    @Bean
    public Index pineconeIndex(Pinecone pinecone) {
        return pinecone.getIndexConnection(indexName);
    }
    @Bean
    public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) {
        return PineconeEmbeddingStore.builder()
                .apiKey(pineconeApiKey)
                .index(indexName)
                .nameSpace(namespace)
                .createIndex(PineconeServerlessIndexConfig.builder()
                        .cloud("AWS")
                        .region("us-east-1")
                        .dimension(embeddingModel.dimension())
                        .build())
                .build();
    }
}
```
#### 3.4.3 RAG服务实现
**KnowledgeRagService.java**
```java
public interface KnowledgeRagService {
    void processVectorAsync(Long vectorId);
    void processVector(Long vectorId);
    List<String> searchRelevantContent(String namespace, String query, int maxResults);
    void deleteEmbeddings(String namespace, Long storageBlobId);
}
```
**KnowledgeRagServiceImpl.java**(核心实现)
```java
@Slf4j
@Service
public class KnowledgeRagServiceImpl implements KnowledgeRagService {
    private final KnowledgeBaseVectorService knowledgeBaseVectorService;
    private final StorageBlobService storageBlobService;
    private final EmbeddingModel embeddingModel;
    private final EmbeddingStore<TextSegment> embeddingStore;
    private final FileProperties fileProperties;
    private final Index pineconeIndex;
    @Value("${pinecone.namespace}")
    private String namespace;
    private static final int CHUNK_SIZE = 500;
    private static final int CHUNK_OVERLAP = 100;
    private static final long CHUNK_THRESHOLD_BYTES = 80L * 1024 * 1024;
    private static final int EMBEDDING_MAX_LENGTH = 8000;
    @Override
    @Async("threadPoolTaskExecutor")
    public void processVectorAsync(Long vectorId) {
        processVector(vectorId);
    }
    @Override
    public void processVector(Long vectorId) {
        KnowledgeBaseVector vector = knowledgeBaseVectorService.getById(vectorId);
        if (vector == null) return;
        try {
            // æ›´æ–°çŠ¶æ€ä¸ºå¤„ç†ä¸­
            knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_PROCESSING, null, null);
            // èŽ·å–æ–‡ä»¶å†…å®¹
            StorageBlob blob = storageBlobService.getById(vector.getStorageBlobId());
            File file = getFile(blob);
            String content = extractFileContent(file, vector.getFileName());
            if (content == null || content.trim().isEmpty()) {
                throw new RuntimeException("文件内容为空");
            }
            // æ–‡æœ¬åˆ‡ç‰‡
            List<TextSegment> chunks;
            boolean needChunk = file.length() > CHUNK_THRESHOLD_BYTES || content.length() > EMBEDDING_MAX_LENGTH;
            if (needChunk) {
                chunks = splitText(content, vector);
            } else {
                Map<String, Object> metadata = buildMetadata(vector);
                chunks = List.of(TextSegment.from(content, new Metadata(metadata)));
            }
            // ç”ŸæˆåµŒå…¥å‘量并存储
            int chunkCount = 0;
            for (TextSegment chunk : chunks) {
                Embedding embedding = embeddingModel.embed(chunk).content();
                embeddingStore.add(embedding, chunk);
                chunkCount++;
            }
            // æ›´æ–°çŠ¶æ€ä¸ºå®Œæˆ
            knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_COMPLETED, chunkCount, null);
        } catch (Exception e) {
            log.error("向量化处理失败", e);
            knowledgeBaseVectorService.updateVectorStatus(vectorId, STATUS_FAILED, null, e.getMessage());
        }
    }
    @Override
    public List<String> searchRelevantContent(String namespace, String query, int maxResults) {
        Embedding queryEmbedding = embeddingModel.embed(query).content();
        EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
                .queryEmbedding(queryEmbedding)
                .maxResults(maxResults)
                .minScore(0.7)
                .build();
        EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest);
        return searchResult.matches().stream()
                .map(match -> match.embedded().text())
                .collect(Collectors.toList());
    }
    @Override
    public void deleteEmbeddings(String namespace, Long storageBlobId) {
        Struct filter = Struct.newBuilder()
                .putFields("storageBlobId", Value.newBuilder()
                        .setStructValue(Struct.newBuilder()
                                .putFields("$eq", Value.newBuilder()
                                        .setNumberValue(storageBlobId.doubleValue())
                                        .build()))
                        .build())
                .build();
        pineconeIndex.delete(new ArrayList<>(), false, this.namespace, filter);
    }
    private String extractFileContent(File file, String fileName) throws Exception {
        String ext = getFileExtension(fileName);
        if (isPlainText(ext)) {
            return readFileWithEncoding(file);
        }
        if ("docx".equals(ext)) {
            return extractDocx(file);
        }
        if ("xlsx".equals(ext) || "xls".equals(ext)) {
            return extractExcel(file);
        }
        return readFileWithEncoding(file);
    }
    // ... å…¶ä»–辅助方法
}
```
#### 3.4.4 çŸ¥è¯†åº“问答Agent
**KnowledgeChatAgent.java**
```java
@AiService(
        wiringMode = EXPLICIT,
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "chatMemoryProvider"
)
public interface KnowledgeChatAgent {
    @SystemMessage("""
            ä½ æ˜¯ä¼ä¸šçŸ¥è¯†åº“问答助手。
            ä½ éœ€è¦åŸºäºŽæä¾›çš„知识库内容回答用户问题。
            éµå¾ªä»¥ä¸‹è§„则:
            1. ä¸¥æ ¼åŸºäºŽçŸ¥è¯†åº“内容回答,不要编造信息
            2. å¦‚果知识库中没有相关信息,明确告知用户
            3. å›žç­”要准确、简洁、有条理
            4. å¼•用来源时注明"根据知识库内容"
            """)
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
}
```
#### 3.4.5 Controller层
**KnowledgeBaseController.java**
```java
@RestController
@RequestMapping("/knowledgeBase")
@Tag(name = "知识库管理")
public class KnowledgeBaseController {
    @GetMapping("/getList")
    public AjaxResult getList(@RequestParam(defaultValue = "1") long current,
                              @RequestParam(defaultValue = "10") long size,
                              KnowledgeBase knowledgeBase) {
        Page page = new Page(current, size);
        return AjaxResult.success(knowledgeBaseService.listpage(page, knowledgeBase));
    }
    @PostMapping("/add")
    public AjaxResult add(@RequestBody KnowledgeBase knowledgeBase) {
        return AjaxResult.success(knowledgeBaseService.save(knowledgeBase));
    }
    @PostMapping("/update")
    public AjaxResult update(@RequestBody KnowledgeBase knowledgeBase) {
        return AjaxResult.success(knowledgeBaseService.updateById(knowledgeBase));
    }
    @DeleteMapping("/delete")
    public AjaxResult delete(@RequestBody List<Long> ids) {
        return AjaxResult.success(knowledgeBaseService.removeByIds(ids));
    }
    @GetMapping("/vector/status/{knowledgeBaseId}")
    @Operation(summary = "查询知识库文件向量化状态")
    public AjaxResult getVectorStatus(@PathVariable Long knowledgeBaseId) {
        return AjaxResult.success(knowledgeBaseVectorService.getVectorStatusByKnowledgeBaseId(knowledgeBaseId));
    }
    @PostMapping("/vector/reprocess/{vectorId}")
    @Operation(summary = "重新向量化文件")
    public AjaxResult reprocessVector(@PathVariable Long vectorId) {
        knowledgeBaseVectorService.reprocessVector(vectorId);
        return AjaxResult.success("已重新提交向量化任务");
    }
    @PostMapping("/file/save")
    @Operation(summary = "保存知识库文件关联")
    public AjaxResult saveKnowledgeBaseFiles(@RequestBody KnowledgeBaseFileDTO dto) {
        // ä¿å­˜é™„件关联并触发向量化
        // ...
    }
    @DeleteMapping("/file/delete")
    @Operation(summary = "删除知识库文件")
    public AjaxResult deleteKnowledgeBaseFiles(@RequestBody List<Long> vectorIds) {
        knowledgeBaseVectorService.deleteVectors(vectorIds);
        return AjaxResult.success();
    }
}
```
**KnowledgeChatController.java**
```java
@RestController
@RequestMapping("/ai/knowledge")
@Tag(name = "知识库问答")
public class KnowledgeChatController {
    private final KnowledgeChatAgent knowledgeChatAgent;
    private final KnowledgeRagService knowledgeRagService;
    private final KnowledgeBaseService knowledgeBaseService;
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    @Operation(summary = "知识库问答")
    public Flux<String> chat(@RequestBody KnowledgeChatRequest request) {
        // æ£€ç´¢ç›¸å…³å†…容
        String namespace = "kb-" + request.getKnowledgeBaseId();
        List<String> relevantContents = knowledgeRagService.searchRelevantContent(
                namespace, request.getQuestion(), 5);
        if (relevantContents.isEmpty()) {
            return Flux.just("知识库中未找到相关内容");
        }
        // æž„建上下文
        StringBuilder context = new StringBuilder();
        context.append("以下是从知识库中检索到的相关内容:\n\n");
        for (int i = 0; i < relevantContents.size(); i++) {
            context.append("【内容").append(i + 1).append("】\n");
            context.append(relevantContents.get(i)).append("\n\n");
        }
        context.append("---\n请基于以上知识库内容回答:\n").append(request.getQuestion());
        return knowledgeChatAgent.chat(request.getMemoryId(), context.toString());
    }
    @GetMapping("/list")
    @Operation(summary = "知识库列表")
    public AjaxResult listKnowledgeBases() {
        return AjaxResult.success(knowledgeBaseService.list());
    }
}
```
---
## å››ã€API接口文档
### 4.1 çŸ¥è¯†åº“管理接口
| æŽ¥å£ | æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|------|
| èŽ·å–åˆ—è¡¨ | GET | /knowledgeBase/getList | åˆ†é¡µæŸ¥è¯¢çŸ¥è¯†åº“列表 |
| æ–°å¢žçŸ¥è¯†åº“ | POST | /knowledgeBase/add | åˆ›å»ºçŸ¥è¯†åº“ |
| æ›´æ–°çŸ¥è¯†åº“ | POST | /knowledgeBase/update | æ›´æ–°çŸ¥è¯†åº“信息 |
| åˆ é™¤çŸ¥è¯†åº“ | DELETE | /knowledgeBase/delete | æ‰¹é‡åˆ é™¤çŸ¥è¯†åº“ |
| æŸ¥è¯¢å‘量化状态 | GET | /knowledgeBase/vector/status/{id} | æŸ¥è¯¢æ–‡ä»¶å‘量化状态 |
| é‡æ–°å‘量化 | POST | /knowledgeBase/vector/reprocess/{id} | é‡æ–°å¤„理失败的文件 |
| ä¿å­˜æ–‡ä»¶å…³è” | POST | /knowledgeBase/file/save | ä¸Šä¼ æ–‡ä»¶åŽå…³è”到知识库 |
| åˆ é™¤æ–‡ä»¶ | DELETE | /knowledgeBase/file/delete | åˆ é™¤çŸ¥è¯†åº“文件 |
### 4.2 çŸ¥è¯†åº“问答接口
| æŽ¥å£ | æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|------|
| çŸ¥è¯†åº“问答 | POST | /ai/knowledge/chat | æµå¼è¿”回问答结果 |
| çŸ¥è¯†åº“列表 | GET | /ai/knowledge/list | èŽ·å–å¯é€‰çŸ¥è¯†åº“åˆ—è¡¨ |
### 4.3 æŽ¥å£è¯¦ç»†è¯´æ˜Ž
#### 4.3.1 ä¿å­˜çŸ¥è¯†åº“文件关联
**请求**
```json
POST /knowledgeBase/file/save
{
    "knowledgeBaseId": 1,
    "storageBlobIds": [100, 101, 102]
}
```
**响应**
```json
{
    "code": 200,
    "msg": "操作成功"
}
```
#### 4.3.2 çŸ¥è¯†åº“问答
**请求**
```json
POST /ai/knowledge/chat
Content-Type: application/json
{
    "knowledgeBaseId": 1,
    "memoryId": "session-uuid",
    "question": "如何处理库存盘点差异?"
}
```
**响应**(SSE流式)
```
根据知识库内容,库存盘点差异的处理流程如下:
1. å‘现差异后,首先核对盘点记录...
2. æ£€æŸ¥æ˜¯å¦æœ‰æ¼ç›˜æˆ–错盘...
3. ...
```
---
## äº”、前端实现
### 5.1 çŸ¥è¯†åº“管理页面
```vue
<template>
  <div class="knowledge-base">
    <!-- åˆ—表 -->
    <el-table :data="tableData" border>
      <el-table-column prop="title" label="知识标题" />
      <el-table-column prop="type" label="知识类型" />
      <el-table-column prop="fileCount" label="文件数量" />
      <el-table-column prop="totalChunkCount" label="切片数量" />
      <el-table-column label="操作">
        <template #default="{ row }">
          <el-button @click="handleEdit(row)">编辑</el-button>
          <el-button @click="handleFiles(row)">文件管理</el-button>
          <el-button @click="handleChat(row)">问答</el-button>
          <el-button type="danger" @click="handleDelete(row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getKnowledgeBaseList, deleteKnowledgeBase } from '@/api/knowledge'
const tableData = ref([])
const loadData = async () => {
  const res = await getKnowledgeBaseList({ current: 1, size: 10 })
  tableData.value = res.data.records
}
onMounted(loadData)
</script>
```
### 5.2 æ–‡ä»¶ä¸Šä¼ ä¸Žå‘量化状态
```vue
<template>
  <div class="file-manager">
    <!-- æ–‡ä»¶ä¸Šä¼  -->
    <el-upload
      :action="uploadUrl"
      :on-success="handleUploadSuccess"
      multiple
    >
      <el-button type="primary">上传文件</el-button>
    </el-upload>
    <!-- æ–‡ä»¶åˆ—表与向量化状态 -->
    <el-table :data="fileList">
      <el-table-column prop="fileName" label="文件名" />
      <el-table-column label="向量化状态">
        <template #default="{ row }">
          <el-tag :type="getStatusType(row.vectorStatus)">
            {{ getStatusText(row.vectorStatus) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="chunkCount" label="切片数" />
      <el-table-column label="操作">
        <template #default="{ row }">
          <el-button v-if="row.vectorStatus === 3" @click="reprocess(row)">
            é‡æ–°å¤„理
          </el-button>
          <el-button type="danger" @click="deleteFile(row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>
<script setup>
const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/upload'
// ä¸Šä¼ æˆåŠŸåŽä¿å­˜å…³è”
const uploadedBlobIds = ref([])
const handleUploadSuccess = (response, file) => {
  if (response.code === 200) {
    uploadedBlobIds.value.push(response.data.id)
  }
}
// ä¿å­˜æ–‡ä»¶å…³è”
const saveFiles = async () => {
  await saveKnowledgeBaseFiles({
    knowledgeBaseId: props.knowledgeBaseId,
    storageBlobIds: uploadedBlobIds.value
  })
  // åˆ·æ–°æ–‡ä»¶åˆ—表
  loadFileList()
}
// çŠ¶æ€æ–‡æœ¬æ˜ å°„
const getStatusText = (status) => {
  const map = {
    0: '待处理',
    1: '处理中',
    2: '已完成',
    3: '失败'
  }
  return map[status] || '未知'
}
const getStatusType = (status) => {
  const map = {
    0: 'info',
    1: 'warning',
    2: 'success',
    3: 'danger'
  }
  return map[status] || 'info'
}
</script>
```
### 5.3 çŸ¥è¯†åº“问答界面
```vue
<template>
  <div class="knowledge-chat">
    <!-- çŸ¥è¯†åº“选择 -->
    <el-select v-model="selectedKbId" placeholder="选择知识库">
      <el-option
        v-for="kb in knowledgeBases"
        :key="kb.id"
        :label="kb.title"
        :value="kb.id"
      />
    </el-select>
    <!-- å¯¹è¯åŒºåŸŸ -->
    <div class="chat-messages">
      <div
        v-for="(msg, index) in messages"
        :key="index"
        :class="['message', msg.role]"
      >
        <div class="content">{{ msg.content }}</div>
      </div>
    </div>
    <!-- è¾“入框 -->
    <el-input
      v-model="inputQuestion"
      placeholder="请输入问题"
      @keyup.enter="sendMessage"
    >
      <template #append>
        <el-button @click="sendMessage" :loading="loading">发送</el-button>
      </template>
    </el-input>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getKnowledgeBaseList, knowledgeChat } from '@/api/knowledge'
const knowledgeBases = ref([])
const selectedKbId = ref(null)
const messages = ref([])
const inputQuestion = ref('')
const loading = ref(false)
const memoryId = ref(crypto.randomUUID())
const sendMessage = async () => {
  if (!inputQuestion.value.trim()) return
  if (!selectedKbId.value) {
    ElMessage.warning('请选择知识库')
    return
  }
  // æ·»åŠ ç”¨æˆ·æ¶ˆæ¯
  messages.value.push({
    role: 'user',
    content: inputQuestion.value
  })
  loading.value = true
  try {
    // æµå¼è¯·æ±‚
    const response = await fetch('/api/ai/knowledge/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        knowledgeBaseId: selectedKbId.value,
        memoryId: memoryId.value,
        question: inputQuestion.value
      })
    })
    // å¤„理SSE流式响应
    const reader = response.body.getReader()
    const decoder = new TextDecoder()
    let aiContent = ''
    messages.value.push({ role: 'assistant', content: '' })
    while (true) {
      const { done, value } = await reader.read()
      if (done) break
      const text = decoder.decode(value)
      aiContent += text
      messages.value[messages.value.length - 1].content = aiContent
    }
  } finally {
    loading.value = false
    inputQuestion.value = ''
  }
}
onMounted(async () => {
  const res = await getKnowledgeBaseList()
  knowledgeBases.value = res.data
})
</script>
```
### 5.4 API封装
```javascript
// api/knowledge.js
import request from '@/utils/request'
// èŽ·å–çŸ¥è¯†åº“åˆ—è¡¨
export function getKnowledgeBaseList(params) {
  return request({
    url: '/knowledgeBase/getList',
    method: 'get',
    params
  })
}
// æ–°å¢žçŸ¥è¯†åº“
export function addKnowledgeBase(data) {
  return request({
    url: '/knowledgeBase/add',
    method: 'post',
    data
  })
}
// æ›´æ–°çŸ¥è¯†åº“
export function updateKnowledgeBase(data) {
  return request({
    url: '/knowledgeBase/update',
    method: 'post',
    data
  })
}
// åˆ é™¤çŸ¥è¯†åº“
export function deleteKnowledgeBase(ids) {
  return request({
    url: '/knowledgeBase/delete',
    method: 'delete',
    data: ids
  })
}
// èŽ·å–æ–‡ä»¶å‘é‡åŒ–çŠ¶æ€
export function getVectorStatus(knowledgeBaseId) {
  return request({
    url: `/knowledgeBase/vector/status/${knowledgeBaseId}`,
    method: 'get'
  })
}
// é‡æ–°å‘量化
export function reprocessVector(vectorId) {
  return request({
    url: `/knowledgeBase/vector/reprocess/${vectorId}`,
    method: 'post'
  })
}
// ä¿å­˜æ–‡ä»¶å…³è”
export function saveKnowledgeBaseFiles(data) {
  return request({
    url: '/knowledgeBase/file/save',
    method: 'post',
    data
  })
}
// åˆ é™¤æ–‡ä»¶
export function deleteKnowledgeBaseFiles(vectorIds) {
  return request({
    url: '/knowledgeBase/file/delete',
    method: 'delete',
    data: vectorIds
  })
}
// çŸ¥è¯†åº“问答(流式)
export async function knowledgeChat(data) {
  const response = await fetch('/api/ai/knowledge/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  })
  return response.body
}
// èŽ·å–çŸ¥è¯†åº“åˆ—è¡¨ï¼ˆé—®ç­”ç”¨ï¼‰
export function getKnowledgeBaseListForChat() {
  return request({
    url: '/ai/knowledge/list',
    method: 'get'
  })
}
```
---
## å…­ã€æ ¸å¿ƒæµç¨‹
### 6.1 æ–‡ä»¶ä¸Šä¼ ä¸Žå‘量化流程
```
1. å‰ç«¯è°ƒç”¨ /common/upload ä¸Šä¼ æ–‡ä»¶ â†’ è¿”回 storageBlobId
2. å‰ç«¯è°ƒç”¨ /knowledgeBase/file/save å…³è”文件到知识库
3. åŽç«¯åˆ›å»º KnowledgeBaseVector è®°å½•(状态:待处理)
4. åŽç«¯å¼‚步调用 KnowledgeRagService.processVectorAsync()
   â”œâ”€â”€ æ›´æ–°çŠ¶æ€ä¸º"处理中"
   â”œâ”€â”€ æå–文件内容(支持多种格式)
   â”œâ”€â”€ è‡ªåŠ¨æ£€æµ‹æ–‡ä»¶ç¼–ç ï¼ˆUTF-8/GBK)
   â”œâ”€â”€ æ–‡æœ¬åˆ‡ç‰‡ï¼ˆå¤§æ–‡ä»¶æˆ–长内容才切片)
   â”œâ”€â”€ ç”Ÿæˆ Embedding å‘量
   â”œâ”€â”€ å­˜å‚¨åˆ° Pinecone
   â””── æ›´æ–°çŠ¶æ€ä¸º"完成"或"失败"
```
### 6.2 çŸ¥è¯†åº“问答流程
```
1. ç”¨æˆ·é€‰æ‹©çŸ¥è¯†åº“,输入问题
2. å‰ç«¯è°ƒç”¨ /ai/knowledge/chat(流式接口)
3. åŽç«¯å¤„理:
   â”œâ”€â”€ æž„建命名空间:kb-{knowledgeBaseId}
   â”œâ”€â”€ è°ƒç”¨ Embedding æ¨¡åž‹ç”Ÿæˆé—®é¢˜å‘量
   â”œâ”€â”€ ä»Ž Pinecone æ£€ç´¢ç›¸å…³å†…容(minScore=0.7, maxResults=5)
   â”œâ”€â”€ æž„建上下文 Prompt
   â”œâ”€â”€ è°ƒç”¨ LLM ç”Ÿæˆå›žç­”
   â””── æµå¼è¿”回结果
```
---
## ä¸ƒã€æ³¨æ„äº‹é¡¹
1. **Pinecone å‘½åç©ºé—´**:不能使用 `__default__`,必须使用自定义命名空间
2. **文件编码**:自动检测 UTF-8/GBK,避免乱码
3. **切片策略**:
   - æ–‡ä»¶ > 80MB æˆ–内容 > 8000 å­—符时才切片
   - åˆ‡ç‰‡å¤§å° 500 å­—符,重叠 100 å­—符
   - ä¼˜å…ˆåœ¨å¥å­è¾¹ç•Œåˆ‡åˆ†
4. **Embedding é™åˆ¶**:阿里云 DashScope é™åˆ¶å•次输入最大 8192 å­—符
5. **向量删除**:使用 Pinecone åŽŸç”Ÿå®¢æˆ·ç«¯ï¼Œé€šè¿‡ metadata filter åˆ é™¤
6. **异步处理**:向量化使用 `@Async` å¼‚步执行,避免阻塞接口
---
## å…«ã€æ–‡ä»¶æ¸…单
### åŽç«¯æ–‡ä»¶
```
src/main/java/com/ruoyi/
├── approve/
│   â”œâ”€â”€ controller/
│   â”‚   â””── KnowledgeBaseController.java
│   â”œâ”€â”€ pojo/
│   â”‚   â”œâ”€â”€ KnowledgeBase.java
│   â”‚   â””── KnowledgeBaseVector.java
│   â”œâ”€â”€ service/
│   â”‚   â”œâ”€â”€ KnowledgeBaseService.java
│   â”‚   â”œâ”€â”€ KnowledgeBaseVectorService.java
│   â”‚   â””── impl/
│   â”‚       â”œâ”€â”€ KnowledgeBaseServiceImpl.java
│   â”‚       â””── KnowledgeBaseVectorServiceImpl.java
│   â”œâ”€â”€ mapper/
│   â”‚   â”œâ”€â”€ KnowledgeBaseMapper.java
│   â”‚   â””── KnowledgeBaseVectorMapper.java
│   â””── dto/
│       â””── KnowledgeBaseVectorVO.java
└── ai/
    â”œâ”€â”€ config/
    â”‚   â”œâ”€â”€ EmbeddingStoreConfig.java
    â”‚   â””── XiaozhiAgentConfig.java
    â”œâ”€â”€ controller/
    â”‚   â””── KnowledgeChatController.java
    â”œâ”€â”€ assistant/
    â”‚   â””── KnowledgeChatAgent.java
    â”œâ”€â”€ service/
    â”‚   â”œâ”€â”€ KnowledgeRagService.java
    â”‚   â””── impl/
    â”‚       â””── KnowledgeRagServiceImpl.java
    â””── dto/
        â””── KnowledgeChatRequest.java
```
### å‰ç«¯æ–‡ä»¶
```
src/views/knowledge/
├── index.vue              # çŸ¥è¯†åº“列表
├── form.vue               # æ–°å¢ž/编辑表单
├── files.vue              # æ–‡ä»¶ç®¡ç†
└── chat.vue               # çŸ¥è¯†åº“问答
src/api/knowledge.js       # API封装
```
docs/mock_data_check.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,183 @@
# æ•°æ®æ¨¡æ‹Ÿ - åŸºç¡€æ•°æ®æ£€æµ‹
## æ¶‰åŠé¡µé¢
- æ•°æ®æ¨¡æ‹Ÿæ“ä½œé¡µé¢ï¼ˆæ–°å¢žï¼‰
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mock/dataCheck | æ£€æµ‹æŒ‡å®šæ¨¡å—的基础数据是否就绪 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| modules | List\<String\> | æ˜¯ | è¦æ£€æµ‹çš„æ¨¡å—列表,可选值:sales(销售)、purchase(采购)、quality(质量)、production(生产) |
请求体示例:
```json
{ "modules": ["sales", "purchase", "quality", "production"] }
```
**响应:**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "totalItems": 6,
    "passedItems": 3,
    "items": [
      {
        "module": "common",
        "itemName": "产品数据",
        "minRequired": 1,
        "currentCount": 5,
        "passed": true,
        "message": "通过"
      },
      {
        "module": "sales",
        "itemName": "客户数据",
        "minRequired": 1,
        "currentCount": 0,
        "passed": false,
        "message": "缺少客户数据,请先在【基础数据-客户管理】中添加至少1条客户"
      }
    ]
  }
}
```
## æ£€æµ‹è§„则
### é”€å”®æ¨¡å— (sales)
| æ£€æµ‹é¡¹ | æœ€ä½Žæ•°é‡ | æœªé€šè¿‡æç¤º |
|--------|----------|------------|
| äº§å“æ•°æ® | 1 | ç¼ºå°‘产品数据,请先在【基础数据-产品管理】中添加至少1条产品 |
| å®¢æˆ·æ•°æ® | 1 | ç¼ºå°‘客户数据,请先在【基础数据-客户管理】中添加至少1条客户 |
| æŠ¥ä»·å®¡æ‰¹æ¨¡æ¿ | 1 | ç¼ºå°‘报价审批模板,请先在【系统管理-审批模板】中创建报价审批模板 |
| å‘货审批模板 | 1 | ç¼ºå°‘发货审批模板,请先在【系统管理-审批模板】中创建发货审批模板 |
### é‡‡è´­æ¨¡å— (purchase)
| æ£€æµ‹é¡¹ | æœ€ä½Žæ•°é‡ | æœªé€šè¿‡æç¤º |
|--------|----------|------------|
| äº§å“æ•°æ® | 1 | ç¼ºå°‘产品数据,请先在【基础数据-产品管理】中添加至少1条产品 |
| ä¾›åº”商数据 | 1 | ç¼ºå°‘供应商数据,请先在【基础数据-供应商管理】中添加至少1条供应商 |
| é‡‡è´­å®¡æ‰¹æ¨¡æ¿ | 1 | ç¼ºå°‘采购审批模板,请先在【系统管理-审批模板】中创建采购审批模板 |
### ç”Ÿäº§æ¨¡å— (production)
| æ£€æµ‹é¡¹ | æœ€ä½Žæ•°é‡ | æœªé€šè¿‡æç¤º |
|--------|----------|------------|
| äº§å“æ•°æ® | 1 | ç¼ºå°‘产品数据,请先在【基础数据-产品管理】中添加至少1条产品 |
| äº§å“è§„æ ¼ | 1 | ç¼ºå°‘产品规格,请先在【基础数据-产品管理】中为产品添加规格型号 |
| å·¥åº | 1 | ç¼ºå°‘工序,请先在【工艺设计-工序管理】中创建工序 |
| BOM | 1 | ç¼ºå°‘BOM,请先在【工艺设计-BOM管理】中创建BOM |
| BOM产品结构 | 1 | ç¼ºå°‘BOM产品结构,请先在【工艺设计-BOM管理】中为BOM添加产品结构节点 |
| å·¥è‰ºè·¯çº¿ | 1 | ç¼ºå°‘工艺路线,请先在【工艺设计-工艺路线】中创建工艺路线 |
| å·¥è‰ºè·¯çº¿å·¥åº | 1 | ç¼ºå°‘工艺路线工序,请先在【工艺设计-工艺路线】中为工艺路线添加工序 |
| äº§å“-工艺路线关联 | â‰¥1 | éƒ¨åˆ†äº§å“è§„格未关联工艺路线,请先在【工艺设计-工艺路线】中为产品规格创建工艺路线 |
| å·¥è‰ºè·¯çº¿-工序关联 | â‰¥1 | éƒ¨åˆ†å·¥è‰ºè·¯çº¿æœªæ·»åŠ å·¥åºï¼Œè¯·å…ˆåœ¨ã€å·¥è‰ºè®¾è®¡-工艺路线】中为工艺路线添加工序 |
| BOM-产品结构关联 | â‰¥1 | éƒ¨åˆ†BOM未添加产品结构,请先在【工艺设计-BOM管理】中为BOM添加产品结构节点 |
### è´¨é‡æ¨¡å— (quality)
| æ£€æµ‹é¡¹ | æœ€ä½Žæ•°é‡ | æœªé€šè¿‡æç¤º |
|--------|----------|------------|
| äº§å“æ•°æ® | 1 | ç¼ºå°‘产品数据,请先在【基础数据-产品管理】中添加至少1条产品 |
| æ£€æµ‹æ ‡å‡† | 1 | ç¼ºå°‘检测标准,请先在【质量管理-检测标准】中创建检测标准 |
| æŒ‡æ ‡ç»‘定 | 1 | ç¼ºå°‘指标绑定,请先在【质量管理-检测标准绑定】中将检测标准与产品绑定 |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æ•°æ®æ¨¡æ‹Ÿé¡µé¢å…¥å£
在左侧菜单新增"数据模拟"菜单项,路由 `/mock`。
### 2. æ•°æ®æ£€æµ‹åŒºåŸŸ
```html
<template>
  <div class="mock-container">
    <el-card header="基础数据检测">
      <el-checkbox-group v-model="selectedModules">
        <el-checkbox label="sales">销售模块</el-checkbox>
        <el-checkbox label="purchase">采购模块</el-checkbox>
        <el-checkbox label="quality">质量模块</el-checkbox>
        <el-checkbox label="production">生产模块</el-checkbox>
      </el-checkbox-group>
      <el-button type="primary" @click="handleCheck" :loading="checking">
        å¼€å§‹æ£€æµ‹
      </el-button>
    </el-card>
    <el-card v-if="checkResult" header="检测结果" style="margin-top: 16px">
      <el-alert
        :title="`通过 ${checkResult.passedItems} / ${checkResult.totalItems} é¡¹`"
        :type="checkResult.passedItems === checkResult.totalItems ? 'success' : 'warning'"
        :closable="false"
        show-icon
      />
      <el-table :data="checkResult.items" style="margin-top: 12px">
        <el-table-column prop="module" label="模块" width="100" />
        <el-table-column prop="itemName" label="检测项" width="160" />
        <el-table-column prop="minRequired" label="最低要求" width="80" />
        <el-table-column prop="currentCount" label="当前数量" width="80" />
        <el-table-column label="状态" width="80">
          <template #default="{ row }">
            <el-tag :type="row.passed ? 'success' : 'danger'">
              {{ row.passed ? '通过' : '未通过' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="message" label="提示" />
      </el-table>
    </el-card>
  </div>
</template>
```
### 3. data æ•°æ®
```js
data() {
  return {
    selectedModules: ['sales', 'purchase', 'quality', 'production'],
    checking: false,
    checkResult: null,
  }
}
```
### 4. æ–¹æ³•
```js
import request from '@/utils/request'
methods: {
  async handleCheck() {
    if (this.selectedModules.length === 0) {
      this.$message.warning('请至少选择一个模块')
      return
    }
    this.checking = true
    try {
      const res = await request.post('/mock/dataCheck', {
        modules: this.selectedModules
      })
      this.checkResult = res.data
    } finally {
      this.checking = false
    }
  }
}
```
## æ³¨æ„äº‹é¡¹
- äº§å“æ•°æ®ï¼ˆå…¬å…±åŸºç¡€ï¼‰åœ¨å¤šä¸ªæ¨¡å—间共享,检测结果中只出现一次,不会重复展示
- æ£€æµ‹ä»…做只读查询,不写入任何数据
- æ¨¡å—参数为空数组时返回空检测列表
- å»ºè®®åœ¨æ•°æ®æ¨¡æ‹Ÿå¼€å§‹å‰å…ˆè°ƒç”¨æ­¤æŽ¥å£ç¡®è®¤åŸºç¡€æ•°æ®å°±ç»ª
- ç”Ÿäº§æ¨¡å—除了检测数据量,还会检测关联性(产品-工艺路线、工艺路线-工序、BOM-产品结构),确保工艺设计数据完整
docs/mock_data_generate.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,243 @@
# æ•°æ®æ¨¡æ‹Ÿ - AI æ•°æ®ç”Ÿæˆ
## æ¶‰åŠé¡µé¢
- æ•°æ®æ¨¡æ‹Ÿæ“ä½œé¡µé¢ï¼ˆæ–°å¢ž/修改)
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mock/generate | AI ç”Ÿæˆæ¨¡æ‹Ÿæ•°æ®ï¼ˆå«å‰ç½®åŸºç¡€æ•°æ®æ£€æµ‹ï¼‰ |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| modules | List\<String\> | æ˜¯ | è¦ç”Ÿæˆæ•°æ®çš„æ¨¡å—:sales(销售)、purchase(采购)、quality(质量)、production(生产)、stock(库存) |
| industries | List\<String\> | æ˜¯ | è¡Œä¸šï¼Œå¦‚:["机械制造", "食品加工", "电子装配"] |
| countMin | int | å¦ | æ¯ä¸ªå®žä½“生成条数最小值,默认 3 |
| countMax | int | å¦ | æ¯ä¸ªå®žä½“生成条数最大值,默认 10 |
| dateStart | String | å¦ | æ—¶é—´èŒƒå›´å¼€å§‹ï¼ˆyyyy-MM-dd) |
| dateEnd | String | å¦ | æ—¶é—´èŒƒå›´ç»“束(yyyy-MM-dd) |
| additionalInfo | String | å¦ | å…¶ä»–补充描述,如"产品以金属零部件为主" |
请求体示例:
```json
{
  "modules": ["sales", "purchase", "quality"],
  "industries": ["机械制造", "电子装配"],
  "countMin": 3,
  "countMax": 8,
  "dateStart": "2026-01-01",
  "dateEnd": "2026-06-01",
  "additionalInfo": "产品以金属零部件和电子元器件为主"
}
```
**响应(成功):**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "status": "SUCCESS",
    "totalGenerated": 24,
    "moduleSummaries": [
      { "module": "sales", "entityName": "客户", "generatedCount": 5, "successCount": 5, "failCount": 0 },
      { "module": "sales", "entityName": "销售台账", "generatedCount": 5, "successCount": 5, "failCount": 0 },
      { "module": "purchase", "entityName": "供应商", "generatedCount": 4, "successCount": 4, "failCount": 0 },
      { "module": "purchase", "entityName": "采购台账", "generatedCount": 4, "successCount": 4, "failCount": 0 },
      { "module": "quality", "entityName": "检测标准", "generatedCount": 3, "successCount": 3, "failCount": 0 },
      { "module": "quality", "entityName": "指标绑定", "generatedCount": 3, "successCount": 3, "failCount": 0 }
    ],
    "errors": [],
    "checkResult": null
  }
}
```
**响应(基础数据不足,前置检测未通过):**
```json
{
  "code": 500,
  "msg": "基础数据不足,请先补充后再生成",
  "data": {
    "status": "CHECK_FAILED",
    "checkResult": {
      "totalItems": 6,
      "passedItems": 3,
      "items": [
        { "module": "common", "itemName": "产品数据", "passed": true, "message": "通过" },
        { "module": "sales", "itemName": "客户数据", "passed": false, "message": "缺少客户数据,请先..." }
      ]
    }
  }
}
```
## å·¥ä½œæµç¨‹
1. ç”¨æˆ·åœ¨é¡µé¢é€‰æ‹©æ¨¡å— + å¡«å†™è¡Œä¸š/数量/时间等信息
2. è°ƒç”¨ `POST /mock/generate`
3. åŽç«¯é¦–先调用 `POST /mock/dataCheck` æ£€æµ‹åŸºç¡€æ•°æ®æ˜¯å¦å°±ç»ª
4. è‹¥åŸºç¡€æ•°æ®ä¸è¶³ï¼Œè¿”回检测结果,提示用户先补齐
5. è‹¥åŸºç¡€æ•°æ®å°±ç»ªï¼Œè°ƒç”¨ AI å¤§æ¨¡åž‹ç”Ÿæˆç¬¦åˆè¡Œä¸šç‰¹å¾çš„ JSON æ•°æ®
6. æŒ‰ä¾èµ–顺序创建数据(客户→销售台账、供应商→采购台账、检测标准→指标绑定...)
7. è¿”回生成摘要
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æ•°æ®ç”ŸæˆåŒºåŸŸï¼ˆåœ¨æ•°æ®æ£€æµ‹åŒºåŸŸä¸‹æ–¹ï¼‰
```html
<el-card header="AI æ•°æ®ç”Ÿæˆ" style="margin-top: 16px">
  <el-form :model="generateForm" label-width="100px">
    <el-form-item label="选择模块">
      <el-checkbox-group v-model="generateForm.modules">
        <el-checkbox label="sales">销售</el-checkbox>
        <el-checkbox label="purchase">采购</el-checkbox>
        <el-checkbox label="quality">质量</el-checkbox>
        <el-checkbox label="production">生产</el-checkbox>
        <el-checkbox label="stock">库存</el-checkbox>
      </el-checkbox-group>
    </el-form-item>
    <el-form-item label="行业">
      <el-select
        v-model="generateForm.industries"
        multiple
        filterable
        allow-create
        placeholder="输入或选择行业"
      >
        <el-option label="机械制造" value="机械制造" />
        <el-option label="食品加工" value="食品加工" />
        <el-option label="电子装配" value="电子装配" />
        <el-option label="汽车零部件" value="汽车零部件" />
        <el-option label="医疗器械" value="医疗器械" />
        <el-option label="化工材料" value="化工材料" />
        <el-option label="纺织服装" value="纺织服装" />
        <el-option label="家具制造" value="家具制造" />
      </el-select>
    </el-form-item>
    <el-row :gutter="16">
      <el-col :span="12">
        <el-form-item label="生成条数">
          <el-slider
            v-model="generateForm.countRange"
            range
            :min="1"
            :max="50"
            :marks="{1:'1', 10:'10', 20:'20', 50:'50'}"
          />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="时间范围">
          <el-date-picker
            v-model="generateForm.dateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="yyyy-MM-dd"
          />
        </el-form-item>
      </el-col>
    </el-row>
    <el-form-item label="补充信息">
      <el-input
        v-model="generateForm.additionalInfo"
        type="textarea"
        :rows="2"
        placeholder="如:产品以金属零部件为主,客户集中在华东地区"
      />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="handleGenerate" :loading="generating">
        å¼€å§‹ç”Ÿæˆ
      </el-button>
      <el-button @click="handleCheckFirst" :loading="checking">
        å…ˆæ£€æµ‹å†ç”Ÿæˆ
      </el-button>
    </el-form-item>
  </el-form>
</el-card>
```
### 3. data æ•°æ®
```js
data() {
  return {
    generateForm: {
      modules: [],
      industries: [],
      countRange: [3, 10],
      dateRange: [],
      additionalInfo: '',
    },
    generating: false,
    generateResult: null,
  }
}
```
### 4. æ–¹æ³•
```js
methods: {
  // ç›´æŽ¥ç”Ÿæˆï¼ˆå†…部自动检测)
  async handleGenerate() {
    if (this.generateForm.modules.length === 0) {
      this.$message.warning('请至少选择一个模块')
      return
    }
    this.generating = true
    try {
      const res = await request.post('/mock/generate', {
        modules: this.generateForm.modules,
        industries: this.generateForm.industries,
        countMin: this.generateForm.countRange[0],
        countMax: this.generateForm.countRange[1],
        dateStart: this.generateForm.dateRange[0] || null,
        dateEnd: this.generateForm.dateRange[1] || null,
        additionalInfo: this.generateForm.additionalInfo,
      })
      if (res.code === 200) {
        this.generateResult = res.data
        this.$message.success(`成功生成 ${res.data.totalGenerated} æ¡æ•°æ®`)
      } else {
        // åŸºç¡€æ•°æ®ä¸è¶³
        this.generateResult = res.data
        this.checkResult = res.data.checkResult
        this.$message.warning(res.msg)
      }
    } finally {
      this.generating = false
    }
  },
  // å…ˆæ£€æµ‹å†ç”Ÿæˆ
  async handleCheckFirst() {
    await this.handleCheck()
    if (this.checkResult && this.checkResult.passedItems === this.checkResult.totalItems) {
      await this.handleGenerate()
    } else {
      this.$message.warning('请先补齐基础数据后再生成')
    }
  },
}
```
## æ³¨æ„äº‹é¡¹
- `POST /mock/generate` å†…部会自动调用数据检测,不需要手动分两步走
- AI ç”Ÿæˆçš„æ•°æ®ä¼šå°½é‡ç¬¦åˆè¡Œä¸šç‰¹å¾ï¼Œä½†å»ºè®®ç”ŸæˆåŽæ£€æŸ¥å…³é”®æ•°æ®çš„合理性
- ç”Ÿäº§æ¨¡å—和库存模块依赖产品数据(Product/ProductModel),请确保基础数据中有产品
- é”€å”®å°è´¦çš„合同金额由产品明细自动计算,AI åªéœ€ç”Ÿæˆäº§å“è¡Œçš„单价和数量
- äº§ç”Ÿçš„æ•°æ®é€šè¿‡æœåŠ¡å±‚åˆ›å»ºï¼Œä¼šè‡ªåŠ¨å¡«å…… tenantId/deptId/createUser ç­‰å­—段
docs/routing_operation_plan_person.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
# ç”Ÿäº§å·¥åºæ–°å¢žè®¡åˆ’人员和计划执行人员
## æ¶‰åŠé¡µé¢
- å·¥è‰ºè·¯çº¿å·¥åºç®¡ç†é¡µé¢ï¼ˆtechnology_routing_operation)
## API
涉及以下接口的请求/响应新增字段:
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /technologyRoutingOperation/page | åˆ†é¡µæŸ¥è¯¢ |
| GET | /technologyRoutingOperation/list | åˆ—表查询 |
| GET | /technologyRoutingOperation/{id} | è¯¦æƒ… |
| POST | /technologyRoutingOperation/add | æ–°å¢ž |
| PUT | /technologyRoutingOperation | ä¿®æ”¹ |
**新增响应/请求字段:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| planPerson | Long | å¦ | è®¡åˆ’人员ID |
| executor | Long | å¦ | è®¡åˆ’执行人员ID |
| planPersonName | String | å¦ | è®¡åˆ’人员姓名(响应字段,后端自动关联查询) |
| executorName | String | å¦ | è®¡åˆ’执行人员姓名(响应字段,后端自动关联查询) |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. è¡¨å•新增字段
在工序表单中新增两个人员选择下拉框:
```html
<el-form-item label="计划人员" prop="planPerson">
  <el-select v-model="processForm.planPerson"
             placeholder="请选择计划人员"
             clearable
             filterable
             style="width: 100%">
    <el-option v-for="item in employeeOptions"
               :key="item.id"
               :label="item.staffName"
               :value="item.id" />
  </el-select>
</el-form-item>
<el-form-item label="计划执行人员" prop="executor">
  <el-select v-model="processForm.executor"
             placeholder="请选择计划执行人员"
             clearable
             filterable
             style="width: 100%">
    <el-option v-for="item in employeeOptions"
               :key="item.id"
               :label="item.staffName"
               :value="item.id" />
  </el-select>
</el-form-item>
```
### 2. data æ•°æ®
```js
data() {
  return {
    processForm: {
      planPerson: null,
      executor: null,
      // ...其他字段
    },
    employeeOptions: [],  // å‘˜å·¥åˆ—表,需从接口获取
  }
}
```
### 3. è¡¨æ ¼åˆ—新增
列表中可直接使用后端返回的姓名展示:
```html
<el-table-column label="计划人员" prop="planPersonName" />
<el-table-column label="计划执行人员" prop="executorName" />
```
## æ³¨æ„äº‹é¡¹
- `planPerson` å’Œ `executor` å­˜å‚¨çš„æ˜¯å‘˜å·¥ID(Long类型),提交表单时传ID
- å“åº”中 `planPersonName` å’Œ `executorName` ç”±åŽç«¯é€šè¿‡ `staff_on_job` è¡¨è‡ªåŠ¨å…³è”æŸ¥è¯¢ï¼Œå‰ç«¯æ— éœ€é¢å¤–è½¬æ¢
- å‘˜å·¥ä¸‹æ‹‰åˆ—表数据需从员工接口获取(参考现有 `employeeOptions` æ•°æ®æºï¼‰
- æ•°æ®åº“迁移SQL:`doc/20260615_add_plan_person_and_executor_to_routing_operation.sql`
- ç”Ÿäº§è®¢å•工序(`production_order_routing_operation`)同步支持这两个字段
sql/product-inventory-management-new-pro.sql
¶Ô±ÈÐÂÎļþ
ÎļþÌ«´ó
src/main/java/com/ruoyi/CodeGenerator.java
@@ -20,11 +20,11 @@
// æ¼”示例子,执行 main æ–¹æ³•控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {
    public static String database_url = "jdbc:mysql://localhost:3300/product-inventory-management-new-pro";
    public static String database_url = "jdbc:mysql://localhost:3306/product-inventory-management-new-pro";
    public static String database_username = "root";
    public static String database_password= "root";
    public static String database_password= "123456";
    public static String author = "芯导软件(江苏)有限公司";
    public static String model = "sales"; // æ¨¡å—
    public static String model = "account"; // æ¨¡å—
    public static String setParent = "com.ruoyi."+ model; // åŒ…路径
    public static String tablePrefix = ""; // è®¾ç½®è¿‡æ»¤è¡¨å‰ç¼€
    public static void main(String[] args) {
src/main/java/com/ruoyi/account/bean/dto/AccountReportDto.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.time.LocalDate;
@Data
@Schema(name = "AccountReportDto", description = "财务报表--日期参数")
public class AccountReportDto {
    /**
     * å¼€å§‹æ—¶é—´
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate entryDateStart;
    /**
     * ç»“束时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate entryDateEnd;
}
src/main/java/com/ruoyi/account/bean/dto/DeviceTypeDetail.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package com.ruoyi.account.bean.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
 * @author :yys
 * @date : 2026/1/17 13:42
 */
@Data
public class DeviceTypeDetail {
    private String type;       // è®¾å¤‡ç±»åž‹
    private BigDecimal count;  // æ•°é‡
    private BigDecimal amount; // ä»·æ ¼
}
src/main/java/com/ruoyi/account/bean/dto/DeviceTypeDistributionVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package com.ruoyi.account.bean.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
 * @author :yys
 * @date : 2026/1/17 13:19
 */
@Data
public class DeviceTypeDistributionVO {
    private Integer totalCount = 0; // è®¾å¤‡ç±»åž‹æ€»æ•°
    // é¥¼å›¾å’ŒæŠ˜çº¿å›¾çš„分类(设备类型)
    private List<String> categories = new ArrayList<>();
    // å„类型设备的数量
    private List<BigDecimal> countData = new ArrayList<>();
    // å„类型设备的总金额
    private List<BigDecimal> amountData = new ArrayList<>();
    // æ˜Žç»†åˆ—表,包含类型、数量、金额
    private List<DeviceTypeDetail> details = new ArrayList<>();
}
src/main/java/com/ruoyi/account/bean/dto/SalesReceiptReturnDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
package com.ruoyi.account.bean.dto;
import com.ruoyi.account.pojo.SalesReceiptReturn;
import lombok.Data;
@Data
public class SalesReceiptReturnDto extends SalesReceiptReturn {
}
src/main/java/com/ruoyi/account/bean/dto/SalesRefundAmountOrderDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.account.bean.dto;
import com.ruoyi.account.pojo.SalesRefundAmountOrder;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class SalesRefundAmountOrderDto extends SalesRefundAmountOrder {
    @Schema(description = "退货单号")
    private String returnManagementNo;
    @Schema(description = "客户名称")
    private String customerName;
    @Schema(description = "销售单号")
    private String salesContractNo;
    @Schema(description = "创建人名称")
    private String createUserName;
}
src/main/java/com/ruoyi/account/bean/dto/StatementAccountDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
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.time.LocalDate;
@Data
@Schema(name = "StatementAccountDto", description = "财务管理--生成对账单(传参)")
public class StatementAccountDto {
    //业务类型(1应收对账;2应付对账)
    @Schema(name = "accountType", description = "业务类型(1应收对账;2应付对账)")
    private Integer accountType;
    //选择的客户(应收是客户,应付是供应商supplierId)
    @Schema(name = "customerId", description = "客户ID")
    private Long customerId;
    //对账月份yyyy-MM
    @Schema(name = "statementMonth", description = "对账月份")
    private String statementMonth;
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
package com.ruoyi.account.bean.dto.financial;
import com.ruoyi.account.pojo.financial.AccountSubject;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class AccountSubjectDto extends AccountSubject {
}
src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectImportDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package com.ruoyi.account.bean.dto.financial;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class AccountSubjectImportDto {
    @Schema(description = "科目编码")
    @Excel(name = "科目编码")
    private String subjectCode;
    @Schema(description = "科目名称")
    @Excel(name = "科目名称")
    private String subjectName;
    @Schema(description = "科目类型")
    @Excel(name = "科目类型")
    private String subjectType;
    @Schema(description = "余额方向")
    @Excel(name = "余额方向")
    private String balanceDirection;
    /**
     * çŠ¶æ€ 0启用 1禁用
     */
    @Schema(description = "状态")
    @Excel(name = "状态",readConverterExp = "0=启用,1=禁用")
    private Integer status;
    @Schema(description = "备注")
    @Excel(name = "备注")
    private String remark;
}
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/dto/purchase/AccountPaymentApplicationDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.account.bean.dto.purchase;
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.time.LocalDate;
@Data
@Schema(name = "AccountPaymentApplicationDto", description = "财务管理--付款申请台账(传参)")
public class AccountPaymentApplicationDto {
    @Schema(description = "供应商ID")
    private Integer supplierId;
    @Schema(description = "申请单号")
    private String invoiceApplicationNo;
    @Schema(description = "审核状态:0待审核1审核通过2审核不通过")
    private Integer status;
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/dto/purchase/AccountPurchaseInvoiceDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package com.ruoyi.account.bean.dto.purchase;
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.time.LocalDate;
@Data
@Schema(name = "AccountPurchaseInvoiceDto", description = "财务管理--进项发票台账(传参)")
public class AccountPurchaseInvoiceDto {
    @Schema(description = "供应商ID")
    private Integer supplierId;
    @Schema(description = "发票号码")
    private String invoiceNumber;
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
    @Schema(description = "状态")
    private Integer status;
}
src/main/java/com/ruoyi/account/bean/dto/purchase/AccountPurchasePaymentDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.account.bean.dto.purchase;
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.time.LocalDate;
@Data
@Schema(name = "AccountPurchasePaymentDto", description = "财务管理--付款单台账(传参)")
public class AccountPurchasePaymentDto {
    @Schema(description = "供应商ID")
    private Integer supplierId;
    @Schema(description = "付款单号")
    private String paymentNumber;
    @Schema(description = "付款方式")
    private String paymentMethod;
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseInboundDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.account.bean.dto.purchase;
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.time.LocalDate;
@Data
@Schema(name = "PurchaseInboundDto", description = "财务管理--采购入库台账(传参)")
public class PurchaseInboundDto {
    @Schema(description = "入库单号")
    private String inboundBatches;
    private Long supplierId;
    @Schema(description = "供应商")
    private String supplierName;
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseReturnDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.account.bean.dto.purchase;
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.time.LocalDate;
@Data
@Schema(name = "PurchaseReturnDto", description = "财务管理--采购退货台账(传参)")
public class PurchaseReturnDto {
    @Schema(description = "退货单号")
    private String returnNo;
    private Long supplierId;
    @Schema(description = "供应商")
    private String supplierName;
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/dto/sales/AccountInvoiceApplicationDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.account.bean.dto.sales;
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.time.LocalDate;
@Data
@Schema(name = "AccountInvoiceApplicationDto", description = "财务管理--开票申请台账(传参)")
public class AccountInvoiceApplicationDto  {
    @Schema(description = "客户ID")
    private Integer customerId;
    @Schema(description = "申请单号")
    private String invoiceApplicationNo;
    @Schema(description = "审核状态:0待审核1审核通过2审核不通过")
    private Integer status;
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/dto/sales/AccountSalesCollectionDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.account.bean.dto.sales;
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.time.LocalDate;
@Data
@Schema(name = "AccountSalesCollectionDto", description = "财务管理--收款单台账(传参)")
public class AccountSalesCollectionDto {
    @Schema(description = "客户ID")
    private Integer customerId;
    @Schema(description = "收款单号")
    private String collectionNumber;
    @Schema(description = "收款方式")
    private String collectionMethod;
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/dto/sales/AccountSalesInvoiceDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.account.bean.dto.sales;
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.time.LocalDate;
@Data
@Schema(name = "AccountSalesInvoiceDto", description = "财务管理--销项发票台账(传参)")
public class AccountSalesInvoiceDto {
    @Schema(description = "客户ID")
    private Integer customerId;
    @Schema(description = "发票号码")
    private String invoiceNumber;
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
    @Schema(description = "状态")
    private Integer status;
}
src/main/java/com/ruoyi/account/bean/dto/sales/SalesOutboundDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.account.bean.dto.sales;
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.time.LocalDate;
@Data
@Schema(name = "SalesOutboundDto", description = "财务管理--销售出库台账(传参)")
public class SalesOutboundDto {
    @Schema(description = "出库单号")
    private String outboundBatches;
    private Long customerId;
    @Schema(description = "客户名称")
    private String customerName;
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/dto/sales/SalesReturnDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.account.bean.dto.sales;
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.time.LocalDate;
@Data
@Schema(name = "SalesReturnDto", description = "财务管理--销售退货台账(传参)")
public class SalesReturnDto {
    @Schema(description = "退货单号")
    private String returnNo;
    private Long customerId;
    @Schema(description = "客户名称")
    private String customerName;
    @Schema(description = "开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/vo/AccountReportVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package com.ruoyi.account.bean.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
@Schema(name = "AccountReportVo", description = "财务报表--返回参数")
public class AccountReportVo {
    @Schema(description = "总营收")
    private BigDecimal totalIncome;
    @Schema(description = "总支出")
    private BigDecimal totalExpense;
    @Schema(description = "应收账款")
    private BigDecimal accountsReceivable;
    @Schema(description = "应付账款")
    private BigDecimal accountsPayable;
    @Schema(description = "净收入")
    private BigDecimal netRevenue;
    // --- æŠ˜çº¿å›¾ï¼šæœˆåº¦è¶‹åŠ¿æ•°æ® ---
    @Schema(description = "月度趋势数据列表")
    private List<MonthlyTrendVO> monthlyTrendList;
    // --- æŸ±çŠ¶å›¾ï¼šåº”æ”¶åº”ä»˜æœˆåº¦æ•°æ® ---
    @Schema(description = "应收应付月度数据列表")
    private List<ReceivablePayableVO> receivablePayableList;
    @Data
    @Schema(description = "月度趋势VO(折线图用)")
    public static class MonthlyTrendVO {
        @Schema(description = "月份,格式:yyyy-MM")
        private String month;
        @Schema(description = "当月营收")
        private BigDecimal income;
        @Schema(description = "当月支出")
        private BigDecimal expense;
        @Schema(description = "当月净利润")
        private BigDecimal profit;
    }
    @Data
    @Schema(description = "应收应付月度VO(柱状图用)")
    public static class ReceivablePayableVO {
        @Schema(description = "月份,格式:yyyy-MM")
        private String month;
        @Schema(description = "应收账款金额")
        private BigDecimal receivable;
        @Schema(description = "应付账款金额")
        private BigDecimal payable;
    }
}
src/main/java/com/ruoyi/account/bean/vo/StatementAccountVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.ruoyi.account.bean.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.account.pojo.AccountStatementDetails;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(name = "StatementAccountVo", description = "财务管理--对账单详情(返回)")
@ExcelIgnoreUnannotated
public class StatementAccountVo extends AccountStatement {
    //客户名称(应收是客户,应付是供应商)
    @Schema(description = "客户名称")
    @Excel(name = "客户名称")
    private String customerName;
    //对账明细
    @Schema(description = "对账明细")
    private List<AccountStatementDetails> accountStatementDetails;
}
src/main/java/com/ruoyi/account/bean/vo/financial/AccountSubjectVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.account.bean.vo.financial;
import com.ruoyi.account.pojo.financial.AccountSubject;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class AccountSubjectVo extends AccountSubject {
    /**
     * å­ç§‘目列表(递归结构)。
     */
    private List<AccountSubjectVo> children = new ArrayList<>();
    /**
     * æ˜¯å¦å¶å­èŠ‚ç‚¹ã€‚
     */
    private Boolean leaf;
}
src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerEntryRecordVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package com.ruoyi.account.bean.vo.financial;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * ç§‘目账基础分录查询对象(SQL映射使用)。
 */
@Data
public class FinLedgerEntryRecordVo {
    /**
     * å‡­è¯æ—¥æœŸã€‚
     */
    private LocalDate voucherDate;
    /**
     * å‡­è¯å­—号。
     */
    private String voucherNo;
    /**
     * æ‘˜è¦ã€‚
     */
    private String summary;
    /**
     * å€Ÿæ–¹é‡‘额。
     */
    private BigDecimal debit;
    /**
     * è´·æ–¹é‡‘额。
     */
    private BigDecimal credit;
    /**
     * è¡Œå·ï¼ˆæŽ’序字段)。
     */
    private Integer rowNo;
}
src/main/java/com/ruoyi/account/bean/vo/financial/FinLedgerRowVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package com.ruoyi.account.bean.vo.financial;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * ç§‘目账行数据返回对象。
 */
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class FinLedgerRowVo {
    /**
     * è¡Œç±»åž‹ï¼šopening/entry/monthly_total/yearly_total。
     */
    private String rowType;
    /**
     * æ—¥æœŸã€‚
     */
    private LocalDate date;
    /**
     * å‡­è¯å­—号。
     */
    private String voucherNo;
    /**
     * æ‘˜è¦ã€‚
     */
    private String summary;
    /**
     * å€Ÿæ–¹é‡‘额。
     */
    private BigDecimal debit;
    /**
     * è´·æ–¹é‡‘额。
     */
    private BigDecimal credit;
    /**
     * ä½™é¢æ–¹å‘:借/贷。
     */
    private String direction;
    /**
     * ä½™é¢ï¼ˆå€Ÿæ­£è´·è´Ÿï¼‰ã€‚
     */
    private BigDecimal balance;
}
src/main/java/com/ruoyi/account/bean/vo/financial/FinVoucherDetailVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.account.bean.vo.financial;
import com.ruoyi.account.pojo.financial.FinVoucher;
import com.ruoyi.account.pojo.financial.FinVoucherEntry;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.basic.dto.StorageBlobVO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
 * å‡­è¯è¯¦æƒ…返回对象。
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class FinVoucherDetailVo extends FinVoucher {
    /**
     * å‡­è¯åˆ†å½•列表。
     */
    private List<FinVoucherEntry> entries;
    private List<StorageBlobVO> storageBlobVOList;
}
src/main/java/com/ruoyi/account/bean/vo/purchase/AccountPaymentApplicationVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package com.ruoyi.account.bean.vo.purchase;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.ruoyi.account.pojo.purchase.AccountPaymentApplication;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AccountPaymentApplicationVo", description = "财务管理--付款申请台账(返回)")
@ExcelIgnoreUnannotated
public class AccountPaymentApplicationVo extends AccountPaymentApplication {
    @Schema(description = "供应商名称")
    @Excel(name = "供应商名称")
    private String supplierName;
    @Schema(description = "入库单号")
    @Excel(name = "入库单号")
    private String inboundBatches;
}
src/main/java/com/ruoyi/account/bean/vo/purchase/AccountPurchaseInvoiceVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.account.bean.vo.purchase;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.ruoyi.account.pojo.purchase.AccountPurchaseInvoice;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AccountPurchaseInvoiceVo", description = "财务管理--进项发票台账(返回)")
@ExcelIgnoreUnannotated
public class AccountPurchaseInvoiceVo extends AccountPurchaseInvoice {
    @Schema(description = "供应商名称")
    @Excel(name = "供应商名称")
    private String supplierName;
}
src/main/java/com/ruoyi/account/bean/vo/purchase/AccountPurchasePaymentVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.account.bean.vo.purchase;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AccountPurchasePaymentVo", description = "财务管理--付款单台账(返回)")
@ExcelIgnoreUnannotated
public class AccountPurchasePaymentVo extends AccountPurchasePayment {
    @Schema(description = "供应商名称")
    @Excel(name = "供应商名称")
    private String supplierName;
    @Schema(description = "付款申请单号")
    @Excel(name = "付款申请单号")
    private String invoiceApplicationNo;
    @Schema(description = "开户行")
    @Excel(name = "开户行")
    private String bankAccountName;
    @Schema(description = "银行账号")
    @Excel(name = "银行账号")
    private String bankAccountNum;
    @Schema(description = "是否生成了对账单")
    private boolean isAccountStatemen;
}
src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseInboundVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package com.ruoyi.account.bean.vo.purchase;
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.LocalDate;
@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 LocalDate 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/purchase/PurchaseReturnVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
package com.ruoyi.account.bean.vo.purchase;
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 = "发货类型")
    private Integer shippingType;
    @Schema(description = "发货车牌号")
    @Excel(name = "发货车牌号")
    private String truckPlateNo;
    @Schema(description = "快递公司")
    @Excel(name = "快递公司")
    private String expressCompany;
    @Schema(description = "快递单号")
    @Excel(name = "快递单号")
    private String expressNo;
    @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/sales/AccountInvoiceApplicationVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package com.ruoyi.account.bean.vo.sales;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.ruoyi.account.pojo.sales.AccountInvoiceApplication;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AccountInvoiceApplicationVo", description = "财务管理--开票申请台账(返回)")
@ExcelIgnoreUnannotated
public class AccountInvoiceApplicationVo extends AccountInvoiceApplication {
    @Schema(description = "客户名称")
    @Excel(name = "客户名称")
    private String customerName;
    @Schema(description = "出库单号")
    @Excel(name = "出库单号")
    private String outboundBatches;
}
src/main/java/com/ruoyi/account/bean/vo/sales/AccountSalesCollectionVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.ruoyi.account.bean.vo.sales;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AccountSalesCollectionVo", description = "财务管理--收款单台账(返回)")
@ExcelIgnoreUnannotated
public class AccountSalesCollectionVo extends AccountSalesCollection {
    @Schema(description = "客户名称")
    @Excel(name = "客户名称")
    private String customerName;
    @Schema(description = "出库单号")
    @Excel(name = "出库单号")
    private String outboundBatches;
    @Schema(description = "是否生成了对账单")
    private boolean isAccountStatemen;
}
src/main/java/com/ruoyi/account/bean/vo/sales/AccountSalesInvoiceVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.account.bean.vo.sales;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.ruoyi.account.pojo.sales.AccountSalesInvoice;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(name = "AccountSalesInvoiceVo", description = "财务管理--销项发票台账(返回)")
@ExcelIgnoreUnannotated
public class AccountSalesInvoiceVo extends AccountSalesInvoice {
    @Schema(description = "客户名称")
    @Excel(name = "客户名称")
    private String customerName;
}
src/main/java/com/ruoyi/account/bean/vo/sales/SalesOutboundVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
package com.ruoyi.account.bean.vo.sales;
import com.alibaba.excel.annotation.ExcelIgnore;
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.LocalDate;
@Data
@Schema(name = "SalesOutboundVo", description = "财务管理--销售出库台账(返回)")
@ExcelIgnoreUnannotated
public class SalesOutboundVo {
    @Schema(description = "出库单id")
    private Long id;
    @Schema(description = "出库单号")
    @Excel(name = "出库单号")
    private String outboundBatches;
    @Schema(description = "客户名称")
    @Excel(name = "客户名称")
    private String customerName;
    @Schema(description = "出库日期")
    @Excel(name = "出库日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate shippingDate;
    @Schema(description = "产品名称")
    @Excel(name = "产品名称")
    private String productName;
    @Schema(description = "产品规格")
    @Excel(name = "产品规格")
    private String  specificationModel;
    @Schema(description = "金额")
    @Excel(name = "金额")
    private BigDecimal outboundAmount;
    @Schema(description = "税率")
    private BigDecimal taxRate;
    @Schema(description = "发货编号")
    @Excel(name = "发货编号")
    private String shippingNo;
    @Schema(description = "销售订单号")
    @Excel(name = "销售订单号")
    private String salesContractNo;
    @Schema(description = "已收款金额")
    @ExcelIgnore
    private BigDecimal amountReceived;
}
src/main/java/com/ruoyi/account/bean/vo/sales/SalesReturnVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
package com.ruoyi.account.bean.vo.sales;
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 = "SalesReturnVo", description = "财务管理--销售退货台账(返回)")
@ExcelIgnoreUnannotated
public class SalesReturnVo {
    @Schema(description = "退货单id")
    private Long id;
    @Excel(name = "退货单号")
    @Schema(description = "退货单号")
    private String returnNo;
    @Schema(description = "客户名称")
    @Excel(name = "客户名称")
    private String customerName;
    @Schema(description = "关联发货单号")
    @Excel(name = "关联发货单号")
    private String shippingNo;
    @Schema(description = "发货类型")
    @Excel(name = "发货类型")
    private Integer shippingType;
    @Schema(description = "发货车牌号")
    @Excel(name = "发货车牌号")
    private String truckPlateNo;
    @Schema(description = "快递公司")
    @Excel(name = "快递公司")
    private String expressCompany;
    @Schema(description = "快递单号")
    @Excel(name = "快递单号")
    private String expressNo;
    @Schema(description = "退货日期")
    @Excel(name = "退货日期")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime makeTime;
    @Schema(description = "退款总额")
    @Excel(name = "退款总额")
    private BigDecimal refundAmount;
    @Schema(description = "退货原因")
    @Excel(name = "退货原因")
    private String returnReason;
    @Schema(description = "销售订单号")
    @Excel(name = "销售订单号")
    private String salesContractNo;
}
src/main/java/com/ruoyi/account/controller/AccountExpenseController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/controller/AccountFileController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/controller/AccountIncomeController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/controller/AccountStatementController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
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.StatementAccountDto;
import com.ruoyi.account.bean.vo.StatementAccountVo;
import com.ruoyi.account.service.AccountStatementService;
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.*;
import java.util.Arrays;
/**
 * <p>
 *  å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 09:42:47
 */
@RestController
@RequestMapping("/accountStatement")
@RequiredArgsConstructor
@Tag(name = "财务管理-对账单")
public class AccountStatementController {
    private final AccountStatementService accountStatementService;
    @GetMapping("/getAccountStatementDetailsByMonth")
    @Log(title = "根据客户和月份查询对账单明细", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--根据客户和月份查询对账单明细")
    public R getAccountStatementDetailsByMonth(StatementAccountDto statementAccountDto) {
        return R.ok(accountStatementService.getAccountStatementDetailsByMonth(statementAccountDto));
    }
    @PostMapping("/addAccountStatement")
    @Log(title = "新增对账单", businessType = BusinessType.INSERT)
    @Operation(summary = "财务管理--新增对账单")
    public R addAccountStatement(@RequestBody StatementAccountVo statementAccountVo) {
        return R.ok(accountStatementService.addAccountStatement(statementAccountVo));
    }
    @DeleteMapping("/deleteAccountStatement")
    @Log(title = "删除对账单", businessType = BusinessType.DELETE)
    @Operation(summary = "财务管理--删除对账单")
    public R deleteAccountStatement(@RequestParam("ids") Long[] ids) {
        return R.ok(accountStatementService.deleteAccountStatement(Arrays.asList(ids)));
    }
    @GetMapping("/listPageAccountStatement")
    @Log(title = "对账单台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--对账单台账")
    public R<IPage<StatementAccountVo>> listPageAccountStatement(Page page, StatementAccountDto statementAccountDto) {
        IPage<StatementAccountVo> listPage = accountStatementService.listPageAccountStatement(page,statementAccountDto);
        return R.ok(listPage);
    }
    @PostMapping("/exportAccountStatement")
    @Operation(summary = "导出对账单文件")
    @Log(title = "导出对账单文件", businessType = BusinessType.EXPORT)
    public void exportAccountStatement(HttpServletResponse response, StatementAccountDto statementAccountDto) {
        accountStatementService.exportAccountStatement(response,statementAccountDto);
    }
}
src/main/java/com/ruoyi/account/controller/AccountingController.java
@@ -1,12 +1,16 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.service.impl.AccountingServiceImpl;
import com.ruoyi.account.bean.dto.AccountReportDto;
import com.ruoyi.account.service.AccountingService;
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 io.swagger.v3.oas.annotations.tags.Tag;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
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.RequestParam;
@@ -19,11 +23,11 @@
@Tag(name = "会计核算")
@RestController
@RequestMapping("/accounting")
@AllArgsConstructor
@RequiredArgsConstructor
public class AccountingController extends BaseController {
    private AccountingServiceImpl accountingService;
    private final AccountingService accountingService;
    @Operation(summary = "总计")
    @GetMapping("/total")
@@ -43,4 +47,14 @@
        return accountingService.calculateDepreciation(page,year);
    }
    /*****************************************财务报表******************************************************************************/
    @GetMapping("/accountStatementDetailsByMonth")
    @Log(title = "财务报表", businessType = BusinessType.OTHER)
    @Operation(summary = "财务报表")
    public R getAccountStatementDetailsByMonth(AccountReportDto accountReportDto) {
        return R.ok(accountingService.getAccountStatementDetailsByMonth(accountReportDto));
    }
}
src/main/java/com/ruoyi/account/controller/BorrowInfoController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/controller/SalesReceiptReturnController.java
@@ -1,7 +1,7 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.account.dto.SalesReceiptReturnDto;
import com.ruoyi.account.bean.dto.SalesReceiptReturnDto;
import com.ruoyi.account.pojo.SalesReceiptReturn;
import com.ruoyi.account.service.SalesReceiptReturnService;
import com.ruoyi.account.service.impl.SalesReceiptReturnServiceImpl;
src/main/java/com/ruoyi/account/controller/SalesRefundAmountOrderController.java
@@ -1,7 +1,7 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.dto.SalesRefundAmountOrderDto;
import com.ruoyi.account.bean.dto.SalesRefundAmountOrderDto;
import com.ruoyi.account.pojo.SalesRefundAmountOrder;
import com.ruoyi.account.service.SalesRefundAmountOrderService;
import com.ruoyi.framework.web.domain.R;
src/main/java/com/ruoyi/account/controller/financial/AccountSubjectController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
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.AccountSubjectDto;
import com.ruoyi.account.bean.vo.financial.AccountSubjectVo;
import com.ruoyi.account.service.purchase.AccountSubjectService;
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.*;
import java.util.Arrays;
/**
 * <p>
 * æ€»è´¦ç§‘目表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-07 04:45:30
 */
@RestController
@RequestMapping("/accountSubject")
@RequiredArgsConstructor
@Tag(name = "总账科目")
public class AccountSubjectController {
    private final AccountSubjectService accountSubjectService;
    @GetMapping("/list")
    @Log(title = "总账科目数据集合", businessType = BusinessType.OTHER)
    @Operation(summary = "总账科目树形查询(递归)")
    public R<IPage<AccountSubjectVo>> AccountSubjectDtoList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto) {
        IPage<AccountSubjectVo> paramList = accountSubjectService.baseList(page, accountSubjectDto);
        return R.ok(paramList);
    }
    @PostMapping("/add")
    @Log(title = "新增总账科目", businessType = BusinessType.INSERT)
    @Operation(summary = "新增总账科目")
    public R AccountSubjectDtoAdd(@RequestBody AccountSubjectDto 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.updateAccountSubject(accountSubjectDto));
    }
    @DeleteMapping("/remove/{ids}")
    @Log(title = "删除总账科目", businessType = BusinessType.DELETE)
    @Operation(summary = "删除总账科目")
    public R AccountSubjectDtooRemove(@PathVariable Long[] ids) {
        return R.ok(accountSubjectService.removeAccountSubjectByIds(Arrays.asList(ids)));
    }
    @PostMapping("/export")
    @Operation(summary = "导出总账科目文件")
    @Log(title = "导出总账科目文件", businessType = BusinessType.EXPORT)
    public void exportAccountSubject(HttpServletResponse response) {
        accountSubjectService.exportAccountSubject(response);
    }
}
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/controller/purchase/AccounPurchaseController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
package com.ruoyi.account.controller.purchase;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.purchase.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo;
import com.ruoyi.account.service.financial.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/purchase/AccountPaymentApplicationController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,86 @@
package com.ruoyi.account.controller.purchase;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.purchase.AccountPaymentApplicationDto;
import com.ruoyi.account.bean.vo.purchase.AccountPaymentApplicationVo;
import com.ruoyi.account.pojo.purchase.AccountPaymentApplication;
import com.ruoyi.account.service.purchase.AccountPaymentApplicationService;
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.*;
import java.util.Arrays;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--付款申请 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 03:44:22
 */
@RestController
@RequestMapping("/accountPaymentApplication")
@Tag(name = "财务管理--付款申请")
@RequiredArgsConstructor
public class AccountPaymentApplicationController {
    private final AccountPaymentApplicationService accountPaymentApplicationService;
    @GetMapping("/listPageAccountPaymentApplication")
    @Log(title = "付款申请台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--付款申请台账")
    public R<IPage<AccountPaymentApplicationVo>> listPageAccountPaymentApplication(Page page, AccountPaymentApplicationDto accountPaymentApplicationDto) {
        IPage<AccountPaymentApplicationVo> listPage = accountPaymentApplicationService.listPageAccountPaymentApplication(page,accountPaymentApplicationDto);
        return R.ok(listPage);
    }
    @GetMapping("/getInboundBatchesBySupplier")
    @Log(title = "根据供应商查询入库单号(付款申请)", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--根据供应商查询入库单号(付款申请)")
    public R getInboundBatchesBySupplier(Integer supplierId) {
        return R.ok(accountPaymentApplicationService.getInboundBatchesBySupplier(supplierId));
    }
    @PostMapping("/addAccountPaymentApplication")
    @Log(title = "新增付款申请", businessType = BusinessType.INSERT)
    @Operation(summary = "财务管理--新增付款申请")
    public R addAccountPaymentApplication(@RequestBody AccountPaymentApplication accountPaymentApplication) {
        return R.ok(accountPaymentApplicationService.addAccountPaymentApplication(accountPaymentApplication));
    }
    @PutMapping("/updateAccountPaymentApplication")
    @Log(title = "修改付款申请", businessType = BusinessType.UPDATE)
    @Operation(summary = "财务管理--修改付款申请")
    public R updateAccountPaymentApplication(@RequestBody AccountPaymentApplication accountPaymentApplication) {
        return R.ok(accountPaymentApplicationService.updateById(accountPaymentApplication));
    }
    @PutMapping("/auditAccountPaymentApplication")
    @Log(title = "审核付款申请", businessType = BusinessType.UPDATE)
    @Operation(summary = "财务管理--审核付款申请")
    public R auditAccountPaymentApplication(@RequestBody AccountPaymentApplication accountPaymentApplication) {
        return R.ok(accountPaymentApplicationService.updateById(accountPaymentApplication));
    }
    @DeleteMapping("/deleteAccountPaymentApplication")
    @Log(title = "删除付款申请", businessType = BusinessType.DELETE)
    @Operation(summary = "财务管理--删除付款申请")
    public R deleteAccountPaymentApplication(@RequestParam("ids") Long[] ids) {
        return R.ok(accountPaymentApplicationService.deleteAccountPaymentApplication(Arrays.asList(ids)));
    }
    @PostMapping("/exportAccountPaymentApplication")
    @Operation(summary = "导出付款申请文件")
    @Log(title = "导出付款申请文件", businessType = BusinessType.EXPORT)
    public void exportAccountPaymentApplication(HttpServletResponse response, AccountPaymentApplicationDto accountPaymentApplicationDto) {
        accountPaymentApplicationService.exportAccountPaymentApplication(response,accountPaymentApplicationDto);
    }
}
src/main/java/com/ruoyi/account/controller/purchase/AccountPurchaseInvoiceController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
package com.ruoyi.account.controller.purchase;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.purchase.AccountPurchaseInvoiceDto;
import com.ruoyi.account.bean.vo.purchase.AccountPurchaseInvoiceVo;
import com.ruoyi.account.pojo.purchase.AccountPurchaseInvoice;
import com.ruoyi.account.service.purchase.AccountPurchaseInvoiceService;
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.*;
import java.util.Arrays;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--进项发票 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 03:06:17
 */
@RestController
@RequestMapping("/accountPurchaseInvoice")
@Tag(name = "财务管理--进项发票")
@RequiredArgsConstructor
public class AccountPurchaseInvoiceController {
    private final AccountPurchaseInvoiceService accountPurchaseInvoiceService;
    @GetMapping("/listPageAccountPurchaseInvoice")
    @Log(title = "进项发票台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--进项发票台账")
    public R<IPage<AccountPurchaseInvoiceVo>> listPageAccountPurchaseInvoice(Page page, AccountPurchaseInvoiceDto accountPurchaseInvoiceDto) {
        IPage<AccountPurchaseInvoiceVo> listPage = accountPurchaseInvoiceService.listPageAccountPurchaseInvoice(page,accountPurchaseInvoiceDto);
        return R.ok(listPage);
    }
    @GetMapping("/getInboundBatchesBySupplier")
    @Log(title = "根据供应商查询入库单号(进项发票)", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--根据供应商查询入库单号(进项发票)")
    public R getInboundBatchesBySupplier(Integer supplierId) {
        return R.ok(accountPurchaseInvoiceService.getInboundBatchesBySupplier(supplierId));
    }
    @PostMapping("/addAccountPurchaseInvoice")
    @Log(title = "新增进项发票", businessType = BusinessType.INSERT)
    @Operation(summary = "财务管理--新增进项发票")
    public R addAccountPurchaseInvoice(@RequestBody AccountPurchaseInvoice accountPurchaseInvoice) {
        return R.ok(accountPurchaseInvoiceService.save(accountPurchaseInvoice));
    }
    @PutMapping("/cancelAccountPurchaseInvoice")
    @Log(title = "作废进项发票", businessType = BusinessType.INSERT)
    @Operation(summary = "财务管理--作废销项发票")
    public R cancelAccountPurchaseInvoice(@RequestBody AccountPurchaseInvoice accountPurchaseInvoice) {
        return R.ok(accountPurchaseInvoiceService.updateById(accountPurchaseInvoice));
    }
    @DeleteMapping("/deleteAccountPurchaseInvoice")
    @Log(title = "删除进项发票", businessType = BusinessType.DELETE)
    @Operation(summary = "财务管理--删除进项发票")
    public R deleteAccountPurchaseInvoice(@RequestParam("ids") Long[] ids) {
        return R.ok(accountPurchaseInvoiceService.deleteAccountPurchaseInvoice(Arrays.asList(ids)));
    }
    @PostMapping("/exportAccountPurchaseInvoice")
    @Operation(summary = "导出进项发票文件")
    @Log(title = "导出进项发票文件", businessType = BusinessType.EXPORT)
    public void exportAccountPurchaseInvoice(HttpServletResponse response, AccountPurchaseInvoiceDto accountPurchaseInvoiceDto) {
        accountPurchaseInvoiceService.exportAccountPurchaseInvoice(response,accountPurchaseInvoiceDto);
    }
}
src/main/java/com/ruoyi/account/controller/purchase/AccountPurchasePaymentController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
package com.ruoyi.account.controller.purchase;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.purchase.AccountPurchasePaymentDto;
import com.ruoyi.account.bean.vo.purchase.AccountPurchasePaymentVo;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.service.purchase.AccountPurchasePaymentService;
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.*;
import java.util.Arrays;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--付款单 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 04:14:51
 */
@RestController
@RequestMapping("/accountPurchasePayment")
@Tag(name = "财务管理--付款单")
@RequiredArgsConstructor
public class AccountPurchasePaymentController {
    private final AccountPurchasePaymentService accountPurchasePaymentService;
    @GetMapping("/listPageAccountPurchasePayment")
    @Log(title = "付款单台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--付款单台账")
    public R<IPage<AccountPurchasePaymentVo>> listPageAccountPurchasePayment(Page page, AccountPurchasePaymentDto accountPurchasePaymentDto) {
        IPage<AccountPurchasePaymentVo> listPage = accountPurchasePaymentService.listPageAccountPurchasePayment(page,accountPurchasePaymentDto);
        return R.ok(listPage);
    }
    @PostMapping("/addAccountPurchasePayment")
    @Log(title = "新增付款单", businessType = BusinessType.INSERT)
    @Operation(summary = "财务管理--新增付款单")
    public R addAccountPurchasePayment(@RequestBody AccountPurchasePayment accountPurchasePayment) {
        return R.ok(accountPurchasePaymentService.addAccountPurchasePayment(accountPurchasePayment));
    }
    @PutMapping("/updateAccountPurchasePayment")
    @Log(title = "编辑付款单", businessType = BusinessType.UPDATE)
    @Operation(summary = "财务管理--编辑付款单")
    public R updateAccountPurchasePayment(@RequestBody AccountPurchasePayment accountPurchasePayment) {
        return R.ok(accountPurchasePaymentService.updateById(accountPurchasePayment));
    }
    @DeleteMapping("/deleteAccountPurchasePayment")
    @Log(title = "删除付款单", businessType = BusinessType.DELETE)
    @Operation(summary = "财务管理--删除付款单")
    public R deleteAccountPurchasePayment(@RequestParam("ids") Long[] ids) {
        return R.ok(accountPurchasePaymentService.deleteAccountPurchasePayment(Arrays.asList(ids)));
    }
    @PostMapping("/exportAccountPurchasePayment")
    @Operation(summary = "导出付款单文件")
    @Log(title = "导出付款单文件", businessType = BusinessType.EXPORT)
    public void exportAccountPurchasePayment(HttpServletResponse response, AccountPurchasePaymentDto accountPurchasePaymentDto) {
        accountPurchasePaymentService.exportAccountPurchasePayment(response,accountPurchasePaymentDto);
    }
}
src/main/java/com/ruoyi/account/controller/sales/AccountInvoiceApplicationController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
package com.ruoyi.account.controller.sales;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.sales.AccountInvoiceApplicationDto;
import com.ruoyi.account.bean.vo.sales.AccountInvoiceApplicationVo;
import com.ruoyi.account.pojo.sales.AccountInvoiceApplication;
import com.ruoyi.account.service.sales.AccountInvoiceApplicationService;
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.*;
import java.util.Arrays;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--开票申请 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 01:38:32
 */
@RestController
@RequestMapping("/accountInvoiceApplication")
@Tag(name = "财务管理--开票申请")
@RequiredArgsConstructor
public class AccountInvoiceApplicationController {
    private final AccountInvoiceApplicationService accountInvoiceApplicationService;
    @GetMapping("/listPageAccountInvoiceApplication")
    @Log(title = "开票申请台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--开票申请台账")
    public R<IPage<AccountInvoiceApplicationVo>> listPageAccountInvoiceApplication(Page page, AccountInvoiceApplicationDto accountInvoiceApplicationDto) {
        IPage<AccountInvoiceApplicationVo> listPage = accountInvoiceApplicationService.listPageAccountInvoiceApplication(page,accountInvoiceApplicationDto);
        return R.ok(listPage);
    }
    @GetMapping("/getAccountInvoiceApplicationList")
    @Log(title = "根据客户id查询开票申请台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--开票申请台账")
    public R<List<AccountInvoiceApplicationVo>> getAccountInvoiceApplicationList(@RequestParam Integer customerId){
        return R.ok(accountInvoiceApplicationService.getAccountInvoiceApplicationList(customerId));
    }
    @GetMapping("/getOutboundBatchesByCustomer")
    @Log(title = "根据客户查询出库单号(开票申请)", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--根据客户查询出库单号(开票申请)")
    public R getOutboundBatchesByCustomer(Integer customerId) {
        return R.ok(accountInvoiceApplicationService.getOutboundBatchesByCustomer(customerId));
    }
    @PostMapping("/addAccountInvoiceApplication")
    @Log(title = "新增开票申请", businessType = BusinessType.INSERT)
    @Operation(summary = "财务管理--新增开票申请")
    public R addAccountInvoiceApplication(@RequestBody AccountInvoiceApplication accountInvoiceApplication) {
        return R.ok(accountInvoiceApplicationService.addAccountInvoiceApplication(accountInvoiceApplication));
    }
    @PutMapping("/updateAccountInvoiceApplication")
    @Log(title = "修改开票申请", businessType = BusinessType.UPDATE)
    @Operation(summary = "财务管理--修改开票申请")
    public R updateAccountInvoiceApplication(@RequestBody AccountInvoiceApplication accountInvoiceApplication) {
        return R.ok(accountInvoiceApplicationService.updateById(accountInvoiceApplication));
    }
    @PutMapping("/auditAccountInvoiceApplication")
    @Log(title = "审核开票申请", businessType = BusinessType.UPDATE)
    @Operation(summary = "财务管理--审核开票申请")
    public R auditAccountInvoiceApplication(@RequestBody AccountInvoiceApplication accountInvoiceApplication) {
        return R.ok(accountInvoiceApplicationService.updateById(accountInvoiceApplication));
    }
    @DeleteMapping("/deleteAccountInvoiceApplication")
    @Log(title = "删除开票申请", businessType = BusinessType.DELETE)
    @Operation(summary = "财务管理--删除开票申请")
    public R deleteAccountInvoiceApplication(@RequestParam("ids") Long[] ids) {
        return R.ok(accountInvoiceApplicationService.deleteAccountInvoiceApplication(Arrays.asList(ids)));
    }
    @PostMapping("/exportAccountInvoiceApplication")
    @Operation(summary = "导出开票申请文件")
    @Log(title = "导出开票申请文件", businessType = BusinessType.EXPORT)
    public void exportAccountInvoiceApplication(HttpServletResponse response, AccountInvoiceApplicationDto accountInvoiceApplicationDto) {
        accountInvoiceApplicationService.exportAccountInvoiceApplication(response,accountInvoiceApplicationDto);
    }
}
src/main/java/com/ruoyi/account/controller/sales/AccountSalesCollectionController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
package com.ruoyi.account.controller.sales;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.sales.AccountSalesCollectionDto;
import com.ruoyi.account.bean.vo.sales.AccountSalesCollectionVo;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.account.service.sales.AccountSalesCollectionService;
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.*;
import java.util.Arrays;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--收款单 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:49:56
 */
@RestController
@RequestMapping("/accountSalesCollection")
@Tag(name = "财务管理--收款单")
@RequiredArgsConstructor
public class AccountSalesCollectionController {
    private final AccountSalesCollectionService accountSalesCollectionService;
    @GetMapping("/listPageAccountSalesCollection")
    @Log(title = "收款单台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--收款单台账")
    public R<IPage<AccountSalesCollectionVo>> listPageAccountSalesCollection(Page page, AccountSalesCollectionDto accountSalesCollectionDto) {
        IPage<AccountSalesCollectionVo> listPage = accountSalesCollectionService.listPageAccountSalesCollection(page,accountSalesCollectionDto);
        return R.ok(listPage);
    }
    @GetMapping("/getOutboundBatchesByCustomer")
    @Log(title = "根据客户查询出库单号(收款单)", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--根据客户查询出库单号(收款单)")
    public R getOutboundBatchesByCustomer(Integer customerId) {
        return R.ok(accountSalesCollectionService.getOutboundBatchesByCustomer(customerId));
    }
    @PostMapping("/addAccountSalesCollection")
    @Log(title = "新增收款单", businessType = BusinessType.INSERT)
    @Operation(summary = "财务管理--新增收款单")
    public R addAccountSalesCollection(@RequestBody AccountSalesCollection accountSalesCollection) {
        return R.ok(accountSalesCollectionService.addAccountSalesCollection(accountSalesCollection));
    }
    @PutMapping("/updateAccountSalesCollection")
    @Log(title = "编辑收款单", businessType = BusinessType.UPDATE)
    @Operation(summary = "财务管理--编辑收款单")
    public R updateAccountSalesCollection(@RequestBody AccountSalesCollection accountSalesCollection) {
        return R.ok(accountSalesCollectionService.updateById(accountSalesCollection));
    }
    @DeleteMapping("/deleteAccountSalesCollection")
    @Log(title = "删除收款单", businessType = BusinessType.DELETE)
    @Operation(summary = "财务管理--删除收款单")
    public R deleteAccountSalesCollection(@RequestParam("ids") Long[] ids) {
        return R.ok(accountSalesCollectionService.deleteAccountSalesCollection(Arrays.asList(ids)));
    }
    @PostMapping("/exportAccountSalesCollection")
    @Operation(summary = "导出收款单文件")
    @Log(title = "导出收款单文件", businessType = BusinessType.EXPORT)
    public void exportAccountSalesCollection(HttpServletResponse response, AccountSalesCollectionDto accountSalesCollectionDto) {
        accountSalesCollectionService.exportAccountSalesCollection(response,accountSalesCollectionDto);
    }
}
src/main/java/com/ruoyi/account/controller/sales/AccountSalesController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
package com.ruoyi.account.controller.sales;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.sales.SalesOutboundDto;
import com.ruoyi.account.bean.dto.sales.SalesReturnDto;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.bean.vo.sales.SalesReturnVo;
import com.ruoyi.account.service.sales.AccountSalesService;
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("/accountSales")
@RequiredArgsConstructor
@Tag(name = "财务管理的销售部分")
public class AccountSalesController {
    private final AccountSalesService accountSalesService;
    @GetMapping("/listPageAccountSales")
    @Log(title = "销售出库台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--销售出库台账")
    public R<IPage<SalesOutboundVo>> listPageAccountSales(Page page, SalesOutboundDto salesOutboundDto) {
        IPage<SalesOutboundVo> listPage = accountSalesService.listPageAccountSales(page,salesOutboundDto);
        return R.ok(listPage);
    }
    @PostMapping("/exportAccountSalesOutbound")
    @Operation(summary = "导出销售出库文件")
    @Log(title = "导出销售出库文件", businessType = BusinessType.EXPORT)
    public void exportAccountSalesOutbound(HttpServletResponse response,SalesOutboundDto salesOutboundDto) {
        accountSalesService.exportAccountSalesOutbound(response,salesOutboundDto);
    }
    @GetMapping("/listPageAccountSalesReturn")
    @Log(title = "销售退货台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--销售退货台账")
    public R<IPage<SalesReturnVo>> listPageAccountSalesReturn(Page page, SalesReturnDto salesReturnDto) {
        IPage<SalesReturnVo> listPage = accountSalesService.listPageAccountSalesReturn(page,salesReturnDto);
        return R.ok(listPage);
    }
    @PostMapping("/exportAccountSalesReturn")
    @Operation(summary = "导出销售退货文件")
    @Log(title = "导出销售退货文件", businessType = BusinessType.EXPORT)
    public void exportAccountSalesReturn(HttpServletResponse response,SalesReturnDto salesReturnDto) {
        accountSalesService.exportAccountSalesReturn(response,salesReturnDto);
    }
}
src/main/java/com/ruoyi/account/controller/sales/AccountSalesInvoiceController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package com.ruoyi.account.controller.sales;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.sales.AccountSalesInvoiceDto;
import com.ruoyi.account.bean.vo.sales.AccountSalesInvoiceVo;
import com.ruoyi.account.pojo.sales.AccountSalesInvoice;
import com.ruoyi.account.service.sales.AccountSalesInvoiceService;
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.*;
import java.util.Arrays;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--销项发票 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:10:20
 */
@RestController
@RequestMapping("/accountSalesInvoice")
@Tag(name = "财务管理--销项发票")
@RequiredArgsConstructor
public class AccountSalesInvoiceController {
    private final AccountSalesInvoiceService accountSalesInvoiceService;
    @GetMapping("/listPageAccountSalesInvoice")
    @Log(title = "销项发票台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--销项发票台账")
    public R<IPage<AccountSalesInvoiceVo>> listPageAccountSalesInvoice(Page page, AccountSalesInvoiceDto accountSalesInvoiceDto) {
        IPage<AccountSalesInvoiceVo> listPage = accountSalesInvoiceService.listPageAccountSalesInvoice(page,accountSalesInvoiceDto);
        return R.ok(listPage);
    }
    @PostMapping("/addAccountSalesInvoice")
    @Log(title = "新增销项发票", businessType = BusinessType.INSERT)
    @Operation(summary = "财务管理--新增销项发票")
    public R addAccountSalesInvoice(@RequestBody AccountSalesInvoice accountSalesInvoice) {
        return R.ok(accountSalesInvoiceService.save(accountSalesInvoice));
    }
    @PutMapping("/cancelAccountSalesInvoice")
    @Log(title = "作废销项发票", businessType = BusinessType.INSERT)
    @Operation(summary = "财务管理--作废销项发票")
    public R cancelAccountSalesInvoice(@RequestBody AccountSalesInvoice accountSalesInvoice) {
        return R.ok(accountSalesInvoiceService.updateById(accountSalesInvoice));
    }
    @DeleteMapping("/deleteAccountSalesInvoice")
    @Log(title = "删除销项发票", businessType = BusinessType.DELETE)
    @Operation(summary = "财务管理--删除销项发票")
    public R deleteAccountSalesInvoice(@RequestParam("ids") Long[] ids) {
        return R.ok(accountSalesInvoiceService.deleteAccountSalesInvoice(Arrays.asList(ids)));
    }
    @PostMapping("/exportAccountSalesInvoice")
    @Operation(summary = "导出销项发票文件")
    @Log(title = "导出销项发票文件", businessType = BusinessType.EXPORT)
    public void exportAccountSalesInvoice(HttpServletResponse response, AccountSalesInvoiceDto accountSalesInvoiceDto) {
        accountSalesInvoiceService.exportAccountSalesInvoice(response,accountSalesInvoiceDto);
    }
}
src/main/java/com/ruoyi/account/dto/AccountDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/dto/AccountDto2.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/dto/AccountDto3.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/dto/DeviceTypeDetail.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/dto/DeviceTypeDistributionVO.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/dto/ReportDateDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/dto/SalesReceiptReturnDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/dto/SalesRefundAmountOrderDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/mapper/AccountExpenseMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/mapper/AccountFileMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/mapper/AccountIncomeMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/mapper/AccountStatementDetailsMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.account.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.AccountStatementDetails;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--对账单明细 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 10:12:42
 */
@Mapper
public interface AccountStatementDetailsMapper extends BaseMapper<AccountStatementDetails> {
}
src/main/java/com/ruoyi/account/mapper/AccountStatementMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.account.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.StatementAccountDto;
import com.ruoyi.account.bean.vo.StatementAccountVo;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.purchase.dto.VatDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 *  Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 09:42:47
 */
@Mapper
public interface AccountStatementMapper extends BaseMapper<AccountStatement> {
    IPage<StatementAccountVo> listPageAccountStatement(Page page, @Param("req") StatementAccountDto statementAccountDto);
    IPage<VatDto> selectVatDtoPage(Page page, @Param("month") String month);
}
src/main/java/com/ruoyi/account/mapper/BorrowInfoMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/mapper/SalesRefundAmountOrderMapper.java
@@ -2,7 +2,7 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.dto.SalesRefundAmountOrderDto;
import com.ruoyi.account.bean.dto.SalesRefundAmountOrderDto;
import com.ruoyi.account.pojo.SalesRefundAmountOrder;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
src/main/java/com/ruoyi/account/mapper/financial/AccountSubjectMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package com.ruoyi.account.mapper.financial;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.financial.AccountSubject;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
 * æ€»è´¦ç§‘目表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-07 04:45:30
 */
@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/mapper/purchase/AccountPaymentApplicationMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package com.ruoyi.account.mapper.purchase;
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.purchase.AccountPaymentApplicationDto;
import com.ruoyi.account.bean.vo.purchase.AccountPaymentApplicationVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.pojo.purchase.AccountPaymentApplication;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--付款申请 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 03:44:22
 */
@Mapper
public interface AccountPaymentApplicationMapper extends BaseMapper<AccountPaymentApplication> {
    IPage<AccountPaymentApplicationVo> listPageAccountPaymentApplication(Page page, @Param("req") AccountPaymentApplicationDto accountPaymentApplicationDto);
    List<PurchaseInboundVo> getInboundBatchesBySupplier(@Param("supplierId") Integer supplierId);
    //判断该出库记录是否有开票申请
    boolean existsByStockInRecordId(@Param("stockInRecordIds") List<Long> stockInRecordIds);
}
src/main/java/com/ruoyi/account/mapper/purchase/AccountPurchaseInvoiceMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package com.ruoyi.account.mapper.purchase;
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.purchase.AccountPurchaseInvoiceDto;
import com.ruoyi.account.bean.vo.purchase.AccountPurchaseInvoiceVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.pojo.purchase.AccountPurchaseInvoice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--进项发票 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 03:06:17
 */
@Mapper
public interface AccountPurchaseInvoiceMapper extends BaseMapper<AccountPurchaseInvoice> {
    IPage<AccountPurchaseInvoiceVo> listPageAccountPurchaseInvoice(Page page, @Param("req") AccountPurchaseInvoiceDto accountPurchaseInvoiceDto);
    List<PurchaseInboundVo> getInboundBatchesBySupplier(@Param("supplierId") Integer supplierId);
}
src/main/java/com/ruoyi/account/mapper/purchase/AccountPurchasePaymentMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package com.ruoyi.account.mapper.purchase;
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.purchase.AccountPurchasePaymentDto;
import com.ruoyi.account.bean.vo.purchase.AccountPurchasePaymentVo;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.home.dto.IncomeExpenseAnalysisDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--付款单 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 04:14:51
 */
@Mapper
public interface AccountPurchasePaymentMapper extends BaseMapper<AccountPurchasePayment> {
    IPage<AccountPurchasePaymentVo> listPageAccountPurchasePayment(Page page, @Param("req") AccountPurchasePaymentDto accountPurchasePaymentDto);
    List<IncomeExpenseAnalysisDto> selectPayment(@Param("startStr") String startStr, @Param("endStr") String endStr, @Param("dateFormat") String dateFormat);
}
src/main/java/com/ruoyi/account/mapper/sales/AccountInvoiceApplicationMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.account.mapper.sales;
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.sales.AccountInvoiceApplicationDto;
import com.ruoyi.account.bean.vo.sales.AccountInvoiceApplicationVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.pojo.sales.AccountInvoiceApplication;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--开票申请 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 01:38:32
 */
@Mapper
public interface AccountInvoiceApplicationMapper extends BaseMapper<AccountInvoiceApplication> {
    IPage<AccountInvoiceApplicationVo> listPageAccountInvoiceApplication(Page page, @Param("req") AccountInvoiceApplicationDto accountInvoiceApplicationDto);
    List<SalesOutboundVo> getOutboundBatchesByCustomer(@Param("customerId") Integer customerId);
    //判断该出库记录是否有开票申请
    boolean existsByStockOutRecordId(@Param("stockOutRecordIds") List<Long> stockOutRecordIds);
    List<AccountInvoiceApplicationVo> selectAccountInvoiceApplicationList(@Param("customerId") Integer customerId);
}
src/main/java/com/ruoyi/account/mapper/sales/AccountSalesCollectionMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package com.ruoyi.account.mapper.sales;
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.sales.AccountSalesCollectionDto;
import com.ruoyi.account.bean.vo.sales.AccountSalesCollectionVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.home.dto.IncomeExpenseAnalysisDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--收款单 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:49:56
 */
@Mapper
public interface AccountSalesCollectionMapper extends BaseMapper<AccountSalesCollection> {
    IPage<AccountSalesCollectionVo> listPageAccountSalesCollection(Page page, @Param("req") AccountSalesCollectionDto accountSalesCollectionDto);
    //判断该出库记录是否有收款单
    boolean existsByStockOutRecordId(@Param("stockOutRecordIds") List<Long> stockOutRecordIds);
    List<SalesOutboundVo> getOutboundBatchesByCustomer(@Param("customerId") Integer customerId);
    List<IncomeExpenseAnalysisDto> selectIncomeStats(@Param("startStr") String startStr, @Param("endStr") String endStr, @Param("dateFormat") String dateFormat);
}
src/main/java/com/ruoyi/account/mapper/sales/AccountSalesInvoiceMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.account.mapper.sales;
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.sales.AccountSalesInvoiceDto;
import com.ruoyi.account.bean.vo.sales.AccountSalesInvoiceVo;
import com.ruoyi.account.pojo.sales.AccountSalesInvoice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--销项发票 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:10:20
 */
@Mapper
public interface AccountSalesInvoiceMapper extends BaseMapper<AccountSalesInvoice> {
    IPage<AccountSalesInvoiceVo> listPageAccountSalesInvoice(Page page, @Param("req") AccountSalesInvoiceDto accountSalesInvoiceDto);
}
src/main/java/com/ruoyi/account/pojo/AccountExpense.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/pojo/AccountFile.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/pojo/AccountIncome.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/pojo/AccountStatement.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
package com.ruoyi.account.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
 *
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 09:42:47
 */
@Getter
@Setter
@ToString
@TableName("account_statement")
@ApiModel(value = "AccountStatement对象", description = "财务管理--对账单")
public class AccountStatement implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * å®¢æˆ·id(应收是客户customer,应付是供应商supplier)
     */
    @ApiModelProperty("客户id")
    private Integer customerId;
    /**
     * å¯¹è´¦æœˆä»½(yyyy-MM)
     */
    @ApiModelProperty("对账月份(yyyy-MM)")
    @Excel(name = "对账月份")
    private String statementMonth;
    /**
     * ä¸šåŠ¡ç±»åž‹(1应收对账;2应付对账)
     */
    @ApiModelProperty("业务类型(1应收对账;2应付对账)")
    @Excel(name = "业务类型",readConverterExp = "1=应收对账,2=应付对账")
    private Integer accountType;
    /**
     * å¯¹è´¦å•号
     */
    @ApiModelProperty("对账单号")
    @Excel(name = "对账单号")
    private String statementNumber;
    /**
     * æœŸåˆä½™é¢
     */
    @ApiModelProperty("期初余额")
    @Excel(name = "期初余额")
    private BigDecimal openingBalance;
    /**
     * æœ¬æœŸåº”æ”¶/应付
     */
    @ApiModelProperty("本期应收/应付")
    @Excel(name = "本期应收/应付")
    private BigDecimal currentPlan;
    /**
     * æœ¬æœŸæ”¶æ¬¾/付款
     */
    @ApiModelProperty("本期收款/付款")
    @Excel(name = "本期收款/付款")
    private BigDecimal currentActually;
    /**
     * æœŸæœ«ä½™é¢
     */
    @ApiModelProperty("期末余额")
    @Excel(name = "期末余额")
    private BigDecimal closingBalance;
    /**
     * åˆ›å»ºäºº
     */
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    /**
     * ä¿®æ”¹äºº
     */
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    /**
     * ä¿®æ”¹æ—¶é—´
     */
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
    /**
     * éƒ¨é—¨ID
     */
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/account/pojo/AccountStatementDetails.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,115 @@
package com.ruoyi.account.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--对账单明细
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 10:12:42
 */
@Getter
@Setter
@ToString
@TableName("account_statement_details")
@ApiModel(value = "AccountStatementDetails对象", description = "财务管理--对账单明细")
public class AccountStatementDetails implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * å…³è”对账单id
     */
    @ApiModelProperty("关联对账单id")
    private Integer accountStatementId;
    /**
     * æ•°æ®æ—¥æœŸ
     */
    @ApiModelProperty("数据日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate occurrenceDate;
    /**
     * å•据编号
     */
    @ApiModelProperty("单据编号")
    private String receiptNumber;
    /**
     * æ•°æ®ç±»åž‹(1出库;2入库;3收款;4付款;5退货)
     */
    @ApiModelProperty("数据类型(1出库;2入库;3收款;4付款;5退货)")
    private Integer type;
    /**
     * é‡‘额
     */
    @ApiModelProperty("金额")
    private BigDecimal amount;
    /**
     * å¤‡æ³¨
     */
    @ApiModelProperty("备注")
    private String remark;
    /**
     * åˆ›å»ºäºº
     */
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    /**
     * ä¿®æ”¹äºº
     */
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    /**
     * ä¿®æ”¹æ—¶é—´
     */
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
    /**
     * éƒ¨é—¨ID
     */
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/account/pojo/BorrowInfo.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/pojo/financial/AccountSubject.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,117 @@
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.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * æ€»è´¦ç§‘目表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-07 04:45:30
 */
@Getter
@Setter
@ToString
@TableName("account_subject")
@ApiModel(value = "AccountSubject对象", description = "总账科目表")
public class AccountSubject implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @ApiModelProperty("主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * çˆ¶ç§‘ç›®ID(为空表示根节点)
     */
    @ApiModelProperty("父科目ID(为空表示根节点)")
    private Long parentId;
    /**
     * ç§‘目编码(唯一标识)
     */
    @ApiModelProperty("科目编码(唯一标识)")
    private String subjectCode;
    /**
     * ç§‘目名称
     */
    @ApiModelProperty("科目名称")
    private String subjectName;
    /**
     * ç§‘目类型
     */
    @ApiModelProperty("科目类型")
    private String subjectType;
    /**
     * ä½™é¢æ–¹å‘
     */
    @ApiModelProperty("余额方向")
    private String balanceDirection;
    /**
     * çŠ¶æ€ 0启用 1禁用
     */
    @ApiModelProperty("状态 0启用 1禁用")
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    @ApiModelProperty("备注")
    private String remark;
    /**
     * åˆ›å»ºäºº
     */
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * ä¿®æ”¹äºº
     */
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    /**
     * ä¿®æ”¹æ—¶é—´
     */
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * éƒ¨é—¨ID
     */
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
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/pojo/purchase/AccountPaymentApplication.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
package com.ruoyi.account.pojo.purchase;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--付款申请
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 03:44:22
 */
@Getter
@Setter
@ToString
@TableName("account_payment_application")
@ApiModel(value = "AccountPaymentApplication对象", description = "财务管理--付款申请")
public class AccountPaymentApplication implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * åˆ›å»ºäºº
     */
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    /**
     * ä¿®æ”¹äºº
     */
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    /**
     * ä¿®æ”¹æ—¶é—´
     */
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
    /**
     * éƒ¨é—¨ID
     */
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * ä¾›åº”商id
     */
    @ApiModelProperty("供应商id")
    private Integer supplierId;
    /**
     * å…³è”入库单id(多选)
     */
    @ApiModelProperty("关联入库单id(多选)")
    private String stockInRecordIds;
    /**
     * ä»˜æ¬¾ç”³è¯·å•号
     */
    @ApiModelProperty("付款申请单号")
    @Excel(name = "付款申请单号")
    private String invoiceApplicationNo;
    /**
     * ä»˜æ¬¾æ–¹å¼
     */
    @ApiModelProperty("付款方式")
    @Excel(name = "付款方式")
    private String paymentMethod;
    /**
     * ä»˜æ¬¾äº‹ç”±
     */
    @ApiModelProperty("付款事由")
    @Excel(name = "付款事由")
    private String paymentContent;
    /**
     * ç”³è¯·æ—¥æœŸ
     */
    @ApiModelProperty("申请日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "申请日期")
    private LocalDate applyDate;
    /**
     * å¤‡æ³¨
     */
    @ApiModelProperty("备注")
    @Excel(name = "备注")
    private String remark;
    /**
     * å®¡æ ¸çŠ¶æ€:0待审核1审核通过2审核不通过
     */
    @ApiModelProperty("审核状态:0待审核1审核通过2审核不通过")
    @Excel(name = "审核状态",readConverterExp = "0=待审核,1=审核通过,2=审核不通过")
    private Integer status;
    /**
     * ä»˜æ¬¾é‡‘额
     */
    @ApiModelProperty("付款金额")
    @Excel(name = "付款金额")
    private BigDecimal paymentAmount;
}
src/main/java/com/ruoyi/account/pojo/purchase/AccountPurchaseInvoice.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,168 @@
package com.ruoyi.account.pojo.purchase;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--进项发票
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 03:06:17
 */
@Getter
@Setter
@ToString
@TableName("account_purchase_invoice")
@ApiModel(value = "AccountPurchaseInvoice对象", description = "财务管理--进项发票")
public class AccountPurchaseInvoice implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * åˆ›å»ºäºº
     */
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    /**
     * ä¿®æ”¹äºº
     */
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    /**
     * ä¿®æ”¹æ—¶é—´
     */
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
    /**
     * éƒ¨é—¨ID
     */
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * å‘票号码
     */
    @ApiModelProperty("发票号码")
    @Excel(name = "发票号码")
    private String invoiceNumber;
    /**
     * ç¨Žçއ
     */
    @ApiModelProperty("税率")
    @Excel(name = "税率")
    private Integer taxRate;
    /**
     * å‘票类型
     */
    @ApiModelProperty("发票类型")
    @Excel(name = "发票类型")
    private String invoiceType;
    /**
     * å¼€ç¥¨æ—¥æœŸ
     */
    @ApiModelProperty("开票日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "开票日期")
    private LocalDate issueDate;
    /**
     * é‡‘额(不含税)
     */
    @ApiModelProperty("金额(不含税)")
    @Excel(name = "金额(不含税)")
    private BigDecimal taxExclusivelPrice;
    /**
     * ç¨Žé¢
     */
    @ApiModelProperty("税额")
    @Excel(name = "税额")
    private BigDecimal taxPrice;
    /**
     * ä»·ç¨Žåˆè®¡
     */
    @ApiModelProperty("价税合计")
    @Excel(name = "价税合计")
    private BigDecimal taxInclusivePrice;
    /**
     * å¤‡æ³¨
     */
    @ApiModelProperty("备注")
    @Excel(name = "备注")
    private String remark;
    /**
     * å‘票内容
     */
    @ApiModelProperty("发票内容")
    @Excel(name = "发票内容")
    private String invoiceContent;
    /**
     * ä¾›åº”商id
     */
    @ApiModelProperty("供应商id")
    private Integer supplierId;
    /**
     * å…³è”上传的发票附件id
     */
    @ApiModelProperty("关联上传的发票附件id")
    private Integer storageAttachmentId;
    /**
     * å…³è”入库单id(多选)
     */
    @ApiModelProperty("关联入库单id(多选)")
    private String stockInRecordIds;
    /**
     * çŠ¶æ€ 0启用 1禁用
     */
    @ApiModelProperty("状态 0启用 1禁用")
    @Excel(name = "状态", readConverterExp = "0=正常,1=作废")
    private Integer status;
}
src/main/java/com/ruoyi/account/pojo/purchase/AccountPurchasePayment.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
package com.ruoyi.account.pojo.purchase;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--付款单
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 04:14:51
 */
@Getter
@Setter
@ToString
@TableName("account_purchase_payment")
@ApiModel(value = "AccountPurchasePayment对象", description = "财务管理--付款单")
public class AccountPurchasePayment implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * å…³è”付款申请id
     */
    @ApiModelProperty("关联付款申请id")
    private Integer accountPaymentApplicationId;
    /**
     * åˆ›å»ºäºº
     */
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    /**
     * ä¿®æ”¹äºº
     */
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    /**
     * ä¿®æ”¹æ—¶é—´
     */
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
    /**
     * éƒ¨é—¨ID
     */
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * ä¾›åº”商id
     */
    @ApiModelProperty("供应商id")
    private Integer supplierId;
    /**
     * ä»˜æ¬¾æ—¥æœŸ
     */
    @ApiModelProperty("付款日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "付款日期")
    private LocalDate paymentDate;
    /**
     * ä»˜æ¬¾æ–¹å¼
     */
    @ApiModelProperty("付款方式")
    @Excel(name = "付款方式",dictType = "checkout_payment")
    private String paymentMethod;
    /**
     * ä»˜æ¬¾é‡‘额
     */
    @ApiModelProperty("付款金额")
    @Excel(name = "付款金额")
    private BigDecimal paymentAmount;
    /**
     * ä»˜æ¬¾å•号
     */
    @ApiModelProperty("付款单号")
    @Excel(name = "付款单号")
    private String paymentNumber;
    /**
     * å¤‡æ³¨
     */
    @ApiModelProperty("备注")
    @Excel(name = "备注")
    private String remark;
}
src/main/java/com/ruoyi/account/pojo/sales/AccountInvoiceApplication.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,138 @@
package com.ruoyi.account.pojo.sales;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--开票申请
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 01:38:32
 */
@Getter
@Setter
@ToString
@TableName("account_invoice_application")
@ApiModel(value = "AccountInvoiceApplication对象", description = "财务管理--开票申请")
public class AccountInvoiceApplication implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * åˆ›å»ºäºº
     */
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    /**
     * ä¿®æ”¹äºº
     */
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    /**
     * ä¿®æ”¹æ—¶é—´
     */
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
    /**
     * éƒ¨é—¨ID
     */
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * å®¢æˆ·id
     */
    @ApiModelProperty("客户id")
    private Integer customerId;
    /**
     * å…³è”出库单id(多选)
     */
    @ApiModelProperty("关联出库单id(多选)")
    private String stockOutRecordIds;
    /**
     * å¼€ç¥¨ç”³è¯·å•号
     */
    @ApiModelProperty("开票申请单号")
    @Excel(name = "开票申请单号")
    private String invoiceApplicationNo;
    /**
     * å‘票类型
     */
    @ApiModelProperty("发票类型")
    @Excel(name = "发票类型")
    private String invoiceType;
    /**
     * ç”³è¯·æ—¥æœŸ
     */
    @ApiModelProperty("申请日期")
    @Excel(name = "申请日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate applyDate;
    /**
     * å‘票内容
     */
    @ApiModelProperty("发票内容")
    @Excel(name = "发票内容")
    private String invoiceContent;
    /**
     * å¤‡æ³¨
     */
    @ApiModelProperty("备注")
    @Excel(name = "备注")
    private String remark;
    /**
     * å®¡æ ¸çŠ¶æ€:0待审核1审核通过2审核不通过
     */
    @ApiModelProperty("审核状态:0待审核1审核通过2审核不通过")
    @Excel(name = "审核状态",readConverterExp = "0=待审核,1=审核通过,2=审核不通过")
    private Integer status;
    @ApiModelProperty("开票金额")
    @Excel(name = "开票金额")
    private BigDecimal invoiceAmount;
    @ApiModelProperty("税率")
    @Excel(name = "税率")
    private BigDecimal taxRate;
}
src/main/java/com/ruoyi/account/pojo/sales/AccountSalesCollection.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
package com.ruoyi.account.pojo.sales;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--收款单
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:49:56
 */
@Getter
@Setter
@ToString
@TableName("account_sales_collection")
@ApiModel(value = "AccountSalesCollection对象", description = "财务管理--收款单")
public class AccountSalesCollection implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * å…³è”出库单id(多选)
     */
    @ApiModelProperty("关联出库单id(多选)")
    private String stockOutRecordIds;
    /**
     * åˆ›å»ºäºº
     */
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    /**
     * ä¿®æ”¹äºº
     */
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    /**
     * ä¿®æ”¹æ—¶é—´
     */
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
    /**
     * éƒ¨é—¨ID
     */
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * å®¢æˆ·id
     */
    @ApiModelProperty("客户id")
    private Integer customerId;
    /**
     * æ”¶æ¬¾æ—¥æœŸ
     */
    @ApiModelProperty("收款日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "收款日期")
    private LocalDate collectionDate;
    /**
     * æ”¶æ¬¾é‡‘额
     */
    @ApiModelProperty("收款金额")
    @Excel(name = "收款金额")
    private BigDecimal collectionAmount;
    /**
     * æ”¶æ¬¾æ–¹å¼
     */
    @ApiModelProperty("收款方式")
    @Excel(name = "收款方式",dictType = "payment_methods")
    private String collectionMethod;
    /**
     * æ”¶æ¬¾å•号
     */
    @ApiModelProperty("收款单号")
    @Excel(name = "收款单号")
    private String collectionNumber;
    /**
     * å¤‡æ³¨
     */
    @ApiModelProperty("备注")
    @Excel(name = "备注")
    private String remark;
}
src/main/java/com/ruoyi/account/pojo/sales/AccountSalesInvoice.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,168 @@
package com.ruoyi.account.pojo.sales;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--销项发票
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:10:20
 */
@Getter
@Setter
@ToString
@TableName("account_sales_invoice")
@ApiModel(value = "AccountSalesInvoice对象", description = "财务管理--销项发票")
public class AccountSalesInvoice implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * å…³è”开票申请id
     */
    @ApiModelProperty("关联开票申请id")
    private Integer accountInvoiceApplicationId;
    /**
     * åˆ›å»ºäºº
     */
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    /**
     * ä¿®æ”¹äºº
     */
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    /**
     * ä¿®æ”¹æ—¶é—´
     */
    @ApiModelProperty("修改时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
    /**
     * éƒ¨é—¨ID
     */
    @ApiModelProperty("部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * å‘票号码
     */
    @ApiModelProperty("发票号码")
    @Excel(name = "发票号码")
    private String invoiceNumber;
    /**
     * ç¨Žçއ
     */
    @ApiModelProperty("税率")
    @Excel(name = "税率")
    private BigDecimal taxRate;
    /**
     * å‘票类型
     */
    @ApiModelProperty("发票类型")
    @Excel(name = "发票类型")
    private String invoiceType;
    /**
     * å¼€ç¥¨æ—¥æœŸ
     */
    @ApiModelProperty("开票日期")
    @Excel(name = "开票日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate issueDate;
    /**
     * é‡‘额(不含税)
     */
    @ApiModelProperty("金额(不含税)")
    @Excel(name = "金额(不含税)")
    private BigDecimal taxExclusivelPrice;
    /**
     * ç¨Žé¢
     */
    @ApiModelProperty("税额")
    @Excel(name = "税额")
    private BigDecimal taxPrice;
    /**
     * ä»·ç¨Žåˆè®¡
     */
    @ApiModelProperty("价税合计")
    @Excel(name = "价税合计")
    private BigDecimal taxInclusivePrice;
    /**
     * å¤‡æ³¨
     */
    @ApiModelProperty("备注")
    @Excel(name = "备注")
    private String remark;
    /**
     * å‘票内容
     */
    @ApiModelProperty("发票内容")
    @Excel(name = "发票内容")
    private String invoiceContent;
    /**
     * å®¢æˆ·id
     */
    @ApiModelProperty("客户id")
    private Integer customerId;
    /**
     * å…³è”上传的发票附件id
     */
    @ApiModelProperty("关联上传的发票附件id")
    private Integer storageAttachmentId;
    /**
     * çŠ¶æ€ 0启用 1禁用
     */
    @ApiModelProperty("状态 0启用 1禁用")
    @Excel(name = "状态", readConverterExp = "0=正常,1=作废")
    private Integer status;
}
src/main/java/com/ruoyi/account/service/AccountExpenseService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/AccountFileService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/AccountIncomeService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/AccountStatementDetailsService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.account.service;
import com.ruoyi.account.pojo.AccountStatementDetails;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--对账单明细 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 10:12:42
 */
public interface AccountStatementDetailsService extends IService<AccountStatementDetails> {
}
src/main/java/com/ruoyi/account/service/AccountStatementService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package com.ruoyi.account.service;
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.StatementAccountDto;
import com.ruoyi.account.bean.vo.StatementAccountVo;
import com.ruoyi.account.pojo.AccountStatement;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * <p>
 *  æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 09:42:47
 */
public interface AccountStatementService extends IService<AccountStatement> {
    StatementAccountVo getAccountStatementDetailsByMonth(StatementAccountDto statementAccountDto);
    boolean addAccountStatement(StatementAccountVo statementAccountVo);
    boolean deleteAccountStatement(List<Long> ids);
    IPage<StatementAccountVo> listPageAccountStatement(Page page, StatementAccountDto statementAccountDto);
    void exportAccountStatement(HttpServletResponse response, StatementAccountDto statementAccountDto);
}
src/main/java/com/ruoyi/account/service/AccountingService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.account.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.AccountReportDto;
import com.ruoyi.account.bean.vo.AccountReportVo;
import com.ruoyi.framework.web.domain.AjaxResult;
public interface AccountingService {
    AjaxResult total(Integer year);
    AjaxResult deviceTypeDistribution(Integer year);
    AjaxResult calculateDepreciation(Page page, Integer year);
    AccountReportVo getAccountStatementDetailsByMonth(AccountReportDto accountReportDto);
}
src/main/java/com/ruoyi/account/service/BorrowInfoService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/SalesReceiptReturnService.java
@@ -1,7 +1,7 @@
package com.ruoyi.account.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.account.dto.SalesReceiptReturnDto;
import com.ruoyi.account.bean.dto.SalesReceiptReturnDto;
import com.ruoyi.account.pojo.SalesReceiptReturn;
import com.baomidou.mybatisplus.extension.service.IService;
src/main/java/com/ruoyi/account/service/SalesRefundAmountOrderService.java
@@ -2,7 +2,7 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.dto.SalesRefundAmountOrderDto;
import com.ruoyi.account.bean.dto.SalesRefundAmountOrderDto;
import com.ruoyi.account.pojo.SalesRefundAmountOrder;
import com.baomidou.mybatisplus.extension.service.IService;
src/main/java/com/ruoyi/account/service/financial/AccountPurchaseService.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.ruoyi.account.bean.dto.purchase.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.purchase.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/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/AccountExpenseServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/impl/AccountFileServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/impl/AccountIncomeServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/impl/AccountStatementDetailsServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.account.service.impl;
import com.ruoyi.account.pojo.AccountStatementDetails;
import com.ruoyi.account.mapper.AccountStatementDetailsMapper;
import com.ruoyi.account.service.AccountStatementDetailsService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--对账单明细 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 10:12:42
 */
@Service
public class AccountStatementDetailsServiceImpl extends ServiceImpl<AccountStatementDetailsMapper, AccountStatementDetails> implements AccountStatementDetailsService {
}
src/main/java/com/ruoyi/account/service/impl/AccountStatementServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,298 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
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.account.bean.dto.StatementAccountDto;
import com.ruoyi.account.bean.dto.purchase.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto;
import com.ruoyi.account.bean.dto.sales.SalesOutboundDto;
import com.ruoyi.account.bean.dto.sales.SalesReturnDto;
import com.ruoyi.account.bean.vo.StatementAccountVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.bean.vo.sales.SalesReturnVo;
import com.ruoyi.account.mapper.AccountStatementDetailsMapper;
import com.ruoyi.account.mapper.AccountStatementMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.account.pojo.AccountStatementDetails;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.account.service.AccountStatementService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
 * <p>
 *  æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 09:42:47
 */
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class AccountStatementServiceImpl extends ServiceImpl<AccountStatementMapper, AccountStatement> implements AccountStatementService {
    private final AccountStatementMapper accountStatementMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final StockOutRecordMapper stockOutRecordMapper;
    private final StockInRecordMapper stockInRecordMapper;
    private final ReturnManagementMapper returnManagementMapper;
    private final AccountStatementDetailsMapper accountStatementDetailsMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
    private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd");
    @Override
    public StatementAccountVo getAccountStatementDetailsByMonth(StatementAccountDto statementAccountDto) {
        //对账月份转换成开始日期和结束日期区间
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
        YearMonth yearMonth = YearMonth.parse(statementAccountDto.getStatementMonth(), formatter);
        statementAccountDto.setStartDate(yearMonth.atDay(1));
        statementAccountDto.setEndDate(yearMonth.atEndOfMonth());
        if (statementAccountDto.getAccountType() == 1){
            //应收对账--Customer
            return getAccountStatementDetailsByCustomerAndMonth(statementAccountDto);
        }else {
            //应付对账--SupplierManage
            return getAccountStatementDetailsBySupplierAndMonth(statementAccountDto);
        }
    }
    @Override
    public boolean addAccountStatement(StatementAccountVo statementAccountVo) {
        //同一客户或者同一供应商,一个月份只能有一个对账单
        List<AccountStatement> accountStatements = accountStatementMapper.selectList(Wrappers.<AccountStatement>lambdaQuery()
                .eq(AccountStatement::getStatementMonth, statementAccountVo.getStatementMonth())
                .eq(AccountStatement::getAccountType, statementAccountVo.getAccountType())
                .eq(AccountStatement::getCustomerId, statementAccountVo.getCustomerId()));
        if (CollectionUtils.isNotEmpty(accountStatements)){
            throw new ServiceException("同一客户或者同一供应商,一个月份只能有一个对账单");
        }
        AccountStatement accountStatement = new AccountStatement();
        BeanUtils.copyProperties(statementAccountVo, accountStatement);
        accountStatement.setStatementNumber(genStatementAccountNo());
        boolean save = save(accountStatement);
        statementAccountVo.getAccountStatementDetails().stream().forEach(accountStatementDetails -> {
            accountStatementDetails.setAccountStatementId(accountStatement.getId());
            //添加对账单明细
            accountStatementDetailsMapper.insert(accountStatementDetails);
        });
        return save;
    }
    @Override
    public boolean deleteAccountStatement(List<Long> ids) {
        //删除对账单明细
        accountStatementDetailsMapper.delete(Wrappers.<AccountStatementDetails>lambdaQuery().in(AccountStatementDetails::getAccountStatementId, ids));
        return removeByIds(ids);
    }
    @Override
    public IPage<StatementAccountVo> listPageAccountStatement(Page page, StatementAccountDto statementAccountDto) {
        return accountStatementMapper.listPageAccountStatement(page, statementAccountDto);
    }
    @Override
    public void exportAccountStatement(HttpServletResponse response, StatementAccountDto statementAccountDto) {
        List<StatementAccountVo> list = accountStatementMapper.listPageAccountStatement(new Page(1,-1),statementAccountDto).getRecords();
        ExcelUtil<StatementAccountVo> util = new ExcelUtil<>(StatementAccountVo.class);
        util.exportExcel(response, list , "对账单");
    }
    //根据客户和月份获取对账详情(销售)
    private StatementAccountVo getAccountStatementDetailsByCustomerAndMonth(StatementAccountDto statementAccountDto) {
        StatementAccountVo statementAccountVo = new StatementAccountVo();
        statementAccountVo.setAccountType(1);//应收对账
        List<AccountStatementDetails> accountStatementDetailsList = new ArrayList<>();
        /*查询出库明细*/
        SalesOutboundDto salesOutboundDto = new SalesOutboundDto();
        salesOutboundDto.setCustomerId(statementAccountDto.getCustomerId());
        salesOutboundDto.setStartDate(statementAccountDto.getStartDate());
        salesOutboundDto.setEndDate(statementAccountDto.getEndDate());
        List<SalesOutboundVo> salesOutboundVos = stockOutRecordMapper.listPageAccountSales(new Page(1, -1), salesOutboundDto).getRecords();
        salesOutboundVos.stream().forEach(salesOutboundVo -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=出库日期
            accountStatementDetails.setOccurrenceDate(salesOutboundVo.getShippingDate());
            //单据编号=出库单号
            accountStatementDetails.setReceiptNumber(salesOutboundVo.getOutboundBatches());
            //类型=出库
            accountStatementDetails.setType(1);
            //金额=出库金额
            accountStatementDetails.setAmount(salesOutboundVo.getOutboundAmount());
            //备注
            accountStatementDetails.setRemark("产品销售出库,产品:"+salesOutboundVo.getProductName());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        /*查询收款明细*/
        List<AccountSalesCollection> accountSalesCollections = accountSalesCollectionMapper.selectList(Wrappers.<AccountSalesCollection>lambdaQuery()
                .eq(AccountSalesCollection::getCustomerId, statementAccountDto.getCustomerId())
                .between(AccountSalesCollection::getCollectionDate, statementAccountDto.getStartDate(), statementAccountDto.getEndDate()));
        accountSalesCollections.stream().forEach(accountSalesCollection -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=收款日期
            accountStatementDetails.setOccurrenceDate(accountSalesCollection.getCollectionDate());
            //单据编号=收款单号
            accountStatementDetails.setReceiptNumber(accountSalesCollection.getCollectionNumber());
            //类型=收款
            accountStatementDetails.setType(3);
            //金额=收款金额
            accountStatementDetails.setAmount(accountSalesCollection.getCollectionAmount());
            //备注
            accountStatementDetails.setRemark("客户回款,备注:"+accountSalesCollection.getRemark());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        /*查询退货明细*/
        SalesReturnDto salesReturnDto = new SalesReturnDto();
        salesReturnDto.setCustomerId(statementAccountDto.getCustomerId());
        salesReturnDto.setStartDate(statementAccountDto.getStartDate());
        salesReturnDto.setEndDate(statementAccountDto.getEndDate());
        List<SalesReturnVo> salesReturnVos = returnManagementMapper.listPageAccountSalesReturn(new Page(1, -1), salesReturnDto).getRecords();
        salesReturnVos.stream().forEach(salesReturnVo -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=退货日期
            accountStatementDetails.setOccurrenceDate(salesReturnVo.getMakeTime().toLocalDate());
            //单据编号=退货单号
            accountStatementDetails.setReceiptNumber(salesReturnVo.getReturnNo());
            //类型=退货
            accountStatementDetails.setType(5);
            //金额=退款金额
            accountStatementDetails.setAmount(salesReturnVo.getRefundAmount());
            //备注
            accountStatementDetails.setRemark("产品退货,原因:"+salesReturnVo.getReturnReason());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        //期初余额=上个月的期末余额
        statementAccountVo.setOpeningBalance(BigDecimal.ZERO);
        List<AccountStatement> accountStatements = accountStatementMapper.selectList(Wrappers.<AccountStatement>lambdaQuery()
                .eq(AccountStatement::getAccountType, 1)
                .eq(AccountStatement::getCustomerId, statementAccountDto.getCustomerId())
                .eq(AccountStatement::getStatementMonth,
                        YearMonth.parse(statementAccountDto.getStatementMonth()).minusMonths(1).toString()));
        if (CollectionUtils.isNotEmpty(accountStatements)){
            statementAccountVo.setOpeningBalance(accountStatements.get(accountStatements.size() - 1).getClosingBalance());
        }
        //本期应收=出库-退货金额累计
        statementAccountVo.setCurrentPlan(salesOutboundVos.stream().map(SalesOutboundVo::getOutboundAmount).reduce(BigDecimal.ZERO, BigDecimal::add)
                .subtract(salesReturnVos.stream().map(SalesReturnVo::getRefundAmount).reduce(BigDecimal.ZERO, BigDecimal::add)));
        //本期收款=收款金额累计
        statementAccountVo.setCurrentActually(accountSalesCollections.stream().map(AccountSalesCollection::getCollectionAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
        //期末余额=期初+应收-收款
        statementAccountVo.setClosingBalance(statementAccountVo.getOpeningBalance().add(statementAccountVo.getCurrentPlan()).subtract(statementAccountVo.getCurrentActually()));
        statementAccountVo.setAccountStatementDetails(accountStatementDetailsList);
        return statementAccountVo;
    }
    //根据供应商和月份获取对账详情(采购)
    private StatementAccountVo getAccountStatementDetailsBySupplierAndMonth(StatementAccountDto statementAccountDto) {
        StatementAccountVo statementAccountVo = new StatementAccountVo();
        statementAccountVo.setAccountType(2);//应付对账
        List<AccountStatementDetails> accountStatementDetailsList = new ArrayList<>();
        /*查询入库明细*/
        PurchaseInboundDto purchaseInboundDto = new PurchaseInboundDto();
        purchaseInboundDto.setSupplierId(statementAccountDto.getCustomerId());
        purchaseInboundDto.setStartDate(statementAccountDto.getStartDate());
        purchaseInboundDto.setEndDate(statementAccountDto.getEndDate());
        List<PurchaseInboundVo> purchaseInboundVos = stockInRecordMapper.listPageAccountPurchase(new Page(1, -1), purchaseInboundDto).getRecords();
        purchaseInboundVos.stream().forEach(purchaseInboundVo -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=入库日期
            accountStatementDetails.setOccurrenceDate(purchaseInboundVo.getInboundDate());
            //单据编号=入库单号
            accountStatementDetails.setReceiptNumber(purchaseInboundVo.getInboundBatches());
            //类型=入库
            accountStatementDetails.setType(2);
            //金额=入库金额
            accountStatementDetails.setAmount(purchaseInboundVo.getInboundAmount());
            //备注
            accountStatementDetails.setRemark("产品采购入库,产品:"+purchaseInboundVo.getProductName());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        /*查询付款明细*/
        List<AccountPurchasePayment> accountPurchasePayments = accountPurchasePaymentMapper.selectList(Wrappers.<AccountPurchasePayment>lambdaQuery()
                .eq(AccountPurchasePayment::getSupplierId, statementAccountDto.getCustomerId())
                .between(AccountPurchasePayment::getPaymentDate, statementAccountDto.getStartDate(), statementAccountDto.getEndDate()));
        accountPurchasePayments.stream().forEach(accountPurchasePayment -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=付款日期
            accountStatementDetails.setOccurrenceDate(accountPurchasePayment.getPaymentDate());
            //单据编号=付款单号
            accountStatementDetails.setReceiptNumber(accountPurchasePayment.getPaymentNumber());
            //类型=付款
            accountStatementDetails.setType(4);
            //金额=付款金额
            accountStatementDetails.setAmount(accountPurchasePayment.getPaymentAmount());
            //备注
            accountStatementDetails.setRemark("支付货款,备注:"+accountPurchasePayment.getRemark());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        /*查询退货明细*/
        PurchaseReturnDto purchaseReturnDto = new PurchaseReturnDto();
        purchaseReturnDto.setSupplierId(statementAccountDto.getCustomerId());
        purchaseReturnDto.setStartDate(statementAccountDto.getStartDate());
        purchaseReturnDto.setEndDate(statementAccountDto.getEndDate());
        List<PurchaseReturnVo> purchaseReturnVos = purchaseReturnOrdersMapper.listPageAccountPurchaseReturn(new Page(1, -1), purchaseReturnDto).getRecords();
        purchaseReturnVos.stream().forEach(purchaseReturnVo -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=退货日期
            accountStatementDetails.setOccurrenceDate(purchaseReturnVo.getPreparedAt().toLocalDate());
            //单据编号=退货单号
            accountStatementDetails.setReceiptNumber(purchaseReturnVo.getReturnNo());
            //类型=退货
            accountStatementDetails.setType(5);
            //金额=退款金额
            accountStatementDetails.setAmount(purchaseReturnVo.getTotalAmount());
            //备注
            accountStatementDetails.setRemark("产品退货,退货方式:"+purchaseReturnVo.getReturnType());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        //期初余额=上个月的期末余额
        statementAccountVo.setOpeningBalance(BigDecimal.ZERO);
        List<AccountStatement> accountStatements = accountStatementMapper.selectList(Wrappers.<AccountStatement>lambdaQuery()
                .eq(AccountStatement::getAccountType, 2)
                .eq(AccountStatement::getCustomerId, statementAccountDto.getCustomerId())
                .eq(AccountStatement::getStatementMonth,
                        YearMonth.parse(statementAccountDto.getStatementMonth()).minusMonths(1).toString()));
        if (CollectionUtils.isNotEmpty(accountStatements)){
            statementAccountVo.setOpeningBalance(accountStatements.get(accountStatements.size() - 1).getClosingBalance());
        }
        //本期应付=入库-退货金额累计
        statementAccountVo.setCurrentPlan(purchaseInboundVos.stream().map(PurchaseInboundVo::getInboundAmount).reduce(BigDecimal.ZERO, BigDecimal::add)
                .subtract(purchaseReturnVos.stream().map(PurchaseReturnVo::getTotalAmount).reduce(BigDecimal.ZERO, BigDecimal::add)));
        //本期付款=付款金额累计
        statementAccountVo.setCurrentActually(accountPurchasePayments.stream().map(AccountPurchasePayment::getPaymentAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
        //期末余额=期初+应收-收款
        statementAccountVo.setClosingBalance(statementAccountVo.getOpeningBalance().add(statementAccountVo.getCurrentPlan()).subtract(statementAccountVo.getCurrentActually()));
        statementAccountVo.setAccountStatementDetails(accountStatementDetailsList);
        return statementAccountVo;
    }
    private String genStatementAccountNo() {
        return "DZ" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
}
src/main/java/com/ruoyi/account/service/impl/AccountingServiceImpl.java
@@ -3,27 +3,47 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.dto.DeviceTypeDetail;
import com.ruoyi.account.dto.DeviceTypeDistributionVO;
import com.ruoyi.account.mapper.BorrowInfoMapper;
import com.ruoyi.account.pojo.BorrowInfo;
import com.ruoyi.account.bean.dto.AccountReportDto;
import com.ruoyi.account.bean.dto.DeviceTypeDetail;
import com.ruoyi.account.bean.dto.DeviceTypeDistributionVO;
import com.ruoyi.account.bean.dto.purchase.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto;
import com.ruoyi.account.bean.dto.sales.SalesOutboundDto;
import com.ruoyi.account.bean.dto.sales.SalesReturnDto;
import com.ruoyi.account.bean.vo.AccountReportVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.bean.vo.sales.SalesReturnVo;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.account.service.AccountingService;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.mapper.CustomStorageMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.procurementrecord.pojo.CustomStorage;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Year;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
@@ -35,14 +55,21 @@
@Service
@Slf4j
@RequiredArgsConstructor
public class AccountingServiceImpl {
public class AccountingServiceImpl implements AccountingService {
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final BorrowInfoMapper borrowInfoMapper;
    private final CustomStorageMapper customStorageMapper;
    private final ProcurementRecordMapper procurementRecordMapper;
    private final ProcurementRecordOutMapper procurementRecordOutMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final StockOutRecordMapper stockOutRecordMapper;
    private final ReturnManagementMapper returnManagementMapper;
    private final StockInRecordMapper stockInRecordMapper;
    private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
    @Override
    public AjaxResult total(Integer year) {
        Map<String,Object> map = new HashMap<>();
        map.put("deprAmount",0); // æŠ˜æ—§é‡‘额
@@ -75,17 +102,8 @@
            map.put("netValue",reduce.subtract(total));
        }
        // è´Ÿå€º
        LambdaQueryWrapper<BorrowInfo> borrowInfoLambdaQueryWrapper = new LambdaQueryWrapper<>();
        borrowInfoLambdaQueryWrapper.like(BorrowInfo::getCreateTime,year)
                .eq(BorrowInfo::getStatus,1);
        List<BorrowInfo> borrowInfos = borrowInfoMapper.selectList(borrowInfoLambdaQueryWrapper);
        if(CollectionUtils.isNotEmpty(borrowInfos)){
            BigDecimal reduce = borrowInfos.stream()
                    .map(BorrowInfo::getBorrowAmount)
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            map.put("debt",reduce);
        }
        map.put("debt",BigDecimal.ZERO);
        // åº“存资产
        LambdaQueryWrapper<ProcurementRecordStorage> procurementRecordStorageLambdaQueryWrapper = new LambdaQueryWrapper<>();
        procurementRecordStorageLambdaQueryWrapper.like(ProcurementRecordStorage::getCreateTime,year);
@@ -245,6 +263,7 @@
        return totalDepreciation.setScale(2, BigDecimal.ROUND_HALF_UP);
    }
    @Override
    public AjaxResult deviceTypeDistribution(Integer year) {
        // 2. ç»„装返回VO
       DeviceTypeDistributionVO vo = new DeviceTypeDistributionVO();
@@ -268,6 +287,7 @@
        return AjaxResult.success(vo);
    }
    @Override
    public AjaxResult calculateDepreciation(Page page, Integer year) {
        LambdaQueryWrapper<DeviceLedger> deviceLedgerLambdaQueryWrapper = new LambdaQueryWrapper<>();
        deviceLedgerLambdaQueryWrapper.like(DeviceLedger::getCreateTime,year)
@@ -279,4 +299,159 @@
        }
        return AjaxResult.success(deviceLedgerIPage);
    }
    @Override
    public AccountReportVo getAccountStatementDetailsByMonth(AccountReportDto accountReportDto) {
        AccountReportVo accountReportVo = new AccountReportVo();
        LocalDate start = accountReportDto.getEntryDateStart();
        LocalDate end = accountReportDto.getEntryDateEnd();
        DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM");
        // ========== 1. é¡¶éƒ¨å¡ç‰‡æ•°æ® ==========
        // 1.1 æ€»è¥æ”¶ = æ”¶æ¬¾å•总金额
        List<AccountSalesCollection> accountSalesCollections = accountSalesCollectionMapper.selectList(
                Wrappers.<AccountSalesCollection>lambdaQuery()
                        .between(AccountSalesCollection::getCollectionDate, start, end)
        );
        BigDecimal totalIncome = Optional.of(
                accountSalesCollections.stream()
                        .map(AccountSalesCollection::getCollectionAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setTotalIncome(totalIncome);
        // 1.2 æ€»æ”¯å‡º = ä»˜æ¬¾å•总金额
        List<AccountPurchasePayment> accountPurchasePayments = accountPurchasePaymentMapper.selectList(
                Wrappers.<AccountPurchasePayment>lambdaQuery()
                        .between(AccountPurchasePayment::getPaymentDate, start, end)
        );
        BigDecimal totalExpense = Optional.of(
                accountPurchasePayments.stream()
                        .map(AccountPurchasePayment::getPaymentAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setTotalExpense(totalExpense);
        // 1.3 åº”收账款 = é”€å”®å‡ºåº“金额合计 - é”€å”®é€€è´§é‡‘额合计
        SalesOutboundDto salesOutboundDto = new SalesOutboundDto();
        salesOutboundDto.setStartDate(accountReportDto.getEntryDateStart());
        salesOutboundDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<SalesOutboundVo> salesOutboundVos = stockOutRecordMapper.listPageAccountSales(new Page(1, -1), salesOutboundDto).getRecords();
        BigDecimal salesOutAmount = Optional.of(
                salesOutboundVos.stream()
                        .map(SalesOutboundVo::getOutboundAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        SalesReturnDto salesReturnDto = new SalesReturnDto();
        salesReturnDto.setStartDate(accountReportDto.getEntryDateStart());
        salesReturnDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<SalesReturnVo> salesReturnVos = returnManagementMapper.listPageAccountSalesReturn(new Page(1, -1), salesReturnDto).getRecords();
        BigDecimal salesReturnAmount = Optional.of(
                salesReturnVos.stream()
                        .map(SalesReturnVo::getRefundAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setAccountsReceivable(salesOutAmount.subtract(salesReturnAmount));
        // 1.4 åº”付账款 = é‡‡è´­å…¥åº“金额合计 - é‡‡è´­é€€è´§é‡‘额合计
        PurchaseInboundDto purchaseInboundDto = new PurchaseInboundDto();
        purchaseInboundDto.setStartDate(accountReportDto.getEntryDateStart());
        purchaseInboundDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<PurchaseInboundVo> purchaseInboundVos = stockInRecordMapper.listPageAccountPurchase(new Page(1, -1), purchaseInboundDto).getRecords();
        BigDecimal purchaseInAmount = Optional.of(
                purchaseInboundVos.stream()
                        .map(PurchaseInboundVo::getInboundAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        PurchaseReturnDto purchaseReturnDto = new PurchaseReturnDto();
        purchaseReturnDto.setStartDate(accountReportDto.getEntryDateStart());
        purchaseReturnDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<PurchaseReturnVo> purchaseReturnVos = purchaseReturnOrdersMapper.listPageAccountPurchaseReturn(new Page(1, -1), purchaseReturnDto).getRecords();
        BigDecimal purchaseReturnAmount = Optional.of(
                purchaseReturnVos.stream()
                        .map(PurchaseReturnVo::getTotalAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setAccountsPayable(purchaseInAmount.subtract(purchaseReturnAmount));
        // 1.5 å‡€åˆ©æ¶¦ = æ€»è¥æ”¶ - æ€»æ”¯å‡º
        BigDecimal netProfit = totalIncome.subtract(totalExpense);
        accountReportVo.setNetRevenue(netProfit);
        // ========== 2. æŠ˜çº¿å›¾ï¼šæœˆåº¦è¥æ”¶/支出/净利润趋势 ==========
        Map<String, BigDecimal> monthIncomeMap = new HashMap<>();
        Map<String, BigDecimal> monthExpenseMap = new HashMap<>();
        // æœˆåº¦è¥æ”¶
        accountSalesCollections.forEach(item -> {
            String month = item.getCollectionDate().format(monthFormatter);
            monthIncomeMap.put(month, monthIncomeMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getCollectionAmount()).orElse(BigDecimal.ZERO)));
        });
        // æœˆåº¦æ”¯å‡º
        accountPurchasePayments.forEach(item -> {
            String month = item.getPaymentDate().format(monthFormatter);
            monthExpenseMap.put(month, monthExpenseMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getPaymentAmount()).orElse(BigDecimal.ZERO)));
        });
        // ç”Ÿæˆè¿žç»­æœˆä»½åˆ—表
        List<String> monthList = new ArrayList<>();
        LocalDate current = start.withDayOfMonth(1);
        while (!current.isAfter(end.withDayOfMonth(1))) {
            monthList.add(current.format(monthFormatter));
            current = current.plusMonths(1);
        }
        // ç»„装趋势数据
        List<AccountReportVo.MonthlyTrendVO> trendList = new ArrayList<>();
        for (String month : monthList) {
            BigDecimal income = monthIncomeMap.getOrDefault(month, BigDecimal.ZERO);
            BigDecimal expense = monthExpenseMap.getOrDefault(month, BigDecimal.ZERO);
            AccountReportVo.MonthlyTrendVO trend = new AccountReportVo.MonthlyTrendVO();
            trend.setMonth(month);
            trend.setIncome(income);
            trend.setExpense(expense);
            trend.setProfit(income.subtract(expense));
            trendList.add(trend);
        }
        accountReportVo.setMonthlyTrendList(trendList);
        // ========== 3. æŸ±çŠ¶å›¾ï¼šæœˆåº¦åº”æ”¶/应付数据 ==========
        Map<String, BigDecimal> monthReceivableMap = new HashMap<>();
        Map<String, BigDecimal> monthPayableMap = new HashMap<>();
        // æœˆåº¦åº”收(销售出库-退货)
        salesOutboundVos.forEach(item -> {
            String month = item.getShippingDate().format(monthFormatter);
            monthReceivableMap.put(month, monthReceivableMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getOutboundAmount()).orElse(BigDecimal.ZERO)));
        });
        salesReturnVos.forEach(item -> {
            String month = item.getMakeTime().format(monthFormatter);
            monthReceivableMap.put(month, monthReceivableMap.getOrDefault(month, BigDecimal.ZERO)
                    .subtract(Optional.ofNullable(item.getRefundAmount()).orElse(BigDecimal.ZERO)));
        });
        // æœˆåº¦åº”付(采购入库-退货)
        purchaseInboundVos.forEach(item -> {
            String month = item.getInboundDate().format(monthFormatter);
            monthPayableMap.put(month, monthPayableMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getInboundAmount()).orElse(BigDecimal.ZERO)));
        });
        purchaseReturnVos.forEach(item -> {
            String month = item.getPreparedAt().format(monthFormatter);
            monthPayableMap.put(month, monthPayableMap.getOrDefault(month, BigDecimal.ZERO)
                    .subtract(Optional.ofNullable(item.getTotalAmount()).orElse(BigDecimal.ZERO)));
        });
        // ç»„装应收应付数据
        List<AccountReportVo.ReceivablePayableVO> rpList = new ArrayList<>();
        for (String month : monthList) {
            AccountReportVo.ReceivablePayableVO rp = new AccountReportVo.ReceivablePayableVO();
            rp.setMonth(month);
            rp.setReceivable(monthReceivableMap.getOrDefault(month, BigDecimal.ZERO));
            rp.setPayable(monthPayableMap.getOrDefault(month, BigDecimal.ZERO));
            rpList.add(rp);
        }
        accountReportVo.setReceivablePayableList(rpList);
        return accountReportVo;
    }
}
src/main/java/com/ruoyi/account/service/impl/BorrowInfoServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/impl/SalesReceiptReturnServiceImpl.java
@@ -1,7 +1,7 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.account.dto.SalesReceiptReturnDto;
import com.ruoyi.account.bean.dto.SalesReceiptReturnDto;
import com.ruoyi.account.pojo.SalesReceiptReturn;
import com.ruoyi.account.mapper.SalesReceiptReturnMapper;
import com.ruoyi.account.service.SalesReceiptReturnService;
src/main/java/com/ruoyi/account/service/impl/SalesRefundAmountOrderServiceImpl.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.impl.ServiceImpl;
import com.ruoyi.account.dto.SalesRefundAmountOrderDto;
import com.ruoyi.account.bean.dto.SalesRefundAmountOrderDto;
import com.ruoyi.account.mapper.SalesRefundAmountOrderMapper;
import com.ruoyi.account.pojo.SalesRefundAmountOrder;
import com.ruoyi.account.service.SalesRefundAmountOrderService;
src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,397 @@
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.AccountSubjectDto;
import com.ruoyi.account.bean.dto.financial.AccountSubjectImportDto;
import com.ruoyi.account.bean.vo.financial.AccountSubjectVo;
import com.ruoyi.account.mapper.financial.AccountSubjectMapper;
import com.ruoyi.account.pojo.financial.AccountSubject;
import com.ruoyi.account.service.purchase.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;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
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;
/**
 * <p>
 * æ€»è´¦ç§‘目表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-07 04:45:30
 */
@Service
@RequiredArgsConstructor
public class AccountSubjectServiceImpl extends ServiceImpl<AccountSubjectMapper, AccountSubject> implements AccountSubjectService {
    private final AccountSubjectMapper accountSubjectMapper;
    @Override
    public IPage<AccountSubjectVo> baseList(Page<AccountSubjectDto> page, AccountSubjectDto accountSubjectDto) {
        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);
        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<>(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
    public void exportAccountSubject(HttpServletResponse response) {
        List<AccountSubject> list = accountSubjectMapper.selectList(null);
        List<AccountSubjectImportDto> importDtos = list.stream().map(accountSubject -> {
            AccountSubjectImportDto accountSubjectImportDto = new AccountSubjectImportDto();
            BeanUtils.copyProperties(accountSubject, accountSubjectImportDto);
            return accountSubjectImportDto;
        }).collect(Collectors.toList());
        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.orderByDesc(AccountSubject::getSubjectCode).orderByDesc(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.nullsFirst(String::compareTo)).reversed()
                .thenComparing(AccountSubject::getId, Comparator.nullsFirst(Long::compareTo)).reversed());
        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.financial.AccountSubjectMapper;
import com.ruoyi.account.mapper.financial.FinVoucherEntryMapper;
import com.ruoyi.account.mapper.financial.FinVoucherMapper;
import com.ruoyi.account.pojo.financial.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/account/service/impl/purchase/AccountPaymentApplicationServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
package com.ruoyi.account.service.impl.purchase;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
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.account.bean.dto.purchase.AccountPaymentApplicationDto;
import com.ruoyi.account.bean.vo.purchase.AccountPaymentApplicationVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.mapper.purchase.AccountPaymentApplicationMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.pojo.purchase.AccountPaymentApplication;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.service.purchase.AccountPaymentApplicationService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--付款申请 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 03:44:22
 */
@Service
@RequiredArgsConstructor
public class AccountPaymentApplicationServiceImpl extends ServiceImpl<AccountPaymentApplicationMapper, AccountPaymentApplication> implements AccountPaymentApplicationService {
    private final AccountPaymentApplicationMapper accountPaymentApplicationMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyMMddHHmmss");
    @Override
    public IPage<AccountPaymentApplicationVo> listPageAccountPaymentApplication(Page page, AccountPaymentApplicationDto accountPaymentApplicationDto) {
        return accountPaymentApplicationMapper.listPageAccountPaymentApplication(page, accountPaymentApplicationDto);
    }
    @Override
    public List<PurchaseInboundVo> getInboundBatchesBySupplier(Integer supplierId) {
        return accountPaymentApplicationMapper.getInboundBatchesBySupplier(supplierId);
    }
    @Override
    public boolean addAccountPaymentApplication(AccountPaymentApplication accountPaymentApplication) {
        if (StringUtils.isEmpty(accountPaymentApplication.getInvoiceApplicationNo())) {
            accountPaymentApplication.setInvoiceApplicationNo(genPaymentApplicationNo());
        }
        String stockInRecordIds= accountPaymentApplication.getStockInRecordIds();
        if (stockInRecordIds != null && !stockInRecordIds.isEmpty()) {
            List<Long> ids = Arrays.stream(stockInRecordIds.split(","))
                    .map(Long::valueOf)
                    .toList();
            if (accountPaymentApplicationMapper.existsByStockInRecordId(ids)){
                throw new ServiceException("存在重复的入库单");
            }
        }
        return save(accountPaymentApplication);
    }
    @Override
    public boolean deleteAccountPaymentApplication(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new ServiceException("删除失败,请选择要删除的数据");
        }
        //判断是否已经有对应的付款单,如果有则无法删除
        List<AccountPurchasePayment> accountPurchasePayments = accountPurchasePaymentMapper.selectList(Wrappers.<AccountPurchasePayment>lambdaQuery().in(AccountPurchasePayment::getAccountPaymentApplicationId, ids));
        if (CollectionUtils.isNotEmpty(accountPurchasePayments)){
            throw new ServiceException("删除失败,已经有关联的付款单");
        }
        //删除开票申请
        return removeBatchByIds(ids);
    }
    @Override
    public void exportAccountPaymentApplication(HttpServletResponse response, AccountPaymentApplicationDto accountPaymentApplicationDto) {
        List<AccountPaymentApplicationVo> list = accountPaymentApplicationMapper.listPageAccountPaymentApplication(new Page(1,-1),accountPaymentApplicationDto).getRecords();
        ExcelUtil<AccountPaymentApplicationVo> util = new ExcelUtil<>(AccountPaymentApplicationVo.class);
        util.exportExcel(response, list , "付款申请");
    }
    private String genPaymentApplicationNo() {
        return "FK" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
}
src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchaseInvoiceServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package com.ruoyi.account.service.impl.purchase;
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.purchase.AccountPurchaseInvoiceDto;
import com.ruoyi.account.bean.vo.purchase.AccountPurchaseInvoiceVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.mapper.purchase.AccountPurchaseInvoiceMapper;
import com.ruoyi.account.pojo.purchase.AccountPurchaseInvoice;
import com.ruoyi.account.service.purchase.AccountPurchaseInvoiceService;
import com.ruoyi.basic.mapper.StorageAttachmentMapper;
import com.ruoyi.common.utils.poi.ExcelUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--进项发票 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 03:06:17
 */
@Service
@RequiredArgsConstructor
public class AccountPurchaseInvoiceServiceImpl extends ServiceImpl<AccountPurchaseInvoiceMapper, AccountPurchaseInvoice> implements AccountPurchaseInvoiceService {
    private final AccountPurchaseInvoiceMapper accountPurchaseInvoiceMapper;
    private final StorageAttachmentMapper storageAttachmentMapper;
    @Override
    public IPage<AccountPurchaseInvoiceVo> listPageAccountPurchaseInvoice(Page page, AccountPurchaseInvoiceDto accountPurchaseInvoiceDto) {
        return accountPurchaseInvoiceMapper.listPageAccountPurchaseInvoice(page, accountPurchaseInvoiceDto);
    }
    @Override
    public boolean deleteAccountPurchaseInvoice(List<Long> ids) {
        List<AccountPurchaseInvoice> accountPurchaseInvoices = accountPurchaseInvoiceMapper.selectByIds(ids);
        //删除附件
        storageAttachmentMapper.deleteBatchIds(accountPurchaseInvoices.stream().map(AccountPurchaseInvoice::getStorageAttachmentId).toList());
        return removeBatchByIds(ids);
    }
    @Override
    public void exportAccountPurchaseInvoice(HttpServletResponse response, AccountPurchaseInvoiceDto accountPurchaseInvoiceDto) {
        List<AccountPurchaseInvoiceVo> list = accountPurchaseInvoiceMapper.listPageAccountPurchaseInvoice(new Page(1,-1),accountPurchaseInvoiceDto).getRecords();
        ExcelUtil<AccountPurchaseInvoiceVo> util = new ExcelUtil<>(AccountPurchaseInvoiceVo.class);
        util.exportExcel(response, list , "进项发票");
    }
    @Override
    public List<PurchaseInboundVo> getInboundBatchesBySupplier(Integer supplierId) {
        return accountPurchaseInvoiceMapper.getInboundBatchesBySupplier(supplierId);
    }
}
src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchasePaymentServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
package com.ruoyi.account.service.impl.purchase;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.xiaoymin.knife4j.core.util.CollectionUtils;
import com.ruoyi.account.bean.dto.purchase.AccountPurchasePaymentDto;
import com.ruoyi.account.bean.vo.purchase.AccountPurchasePaymentVo;
import com.ruoyi.account.mapper.AccountStatementDetailsMapper;
import com.ruoyi.account.mapper.purchase.AccountPaymentApplicationMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.pojo.AccountStatementDetails;
import com.ruoyi.account.pojo.purchase.AccountPaymentApplication;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.service.purchase.AccountPurchasePaymentService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Random;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--付款单 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 04:14:51
 */
@Service
@RequiredArgsConstructor
public class AccountPurchasePaymentServiceImpl extends ServiceImpl<AccountPurchasePaymentMapper, AccountPurchasePayment> implements AccountPurchasePaymentService {
    private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyMMddHHmmss");
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final AccountStatementDetailsMapper accountStatementDetailsMapper;
    private final AccountPaymentApplicationMapper accountPaymentApplicationMapper;
    @Override
    public IPage<AccountPurchasePaymentVo> listPageAccountPurchasePayment(Page page, AccountPurchasePaymentDto accountPurchasePaymentDto) {
        return accountPurchasePaymentMapper.listPageAccountPurchasePayment(page, accountPurchasePaymentDto);
    }
    @Override
    public boolean addAccountPurchasePayment(AccountPurchasePayment accountPurchasePayment) {
        if (StringUtils.isEmpty(accountPurchasePayment.getPaymentNumber())) {
            accountPurchasePayment.setPaymentNumber(genAccountPurchasePaymentNo());
        }
        //校验累计付款金额不能超过申请金额
        AccountPaymentApplication accountPaymentApplication = accountPaymentApplicationMapper.selectById(accountPurchasePayment.getAccountPaymentApplicationId());
        List<AccountPurchasePayment> accountPurchasePayments = accountPurchasePaymentMapper.selectList(Wrappers.<AccountPurchasePayment>lambdaQuery().eq(AccountPurchasePayment::getAccountPaymentApplicationId, accountPurchasePayment.getAccountPaymentApplicationId()));
        BigDecimal totalPaymentAmount = accountPurchasePayments.stream().map(AccountPurchasePayment::getPaymentAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
        if (accountPaymentApplication.getPaymentAmount().compareTo(totalPaymentAmount) < 0) {
            throw new ServiceException("累计付款金额不能超过申请金额");
        }
        return save(accountPurchasePayment);
    }
    @Override
    public void exportAccountPurchasePayment(HttpServletResponse response, AccountPurchasePaymentDto accountPurchasePaymentDto) {
        List<AccountPurchasePaymentVo> list = accountPurchasePaymentMapper.listPageAccountPurchasePayment(new Page(1,-1),accountPurchasePaymentDto).getRecords();
        ExcelUtil<AccountPurchasePaymentVo> util = new ExcelUtil<>(AccountPurchasePaymentVo.class);
        util.exportExcel(response, list , "付款单");
    }
    @Override
    public boolean deleteAccountPurchasePayment(List<Long> ids) {
        //如果该付款单已经生成对账单则无法删除
        List<AccountPurchasePayment> accountPurchasePayments = accountPurchasePaymentMapper.selectByIds(ids);
        List<String> strings = accountPurchasePayments.stream().map(AccountPurchasePayment::getPaymentNumber).toList();
        List<AccountStatementDetails> accountStatementDetails = accountStatementDetailsMapper.selectList(Wrappers.<AccountStatementDetails>lambdaQuery()
                .in(AccountStatementDetails::getReceiptNumber, strings));
        if (CollectionUtils.isNotEmpty(accountStatementDetails)){
            throw new ServiceException("该付款单已经生成对账单,无法删除");
        }
        return removeByIds(ids);
    }
    private String genAccountPurchasePaymentNo() {
        return "SK" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
}
src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchaseServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package com.ruoyi.account.service.impl.purchase;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.purchase.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo;
import com.ruoyi.account.service.financial.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/sales/AccountInvoiceApplicationServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package com.ruoyi.account.service.impl.sales;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.bean.dto.sales.AccountInvoiceApplicationDto;
import com.ruoyi.account.bean.vo.sales.AccountInvoiceApplicationVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.mapper.sales.AccountInvoiceApplicationMapper;
import com.ruoyi.account.mapper.sales.AccountSalesInvoiceMapper;
import com.ruoyi.account.pojo.sales.AccountInvoiceApplication;
import com.ruoyi.account.pojo.sales.AccountSalesInvoice;
import com.ruoyi.account.service.sales.AccountInvoiceApplicationService;
import com.ruoyi.basic.mapper.StorageAttachmentMapper;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--开票申请 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 01:38:32
 */
@Service
@RequiredArgsConstructor
public class AccountInvoiceApplicationServiceImpl extends ServiceImpl<AccountInvoiceApplicationMapper, AccountInvoiceApplication> implements AccountInvoiceApplicationService {
    private final AccountInvoiceApplicationMapper accountInvoiceApplicationMapper;
    private final AccountSalesInvoiceMapper accountSalesInvoiceMapper;
    private final StorageAttachmentMapper storageAttachmentMapper;
    private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyMMddHHmmss");
    @Override
    public IPage<AccountInvoiceApplicationVo> listPageAccountInvoiceApplication(Page page, AccountInvoiceApplicationDto accountInvoiceApplicationDto) {
        return accountInvoiceApplicationMapper.listPageAccountInvoiceApplication(page, accountInvoiceApplicationDto);
    }
    @Override
    public List<SalesOutboundVo> getOutboundBatchesByCustomer(Integer customerId) {
        return accountInvoiceApplicationMapper.getOutboundBatchesByCustomer(customerId);
    }
    @Override
    public boolean addAccountInvoiceApplication(AccountInvoiceApplication accountInvoiceApplication) {
        if (StringUtils.isEmpty(accountInvoiceApplication.getInvoiceApplicationNo())) {
            accountInvoiceApplication.setInvoiceApplicationNo(genInvoiceApplicationNo());
        }
        String stockOutRecordIds = accountInvoiceApplication.getStockOutRecordIds();
        if (stockOutRecordIds != null && !stockOutRecordIds.isEmpty()) {
            List<Long> ids = Arrays.stream(stockOutRecordIds.split(","))
                    .map(Long::valueOf)
                    .toList();
            if (accountInvoiceApplicationMapper.existsByStockOutRecordId(ids)){
                throw new ServiceException("存在重复的出库单");
            }
        }
        return save(accountInvoiceApplication);
    }
    @Override
    public void exportAccountInvoiceApplication(HttpServletResponse response, AccountInvoiceApplicationDto accountInvoiceApplicationDto) {
        List<AccountInvoiceApplicationVo> list = accountInvoiceApplicationMapper.listPageAccountInvoiceApplication(new Page(1,-1),accountInvoiceApplicationDto).getRecords();
        ExcelUtil<AccountInvoiceApplicationVo> util = new ExcelUtil<>(AccountInvoiceApplicationVo.class);
        util.exportExcel(response, list , "开票申请");
    }
    @Override
    public boolean deleteAccountInvoiceApplication(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            throw new ServiceException("删除失败,请选择要删除的数据");
        }
        //删除相关附件
        List<AccountSalesInvoice> accountSalesInvoices = accountSalesInvoiceMapper.selectList(Wrappers.<AccountSalesInvoice>lambdaQuery().in(AccountSalesInvoice::getAccountInvoiceApplicationId,ids));
        storageAttachmentMapper.deleteBatchIds(accountSalesInvoices.stream().map(AccountSalesInvoice::getStorageAttachmentId).toList());
        //删除相关的销项发票
        accountSalesInvoiceMapper.delete(Wrappers.<AccountSalesInvoice>lambdaQuery().in(AccountSalesInvoice::getAccountInvoiceApplicationId,ids));
        //删除开票申请
        return removeBatchByIds(ids);
    }
    @Override
    public List<AccountInvoiceApplicationVo> getAccountInvoiceApplicationList(Integer customerId) {
        return accountInvoiceApplicationMapper.selectAccountInvoiceApplicationList(customerId);
    }
    private String genInvoiceApplicationNo() {
        return "KP" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
}
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesCollectionServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
package com.ruoyi.account.service.impl.sales;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
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.account.bean.dto.sales.AccountSalesCollectionDto;
import com.ruoyi.account.bean.vo.sales.AccountSalesCollectionVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.mapper.AccountStatementDetailsMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.AccountStatementDetails;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.account.service.sales.AccountSalesCollectionService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--收款单 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:49:56
 */
@Service
@RequiredArgsConstructor
public class AccountSalesCollectionServiceImpl extends ServiceImpl<AccountSalesCollectionMapper, AccountSalesCollection> implements AccountSalesCollectionService {
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final AccountStatementDetailsMapper accountStatementDetailsMapper;
    private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyMMddHHmmss");
    @Override
    public IPage<AccountSalesCollectionVo> listPageAccountSalesCollection(Page page, AccountSalesCollectionDto accountSalesCollectionDto) {
        return accountSalesCollectionMapper.listPageAccountSalesCollection(page, accountSalesCollectionDto);
    }
    @Override
    public boolean addAccountSalesCollection(AccountSalesCollection accountSalesCollection) {
        if (StringUtils.isEmpty(accountSalesCollection.getCollectionNumber())) {
            accountSalesCollection.setCollectionNumber(genAccountSalesCollectionNo());
        }
//        String stockOutRecordIds = accountSalesCollection.getStockOutRecordIds();
//        if (stockOutRecordIds != null && !stockOutRecordIds.isEmpty()) {
//            List<Long> ids = Arrays.stream(stockOutRecordIds.split(","))
//                    .map(Long::valueOf)
//                    .toList();
//            if (accountSalesCollectionMapper.existsByStockOutRecordId(ids)){
//                throw new ServiceException("存在重复的出库单");
//            }
//        }
        return save(accountSalesCollection);
    }
    @Override
    public void exportAccountSalesCollection(HttpServletResponse response, AccountSalesCollectionDto accountSalesCollectionDto) {
        List<AccountSalesCollectionVo> list = accountSalesCollectionMapper.listPageAccountSalesCollection(new Page(1,-1),accountSalesCollectionDto).getRecords();
        ExcelUtil<AccountSalesCollectionVo> util = new ExcelUtil<>(AccountSalesCollectionVo.class);
        util.exportExcel(response, list , "收款单");
    }
    @Override
    public boolean deleteAccountSalesCollection(List<Long> ids) {
        //如果该收款单已经生成对账单则无法删除
        List<AccountSalesCollection> accountSalesCollections = accountSalesCollectionMapper.selectByIds(ids);
        List<String> strings = accountSalesCollections.stream().map(AccountSalesCollection::getCollectionNumber).toList();
        List<AccountStatementDetails> accountStatementDetails = accountStatementDetailsMapper.selectList(Wrappers.<AccountStatementDetails>lambdaQuery()
                .in(AccountStatementDetails::getReceiptNumber, strings));
        if (CollectionUtils.isNotEmpty(accountStatementDetails)){
            throw new ServiceException("该收款单已经生成对账单,无法删除");
        }
        return removeByIds(ids);
    }
    @Override
    public List<SalesOutboundVo> getOutboundBatchesByCustomer(Integer customerId) {
        return accountSalesCollectionMapper.getOutboundBatchesByCustomer(customerId);
    }
    private String genAccountSalesCollectionNo() {
        return "SK" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
}
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesInvoiceServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
package com.ruoyi.account.service.impl.sales;
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.sales.AccountSalesInvoiceDto;
import com.ruoyi.account.bean.vo.sales.AccountSalesInvoiceVo;
import com.ruoyi.account.mapper.sales.AccountSalesInvoiceMapper;
import com.ruoyi.account.pojo.sales.AccountSalesInvoice;
import com.ruoyi.account.service.sales.AccountSalesInvoiceService;
import com.ruoyi.basic.mapper.StorageAttachmentMapper;
import com.ruoyi.common.utils.poi.ExcelUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--销项发票 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:10:20
 */
@Service
@RequiredArgsConstructor
public class AccountSalesInvoiceServiceImpl extends ServiceImpl<AccountSalesInvoiceMapper, AccountSalesInvoice> implements AccountSalesInvoiceService {
    private final AccountSalesInvoiceMapper accountSalesInvoiceMapper;
    private final StorageAttachmentMapper storageAttachmentMapper;
    @Override
    public IPage<AccountSalesInvoiceVo> listPageAccountSalesInvoice(Page page, AccountSalesInvoiceDto accountSalesInvoiceDto) {
        return accountSalesInvoiceMapper.listPageAccountSalesInvoice(page, accountSalesInvoiceDto);
    }
    @Override
    public void exportAccountSalesInvoice(HttpServletResponse response, AccountSalesInvoiceDto accountSalesInvoiceDto) {
        List<AccountSalesInvoiceVo> list = accountSalesInvoiceMapper.listPageAccountSalesInvoice(new Page(1,-1),accountSalesInvoiceDto).getRecords();
        ExcelUtil<AccountSalesInvoiceVo> util = new ExcelUtil<>(AccountSalesInvoiceVo.class);
        util.exportExcel(response, list , "销项发票");
    }
    @Override
    public boolean deleteAccountSalesInvoice(List<Long> ids) {
        List<AccountSalesInvoice> accountSalesInvoices = accountSalesInvoiceMapper.selectByIds(ids);
        //删除附件
        storageAttachmentMapper.deleteBatchIds(accountSalesInvoices.stream().map(AccountSalesInvoice::getStorageAttachmentId).toList());
        return removeBatchByIds(ids);
    }
}
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package com.ruoyi.account.service.impl.sales;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.sales.SalesOutboundDto;
import com.ruoyi.account.bean.dto.sales.SalesReturnDto;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.bean.vo.sales.SalesReturnVo;
import com.ruoyi.account.service.sales.AccountSalesService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
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 AccountSalesServiceImpl  implements AccountSalesService {
    private final StockOutRecordMapper stockOutRecordMapper;
    private final ReturnManagementMapper returnManagementMapper;
    @Override
    public IPage<SalesOutboundVo> listPageAccountSales(Page page, SalesOutboundDto salesOutboundDto) {
        return stockOutRecordMapper.listPageAccountSales(page,salesOutboundDto);
    }
    @Override
    public void exportAccountSalesOutbound(HttpServletResponse response, SalesOutboundDto salesOutboundDto) {
        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> listPageAccountSalesReturn(Page page, SalesReturnDto salesReturnDto) {
        return returnManagementMapper.listPageAccountSalesReturn(page,salesReturnDto);
    }
    @Override
    public void exportAccountSalesReturn(HttpServletResponse response, SalesReturnDto salesReturnDto) {
        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/purchase/AccountPaymentApplicationService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.account.service.purchase;
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.purchase.AccountPaymentApplicationDto;
import com.ruoyi.account.bean.vo.purchase.AccountPaymentApplicationVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.pojo.purchase.AccountPaymentApplication;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--付款申请 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 03:44:22
 */
public interface AccountPaymentApplicationService extends IService<AccountPaymentApplication> {
    IPage<AccountPaymentApplicationVo> listPageAccountPaymentApplication(Page page, AccountPaymentApplicationDto accountPaymentApplicationDto);
    List<PurchaseInboundVo> getInboundBatchesBySupplier(Integer supplierId);
    boolean addAccountPaymentApplication(AccountPaymentApplication accountPaymentApplication);
    boolean deleteAccountPaymentApplication(List<Long> ids);
    void exportAccountPaymentApplication(HttpServletResponse response, AccountPaymentApplicationDto accountPaymentApplicationDto);
}
src/main/java/com/ruoyi/account/service/purchase/AccountPurchaseInvoiceService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.account.service.purchase;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.purchase.AccountPurchaseInvoiceDto;
import com.ruoyi.account.bean.vo.purchase.AccountPurchaseInvoiceVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.pojo.purchase.AccountPurchaseInvoice;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--进项发票 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 03:06:17
 */
public interface AccountPurchaseInvoiceService extends IService<AccountPurchaseInvoice> {
    IPage<AccountPurchaseInvoiceVo> listPageAccountPurchaseInvoice(Page page, AccountPurchaseInvoiceDto accountPurchaseInvoiceDto);
    boolean deleteAccountPurchaseInvoice(List<Long> ids);
    void exportAccountPurchaseInvoice(HttpServletResponse response, AccountPurchaseInvoiceDto accountPurchaseInvoiceDto);
    List<PurchaseInboundVo> getInboundBatchesBySupplier(Integer supplierId);
}
src/main/java/com/ruoyi/account/service/purchase/AccountPurchasePaymentService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package com.ruoyi.account.service.purchase;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.purchase.AccountPurchasePaymentDto;
import com.ruoyi.account.bean.vo.purchase.AccountPurchasePaymentVo;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--付款单 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 04:14:51
 */
public interface AccountPurchasePaymentService extends IService<AccountPurchasePayment> {
    IPage<AccountPurchasePaymentVo> listPageAccountPurchasePayment(Page page, AccountPurchasePaymentDto accountPurchasePaymentDto);
    boolean addAccountPurchasePayment(AccountPurchasePayment accountPurchasePayment);
    void exportAccountPurchasePayment(HttpServletResponse response, AccountPurchasePaymentDto accountPurchasePaymentDto);
    boolean deleteAccountPurchasePayment(List<Long> ids);
}
src/main/java/com/ruoyi/account/service/purchase/AccountSubjectService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package com.ruoyi.account.service.purchase;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.financial.AccountSubjectDto;
import com.ruoyi.account.bean.vo.financial.AccountSubjectVo;
import com.ruoyi.account.pojo.financial.AccountSubject;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * <p>
 * æ€»è´¦ç§‘目表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-07 04:45:30
 */
public interface AccountSubjectService extends IService<AccountSubject> {
    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/sales/AccountInvoiceApplicationService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package com.ruoyi.account.service.sales;
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.sales.AccountInvoiceApplicationDto;
import com.ruoyi.account.bean.vo.sales.AccountInvoiceApplicationVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.pojo.sales.AccountInvoiceApplication;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--开票申请 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 01:38:32
 */
public interface AccountInvoiceApplicationService extends IService<AccountInvoiceApplication> {
    IPage<AccountInvoiceApplicationVo> listPageAccountInvoiceApplication(Page page, AccountInvoiceApplicationDto accountInvoiceApplicationDto);
    List<SalesOutboundVo> getOutboundBatchesByCustomer(Integer customerId);
    boolean addAccountInvoiceApplication(AccountInvoiceApplication accountInvoiceApplication);
    void exportAccountInvoiceApplication(HttpServletResponse response, AccountInvoiceApplicationDto accountInvoiceApplicationDto);
    boolean deleteAccountInvoiceApplication(List<Long> ids);
    List<AccountInvoiceApplicationVo> getAccountInvoiceApplicationList(Integer customerId);
}
src/main/java/com/ruoyi/account/service/sales/AccountSalesCollectionService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.account.service.sales;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.sales.AccountSalesCollectionDto;
import com.ruoyi.account.bean.vo.sales.AccountSalesCollectionVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--收款单 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:49:56
 */
public interface AccountSalesCollectionService extends IService<AccountSalesCollection> {
    IPage<AccountSalesCollectionVo> listPageAccountSalesCollection(Page page, AccountSalesCollectionDto accountSalesCollectionDto);
    boolean addAccountSalesCollection(AccountSalesCollection accountSalesCollection);
    void exportAccountSalesCollection(HttpServletResponse response, AccountSalesCollectionDto accountSalesCollectionDto);
    boolean deleteAccountSalesCollection(List<Long> ids);
    List<SalesOutboundVo> getOutboundBatchesByCustomer(Integer customerId);
}
src/main/java/com/ruoyi/account/service/sales/AccountSalesInvoiceService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.account.service.sales;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.sales.AccountSalesInvoiceDto;
import com.ruoyi.account.bean.vo.sales.AccountSalesInvoiceVo;
import com.ruoyi.account.pojo.sales.AccountSalesInvoice;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--销项发票 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:10:20
 */
public interface AccountSalesInvoiceService extends IService<AccountSalesInvoice> {
    IPage<AccountSalesInvoiceVo> listPageAccountSalesInvoice(Page page, AccountSalesInvoiceDto accountSalesInvoiceDto);
    void exportAccountSalesInvoice(HttpServletResponse response, AccountSalesInvoiceDto accountSalesInvoiceDto);
    boolean deleteAccountSalesInvoice(List<Long> ids);
}
src/main/java/com/ruoyi/account/service/sales/AccountSalesService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.account.service.sales;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.sales.SalesOutboundDto;
import com.ruoyi.account.bean.dto.sales.SalesReturnDto;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.bean.vo.sales.SalesReturnVo;
import jakarta.servlet.http.HttpServletResponse;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†çš„é”€å”®éƒ¨åˆ† æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-07 04:45:30
 */
public interface AccountSalesService  {
    IPage<SalesOutboundVo> listPageAccountSales(Page page, SalesOutboundDto salesOutboundDto);
    void exportAccountSalesOutbound(HttpServletResponse response, SalesOutboundDto salesOutboundDto);
    IPage<SalesReturnVo> listPageAccountSalesReturn(Page page, SalesReturnDto salesReturnDto);
    void exportAccountSalesReturn(HttpServletResponse response, SalesReturnDto salesReturnDto);
}
src/main/java/com/ruoyi/aftersalesservice/pojo/AfterSalesService.java
@@ -115,7 +115,6 @@
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
src/main/java/com/ruoyi/aftersalesservice/service/impl/AfterSalesServiceServiceImpl.java
@@ -11,6 +11,8 @@
import com.ruoyi.aftersalesservice.service.AfterSalesServiceService;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import java.time.LocalDateTime;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
@@ -69,7 +71,7 @@
        if(sysUser == null) throw new RuntimeException("审核人不存在");
        afterSalesServiceNewDto.setCheckNickName(sysUser.getNickName());
        if (StringUtils.isEmpty(afterSalesServiceNewDto.getAfterSalesServiceNo())) {
            String string = OrderUtils.countAfterServiceTodayByCreateTime(afterSalesServiceMapper, "SH_");
            String string = OrderUtils.countAfterServiceTodayByCreateTime(afterSalesServiceMapper, "SH_", afterSalesServiceNewDto.getCreateTime() != null ? afterSalesServiceNewDto.getCreateTime() : LocalDateTime.now());
            afterSalesServiceNewDto.setAfterSalesServiceNo(string);
        }
        return this.save(afterSalesServiceNewDto);
src/main/java/com/ruoyi/ai/assistant/ApproveTodoAgent.java
@@ -3,6 +3,7 @@
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
@@ -16,5 +17,5 @@
public interface ApproveTodoAgent {
    @SystemMessage(fromResource = "approve-todo-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("currentDate") String currentDate);
}
src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
@@ -14,8 +14,9 @@
@Component
public class ApproveTodoIntentExecutor {
    private static final Pattern APPROVE_ID_PATTERN = Pattern.compile("\\b[A-Za-z]*\\d{8,}\\b");
    private static final Pattern LIMIT_PATTERN = Pattern.compile("(前|最近)?(\\d{1,2})条");
    private static final Pattern APPROVE_ID_BY_LABEL_PATTERN = Pattern.compile("(流程编号|流程号|流程ID|审批编号|编号)\\s*[::]?\\s*([A-Za-z0-9_-]{2,64})");
    private static final Pattern APPROVE_ID_PATTERN = Pattern.compile("\\b[A-Za-z]*\\d{6,}[A-Za-z0-9_-]*\\b");
    private static final Pattern LIMIT_PATTERN = Pattern.compile("(前|最近)?\\s*(\\d{1,2})\\s*条");
    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
    private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\d+(?:\\.\\d+)?)");
    private static final Pattern RECENT_RANGE_PATTERN = Pattern.compile("近\\d+(天|周|个月|月|å¹´)");
@@ -34,54 +35,63 @@
        }
        String text = message.trim();
        String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text);
        if (StringUtils.hasText(quickPromptResponse)) {
            return quickPromptResponse;
        }
        String approveId = extractApproveId(text);
        boolean hasApproveId = StringUtils.hasText(approveId) && !isPlaceholderApproveId(approveId);
        String startDate = extractStartDate(text);
        String endDate = extractEndDate(text);
        String timeRange = extractTimeRange(text);
        if (isStatsIntent(text)) {
            return approveTodoTools.getTodoStats(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    extractTimeRange(text)
                    startDate,
                    endDate,
                    timeRange
            );
        }
        if (containsAny(text, "流转", "进度", "节点", "日志", "卡在", "卡到", "当前审批人", "处理记录")) {
            return StringUtils.hasText(approveId)
            return hasApproveId
                    ? approveTodoTools.getTodoProgress(memoryId, approveId)
                    : missingApproveId("todo_progress", "查询审批进度需要提供流程编号。");
        }
        if (containsAny(text, "详情", "明细") && !containsAny(text, "列表")) {
            return StringUtils.hasText(approveId)
            return hasApproveId
                    ? approveTodoTools.getTodoDetail(memoryId, approveId)
                    : missingApproveId("todo_detail", "查询审批详情需要提供流程编号。");
        }
        if (containsAny(text, "取消审核", "撤销审核", "回退审核", "撤销审批", "撤回审批")
                || (containsAny(text, "撤销", "撤回") && containsAny(text, "审批操作", "审核操作"))) {
            return StringUtils.hasText(approveId)
                    ? approveTodoTools.cancelReviewTodo(memoryId, approveId, firstNonBlank(extractTail(text, "原因"), extractTail(text, "备注")))
            return hasApproveId
                    ? approveTodoTools.cancelReviewTodo(memoryId, approveId, extractRemark(text))
                    : missingApproveId("cancel_review_action", "取消审核需要提供流程编号。");
        }
        if (containsAny(text, "删除", "移除")) {
            return StringUtils.hasText(approveId)
            return hasApproveId
                    ? approveTodoTools.deleteTodo(memoryId, approveId)
                    : missingApproveId("delete_action", "删除审批单需要提供流程编号。");
        }
        if (containsAny(text, "驳回", "拒绝")) {
            return StringUtils.hasText(approveId)
                    ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", firstNonBlank(extractTail(text, "原因"), extractTail(text, "备注")))
            return hasApproveId
                    ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", extractRemark(text))
                    : missingApproveId("review_action", "驳回审批需要提供流程编号。");
        }
        if (containsAny(text, "审核通过", "审批通过", "通过审批", "同意审批", "审批同意")) {
            return StringUtils.hasText(approveId)
                    ? approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractTail(text, "备注"))
            return hasApproveId
                    ? approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractRemark(text))
                    : missingApproveId("review_action", "审批通过需要提供流程编号。");
        }
        if (StringUtils.hasText(approveId)
        if (hasApproveId
                && containsAny(text, "通过", "同意")
                && !containsAny(text, "未通过", "通过率", "审批通过率", "审核通过率")) {
            return approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractTail(text, "备注"));
            return approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractRemark(text));
        }
        if (containsAny(text, "修改", "更新", "变更")) {
            return StringUtils.hasText(approveId)
            return hasApproveId
                    ? approveTodoTools.updateTodo(
                    memoryId,
                    approveId,
@@ -101,7 +111,82 @@
                    extractApproveType(text),
                    extractKeyword(text),
                    extractLimit(text),
                    extractScope(text));
                    extractScope(text),
                    startDate,
                    endDate,
                    timeRange);
        }
        return null;
    }
    private String tryExecuteQuickPrompt(String memoryId, String text) {
        String normalized = normalizeForMatch(text);
        String approveId = extractApproveId(text);
        boolean hasApproveId = StringUtils.hasText(approveId) && !isPlaceholderApproveId(approveId);
        if ("我当前有哪些审批待办需要处理".equals(normalized)) {
            return approveTodoTools.listTodos(memoryId, "pending", null, null, 10, "approver", null, null, null);
        }
        if ("帮我列出今天新增的审批待办".equals(normalized)) {
            return approveTodoTools.listTodos(memoryId, "all", null, null, 10, "related", null, null, "今天");
        }
        if ("当前待我审批的单据按时间倒序列出来".equals(normalized)) {
            return approveTodoTools.listTodos(memoryId, "pending", null, null, 10, "approver", null, null, null);
        }
        if ("我发起的审批里哪些还在处理中".equals(normalized)) {
            return approveTodoTools.listTodos(memoryId, "processing", null, null, 10, "applicant", null, null, null);
        }
        if ("近7天我的审批待办统计情况怎么样".equals(normalized)) {
            return approveTodoTools.getTodoStats(memoryId, null, null, "近7天");
        }
        if ("本月我的审批中通过驳回处理中各有多少".equals(normalized)) {
            return approveTodoTools.getTodoStats(memoryId, null, null, "本月");
        }
        if ("近30天各类型审批数量分布是什么".equals(normalized)) {
            return approveTodoTools.getTodoStats(memoryId, null, null, "近30天");
        }
        if (normalized.startsWith("查询流程编号") && normalized.contains("审批详情")) {
            return hasApproveId
                    ? approveTodoTools.getTodoDetail(memoryId, approveId)
                    : missingApproveId("todo_detail", "查询审批详情需要提供真实流程编号。");
        }
        if (normalized.startsWith("流程编号")
                && normalized.contains("卡在哪个审批节点")
                && normalized.contains("当前审批人是谁")) {
            return hasApproveId
                    ? approveTodoTools.getTodoProgress(memoryId, approveId)
                    : missingApproveId("todo_progress", "查询审批进度需要提供真实流程编号。");
        }
        if (normalized.startsWith("帮我查看流程编号") && normalized.contains("审批流转记录")) {
            return hasApproveId
                    ? approveTodoTools.getTodoProgress(memoryId, approveId)
                    : missingApproveId("todo_progress", "查询审批流转记录需要提供真实流程编号。");
        }
        if (normalized.startsWith("帮我审批通过流程编号")) {
            return hasApproveId
                    ? approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractRemark(text))
                    : missingApproveId("review_action", "审批通过需要提供真实流程编号。");
        }
        if (normalized.startsWith("帮我驳回流程编号")) {
            return hasApproveId
                    ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", extractRemark(text))
                    : missingApproveId("review_action", "驳回审批需要提供真实流程编号。");
        }
        if (normalized.startsWith("撤销我刚刚对流程编号") && normalized.contains("审批操作")) {
            return hasApproveId
                    ? approveTodoTools.cancelReviewTodo(memoryId, approveId, extractRemark(text))
                    : missingApproveId("cancel_review_action", "撤销审批操作需要提供真实流程编号。");
        }
        if (normalized.startsWith("帮我修改流程编号") && normalized.contains("备注为")) {
            return hasApproveId
                    ? approveTodoTools.updateTodo(memoryId, approveId, null, null, null, null, null, null, extractRemark(text))
                    : missingApproveId("update_action", "修改审批单需要提供真实流程编号。");
        }
        if (normalized.startsWith("删除我发起的流程编号")) {
            return hasApproveId
                    ? approveTodoTools.deleteTodo(memoryId, approveId)
                    : missingApproveId("delete_action", "删除审批单需要提供真实流程编号。");
        }
        return null;
    }
@@ -130,6 +215,10 @@
    }
    private String extractApproveId(String text) {
        Matcher keywordMatcher = APPROVE_ID_BY_LABEL_PATTERN.matcher(text);
        if (keywordMatcher.find()) {
            return keywordMatcher.group(2);
        }
        Matcher matcher = APPROVE_ID_PATTERN.matcher(text);
        return matcher.find() ? matcher.group() : null;
    }
@@ -196,6 +285,8 @@
                .replace("单据", "")
                .replace("待办", "")
                .replace("列表", "")
                .replace("流程编号", "")
                .replace("流程号", "")
                .replace("前10条", "")
                .replace("最近10条", "")
                .trim();
@@ -203,7 +294,7 @@
    }
    private String extractValue(String text, String fieldName) {
        Pattern pattern = Pattern.compile(fieldName + "(改为|修改为|是)[::]?[\\s]*([^,,。;;\\s]+)");
        Pattern pattern = Pattern.compile(fieldName + "(改为|修改为|为|是)[::]?[\\s]*([^,,。;;\\s]+)");
        Matcher matcher = pattern.matcher(text);
        return matcher.find() ? matcher.group(2) : null;
    }
@@ -250,7 +341,7 @@
        if (!text.contains(fieldName)) {
            return null;
        }
        Matcher matcher = Pattern.compile(fieldName + "(改为|修改为|是)[::]?[\\s]*(\\d{1,2})").matcher(text);
        Matcher matcher = Pattern.compile(fieldName + "(改为|修改为|为|是)[::]?[\\s]*(\\d{1,2})").matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(2)) : null;
    }
@@ -264,21 +355,85 @@
    }
    private String extractTail(String text, String key) {
        Pattern quotedPattern = Pattern.compile(key + "(是|为)?[::]?[\\s]*[“\"]([^”\"]+)[”\"]");
        Matcher quotedMatcher = quotedPattern.matcher(text);
        if (quotedMatcher.find()) {
            return cleanContent(quotedMatcher.group(2));
        }
        Pattern pattern = Pattern.compile(key + "(是|为)?[::]?[\\s]*(.+)");
        Matcher matcher = pattern.matcher(text);
        return matcher.find() ? matcher.group(2).trim() : null;
        return matcher.find() ? cleanContent(matcher.group(2)) : null;
    }
    private String extractScope(String text) {
        if (containsAny(text, "我发起", "我提交", "我申请", "申请人是我")) {
            return "applicant";
        }
        if (containsAny(text, "待我审批", "待我审核", "我处理", "我审批", "当前待我", "需要我处理")) {
        if (containsAny(text, "待我审批", "待我审核", "我处理", "我审批", "当前待我", "需要我处理", "需要处理")) {
            return "approver";
        }
        return "related";
    }
    private String extractRemark(String text) {
        return firstNonBlank(firstNonBlank(extractTail(text, "备注"), extractTail(text, "原因")), extractQuotedContent(text));
    }
    private String extractQuotedContent(String text) {
        Matcher matcher = Pattern.compile("[“\"]([^”\"]+)[”\"]").matcher(text);
        return matcher.find() ? cleanContent(matcher.group(1)) : null;
    }
    private String normalizeForMatch(String text) {
        if (!StringUtils.hasText(text)) {
            return "";
        }
        return text.replace(",", "")
                .replace(",", "")
                .replace("。", "")
                .replace(".", "")
                .replace("!", "")
                .replace("!", "")
                .replace("?", "")
                .replace("?", "")
                .replace(":", "")
                .replace(":", "")
                .replace(";", "")
                .replace(";", "")
                .replace("“", "")
                .replace("”", "")
                .replace("\"", "")
                .replace(" ", "")
                .trim();
    }
    private boolean isPlaceholderApproveId(String approveId) {
        if (!StringUtils.hasText(approveId)) {
            return true;
        }
        String value = approveId.trim();
        return "xxx".equalsIgnoreCase(value)
                || value.matches("[xX]{2,}")
                || "流程编号".equals(value)
                || "编号".equals(value)
                || value.contains("示例")
                || value.contains("请输入");
    }
    private String cleanContent(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        return text.trim()
                .replace("“", "")
                .replace("”", "")
                .replace("\"", "")
                .replace("。", "")
                .replace(";", "")
                .replace(";", "")
                .trim();
    }
    private String firstNonBlank(String first, String second) {
        return StringUtils.hasText(first) ? first : second;
    }
src/main/java/com/ruoyi/ai/assistant/FinancialAgent.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package com.ruoyi.ai.assistant;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
@AiService(
        wiringMode = EXPLICIT,
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "chatMemoryProviderFinancial",
        tools = "financialAgentTools"
)
public interface FinancialAgent {
    @SystemMessage(fromResource = "financial-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("currentDate") String currentDate);
}
src/main/java/com/ruoyi/ai/assistant/FinancialIntentExecutor.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,284 @@
package com.ruoyi.ai.assistant;
import com.ruoyi.ai.tools.FinancialAgentTools;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class FinancialIntentExecutor {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final Pattern LIMIT_PATTERN = Pattern.compile("(?:前|最近|展示|返回)?\\s*(\\d{1,2})\\s*(?:条|个|名)");
    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
    private static final Pattern RELATIVE_DAY_PATTERN = Pattern.compile("(?:近|最近)\\s*(\\d{1,3})\\s*天");
    private static final Pattern FUTURE_MONTH_PATTERN = Pattern.compile("(?:未来|后续|接下来)\\s*(\\d{1,2})\\s*(?:个)?月");
    private final FinancialAgentTools financialAgentTools;
    public FinancialIntentExecutor(FinancialAgentTools financialAgentTools) {
        this.financialAgentTools = financialAgentTools;
    }
    public String tryExecute(String memoryId, String message) {
        if (!StringUtils.hasText(message)) {
            return null;
        }
        String text = message.trim();
        String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text);
        if (StringUtils.hasText(quickPromptResponse)) {
            return quickPromptResponse;
        }
        DateRange dateRange = extractDateRange(text);
        Integer limit = extractLimit(text);
        String keyword = extractKeyword(text);
        String startDate = dateRange.startDate();
        String endDate = dateRange.endDate();
        String timeRange = dateRange.label();
        if (containsAny(text, "成本核算", "产品成本", "工序成本", "人工成本", "折旧", "材料损耗", "成本最高")) {
            return financialAgentTools.calculateIntelligentCost(memoryId, startDate, endDate, timeRange, keyword, limit);
        }
        if (containsAny(text, "利润分析", "订单利润", "亏损订单", "低利润", "最赚钱客户", "哪个客户最赚钱",
                "客户最赚钱", "利润最高客户", "利润贡献最高", "利润下降")) {
            return financialAgentTools.analyzeOrderProfit(memoryId, startDate, endDate, timeRange, keyword, limit);
        }
        if (containsAny(text, "库存资金", "库存积压", "呆滞库存", "资金占用", "周转率", "库存周转")) {
            return financialAgentTools.analyzeInventoryCapital(memoryId, startDate, endDate, timeRange, keyword, limit);
        }
        if (containsAny(text, "现金流", "回款风险", "付款压力", "资金缺口", "应收", "应付", "回款预测")) {
            return financialAgentTools.forecastCashFlow(memoryId, startDate, endDate, timeRange, extractForecastMonths(text));
        }
        if (containsAny(text, "异常预警", "经营异常", "风险预警", "成本异常", "利润异常", "回款异常", "订单风险")) {
            return financialAgentTools.detectBusinessAnomalies(memoryId, startDate, endDate, timeRange, limit);
        }
        if (containsAny(text, "驾驶舱", "经营看板", "经营总览", "经营仪表盘", "经营大盘")) {
            return financialAgentTools.getBusinessCockpit(memoryId, startDate, endDate, timeRange);
        }
        if (containsAny(text, "日报", "周报", "经营报告", "分析报告")) {
            return financialAgentTools.generateOperationReport(memoryId, startDate, endDate, timeRange,
                    containsAny(text, "周报") ? "weekly" : "daily");
        }
        if (containsAny(text, "业财融合", "业财联动", "口径", "指标解释", "为什么")) {
            return financialAgentTools.retrieveFinancialKnowledge(memoryId, text);
        }
        return null;
    }
    private String tryExecuteQuickPrompt(String memoryId, String text) {
        String normalized = normalizeForMatch(text);
        if ("生成本周经营周报利润与现金流".equals(normalized) || "生成本周经营周报".equals(normalized) || "生成周报".equals(normalized)) {
            DateRange range = weekRange();
            return financialAgentTools.generateOperationReport(memoryId, range.startDate(), range.endDate(), range.label(), "weekly");
        }
        if ("分析本月利润下降原因".equals(normalized)) {
            DateRange range = monthRange();
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("近30天哪个客户利润贡献最高".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("查看本月经营驾驶舱".equals(normalized) || "查看经营驾驶舱".equals(normalized)) {
            DateRange range = monthRange();
            return financialAgentTools.getBusinessCockpit(memoryId, range.startDate(), range.endDate(), range.label());
        }
        if ("查询近30天亏损订单".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("分析近30天库存资金占用".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return financialAgentTools.analyzeInventoryCapital(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("预测未来3个月现金流".equals(normalized)) {
            return financialAgentTools.forecastCashFlow(memoryId, null, null, null, 3);
        }
        if ("哪个工序成本最高".equals(normalized)) {
            return financialAgentTools.calculateIntelligentCost(memoryId, null, null, null, null, null);
        }
        return null;
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.contains(keyword)) {
                return true;
            }
        }
        return false;
    }
    private Integer extractLimit(String text) {
        Matcher matcher = LIMIT_PATTERN.matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(1)) : null;
    }
    private Integer extractForecastMonths(String text) {
        Matcher matcher = FUTURE_MONTH_PATTERN.matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(1)) : null;
    }
    private DateRange extractDateRange(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        if (matcher.find()) {
            String first = matcher.group(1);
            String second = matcher.find() ? matcher.group(1) : first;
            return buildDateRange(first, second, first + "至" + second);
        }
        if (text.contains("本月")) {
            return monthRange();
        }
        if (text.contains("上月")) {
            return lastMonthRange();
        }
        if (text.contains("今年") || text.contains("本年")) {
            return yearRange();
        }
        if (text.contains("本周")) {
            return weekRange();
        }
        Matcher relativeDayMatcher = RELATIVE_DAY_PATTERN.matcher(text);
        if (relativeDayMatcher.find()) {
            int days = Integer.parseInt(relativeDayMatcher.group(1));
            return recentDaysRange(days);
        }
        return new DateRange(null, null, null);
    }
    private DateRange buildDateRange(String start, String end, String label) {
        LocalDate startDate = parseDate(start);
        LocalDate endDate = parseDate(end);
        if (startDate == null || endDate == null) {
            return new DateRange(null, null, null);
        }
        if (startDate.isAfter(endDate)) {
            LocalDate temp = startDate;
            startDate = endDate;
            endDate = temp;
        }
        return new DateRange(formatDate(startDate), formatDate(endDate), label);
    }
    private DateRange recentDaysRange(int days) {
        LocalDate end = LocalDate.now();
        int safeDays = Math.max(days, 1);
        LocalDate start = end.minusDays(safeDays - 1L);
        return new DateRange(formatDate(start), formatDate(end), "近" + safeDays + "天");
    }
    private DateRange monthRange() {
        LocalDate today = LocalDate.now();
        return new DateRange(formatDate(today.withDayOfMonth(1)), formatDate(today), "本月");
    }
    private DateRange weekRange() {
        LocalDate today = LocalDate.now();
        LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
        return new DateRange(formatDate(start), formatDate(today), "本周");
    }
    private DateRange lastMonthRange() {
        YearMonth lastMonth = YearMonth.now().minusMonths(1);
        return new DateRange(formatDate(lastMonth.atDay(1)), formatDate(lastMonth.atEndOfMonth()), "上月");
    }
    private DateRange yearRange() {
        LocalDate today = LocalDate.now();
        return new DateRange(formatDate(today.withDayOfYear(1)), formatDate(today), "今年");
    }
    private LocalDate parseDate(String text) {
        try {
            return LocalDate.parse(text, DATE_FMT);
        } catch (Exception ignored) {
            return null;
        }
    }
    private String formatDate(LocalDate date) {
        return date == null ? null : date.format(DATE_FMT);
    }
    private String normalizeForMatch(String text) {
        if (!StringUtils.hasText(text)) {
            return "";
        }
        return text.replace("(", "")
                .replace(")", "")
                .replace("(", "")
                .replace(")", "")
                .replace(",", "")
                .replace(",", "")
                .replace("。", "")
                .replace(".", "")
                .replace("!", "")
                .replace("!", "")
                .replace("?", "")
                .replace("?", "")
                .replace(":", "")
                .replace(":", "")
                .replace(";", "")
                .replace(";", "")
                .replace(" ", "")
                .trim();
    }
    private String extractKeyword(String text) {
        String cleaned = text
                .replaceAll("\\d{4}-\\d{2}-\\d{2}", "")
                .replaceAll("(?:近|最近)\\s*\\d{1,3}\\s*天", "")
                .replaceAll("(?:前|最近|展示|返回)?\\s*\\d{1,2}\\s*(?:条|个|名)", "")
                .replace("查询", "")
                .replace("查看", "")
                .replace("看下", "")
                .replace("看看", "")
                .replace("帮我", "")
                .replace("请", "")
                .replace("一个", "")
                .replace("为什么", "")
                .replace("哪个客户最赚钱", "")
                .replace("最近哪个客户最赚钱", "")
                .replace("本月哪个客户最赚钱", "")
                .replace("近30天哪个客户最赚钱", "")
                .replace("最赚钱客户", "")
                .replace("客户最赚钱", "")
                .replace("哪个客户利润最高", "")
                .replace("利润最高客户", "")
                .replace("哪个客户利润贡献最高", "")
                .replace("利润贡献最高客户", "")
                .replace("本月", "")
                .replace("本周", "")
                .replace("本年", "")
                .replace("今年", "")
                .replace("上月", "")
                .replace("近30天", "")
                .replace("近7天", "")
                .replace("近90天", "")
                .replace("前10条", "")
                .replace("最近10条", "")
                .replace("前20条", "")
                .replace("最近20条", "")
                .replace("订单利润分析", "")
                .replace("利润分析", "")
                .replace("库存资金分析", "")
                .replace("现金流预测", "")
                .replace("经营驾驶舱", "")
                .replace("日报", "")
                .replace("周报", "")
                .replace("异常预警", "")
                .replace("条", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
    private record DateRange(String startDate, String endDate, String label) {
    }
}
src/main/java/com/ruoyi/ai/assistant/KnowledgeChatAgent.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package com.ruoyi.ai.assistant;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
/**
 * çŸ¥è¯†åº“问答Agent
 * åŸºäºŽRAG检索增强生成
 */
@AiService(
        wiringMode = EXPLICIT,
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "chatMemoryProviderXiaozhi"
)
public interface KnowledgeChatAgent {
    @SystemMessage("""
            ä½ æ˜¯ä¼ä¸šçŸ¥è¯†åº“问答助手。
            ä½ éœ€è¦åŸºäºŽæä¾›çš„知识库内容回答用户问题。
            éµå¾ªä»¥ä¸‹è§„则:
            1. ä¸¥æ ¼åŸºäºŽçŸ¥è¯†åº“内容回答,不要编造信息
            2. å¦‚果知识库中没有相关信息,明确告知用户
            3. å›žç­”要准确、简洁、有条理
            4. å¦‚果内容较多,使用分点列表形式
            5. å¼•用来源时注明"根据知识库内容"
            """)
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
}
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package com.ruoyi.ai.assistant;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
@AiService(
        wiringMode = EXPLICIT,
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "chatMemoryProviderManufacturing",
        tools = "manufacturingAgentTools"
)
public interface ManufacturingAgent {
    @SystemMessage(fromResource = "manufacturing-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("currentDate") String currentDate);
}
src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,136 @@
package com.ruoyi.ai.assistant;
import com.ruoyi.ai.tools.ManufacturingAgentTools;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class ManufacturingIntentExecutor {
    private static final Pattern LIMIT_PATTERN = Pattern.compile("(前|最近)?(\\d{1,2})条");
    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
    private final ManufacturingAgentTools manufacturingAgentTools;
    public ManufacturingIntentExecutor(ManufacturingAgentTools manufacturingAgentTools) {
        this.manufacturingAgentTools = manufacturingAgentTools;
    }
    public String tryExecute(String memoryId, String message) {
        if (!StringUtils.hasText(message)) {
            return null;
        }
        String text = message.trim();
        String keyword = extractKeyword(text);
        Integer limit = extractLimit(text);
        String startDate = extractStartDate(text);
        String endDate = extractEndDate(text);
        if (containsAny(text, "预警", "告警", "风险", "提醒")) {
            return manufacturingAgentTools.getWarningBoard(memoryId, startDate, endDate, text);
        }
        if (containsAny(text, "分析", "统计", "趋势", "看板", "报表", "总览")) {
            return manufacturingAgentTools.analyzeFactory(memoryId, startDate, endDate, text);
        }
        if (containsAny(text, "办", "处理", "派工", "安排", "闭环", "跟进", "处置")) {
            return manufacturingAgentTools.planActions(memoryId, text);
        }
        if (containsAny(text, "生产现场", "现场", "车间")) {
            return manufacturingAgentTools.queryDomain(memoryId, "site", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "计划", "排产", "mps")) {
            return manufacturingAgentTools.queryDomain(memoryId, "plan", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "工单", "作业单", "任务单", "任务")) {
            return manufacturingAgentTools.queryDomain(memoryId, "workorder", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "设备", "ç»´ä¿®", "保养", "故障")) {
            return manufacturingAgentTools.queryDomain(memoryId, "device", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "质量", "质检", "不合格", "检验")) {
            return manufacturingAgentTools.queryDomain(memoryId, "quality", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "物料", "库存", "库位", "入库", "出库")) {
            return manufacturingAgentTools.queryDomain(memoryId, "material", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "异常", "例外", "偏差")) {
            return manufacturingAgentTools.queryDomain(memoryId, "exception", keyword, limit, startDate, endDate, text);
        }
        return null;
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.toLowerCase().contains(keyword.toLowerCase())) {
                return true;
            }
        }
        return false;
    }
    private Integer extractLimit(String text) {
        Matcher matcher = LIMIT_PATTERN.matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(2)) : 10;
    }
    private String extractStartDate(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        return matcher.find() ? matcher.group(1) : null;
    }
    private String extractEndDate(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        if (!matcher.find()) {
            return null;
        }
        return matcher.find() ? matcher.group(1) : null;
    }
    private String extractKeyword(String text) {
        String cleaned = text
                .replace("查询", "")
                .replace("查看", "")
                .replace("帮我", "")
                .replace("请", "")
                .replace("一下", "")
                .replace("所有", "")
                .replace("全部", "")
                .replace("今年", "")
                .replace("本年", "")
                .replace("去年", "")
                .replace("本月", "")
                .replace("上月", "")
                .replace("本周", "")
                .replace("上周", "")
                .replace("今天", "")
                .replace("昨天", "")
                .replace("近30天", "")
                .replace("近7天", "")
                .replace("近15天", "")
                .replace("近60天", "")
                .replace("最近30天", "")
                .replace("最近7天", "")
                .replace("最近15天", "")
                .replace("最近60天", "")
                .replace("生产现场", "")
                .replace("现场", "")
                .replace("生产工单", "")
                .replace("生产", "")
                .replace("计划", "")
                .replace("排产", "")
                .replace("工单", "")
                .replace("设备", "")
                .replace("质量", "")
                .replace("物料", "")
                .replace("库存", "")
                .replace("异常", "")
                .replace("前10条", "")
                .replace("最近10条", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
}
src/main/java/com/ruoyi/ai/assistant/PurchaseAgent.java
@@ -3,6 +3,7 @@
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
@@ -17,5 +18,5 @@
public interface PurchaseAgent {
    @SystemMessage(fromResource = "purchase-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("currentDate") String currentDate);
}
src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
@@ -4,6 +4,9 @@
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -11,8 +14,12 @@
public class PurchaseIntentExecutor {
    private static final Pattern ID_PATTERN = Pattern.compile("\\b\\d{1,12}\\b");
    private static final Pattern LIMIT_PATTERN = Pattern.compile("(前|最近)?(\\d{1,2})条");
    private static final Pattern DATE_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
    private static final Pattern LIMIT_PATTERN = Pattern.compile("(前|最近)?\\s*(\\d{1,2})\\s*条");
    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
    private static final Pattern RELATIVE_RANGE_PATTERN = Pattern.compile("(近|最近)\\s*(\\d{1,3})\\s*(天|周|个月|月|å¹´)");
    private static final Pattern HALF_RANGE_PATTERN = Pattern.compile("(最近|近)?半(个)?(月|å¹´)");
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private final PurchaseAgentTools purchaseAgentTools;
@@ -25,71 +32,63 @@
            return null;
        }
        String text = message.trim();
        String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text);
        if (StringUtils.hasText(quickPromptResponse)) {
            return quickPromptResponse;
        }
        if (containsAny(text, "排行", "排名", "前几", "前五", "前十") && containsAny(text, "物料", "产品", "原材料", "采购金额", "金额")) {
            return purchaseAgentTools.rankPurchaseMaterials(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    text,
                    extractLimit(text)
            );
        String keyword = extractKeyword(text);
        Integer limit = extractLimit(text);
        DateRange dateRange = extractDateRange(text);
        String startDate = dateRange.startDate();
        String endDate = dateRange.endDate();
        if (containsAny(text, "排行", "排名", "前几", "前五", "前十")
                && containsAny(text, "物料", "产品", "原材料", "采购金额", "金额")) {
            return purchaseAgentTools.rankPurchaseMaterials(memoryId, startDate, endDate, text, limit);
        }
        if (containsAny(text, "未入库", "待入库", "没有入库", "还未入库")) {
            return purchaseAgentTools.listUnstockedPurchaseOrders(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    extractKeyword(text),
                    extractLimit(text)
            );
            return purchaseAgentTools.listUnstockedPurchaseOrders(memoryId, startDate, endDate, keyword, limit);
        }
        if (containsAny(text, "到货异常", "到货有异常", "异常到货", "到货问题", "供应商到货异常")) {
            return purchaseAgentTools.listArrivalExceptions(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    text,
                    extractLimit(text)
            );
            return purchaseAgentTools.listArrivalExceptions(memoryId, startDate, endDate, text, limit);
        }
        if (containsAny(text, "待付款", "未付款", "未付清", "待支付", "应付")) {
            return purchaseAgentTools.listPendingPaymentOrders(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    extractKeyword(text),
                    extractLimit(text)
            );
            return purchaseAgentTools.listPendingPaymentOrders(memoryId, startDate, endDate, keyword, limit);
        }
        if (containsAny(text, "退货", "退料", "拒收")) {
            return purchaseAgentTools.listPurchaseReturns(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    extractKeyword(text),
                    extractLimit(text)
            );
            return purchaseAgentTools.listPurchaseReturns(memoryId, startDate, endDate, keyword, limit);
        }
        if (isStatsIntent(text)) {
            return purchaseAgentTools.getPurchaseStats(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    text
            );
            return purchaseAgentTools.getPurchaseStats(memoryId, startDate, endDate, text);
        }
        if (containsAny(text, "详情", "明细") && extractId(text) != null) {
            return purchaseAgentTools.getPurchaseLedgerDetail(memoryId, extractId(text));
        Long ledgerId = extractId(text);
        if (containsAny(text, "详情", "明细") && ledgerId != null) {
            return purchaseAgentTools.getPurchaseLedgerDetail(memoryId, ledgerId);
        }
        if (containsAny(text, "台账", "采购单", "采购订单", "订单", "合同", "列表", "查询")) {
            return purchaseAgentTools.listPurchaseLedgers(
                    memoryId,
                    extractKeyword(text),
                    extractStartDate(text),
                    extractEndDate(text),
                    extractLimit(text)
            );
            return purchaseAgentTools.listPurchaseLedgers(memoryId, keyword, startDate, endDate, limit);
        }
        return null;
    }
    private String tryExecuteQuickPrompt(String memoryId, String text) {
        String normalized = normalizeForMatch(text);
        if ("本月采购金额排名前十的物料有哪些".equals(normalized)) {
            return purchaseAgentTools.rankPurchaseMaterials(memoryId, null, null, "本月", 10);
        }
        if ("哪些采购订单还未入库".equals(normalized)) {
            return purchaseAgentTools.listUnstockedPurchaseOrders(memoryId, null, null, null, 10);
        }
        if ("最近7天供应商到货异常有哪些".equals(normalized)) {
            return purchaseAgentTools.listArrivalExceptions(memoryId, null, null, "最近7天", 10);
        }
        if ("帮我统计待付款采购单".equals(normalized)) {
            return purchaseAgentTools.listPendingPaymentOrders(memoryId, null, null, null, 10);
        }
        if ("列出本月采购退货情况".equals(normalized)) {
            return purchaseAgentTools.listPurchaseReturns(memoryId, null, null, null, 10);
        }
        return null;
    }
@@ -100,8 +99,10 @@
        }
        boolean queryWord = containsAny(text, "查询", "查看", "看下", "看看", "获取");
        boolean dataWord = containsAny(text, "数据", "金额", "数量", "合同额", "付款额", "发票额");
        boolean timeWord = containsAny(text, "今天", "本周", "本月", "上月", "今年", "去年", "近半年", "最近半个月", "半个月")
                || DATE_PATTERN.matcher(text).find();
        boolean timeWord = containsAny(text, "今天", "昨天", "本周", "上周", "本月", "上月", "今年", "去年", "近半年", "最近半个月", "半个月")
                || DATE_PATTERN.matcher(text).find()
                || RELATIVE_RANGE_PATTERN.matcher(text).find()
                || HALF_RANGE_PATTERN.matcher(text).find();
        return queryWord && dataWord && timeWord;
    }
@@ -127,23 +128,125 @@
        return matcher.find() ? Integer.parseInt(matcher.group(2)) : 10;
    }
    private String extractStartDate(String text) {
    private DateRange extractDateRange(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        return matcher.find() ? matcher.group() : null;
        if (matcher.find()) {
            String first = matcher.group(1);
            String second = matcher.find() ? matcher.group(1) : first;
            return buildDateRange(first, second);
        }
        LocalDate today = LocalDate.now(CHINA_ZONE_ID);
        if (text.contains("今天")) {
            return new DateRange(formatDate(today), formatDate(today));
        }
        if (text.contains("昨天")) {
            LocalDate yesterday = today.minusDays(1);
            return new DateRange(formatDate(yesterday), formatDate(yesterday));
        }
        if (text.contains("本周")) {
            LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            return new DateRange(formatDate(start), formatDate(today));
        }
        if (text.contains("上周")) {
            LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            LocalDate start = thisWeekStart.minusWeeks(1);
            LocalDate end = start.plusDays(6);
            return new DateRange(formatDate(start), formatDate(end));
        }
        if (text.contains("本月")) {
            return new DateRange(formatDate(today.withDayOfMonth(1)), formatDate(today));
        }
        if (text.contains("上月")) {
            LocalDate start = today.minusMonths(1).withDayOfMonth(1);
            return new DateRange(formatDate(start), formatDate(start.withDayOfMonth(start.lengthOfMonth())));
        }
        if (text.contains("今年") || text.contains("本年")) {
            return new DateRange(formatDate(today.withDayOfYear(1)), formatDate(today));
        }
        if (text.contains("去年")) {
            LocalDate start = today.minusYears(1).withDayOfYear(1);
            LocalDate end = start.withDayOfYear(start.lengthOfYear());
            return new DateRange(formatDate(start), formatDate(end));
        }
        if (containsAny(text, "近半年", "最近半年")) {
            return new DateRange(formatDate(today.minusMonths(6).plusDays(1)), formatDate(today));
        }
        if (containsAny(text, "近半个月", "最近半个月", "半个月")) {
            return new DateRange(formatDate(today.minusDays(14)), formatDate(today));
        }
        Matcher relativeMatcher = RELATIVE_RANGE_PATTERN.matcher(text);
        if (relativeMatcher.find()) {
            int amount = Integer.parseInt(relativeMatcher.group(2));
            String unit = relativeMatcher.group(3);
            LocalDate start = switch (unit) {
                case "天" -> today.minusDays(Math.max(amount - 1L, 0));
                case "周" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
                case "个月", "月" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
                case "å¹´" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
                default -> today.minusDays(29);
            };
            return new DateRange(formatDate(start), formatDate(today));
        }
        return new DateRange(null, null);
    }
    private String extractEndDate(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        if (!matcher.find()) {
    private DateRange buildDateRange(String start, String end) {
        LocalDate startDate = parseDate(start);
        LocalDate endDate = parseDate(end);
        if (startDate == null || endDate == null) {
            return new DateRange(null, null);
        }
        if (startDate.isAfter(endDate)) {
            LocalDate temp = startDate;
            startDate = endDate;
            endDate = temp;
        }
        return new DateRange(formatDate(startDate), formatDate(endDate));
    }
    private LocalDate parseDate(String text) {
        try {
            return LocalDate.parse(text, DATE_FMT);
        } catch (Exception ignored) {
            return null;
        }
        return matcher.find() ? matcher.group() : null;
    }
    private String formatDate(LocalDate date) {
        return date == null ? null : date.format(DATE_FMT);
    }
    private String normalizeForMatch(String text) {
        if (!StringUtils.hasText(text)) {
            return "";
        }
        return text.replace(",", "")
                .replace(",", "")
                .replace("。", "")
                .replace(".", "")
                .replace("!", "")
                .replace("!", "")
                .replace("?", "")
                .replace("?", "")
                .replace(":", "")
                .replace(":", "")
                .replace(";", "")
                .replace(";", "")
                .replace(" ", "")
                .trim();
    }
    private String extractKeyword(String text) {
        String cleaned = text
                .replace("查询", "")
                .replace("查看", "")
                .replace("看下", "")
                .replace("看看", "")
                .replace("请", "")
                .replace("一下", "")
                .replace("采购", "")
                .replace("采购单", "")
                .replace("采购订单", "")
@@ -153,9 +256,33 @@
                .replace("哪些", "")
                .replace("列出", "")
                .replace("帮我", "")
                .replace("统计", "")
                .replace("分析", "")
                .replace("本月", "")
                .replace("上月", "")
                .replace("本年", "")
                .replace("今年", "")
                .replace("去年", "")
                .replace("本周", "")
                .replace("上周", "")
                .replace("今天", "")
                .replace("昨天", "")
                .replace("近30天", "")
                .replace("近7天", "")
                .replace("近15天", "")
                .replace("近60天", "")
                .replace("最近30天", "")
                .replace("最近7天", "")
                .replace("最近15天", "")
                .replace("最近60天", "")
                .replace("最近10条", "")
                .replace("前10条", "")
                .replace("前20条", "")
                .replace("最近20条", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
    private record DateRange(String startDate, String endDate) {
    }
}
src/main/java/com/ruoyi/ai/assistant/SalesAgent.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package com.ruoyi.ai.assistant;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
@AiService(
        wiringMode = EXPLICIT,
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "chatMemoryProviderSales",
        tools = "salesAgentTools"
)
public interface SalesAgent {
    @SystemMessage(fromResource = "sales-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("currentDate") String currentDate);
}
src/main/java/com/ruoyi/ai/assistant/SalesIntentExecutor.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,270 @@
package com.ruoyi.ai.assistant;
import com.ruoyi.ai.tools.SalesAgentTools;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class SalesIntentExecutor {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final Pattern LIMIT_PATTERN = Pattern.compile("(前|最近)?\\s*(\\d{1,2})\\s*条");
    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
    private static final Pattern RELATIVE_DAY_PATTERN = Pattern.compile("(近|最近)?\\s*(\\d{1,3})\\s*天");
    private final SalesAgentTools salesAgentTools;
    public SalesIntentExecutor(SalesAgentTools salesAgentTools) {
        this.salesAgentTools = salesAgentTools;
    }
    public String tryExecute(String memoryId, String message) {
        if (!StringUtils.hasText(message)) {
            return null;
        }
        String text = message.trim();
        String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text);
        if (StringUtils.hasText(quickPromptResponse)) {
            return quickPromptResponse;
        }
        String keyword = extractKeyword(text);
        Integer limit = extractLimit(text);
        DateRange dateRange = extractDateRange(text);
        String startDate = dateRange.startDate();
        String endDate = dateRange.endDate();
        if (containsAny(text, "流失", "流失风险", "客户流失", "风险分析")) {
            return salesAgentTools.analyzeCustomerChurnRisk(memoryId, startDate, endDate, text, keyword, limit);
        }
        if (containsAny(text, "回款", "收款", "报价")
                && containsAny(text, "建议", "策略", "优化", "方案")) {
            return salesAgentTools.suggestCollectionAndQuotationStrategy(
                    memoryId, startDate, endDate, text, keyword, limit, shouldPrioritizeHighRisk(text));
        }
        if (containsAny(text, "指标", "统计", "看板", "总览", "经营分析")) {
            return salesAgentTools.getSalesDashboard(memoryId, startDate, endDate, text);
        }
        if (containsAny(text, "客户档案", "私海", "公海", "客户池")) {
            return salesAgentTools.listCustomerProfiles(memoryId, extractSeaType(text), keyword, limit);
        }
        if (containsAny(text, "销售报价", "报价单", "报价", "询价")) {
            return salesAgentTools.listSalesQuotations(memoryId, keyword, startDate, endDate, limit);
        }
        if (containsAny(text, "销售退货", "退货", "退款")) {
            return salesAgentTools.listSalesReturns(memoryId, startDate, endDate, keyword, limit);
        }
        if (containsAny(text, "客户往来", "往来", "回款", "应收", "来款", "收款明细")) {
            return salesAgentTools.listCustomerInteractions(memoryId, keyword, startDate, endDate, limit);
        }
        if (containsAny(text, "发货台账", "发货", "物流", "快递", "运输")) {
            return salesAgentTools.listShippingLedgers(memoryId, keyword, startDate, endDate, limit);
        }
        if (containsAny(text, "销售台账", "销售合同", "销售订单", "合同台账", "订单台账")) {
            return salesAgentTools.listSalesLedgers(memoryId, keyword, startDate, endDate, limit);
        }
        return null;
    }
    private String tryExecuteQuickPrompt(String memoryId, String text) {
        String normalized = normalizeForMatch(text);
        if ("查询私海客户档案前10条".equals(normalized)) {
            return salesAgentTools.listCustomerProfiles(memoryId, "private", null, 10);
        }
        if ("查询公海客户档案".equals(normalized)) {
            return salesAgentTools.listCustomerProfiles(memoryId, "public", null, 10);
        }
        if ("查询本月销售报价".equals(normalized)) {
            DateRange range = monthRange();
            return salesAgentTools.listSalesQuotations(memoryId, null, range.startDate(), range.endDate(), 10);
        }
        if ("查询本月销售台账".equals(normalized)) {
            DateRange range = monthRange();
            return salesAgentTools.listSalesLedgers(memoryId, null, range.startDate(), range.endDate(), 10);
        }
        if ("查询近30天销售退货".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return salesAgentTools.listSalesReturns(memoryId, range.startDate(), range.endDate(), null, 10);
        }
        if ("查询近30天客户回款往来".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return salesAgentTools.listCustomerInteractions(memoryId, null, range.startDate(), range.endDate(), 10);
        }
        if ("查询本月发货台账".equals(normalized)) {
            DateRange range = monthRange();
            return salesAgentTools.listShippingLedgers(memoryId, null, range.startDate(), range.endDate(), 10);
        }
        if ("查看销售指标统计".equals(normalized)) {
            return salesAgentTools.getSalesDashboard(memoryId, null, null, "本月");
        }
        if ("帮我做客户流失风险分析近30天前20条".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return salesAgentTools.analyzeCustomerChurnRisk(memoryId, range.startDate(), range.endDate(), "近30天", null, 20);
        }
        if ("生成回款与报价策略建议优先高风险客户".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return salesAgentTools.suggestCollectionAndQuotationStrategy(memoryId, range.startDate(), range.endDate(), "近30天", null, 10, true);
        }
        return null;
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.contains(keyword)) {
                return true;
            }
        }
        return false;
    }
    private String extractSeaType(String text) {
        if (text.contains("公海")) {
            return "public";
        }
        if (text.contains("私海")) {
            return "private";
        }
        return null;
    }
    private Integer extractLimit(String text) {
        Matcher matcher = LIMIT_PATTERN.matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(2)) : 10;
    }
    private DateRange extractDateRange(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        if (matcher.find()) {
            String first = matcher.group(1);
            String second = matcher.find() ? matcher.group(1) : first;
            return buildDateRange(first, second);
        }
        if (text.contains("本月")) {
            return monthRange();
        }
        if (text.contains("上月")) {
            return lastMonthRange();
        }
        if (text.contains("本年") || text.contains("今年")) {
            return yearRange();
        }
        Matcher relativeDayMatcher = RELATIVE_DAY_PATTERN.matcher(text);
        if (relativeDayMatcher.find()) {
            int days = Integer.parseInt(relativeDayMatcher.group(2));
            return recentDaysRange(days);
        }
        return new DateRange(null, null);
    }
    private DateRange buildDateRange(String start, String end) {
        LocalDate startDate = parseDate(start);
        LocalDate endDate = parseDate(end);
        if (startDate == null || endDate == null) {
            return new DateRange(null, null);
        }
        if (startDate.isAfter(endDate)) {
            LocalDate temp = startDate;
            startDate = endDate;
            endDate = temp;
        }
        return new DateRange(formatDate(startDate), formatDate(endDate));
    }
    private DateRange recentDaysRange(int days) {
        LocalDate end = LocalDate.now();
        int safeDays = Math.max(days, 1);
        LocalDate start = end.minusDays(safeDays - 1L);
        return new DateRange(formatDate(start), formatDate(end));
    }
    private DateRange monthRange() {
        LocalDate today = LocalDate.now();
        return new DateRange(formatDate(today.withDayOfMonth(1)), formatDate(today));
    }
    private DateRange lastMonthRange() {
        YearMonth lastMonth = YearMonth.now().minusMonths(1);
        return new DateRange(formatDate(lastMonth.atDay(1)), formatDate(lastMonth.atEndOfMonth()));
    }
    private DateRange yearRange() {
        LocalDate today = LocalDate.now();
        return new DateRange(formatDate(today.withDayOfYear(1)), formatDate(today));
    }
    private LocalDate parseDate(String text) {
        try {
            return LocalDate.parse(text, DATE_FMT);
        } catch (Exception ignored) {
            return null;
        }
    }
    private String formatDate(LocalDate date) {
        return date == null ? null : date.format(DATE_FMT);
    }
    private String normalizeForMatch(String text) {
        if (!StringUtils.hasText(text)) {
            return "";
        }
        return text.replace(",", "")
                .replace(",", "")
                .replace("。", "")
                .replace(".", "")
                .replace("!", "")
                .replace("!", "")
                .replace("?", "")
                .replace("?", "")
                .replace(":", "")
                .replace(":", "")
                .replace(";", "")
                .replace(";", "")
                .replace(" ", "")
                .trim();
    }
    private Boolean shouldPrioritizeHighRisk(String text) {
        return containsAny(text, "优先高风险", "高风险客户", "高风险");
    }
    private String extractKeyword(String text) {
        String cleaned = text
                .replace("查询", "")
                .replace("查看", "")
                .replace("看下", "")
                .replace("看看", "")
                .replace("帮我", "")
                .replace("请", "")
                .replace("一下", "")
                .replace("销售", "")
                .replace("客户档案", "")
                .replace("报价单", "")
                .replace("销售报价", "")
                .replace("销售台账", "")
                .replace("发货台账", "")
                .replace("客户往来", "")
                .replace("销售退货", "")
                .replace("前10条", "")
                .replace("最近10条", "")
                .replace("前20条", "")
                .replace("最近20条", "")
                .replace("近30天", "")
                .replace("本月", "")
                .replace("本年", "")
                .replace("今年", "")
                .replace("条", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
    private record DateRange(String startDate, String endDate) {
    }
}
src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
@@ -5,32 +5,48 @@
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.pinecone.PineconeEmbeddingStore;
import dev.langchain4j.store.embedding.pinecone.PineconeServerlessIndexConfig;
import org.springframework.beans.factory.annotation.Autowired;
import io.pinecone.clients.Index;
import io.pinecone.clients.Pinecone;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * @author :yys
 * @date : 2025/5/2 21:07
 * å‘量存储配置
 */
@Configuration
public class EmbeddingStoreConfig {
    @Autowired
    private EmbeddingModel embeddingModel;
    @Value("${pinecone.api-key:pcsk_4SJLnh_tNB3wSLJU8tc4E5P28PcXX8eCLdURqZpVhg1FMV8CRYxjneWdzqRdB5Ftqooi9}")
    private String pineconeApiKey;
    @Value("${pinecone.index:xiaozhi-index}")
    private String indexName;
    @Value("${pinecone.namespace:knowledge-base}")
    private String namespace;
    @Bean
    public EmbeddingStore<TextSegment> embeddingStore() {
        //创建向量存储
    public Pinecone pinecone() {
        return new Pinecone.Builder(pineconeApiKey).build();
    }
    @Bean
    public Index pineconeIndex(Pinecone pinecone) {
        return pinecone.getIndexConnection(indexName);
    }
    @Bean
    public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) {
        return PineconeEmbeddingStore.builder()
                .apiKey("pcsk_4SJLnh_tNB3wSLJU8tc4E5P28PcXX8eCLdURqZpVhg1FMV8CRYxjneWdzqRdB5Ftqooi9")
                .index("xiaozhi-index")//如果指定的索引不存在,将创建一个新的索引
                .nameSpace("xiaozhi-namespace") //如果指定的名称空间不存在,将创建一个新的名称 ç©ºé—´
                .apiKey(pineconeApiKey)
                .index(indexName)
                .nameSpace(namespace)
                .createIndex(PineconeServerlessIndexConfig.builder()
                        .cloud("AWS") //指定索引部署在 AWS äº‘服务上。
                        .region("us-east-1") //指定索引所在的 AWS åŒºåŸŸä¸º us-east-1。
                        .dimension(embeddingModel.dimension()) //指定索引的向量维度,该维度与 embeddedModel ç”Ÿæˆçš„向量维度相同。
                        .cloud("AWS")
                        .region("us-east-1")
                        .dimension(embeddingModel.dimension())
                        .build())
                .build();
    }
}
}
src/main/java/com/ruoyi/ai/config/FinancialAgentConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.ai.config;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FinancialAgentConfig {
    @Bean
    ChatMemoryProvider chatMemoryProviderFinancial(MongoChatMemoryStore mongoChatMemoryStore) {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(40)
                .chatMemoryStore(mongoChatMemoryStore)
                .build();
    }
}
src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.ai.config;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ManufacturingAgentConfig {
    @Bean
    ChatMemoryProvider chatMemoryProviderManufacturing(MongoChatMemoryStore mongoChatMemoryStore) {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(30)
                .chatMemoryStore(mongoChatMemoryStore)
                .build();
    }
}
src/main/java/com/ruoyi/ai/config/SalesAgentConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.ai.config;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SalesAgentConfig {
    @Bean
    ChatMemoryProvider chatMemoryProviderSales(MongoChatMemoryStore mongoChatMemoryStore) {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(30)
                .chatMemoryStore(mongoChatMemoryStore)
                .build();
    }
}
src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java
@@ -4,6 +4,8 @@
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -11,18 +13,26 @@
public class AiSessionUserContext {
    private final Map<String, LoginUser> loginUserByMemoryId = new ConcurrentHashMap<>();
    private final Map<String, Instant> lastAccessTimeByMemoryId = new ConcurrentHashMap<>();
    private static final Duration SESSION_TIMEOUT = Duration.ofHours(24);
    public void bind(String memoryId, LoginUser loginUser) {
        if (!StringUtils.hasText(memoryId) || loginUser == null) {
            return;
        }
        loginUserByMemoryId.put(memoryId, loginUser);
        lastAccessTimeByMemoryId.put(memoryId, Instant.now());
    }
    public LoginUser get(String memoryId) {
        if (!StringUtils.hasText(memoryId)) {
            return null;
        }
        if (isExpired(memoryId)) {
            remove(memoryId);
            return null;
        }
        lastAccessTimeByMemoryId.put(memoryId, Instant.now());
        return loginUserByMemoryId.get(memoryId);
    }
@@ -31,5 +41,25 @@
            return;
        }
        loginUserByMemoryId.remove(memoryId);
        lastAccessTimeByMemoryId.remove(memoryId);
    }
    public void cleanExpiredSessions() {
        Instant now = Instant.now();
        lastAccessTimeByMemoryId.entrySet().removeIf(entry -> {
            boolean expired = Duration.between(entry.getValue(), now).compareTo(SESSION_TIMEOUT) > 0;
            if (expired) {
                loginUserByMemoryId.remove(entry.getKey());
            }
            return expired;
        });
    }
    private boolean isExpired(String memoryId) {
        Instant lastAccess = lastAccessTimeByMemoryId.get(memoryId);
        if (lastAccess == null) {
            return true;
        }
        return Duration.between(lastAccess, Instant.now()).compareTo(SESSION_TIMEOUT) > 0;
    }
}
src/main/java/com/ruoyi/ai/controller/FinancialAiController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
package com.ruoyi.ai.controller;
import com.ruoyi.ai.assistant.FinancialAgent;
import com.ruoyi.ai.assistant.FinancialIntentExecutor;
import com.ruoyi.ai.bean.ChatForm;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.ai.service.AiChatSessionService;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Tag(name = "财务智能体")
@RestController
@RequestMapping("/financial-ai")
public class FinancialAiController extends BaseController {
    private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private final FinancialAgent financialAgent;
    private final FinancialIntentExecutor financialIntentExecutor;
    private final AiSessionUserContext aiSessionUserContext;
    private final MongoChatMemoryStore mongoChatMemoryStore;
    private final AiChatSessionService aiChatSessionService;
    public FinancialAiController(FinancialAgent financialAgent,
                                 FinancialIntentExecutor financialIntentExecutor,
                                 AiSessionUserContext aiSessionUserContext,
                                 MongoChatMemoryStore mongoChatMemoryStore,
                                 AiChatSessionService aiChatSessionService) {
        this.financialAgent = financialAgent;
        this.financialIntentExecutor = financialIntentExecutor;
        this.aiSessionUserContext = aiSessionUserContext;
        this.mongoChatMemoryStore = mongoChatMemoryStore;
        this.aiChatSessionService = aiChatSessionService;
    }
    @Operation(summary = "财务智能体对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (!StringUtils.hasText(chatForm.getMemoryId())) {
            return Flux.just("memoryId不能为空");
        }
        if (!StringUtils.hasText(chatForm.getMessage())) {
            return Flux.just("message不能为空");
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String memoryId = chatForm.getMemoryId();
        String userMessage = chatForm.getMessage();
        aiSessionUserContext.bind(memoryId, loginUser);
        aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
        String directResponse = financialIntentExecutor.tryExecute(memoryId, userMessage);
        if (StringUtils.isNotEmpty(directResponse)) {
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(directResponse);
        }
        if (isBusinessDataIntent(userMessage)) {
            String noGuessResponse = "未识别到可执行的数据查询条件。为保证结果准确,当前不会推测或编造数据,请补充明确时间范围、客户、供应商或单号后再查询。";
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(noGuessResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(noGuessResponse);
        }
        return financialAgent.chat(memoryId, userMessage, currentDateForPrompt())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
    @Operation(summary = "财务智能体会话列表")
    @GetMapping("/history/sessions")
    public AjaxResult listSessions() {
        return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "财务智能体会话消息")
    @GetMapping("/history/messages/{memoryId}")
    public AjaxResult listMessages(@PathVariable String memoryId) {
        return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "删除财务智能体会话")
    @DeleteMapping("/history/{memoryId}")
    public AjaxResult deleteSession(@PathVariable String memoryId) {
        aiSessionUserContext.remove(memoryId);
        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
    }
    private String currentDateForPrompt() {
        return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
    }
    private boolean isBusinessDataIntent(String message) {
        if (!StringUtils.hasText(message)) {
            return false;
        }
        String text = message.trim();
        return containsAny(text,
                "查询", "查看", "统计", "分析", "建议", "成本核算", "产品成本", "工序成本",
                "订单利润", "亏损订单", "低利润", "库存资金", "库存积压", "呆滞库存",
                "现金流", "回款风险", "付款压力", "资金缺口", "应收", "应付",
                "异常预警", "经营异常", "风险预警", "驾驶舱", "经营看板", "经营总览",
                "日报", "周报", "经营报告", "分析报告", "业财融合", "口径", "指标解释");
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.contains(keyword)) {
                return true;
            }
        }
        return false;
    }
}
src/main/java/com/ruoyi/ai/controller/KnowledgeChatController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
package com.ruoyi.ai.controller;
import com.ruoyi.ai.assistant.KnowledgeChatAgent;
import com.ruoyi.ai.dto.KnowledgeChatRequest;
import com.ruoyi.ai.service.KnowledgeRagService;
import com.ruoyi.approve.pojo.KnowledgeBase;
import com.ruoyi.approve.service.KnowledgeBaseService;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import java.util.List;
/**
 * çŸ¥è¯†åº“问答Controller
 */
@Slf4j
@RestController
@RequestMapping("/ai/knowledge")
@RequiredArgsConstructor
@Tag(name = "知识库问答")
public class KnowledgeChatController {
    private final KnowledgeChatAgent knowledgeChatAgent;
    private final KnowledgeRagService knowledgeRagService;
    private final KnowledgeBaseService knowledgeBaseService;
    /**
     * çŸ¥è¯†åº“问答(流式返回)
     */
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    @Operation(summary = "知识库问答")
    public Flux<String> chat(@RequestBody KnowledgeChatRequest request) {
        if (request.getKnowledgeBaseId() == null) {
            return Flux.just("知识库ID不能为空");
        }
        if (!StringUtils.hasText(request.getMemoryId())) {
            return Flux.just("会话ID不能为空");
        }
        if (!StringUtils.hasText(request.getQuestion())) {
            return Flux.just("问题不能为空");
        }
        KnowledgeBase knowledgeBase = knowledgeBaseService.getById(request.getKnowledgeBaseId());
        if (knowledgeBase == null) {
            return Flux.just("知识库不存在");
        }
        String namespace = "kb-" + request.getKnowledgeBaseId();
        List<String> relevantContents = knowledgeRagService.searchRelevantContent(
                namespace, request.getQuestion(), 5);
        if (relevantContents.isEmpty()) {
            return Flux.just("知识库中未找到相关内容,请先上传相关文档。");
        }
        StringBuilder contextBuilder = new StringBuilder();
        contextBuilder.append("以下是从知识库中检索到的相关内容:\n\n");
        for (int i = 0; i < relevantContents.size(); i++) {
            contextBuilder.append("【内容").append(i + 1).append("】\n");
            contextBuilder.append(relevantContents.get(i)).append("\n\n");
        }
        contextBuilder.append("---\n");
        contextBuilder.append("请基于以上知识库内容回答用户问题:\n");
        contextBuilder.append(request.getQuestion());
        return knowledgeChatAgent.chat(request.getMemoryId(), contextBuilder.toString());
    }
    /**
     * çŸ¥è¯†åº“列表(用于选择知识库)
     */
    @GetMapping("/list")
    @Operation(summary = "知识库列表")
    public AjaxResult listKnowledgeBases() {
        List<KnowledgeBase> list = knowledgeBaseService.list();
        return AjaxResult.success(list);
    }
}
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
package com.ruoyi.ai.controller;
import com.ruoyi.ai.assistant.ManufacturingAgent;
import com.ruoyi.ai.assistant.ManufacturingIntentExecutor;
import com.ruoyi.ai.bean.ChatForm;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.ai.service.AiChatSessionService;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Tag(name = "制造智能助手")
@RestController
@RequestMapping("/manufacturing-ai")
public class ManufacturingAiController extends BaseController {
    private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private final ManufacturingAgent manufacturingAgent;
    private final ManufacturingIntentExecutor manufacturingIntentExecutor;
    private final AiSessionUserContext aiSessionUserContext;
    private final MongoChatMemoryStore mongoChatMemoryStore;
    private final AiChatSessionService aiChatSessionService;
    public ManufacturingAiController(ManufacturingAgent manufacturingAgent,
                                     ManufacturingIntentExecutor manufacturingIntentExecutor,
                                     AiSessionUserContext aiSessionUserContext,
                                     MongoChatMemoryStore mongoChatMemoryStore,
                                     AiChatSessionService aiChatSessionService) {
        this.manufacturingAgent = manufacturingAgent;
        this.manufacturingIntentExecutor = manufacturingIntentExecutor;
        this.aiSessionUserContext = aiSessionUserContext;
        this.mongoChatMemoryStore = mongoChatMemoryStore;
        this.aiChatSessionService = aiChatSessionService;
    }
    @Operation(summary = "制造对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (!StringUtils.hasText(chatForm.getMemoryId())) {
            return Flux.just("memoryId不能为空");
        }
        if (!StringUtils.hasText(chatForm.getMessage())) {
            return Flux.just("message不能为空");
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String memoryId = chatForm.getMemoryId();
        String userMessage = chatForm.getMessage();
        aiSessionUserContext.bind(memoryId, loginUser);
        aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
        String directResponse = manufacturingIntentExecutor.tryExecute(memoryId, userMessage);
        if (StringUtils.isNotEmpty(directResponse)) {
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(directResponse);
        }
        return manufacturingAgent.chat(memoryId, userMessage, currentDateForPrompt())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
    @Operation(summary = "制造会话列表")
    @GetMapping("/history/sessions")
    public AjaxResult listSessions() {
        return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "制造会话消息")
    @GetMapping("/history/messages/{memoryId}")
    public AjaxResult listMessages(@PathVariable String memoryId) {
        return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "删除制造会话")
    @DeleteMapping("/history/{memoryId}")
    public AjaxResult deleteSession(@PathVariable String memoryId) {
        aiSessionUserContext.remove(memoryId);
        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
    }
    private String currentDateForPrompt() {
        return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
    }
}
src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
@@ -1,151 +1,41 @@
package com.ruoyi.ai.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.ai.assistant.PurchaseAgent;
import com.ruoyi.ai.assistant.PurchaseIntentExecutor;
import com.ruoyi.ai.bean.ChatForm;
import com.ruoyi.ai.bean.PurchaseAiConfirmRequest;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.ai.service.AiChatSessionService;
import com.ruoyi.ai.service.AiFileTextExtractor;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import com.ruoyi.basic.mapper.SupplierManageMapper;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.ai.service.PurchaseAiService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
import com.ruoyi.purchase.pojo.PaymentRegistration;
import com.ruoyi.purchase.service.IPaymentRegistrationService;
import com.ruoyi.purchase.service.IPurchaseLedgerService;
import com.ruoyi.purchase.service.PurchaseReturnOrdersService;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import dev.langchain4j.data.image.Image;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.Content;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Base64;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
@Tag(name = "采购智能体")
@RestController
@RequestMapping("/purchase-ai")
public class PurchaseAiController extends BaseController {
    private static final String PURCHASE_FILE_ANALYZE_MEMORY_PREFIX = "purchase-file-analyze::";
    private static final int MAX_FILE_COUNT = 10;
    private static final int MAX_SINGLE_FILE_TEXT_LENGTH = 8000;
    private static final int MAX_TOTAL_FILE_TEXT_LENGTH = 30000;
    private final PurchaseAiService purchaseAiService;
    private final PurchaseAgent purchaseAgent;
    private final PurchaseIntentExecutor purchaseIntentExecutor;
    private final AiSessionUserContext aiSessionUserContext;
    private final MongoChatMemoryStore mongoChatMemoryStore;
    private final AiChatSessionService aiChatSessionService;
    private final AiFileTextExtractor aiFileTextExtractor;
    private final ObjectMapper objectMapper;
    private final IPurchaseLedgerService purchaseLedgerService;
    private final IPaymentRegistrationService paymentRegistrationService;
    private final PurchaseReturnOrdersService purchaseReturnOrdersService;
    private final SupplierManageMapper supplierManageMapper;
    private final StreamingChatLanguageModel purchaseVisionStreamingChatModel;
    public PurchaseAiController(PurchaseAgent purchaseAgent,
                                PurchaseIntentExecutor purchaseIntentExecutor,
                                AiSessionUserContext aiSessionUserContext,
                                MongoChatMemoryStore mongoChatMemoryStore,
                                AiChatSessionService aiChatSessionService,
                                AiFileTextExtractor aiFileTextExtractor,
                                ObjectMapper objectMapper,
                                IPurchaseLedgerService purchaseLedgerService,
                                IPaymentRegistrationService paymentRegistrationService,
                                PurchaseReturnOrdersService purchaseReturnOrdersService,
                                SupplierManageMapper supplierManageMapper,
                                @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) {
        this.purchaseAgent = purchaseAgent;
        this.purchaseIntentExecutor = purchaseIntentExecutor;
        this.aiSessionUserContext = aiSessionUserContext;
        this.mongoChatMemoryStore = mongoChatMemoryStore;
        this.aiChatSessionService = aiChatSessionService;
        this.aiFileTextExtractor = aiFileTextExtractor;
        this.objectMapper = objectMapper;
        this.purchaseLedgerService = purchaseLedgerService;
        this.paymentRegistrationService = paymentRegistrationService;
        this.purchaseReturnOrdersService = purchaseReturnOrdersService;
        this.supplierManageMapper = supplierManageMapper;
        this.purchaseVisionStreamingChatModel = purchaseVisionStreamingChatModel;
    public PurchaseAiController(PurchaseAiService purchaseAiService) {
        this.purchaseAiService = purchaseAiService;
    }
    @Operation(summary = "采购对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (!StringUtils.hasText(chatForm.getMemoryId())) {
            return Flux.just("memoryId不能为空");
        }
        if (!StringUtils.hasText(chatForm.getMessage())) {
            return Flux.just("message不能为空");
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String memoryId = chatForm.getMemoryId();
        String userMessage = chatForm.getMessage();
        aiSessionUserContext.bind(memoryId, loginUser);
        aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
        String directResponse = purchaseIntentExecutor.tryExecute(memoryId, userMessage);
        if (StringUtils.isNotEmpty(directResponse)) {
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(directResponse);
        }
        return purchaseAgent.chat(memoryId, userMessage)
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
        return purchaseAiService.chat(chatForm, loginUser);
    }
    @Operation(summary = "采购多文件分析")
@@ -153,751 +43,34 @@
    public Flux<String> analyzeFiles(@RequestParam("files") MultipartFile[] files,
                                     @RequestParam(value = "message", required = false) String message,
                                     @RequestParam(value = "memoryId", required = false) String memoryId) {
        if (files == null || files.length == 0) {
            return Flux.just("files不能为空");
        }
        if (files.length > MAX_FILE_COUNT) {
            return Flux.just("一次最多分析" + MAX_FILE_COUNT + "个文件");
        }
        String rawMemoryId = StringUtils.hasText(memoryId) ? memoryId : UUID.randomUUID().toString();
        String finalMemoryId = rawMemoryId.startsWith(PURCHASE_FILE_ANALYZE_MEMORY_PREFIX)
                ? rawMemoryId
                : PURCHASE_FILE_ANALYZE_MEMORY_PREFIX + rawMemoryId;
        LoginUser loginUser = SecurityUtils.getLoginUser();
        aiSessionUserContext.bind(finalMemoryId, loginUser);
        String finalMessage = StringUtils.hasText(message)
                ? message
                : "请分析这些采购文件,提取可用于业务处理的数据,并整理成待客户确认的格式";
        String fileContent;
        try {
            fileContent = buildMultiFileContent(files);
        } catch (IllegalArgumentException ex) {
            return Flux.just(ex.getMessage());
        } catch (IOException ex) {
            return Flux.just("文件读取失败");
        }
        if (!StringUtils.hasText(fileContent)) {
            return Flux.just("未提取到有效文件内容");
        }
        String userPrompt = buildPurchaseFileAnalyzePrompt(finalMessage, fileContent);
        aiChatSessionService.touchSession(finalMemoryId, loginUser, "采购多文件分析: " + finalMessage);
        if (containsImageFile(files)) {
            return chatWithPurchaseVisionModel(finalMemoryId, userPrompt, files)
                    .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
                    .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
        }
        return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt))
                .onErrorResume(NoSuchElementException.class, ex -> {
                    mongoChatMemoryStore.deleteMessages(finalMemoryId);
                    return purchaseAgent.chat(finalMemoryId, userPrompt);
                })
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
        return purchaseAiService.analyzeFiles(files, message, memoryId, loginUser);
    }
    @Operation(summary = "采购多文件分析确认处理")
    @PostMapping("/analyze-files/confirm")
    public AjaxResult confirmAnalyzeResult(@RequestBody PurchaseAiConfirmRequest request) {
        if (request == null || !StringUtils.hasText(request.getBusinessType())) {
            return AjaxResult.error("businessType不能为空");
        }
        if (request.getPayload() == null || request.getPayload().isEmpty()) {
            return AjaxResult.error("payload不能为空");
        }
        try {
            String businessType = request.getBusinessType().trim();
            return switch (businessType) {
                case "purchase_ledger" -> processPurchaseLedger(request.getPayload());
                case "payment_registration" -> processPaymentRegistration(request.getPayload());
                case "purchase_return_order" -> processPurchaseReturnOrder(request.getPayload());
                default -> AjaxResult.error("暂不支持该业务类型: " + businessType);
            };
        } catch (Exception ex) {
            return AjaxResult.error(toCustomerMessage(ex));
        }
        return purchaseAiService.confirmAnalyzeResult(request);
    }
    @Operation(summary = "采购会话列表")
    @GetMapping("/history/sessions")
    public AjaxResult listSessions() {
        return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
        LoginUser loginUser = SecurityUtils.getLoginUser();
        return success(purchaseAiService.listSessions(loginUser));
    }
    @Operation(summary = "采购会话消息")
    @GetMapping("/history/messages/{memoryId}")
    public AjaxResult listMessages(@PathVariable String memoryId) {
        return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
        LoginUser loginUser = SecurityUtils.getLoginUser();
        return success(purchaseAiService.listMessages(memoryId, loginUser));
    }
    @Operation(summary = "删除采购会话")
    @DeleteMapping("/history/{memoryId}")
    public AjaxResult deleteSession(@PathVariable String memoryId) {
        aiSessionUserContext.remove(memoryId);
        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
    }
    private String buildMultiFileContent(MultipartFile[] files) throws IOException {
        StringBuilder builder = new StringBuilder();
        int totalLength = 0;
        for (MultipartFile file : files) {
            String text = aiFileTextExtractor.extractText(file);
            if (!StringUtils.hasText(text)) {
                continue;
            }
            String limitedText = text.length() > MAX_SINGLE_FILE_TEXT_LENGTH
                    ? text.substring(0, MAX_SINGLE_FILE_TEXT_LENGTH)
                    : text;
            if (totalLength + limitedText.length() > MAX_TOTAL_FILE_TEXT_LENGTH) {
                int remain = MAX_TOTAL_FILE_TEXT_LENGTH - totalLength;
                if (remain <= 0) {
                    break;
                }
                limitedText = limitedText.substring(0, remain);
            }
            builder.append("\n--- æ–‡ä»¶: ")
                    .append(file.getOriginalFilename())
                    .append(" ---\n")
                    .append(limitedText)
                    .append('\n');
            totalLength += limitedText.length();
        }
        return builder.toString();
    }
    private boolean containsImageFile(MultipartFile[] files) {
        for (MultipartFile file : files) {
            if (aiFileTextExtractor.isImageFile(file)) {
                return true;
            }
        }
        return false;
    }
    private Flux<String> chatWithPurchaseVisionModel(String memoryId, String userPrompt, MultipartFile[] files) {
        return Flux.create(sink -> {
            try {
                List<Content> contents = new ArrayList<>();
                contents.add(TextContent.from(userPrompt));
                for (MultipartFile file : files) {
                    if (!aiFileTextExtractor.isImageFile(file)) {
                        continue;
                    }
                    contents.add(TextContent.from("下面这张图片文件名:" + file.getOriginalFilename()));
                    contents.add(ImageContent.from(Image.builder()
                            .base64Data(Base64.getEncoder().encodeToString(file.getBytes()))
                            .mimeType(resolveImageMimeType(file))
                            .build()));
                }
                List<ChatMessage> messages = List.of(
                        SystemMessage.from("你是采购业务文件分析助手。请从文本和图片中识别采购台账、采购产品明细、付款或退货信息,只输出合法 JSON。"),
                        UserMessage.from(contents)
                );
                purchaseVisionStreamingChatModel.chat(messages, new StreamingChatResponseHandler() {
                    @Override
                    public void onPartialResponse(String partialResponse) {
                        sink.next(partialResponse);
                    }
                    @Override
                    public void onCompleteResponse(ChatResponse completeResponse) {
                        sink.complete();
                    }
                    @Override
                    public void onError(Throwable error) {
                        sink.error(error);
                    }
                });
            } catch (Exception ex) {
                sink.next("图片文件读取失败,请确认图片格式为 png、jpg、jpeg、webp æˆ– bmp,且大小不超过10MB");
                sink.complete();
            }
        });
    }
    private String resolveImageMimeType(MultipartFile file) {
        String contentType = file.getContentType();
        if (StringUtils.hasText(contentType) && contentType.startsWith("image/")) {
            return contentType;
        }
        String filename = file.getOriginalFilename();
        String ext = "";
        if (StringUtils.hasText(filename) && filename.contains(".")) {
            ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
        }
        return switch (ext) {
            case "jpg", "jpeg" -> "image/jpeg";
            case "webp" -> "image/webp";
            case "bmp" -> "image/bmp";
            default -> "image/png";
        };
    }
    private String buildPurchaseFileAnalyzePrompt(String message, String fileContent) {
        return """
                ä½ æ˜¯é‡‡è´­ä¸šåŠ¡æ–‡ä»¶åˆ†æžåŠ©æ‰‹ã€‚è¯·ä¸¥æ ¼æ ¹æ®ç”¨æˆ·ä¸Šä¼ çš„å¤šä¸ªæ–‡ä»¶å’Œç”¨æˆ·è¦æ±‚æå–é‡‡è´­ä¸šåŠ¡æ•°æ®ã€‚
                ç”¨æˆ·è¦æ±‚:
                %s
                è¾“出要求:
                1. åªè¾“出合法 JSON,不要 Markdown,不要额外解释。
                2. JSON é¡¶å±‚字段固定为:
                   - success: boolean
                   - businessType: purchase_ledger | payment_registration | purchase_return_order | unknown
                   - action: confirm_required
                   - description: ä¸­æ–‡è¯´æ˜Ž
                   - confidence: 0到1的小数
                   - missingFields: ç¼ºå¤±å­—段中文名称数组,面向客户展示,不要输出英文字段名
                   - warnings: é£Žé™©æç¤ºæ•°ç»„
                   - payload: å¾…客户确认的数据,字段名必须使用后端 DTO å­—段名
                   - preview: ç»™å®¢æˆ·ç¡®è®¤ç”¨çš„中文摘要数组
                3. å¦‚果可判断为采购台账,businessType ä½¿ç”¨ purchase_ledger,payload.purchaseLedgers ä¸ºé‡‡è´­è®¢å•/采购台账数组:
                   - purchaseLedgers: é‡‡è´­è®¢å•/采购台账数组,每条记录字段名必须与 PurchaseLedgerDto ä¿æŒä¸€è‡´
                   - äº§å“æ˜Žç»†å¿…须放在每条采购台账记录的 productData å­—段中,productData ç±»åž‹ä¸º List<SalesLedgerProduct>
                   - ä¸è¦ä¼˜å…ˆä½¿ç”¨ payload é¡¶å±‚ productData;顶层 productData ä»…作为旧格式兼容
                   - æ–‡ä»¶é‡Œçš„“采购单号”就是“采购合同号”,统一映射为 purchaseContractNumber
                   - æ–‡ä»¶é‡Œçš„“销售单号”就是“销售合同号”,统一映射为 salesContractNo
                   - æ‰€æœ‰æ—¥æœŸå­—段必须使用 yyyy-MM-dd,例如 2026-04-30;不要输出 4/30/26、2026/4/30、2026å¹´4月30日 æˆ–带时分秒的格式
                   - é‡‡è´­å°è´¦ä¸éœ€è¦åœ¨ payload ä¸­ä¼ å®¡æ‰¹äººï¼Œä¸è¦è¾“出 approveUserIds、approverId
                   - missingFields åªå¡«å†™ä¸šåŠ¡å¿…å¡«ä½†æ— æ³•è¯†åˆ«çš„å­—æ®µï¼Œä¸è¦æŠŠ PurchaseLedgerDto çš„æ‰€æœ‰ç©ºå­—段都列为缺失;缺失项必须写中文,例如“供应商名称”“含税单价”,不要写 supplierId、taxInclusiveUnitPrice
                   - é‡‡è´­å°è´¦ä¸»è¡¨å¿…填字段仅按这些判断: purchaseContractNumber、supplierName æˆ– supplierId
                   - productData æ¯æ¡äº§å“å¿…填字段: productCategory、specificationModel、unit、quantity、taxInclusiveUnitPrice æˆ– taxInclusiveTotalPrice;如果只有含税总价和数量,必须计算 taxInclusiveUnitPrice;如果只有含税单价和数量,必须计算 taxInclusiveTotalPrice
                   - äº§å“å­—段按采购导入接口 PurchaseLedgerProductImportDto å¯¹é½: é‡‡è´­å•号、产品大类、规格型号、单位、数量、税率、含税单价、含税总价、发票类型、是否质检
                   - é‡‡è´­äº§å“ type å›ºå®šä¸º 2
                   - purchaseLedgers æ¯æ¡è®°å½•只使用这些 PurchaseLedgerDto å­—段名:
                     entryDateStart, entryDateEnd, id, purchaseContractNumber, supplierId, supplierName, isWhite, recorderId, recorderName, salesContractNo, salesContractNoId, projectName, entryDate, executionDate, remarks, attachmentMaterials, createdAt, updatedAt, salesLedgerId, hasChildren, Type, productData, tempFileIds, SalesLedgerFiles, phoneNumber, businessPersonId, productId, productModelId, invoiceNumber, invoiceAmount, ticketRegistrationId, contractAmount, receiptPaymentAmount, unReceiptPaymentAmount, type, paymentMethod, approvalStatus, templateName
                   - productData æ¯æ¡äº§å“åªä½¿ç”¨è¿™äº› SalesLedgerProduct å­—段名:
                     productCategory, specificationModel, unit, quantity, taxRate, taxInclusiveUnitPrice, taxInclusiveTotalPrice, taxExclusiveTotalPrice, invoiceType, productId, productModelId, isChecked, type
                4. å¦‚果可判断为付款登记,businessType ä½¿ç”¨ payment_registration,payload.records ä¸ºä»˜æ¬¾ç™»è®°æ•°ç»„,字段尽量包含 purchaseLedgerId、salesLedgerProductId、currentPaymentAmount、paymentMethod、paymentDate。
                5. å¦‚果可判断为采购退货,businessType ä½¿ç”¨ purchase_return_order,payload æŒ‰ PurchaseReturnOrderDto ç»„织,明细放 purchaseReturnOrderProductsDtos。
                6. ç¼ºå°‘业务处理必须字段时,不要编造 ID,把字段放入 missingFields,并仍返回可确认的草稿数据。
                7. æ‰€æœ‰ä¸­æ–‡å†…容直接保留,不要转义成 Unicode。
                æ–‡ä»¶å†…容:
                %s
                """.formatted(message, fileContent);
    }
    private AjaxResult processPurchaseLedger(Map<String, Object> payload) throws Exception {
        if (payload.containsKey("purchaseLedgers")) {
            return processPurchaseLedgerBatch(payload);
        }
        Map<String, Object> normalizedPayload = normalizePurchaseLedgerMap(payload);
        PurchaseLedgerDto dto = objectMapper.convertValue(normalizedPayload, PurchaseLedgerDto.class);
        AjaxResult ledgerResult = validatePurchaseLedger(dto, 0);
        if (ledgerResult != null) {
            return ledgerResult;
        }
        AjaxResult supplierResult = fillSupplierIdByName(dto);
        if (supplierResult != null) {
            return supplierResult;
        }
        AjaxResult productResult = validatePurchaseProducts(dto.getProductData(), 0);
        if (productResult != null) {
            return productResult;
        }
        int result = purchaseLedgerService.addOrEditPurchase(dto);
        return AjaxResult.success("采购台账已处理", result);
    }
    private AjaxResult processPurchaseLedgerBatch(Map<String, Object> payload) throws Exception {
        List<Map<String, Object>> purchaseLedgers = toMapList(payload.get("purchaseLedgers"));
        if (purchaseLedgers.isEmpty()) {
            return AjaxResult.error("purchaseLedgers不能为空");
        }
        List<Map<String, Object>> topLevelProductData = toMapList(payload.get("productData"));
        List<Map<String, Object>> results = new ArrayList<>();
        for (int i = 0; i < purchaseLedgers.size(); i++) {
            Map<String, Object> ledgerMap = normalizePurchaseLedgerMap(purchaseLedgers.get(i));
            PurchaseLedgerDto dto = objectMapper.convertValue(ledgerMap, PurchaseLedgerDto.class);
            AjaxResult ledgerResult = validatePurchaseLedger(dto, i);
            if (ledgerResult != null) {
                return ledgerResult;
            }
            AjaxResult supplierResult = fillSupplierIdByName(dto);
            if (supplierResult != null) {
                return supplierResult;
            }
            List<SalesLedgerProduct> products = dto.getProductData();
            if (products == null || products.isEmpty()) {
                products = matchProductsForLedger(ledgerMap, dto, topLevelProductData, purchaseLedgers.size() == 1);
                dto.setProductData(products);
            }
            AjaxResult productResult = validatePurchaseProducts(products, i);
            if (productResult != null) {
                return productResult;
            }
            int result = purchaseLedgerService.addOrEditPurchase(dto);
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("index", i);
            item.put("purchaseContractNumber", dto.getPurchaseContractNumber());
            item.put("supplierId", dto.getSupplierId());
            item.put("supplierName", dto.getSupplierName());
            item.put("productCount", products.size());
            item.put("result", result);
            results.add(item);
        }
        return AjaxResult.success("采购台账已批量处理", results);
    }
    private List<SalesLedgerProduct> matchProductsForLedger(Map<String, Object> ledgerMap,
                                                            PurchaseLedgerDto dto,
                                                            List<Map<String, Object>> productData,
                                                            boolean onlyOneLedger) {
        List<SalesLedgerProduct> products = new ArrayList<>();
        for (Map<String, Object> productMap : productData) {
            if (onlyOneLedger || productBelongsToLedger(productMap, ledgerMap, dto)) {
                products.add(objectMapper.convertValue(normalizeSalesLedgerProductMap(productMap), SalesLedgerProduct.class));
            }
        }
        return products;
    }
    private boolean productBelongsToLedger(Map<String, Object> productMap, Map<String, Object> ledgerMap, PurchaseLedgerDto dto) {
        Long productPurchaseLedgerId = longValue(productMap, "purchaseLedgerId", "purchaseId", "采购订单id", "采购台账id");
        if (productPurchaseLedgerId != null && dto.getId() != null && productPurchaseLedgerId.equals(dto.getId())) {
            return true;
        }
        Long productSalesLedgerId = longValue(productMap, "salesLedgerId");
        if (productSalesLedgerId != null && dto.getId() != null && productSalesLedgerId.equals(dto.getId())) {
            return true;
        }
        String productContractNo = stringValue(productMap, "purchaseContractNumber", "purchaseContractNo", "采购合同号", "采购单号", "采购订单号");
        if (StringUtils.hasText(productContractNo)
                && StringUtils.hasText(dto.getPurchaseContractNumber())
                && productContractNo.trim().equals(dto.getPurchaseContractNumber().trim())) {
            return true;
        }
        String ledgerContractNo = stringValue(ledgerMap, "purchaseContractNumber", "purchaseContractNo", "采购合同号", "采购单号", "采购订单号");
        if (StringUtils.hasText(productContractNo)
                && StringUtils.hasText(ledgerContractNo)
                && productContractNo.trim().equals(ledgerContractNo.trim())) {
            return true;
        }
        String productSalesContractNo = stringValue(productMap, "salesContractNo", "salesContractNumber", "销售合同号", "销售单号", "销售订单号");
        if (StringUtils.hasText(productSalesContractNo)
                && StringUtils.hasText(dto.getSalesContractNo())
                && productSalesContractNo.trim().equals(dto.getSalesContractNo().trim())) {
            return true;
        }
        String ledgerSalesContractNo = stringValue(ledgerMap, "salesContractNo", "salesContractNumber", "销售合同号", "销售单号", "销售订单号");
        if (StringUtils.hasText(productSalesContractNo)
                && StringUtils.hasText(ledgerSalesContractNo)
                && productSalesContractNo.trim().equals(ledgerSalesContractNo.trim())) {
            return true;
        }
        String productSupplierName = stringValue(productMap, "supplierName", "供应商名称");
        return StringUtils.hasText(productSupplierName)
                && StringUtils.hasText(dto.getSupplierName())
                && productSupplierName.trim().equals(dto.getSupplierName().trim());
    }
    private Map<String, Object> normalizePurchaseLedgerMap(Map<String, Object> source) {
        Map<String, Object> target = new LinkedHashMap<>();
        copyPurchaseLedgerDtoFields(source, target);
        putDtoFieldIfPresent(source, target, "entryDateStart", "录入开始日期", "录入日期开始");
        putDtoFieldIfPresent(source, target, "entryDateEnd", "录入结束日期", "录入日期结束");
        putDtoFieldIfPresent(source, target, "id", "采购台账id", "采购订单id", "主键");
        putDtoFieldIfPresent(source, target, "purchaseContractNumber", "purchaseContractNo", "采购合同号", "采购单号", "采购订单号");
        putDtoFieldIfPresent(source, target, "supplierId", "供应商id", "供应商ID", "供应商名称id", "供应商名称ID");
        putDtoFieldIfPresent(source, target, "supplierName", "供应商", "供应商名称");
        putDtoFieldIfPresent(source, target, "isWhite", "是否白名单");
        putDtoFieldIfPresent(source, target, "recorderId", "录入人id", "录入人ID", "录入人姓名id", "录入人姓名ID");
        putDtoFieldIfPresent(source, target, "recorderName", "录入人", "录入人姓名");
        putDtoFieldIfPresent(source, target, "salesContractNo", "salesContractNumber", "销售合同号", "销售单号", "销售订单号");
        putDtoFieldIfPresent(source, target, "salesContractNoId", "销售合同号id", "销售合同号ID", "销售单号id", "销售单号ID");
        putDtoFieldIfPresent(source, target, "projectName", "项目", "项目名称");
        putDtoFieldIfPresent(source, target, "entryDate", "录入日期");
        putDtoFieldIfPresent(source, target, "executionDate", "签订日期", "合同签订日期");
        putDtoFieldIfPresent(source, target, "remarks", "备注", "说明");
        putDtoFieldIfPresent(source, target, "attachmentMaterials", "附件材料", "附件材料路径或名称");
        putDtoFieldIfPresent(source, target, "createdAt", "创建时间", "记录创建时间");
        putDtoFieldIfPresent(source, target, "updatedAt", "更新时间", "记录最后更新时间");
        putDtoFieldIfPresent(source, target, "salesLedgerId", "销售台账id", "销售台账ID", "关联销售台账主表主键");
        putDtoFieldIfPresent(source, target, "hasChildren", "是否有子级", "是否有明细");
        putDtoFieldIfPresent(source, target, "Type", "台账类型", "业务类型");
        putDtoFieldIfPresent(source, target, "productData", "products", "产品明细", "采购产品明细");
        putDtoFieldIfPresent(source, target, "tempFileIds", "临时文件id", "临时文件ID", "临时文件ids");
        putDtoFieldIfPresent(source, target, "SalesLedgerFiles", "附件列表", "销售台账附件");
        putDtoFieldIfPresent(source, target, "phoneNumber", "业务员手机号", "手机号");
        putDtoFieldIfPresent(source, target, "businessPersonId", "业务员id", "业务员ID");
        putDtoFieldIfPresent(source, target, "productId", "产品id", "产品ID");
        putDtoFieldIfPresent(source, target, "productModelId", "产品规格id", "产品规格ID");
        putDtoFieldIfPresent(source, target, "invoiceNumber", "发票号", "发票号码");
        putDtoFieldIfPresent(source, target, "invoiceAmount", "发票金额", "发票金额(元)");
        putDtoFieldIfPresent(source, target, "ticketRegistrationId", "来票登记id", "来票登记ID");
        putDtoFieldIfPresent(source, target, "contractAmount", "合同金额", "合同金额(产品含税总价)");
        putDtoFieldIfPresent(source, target, "receiptPaymentAmount", "来票金额", "已来票金额", "已来票金额(元)");
        putDtoFieldIfPresent(source, target, "unReceiptPaymentAmount", "未来票金额", "未来票金额(元)");
        putDtoFieldIfPresent(source, target, "type", "文件类型");
        putDtoFieldIfPresent(source, target, "paymentMethod", "付款方式");
        putDtoFieldIfPresent(source, target, "approvalStatus", "审批状态");
        putDtoFieldIfPresent(source, target, "templateName", "模板名称");
        target.remove("approveUserIds");
        target.remove("approverId");
        normalizeNestedProductData(target);
        attachImportStyleProductData(source, target);
        if (target.get("type") == null) {
            target.put("type", 2);
        }
        target.putIfAbsent("entryDate", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
        normalizePurchaseLedgerDateFields(target);
        return target;
    }
    private void attachImportStyleProductData(Map<String, Object> source, Map<String, Object> target) {
        if (target.get("productData") != null) {
            return;
        }
        Map<String, Object> productMap = normalizeSalesLedgerProductMap(source);
        if (hasImportStyleProductData(productMap)) {
            target.put("productData", List.of(productMap));
        }
    }
    private boolean hasImportStyleProductData(Map<String, Object> productMap) {
        return hasMapText(productMap, "productCategory")
                || hasMapText(productMap, "specificationModel")
                || productMap.get("quantity") != null
                || productMap.get("taxInclusiveUnitPrice") != null
                || productMap.get("taxInclusiveTotalPrice") != null;
    }
    private boolean hasMapText(Map<String, Object> map, String key) {
        Object value = map.get(key);
        return value != null && StringUtils.hasText(String.valueOf(value));
    }
    private void normalizeNestedProductData(Map<String, Object> target) {
        Object productDataValue = target.get("productData");
        if (productDataValue == null) {
            return;
        }
        List<Map<String, Object>> productMaps = toMapList(productDataValue);
        List<Map<String, Object>> normalizedProducts = new ArrayList<>();
        for (Map<String, Object> productMap : productMaps) {
            normalizedProducts.add(normalizeSalesLedgerProductMap(productMap));
        }
        target.put("productData", normalizedProducts);
    }
    private Map<String, Object> normalizeSalesLedgerProductMap(Map<String, Object> source) {
        Map<String, Object> target = new LinkedHashMap<>();
        copySalesLedgerProductFields(source, target);
        putDtoFieldIfPresent(source, target, "productCategory", "产品大类", "产品名称", "产品", "品名", "物料名称");
        putDtoFieldIfPresent(source, target, "specificationModel", "规格型号", "型号", "规格", "产品规格");
        putDtoFieldIfPresent(source, target, "unit", "单位");
        putDtoFieldIfPresent(source, target, "quantity", "数量", "采购数量");
        putDtoFieldIfPresent(source, target, "taxRate", "税率");
        putDtoFieldIfPresent(source, target, "taxInclusiveUnitPrice", "含税单价", "单价", "采购单价", "含税价格");
        putDtoFieldIfPresent(source, target, "taxInclusiveTotalPrice", "含税总价", "总价", "采购金额", "金额", "合同金额");
        putDtoFieldIfPresent(source, target, "taxExclusiveTotalPrice", "不含税总价");
        putDtoFieldIfPresent(source, target, "invoiceType", "发票类型", "发票类别");
        putDtoFieldIfPresent(source, target, "productId", "产品id", "产品ID");
        putDtoFieldIfPresent(source, target, "productModelId", "产品规格id", "产品规格ID", "型号id", "型号ID");
        putDtoFieldIfPresent(source, target, "isChecked", "是否质检", "是否质检验", "质检");
        putDtoFieldIfPresent(source, target, "type", "台账类型");
        normalizeProductAmounts(target);
        target.putIfAbsent("type", 2);
        return target;
    }
    private void copySalesLedgerProductFields(Map<String, Object> source, Map<String, Object> target) {
        String[] productFields = {
                "id", "salesLedgerId", "warnNum", "productCategory", "specificationModel", "unit",
                "speculativeTradingName", "quantity", "minStock", "taxRate", "taxInclusiveUnitPrice",
                "taxInclusiveTotalPrice", "taxExclusiveTotalPrice", "invoiceType", "type", "ticketsNum",
                "ticketsAmount", "futureTickets", "futureTicketsAmount", "invoiceNum", "noInvoiceNum",
                "invoiceAmount", "noInvoiceAmount", "productId", "productModelId", "register", "registerDate",
                "approveStatus", "pendingInvoiceTotal", "invoiceTotal", "pendingTicketsTotal", "ticketsTotal",
                "isChecked", "isProduction"
        };
        for (String field : productFields) {
            if (source.containsKey(field)) {
                target.put(field, source.get(field));
            }
        }
    }
    private void normalizeProductAmounts(Map<String, Object> target) {
        BigDecimal quantity = decimalValue(target.get("quantity"));
        BigDecimal unitPrice = decimalValue(target.get("taxInclusiveUnitPrice"));
        BigDecimal totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
        if (unitPrice == null && totalPrice != null && quantity != null && quantity.compareTo(BigDecimal.ZERO) != 0) {
            target.put("taxInclusiveUnitPrice", totalPrice.divide(quantity, 6, RoundingMode.HALF_UP));
        }
        if (totalPrice == null && unitPrice != null && quantity != null) {
            target.put("taxInclusiveTotalPrice", unitPrice.multiply(quantity));
        }
        BigDecimal taxRate = decimalValue(target.get("taxRate"));
        totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
        if (target.get("taxExclusiveTotalPrice") == null && totalPrice != null && taxRate != null) {
            BigDecimal divisor = BigDecimal.ONE.add(taxRate.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP));
            target.put("taxExclusiveTotalPrice", totalPrice.divide(divisor, 2, RoundingMode.HALF_UP));
        }
    }
    private AjaxResult validatePurchaseProducts(List<SalesLedgerProduct> products, int ledgerIndex) {
        if (products == null || products.isEmpty()) {
            return null;
        }
        for (int i = 0; i < products.size(); i++) {
            SalesLedgerProduct product = products.get(i);
            String prefix = "第" + (ledgerIndex + 1) + "个采购台账的第" + (i + 1) + "条产品";
            if (!StringUtils.hasText(product.getProductCategory())) {
                return AjaxResult.error(prefix + "缺少产品名称,请补充后再确认");
            }
            if (!StringUtils.hasText(product.getSpecificationModel())) {
                return AjaxResult.error(prefix + "缺少规格型号,请补充后再确认");
            }
            if (!StringUtils.hasText(product.getUnit())) {
                return AjaxResult.error(prefix + "缺少单位,请补充后再确认");
            }
            if (product.getQuantity() == null) {
                return AjaxResult.error(prefix + "缺少数量");
            }
            if (product.getTaxInclusiveUnitPrice() == null) {
                return AjaxResult.error(prefix + "缺少含税单价,请补充后再确认");
            }
            if (product.getTaxInclusiveTotalPrice() == null) {
                return AjaxResult.error(prefix + "缺少含税总价,请补充后再确认");
            }
        }
        return null;
    }
    private AjaxResult validatePurchaseLedger(PurchaseLedgerDto dto, int ledgerIndex) {
        String prefix = "第" + (ledgerIndex + 1) + "个采购台账";
        if (!StringUtils.hasText(dto.getPurchaseContractNumber())) {
            return AjaxResult.error(prefix + "缺少采购合同号,请补充后再确认");
        }
        if (dto.getSupplierId() == null && !StringUtils.hasText(dto.getSupplierName())) {
            return AjaxResult.error(prefix + "缺少供应商名称,请补充后再确认");
        }
        return null;
    }
    private void normalizePurchaseLedgerDateFields(Map<String, Object> target) {
        normalizeDateField(target, "entryDate");
        normalizeDateField(target, "executionDate");
        normalizeDateField(target, "createdAt");
        normalizeDateField(target, "updatedAt");
    }
    private void normalizeDateField(Map<String, Object> target, String fieldName) {
        Object value = target.get(fieldName);
        if (value == null) {
            return;
        }
        String normalizedDate = normalizeDateValue(value);
        if (StringUtils.hasText(normalizedDate)) {
            target.put(fieldName, normalizedDate);
        }
    }
    private String normalizeDateValue(Object value) {
        if (value instanceof Date date) {
            return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
        }
        if (value instanceof Number number) {
            return LocalDate.of(1899, 12, 30)
                    .plusDays(number.longValue())
                    .format(DateTimeFormatter.ISO_LOCAL_DATE);
        }
        String text = String.valueOf(value).trim();
        if (!StringUtils.hasText(text)) {
            return null;
        }
        if (text.length() >= 10 && text.charAt(4) == '-' && text.charAt(7) == '-') {
            return text.substring(0, 10);
        }
        String normalizedText = text.replace("å¹´", "-")
                .replace("月", "-")
                .replace("日", "")
                .replace(".", "-")
                .replace("/", "-")
                .trim();
        DateTimeFormatter[] formatters = {
                DateTimeFormatter.ofPattern("yyyy-M-d"),
                DateTimeFormatter.ofPattern("M-d-yyyy"),
                DateTimeFormatter.ofPattern("M-d-yy")
        };
        for (DateTimeFormatter formatter : formatters) {
            try {
                return LocalDate.parse(normalizedText, formatter).format(DateTimeFormatter.ISO_LOCAL_DATE);
            } catch (DateTimeParseException ignored) {
                // Try the next supported input pattern.
            }
        }
        return text;
    }
    private void copyPurchaseLedgerDtoFields(Map<String, Object> source, Map<String, Object> target) {
        String[] dtoFields = {
                "entryDateStart", "entryDateEnd", "id", "purchaseContractNumber",
                "supplierId", "supplierName", "isWhite", "recorderId", "recorderName", "salesContractNo",
                "salesContractNoId", "projectName", "entryDate", "executionDate", "remarks", "attachmentMaterials",
                "createdAt", "updatedAt", "salesLedgerId", "hasChildren", "Type", "productData", "tempFileIds",
                "SalesLedgerFiles", "phoneNumber", "businessPersonId", "productId", "productModelId", "invoiceNumber",
                "invoiceAmount", "ticketRegistrationId", "contractAmount", "receiptPaymentAmount",
                "unReceiptPaymentAmount", "type", "paymentMethod", "approvalStatus", "templateName"
        };
        for (String field : dtoFields) {
            if (source.containsKey(field)) {
                target.put(field, source.get(field));
            }
        }
    }
    private void putDtoFieldIfPresent(Map<String, Object> source, Map<String, Object> target, String dtoField, String... aliases) {
        if (target.containsKey(dtoField) && target.get(dtoField) != null) {
            return;
        }
        for (String alias : aliases) {
            Object value = source.get(alias);
            if (value != null && StringUtils.hasText(String.valueOf(value))) {
                target.put(dtoField, value);
                return;
            }
        }
    }
    private List<Map<String, Object>> toMapList(Object value) {
        if (value == null) {
            return List.of();
        }
        return objectMapper.convertValue(value, new TypeReference<List<Map<String, Object>>>() {
        });
    }
    private String stringValue(Map<String, Object> map, String... keys) {
        for (String key : keys) {
            Object value = map.get(key);
            if (value != null && StringUtils.hasText(String.valueOf(value))) {
                return String.valueOf(value);
            }
        }
        return null;
    }
    private Long longValue(Map<String, Object> map, String... keys) {
        String value = stringValue(map, keys);
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            return Long.parseLong(value.trim());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }
    private BigDecimal decimalValue(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof BigDecimal decimal) {
            return decimal;
        }
        if (value instanceof Number number) {
            return new BigDecimal(String.valueOf(number));
        }
        String text = String.valueOf(value)
                .replace(",", "")
                .replace(",", "")
                .replace("元", "")
                .replace("ï¿¥", "")
                .trim();
        if (!StringUtils.hasText(text)) {
            return null;
        }
        try {
            return new BigDecimal(text);
        } catch (NumberFormatException ignored) {
            return null;
        }
    }
    private String toCustomerMessage(Exception ex) {
        String message = ex.getMessage();
        if (!StringUtils.hasText(message)) {
            return "处理失败,请检查确认数据后重试";
        }
        if (message.contains("tax_inclusive_unit_price")) {
            return "处理失败:产品明细缺少含税单价,请补充后再确认";
        }
        if (message.contains("tax_inclusive_total_price")) {
            return "处理失败:产品明细缺少含税总价,请补充后再确认";
        }
        if (message.contains("entryDate")) {
            return "处理失败:录入日期格式不正确,请使用 yyyy-MM-dd,例如 2026-04-30";
        }
        if (message.contains("supplier")) {
            return "处理失败:供应商信息不完整,请确认供应商名称或供应商ID";
        }
        if (message.contains("SQL") || message.contains("java.") || message.contains("Exception")) {
            return "处理失败:确认数据不完整或格式不正确,请检查必填字段后重试";
        }
        return "处理失败:" + message;
    }
    private AjaxResult fillSupplierIdByName(PurchaseLedgerDto dto) {
        if (dto.getSupplierId() != null) {
            return null;
        }
        if (!StringUtils.hasText(dto.getSupplierName())) {
            return AjaxResult.error("供应商ID不能为空;未识别到供应商名称,无法自动匹配供应商ID");
        }
        SupplierManage supplier = supplierManageMapper.selectOne(new LambdaQueryWrapper<SupplierManage>()
                .eq(SupplierManage::getSupplierName, dto.getSupplierName().trim())
                .last("limit 1"));
        if (supplier == null) {
            return AjaxResult.error("未找到供应商:" + dto.getSupplierName() + ",请先维护供应商或手动选择供应商ID");
        }
        dto.setSupplierId(supplier.getId());
        return null;
    }
    private AjaxResult processPaymentRegistration(Map<String, Object> payload) {
        Object recordsValue = payload.get("records");
        List<PaymentRegistration> records;
        if (recordsValue == null) {
            records = Collections.singletonList(objectMapper.convertValue(payload, PaymentRegistration.class));
        } else {
            records = objectMapper.convertValue(recordsValue, new TypeReference<List<PaymentRegistration>>() {
            });
        }
        int result = paymentRegistrationService.insertPaymentRegistration(records);
        return AjaxResult.success("付款登记已处理", result);
    }
    private AjaxResult processPurchaseReturnOrder(Map<String, Object> payload) {
        PurchaseReturnOrderDto dto = objectMapper.convertValue(payload, PurchaseReturnOrderDto.class);
        Boolean result = purchaseReturnOrdersService.add(dto);
        return AjaxResult.success("采购退货单已处理", result);
        LoginUser loginUser = SecurityUtils.getLoginUser();
        return toAjax(purchaseAiService.deleteSession(memoryId, loginUser));
    }
}
src/main/java/com/ruoyi/ai/controller/SalesAiController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,141 @@
package com.ruoyi.ai.controller;
import com.ruoyi.ai.assistant.SalesAgent;
import com.ruoyi.ai.assistant.SalesIntentExecutor;
import com.ruoyi.ai.bean.ChatForm;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.ai.service.AiChatSessionService;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Tag(name = "销售助手智能体")
@RestController
@RequestMapping("/sales-ai")
public class SalesAiController extends BaseController {
    private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private final SalesAgent salesAgent;
    private final SalesIntentExecutor salesIntentExecutor;
    private final AiSessionUserContext aiSessionUserContext;
    private final MongoChatMemoryStore mongoChatMemoryStore;
    private final AiChatSessionService aiChatSessionService;
    public SalesAiController(SalesAgent salesAgent,
                             SalesIntentExecutor salesIntentExecutor,
                             AiSessionUserContext aiSessionUserContext,
                             MongoChatMemoryStore mongoChatMemoryStore,
                             AiChatSessionService aiChatSessionService) {
        this.salesAgent = salesAgent;
        this.salesIntentExecutor = salesIntentExecutor;
        this.aiSessionUserContext = aiSessionUserContext;
        this.mongoChatMemoryStore = mongoChatMemoryStore;
        this.aiChatSessionService = aiChatSessionService;
    }
    @Operation(summary = "销售助手对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (!StringUtils.hasText(chatForm.getMemoryId())) {
            return Flux.just("memoryId不能为空");
        }
        if (!StringUtils.hasText(chatForm.getMessage())) {
            return Flux.just("message不能为空");
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String memoryId = chatForm.getMemoryId();
        String userMessage = chatForm.getMessage();
        aiSessionUserContext.bind(memoryId, loginUser);
        aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
        String directResponse = salesIntentExecutor.tryExecute(memoryId, userMessage);
        if (StringUtils.isNotEmpty(directResponse)) {
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(directResponse);
        }
        if (isBusinessDataIntent(userMessage)) {
            String noGuessResponse = "未识别到可执行的数据查询条件。为保证结果准确,当前不会推测或编造数据,请补充明确时间范围、客户或单号后再查询。";
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(noGuessResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(noGuessResponse);
        }
        return salesAgent.chat(memoryId, userMessage, currentDateForPrompt())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
    @Operation(summary = "销售助手会话列表")
    @GetMapping("/history/sessions")
    public AjaxResult listSessions() {
        return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "销售助手会话消息")
    @GetMapping("/history/messages/{memoryId}")
    public AjaxResult listMessages(@PathVariable String memoryId) {
        return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "删除销售助手会话")
    @DeleteMapping("/history/{memoryId}")
    public AjaxResult deleteSession(@PathVariable String memoryId) {
        aiSessionUserContext.remove(memoryId);
        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
    }
    private boolean isBusinessDataIntent(String message) {
        if (!StringUtils.hasText(message)) {
            return false;
        }
        String text = message.trim();
        return containsAny(text,
                "查询", "查看", "统计", "分析", "建议", "客户档案", "私海", "公海",
                "销售报价", "销售台账", "销售退货", "客户往来", "发货台账", "回款", "报价", "风险");
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.contains(keyword)) {
                return true;
            }
        }
        return false;
    }
    private String currentDateForPrompt() {
        return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
    }
}
src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
@@ -29,6 +29,9 @@
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.UUID;
@@ -39,6 +42,8 @@
public class XiaozhiController extends BaseController {
    private static final String FILE_ANALYZE_MEMORY_PREFIX = "file-analyze::";
    private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private final ApproveTodoAgent approveTodoAgent;
    private final ApproveTodoIntentExecutor approveTodoIntentExecutor;
@@ -90,7 +95,17 @@
            return Flux.just(directResponse);
        }
        return approveTodoAgent.chat(memoryId, userMessage)
        if (isApproveTodoBusinessIntent(userMessage)) {
            String noGuessResponse = "未识别到可执行的审批待办操作条件。为保证结果准确,当前不会推测或编造审批数据,请补充流程编号、时间范围或明确操作指令后再试。";
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(noGuessResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(noGuessResponse);
        }
        return approveTodoAgent.chat(memoryId, userMessage, currentDateForPrompt())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
@@ -159,4 +174,29 @@
        aiSessionUserContext.remove(memoryId);
        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
    }
    private boolean isApproveTodoBusinessIntent(String message) {
        if (!StringUtils.hasText(message)) {
            return false;
        }
        String text = message.trim();
        boolean hasDomainWord = containsAny(text,
                "审批", "待办", "流程编号", "流程号", "审批流转", "审批节点", "当前审批人", "驳回", "通过", "撤销", "删除");
        boolean hasIntentWord = containsAny(text,
                "查询", "查看", "列出", "统计", "分析", "分布", "通过", "驳回", "撤销", "删除", "修改", "有哪些", "卡在");
        return hasDomainWord && hasIntentWord;
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.contains(keyword)) {
                return true;
            }
        }
        return false;
    }
    private String currentDateForPrompt() {
        return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
    }
}
src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java
@@ -1,15 +1,28 @@
package com.ruoyi.ai.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AiChatMessageDto {
    private String role;
    private String content;
    private List<String> filePaths;
    public AiChatMessageDto(String role, String content) {
        this.role = role;
        this.content = content;
    }
    public AiChatMessageDto(String role, String content, List<String> filePaths) {
        this.role = role;
        this.content = content;
        this.filePaths = filePaths;
    }
}
src/main/java/com/ruoyi/ai/dto/KnowledgeChatRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.ai.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * çŸ¥è¯†åº“问答请求
 */
@Data
@Schema(description = "知识库问答请求")
public class KnowledgeChatRequest {
    @Schema(description = "知识库ID", required = true)
    private Long knowledgeBaseId;
    @Schema(description = "会话ID,用于保持上下文", required = true)
    private String memoryId;
    @Schema(description = "用户提问内容", required = true)
    private String question;
}
src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
@@ -9,6 +9,7 @@
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
import java.util.List;
@Data
@AllArgsConstructor
@@ -24,6 +25,21 @@
    private String content;
    /**
     * å¤šæ–‡ä»¶åˆ†æžç”¨æˆ·æé—®ä¿¡æ¯ï¼ˆä¸Žæ–‡ä»¶è·¯å¾„分开存储)
     */
    private List<String> analyzeUserQuestions;
    /**
     * å¤šæ–‡ä»¶åˆ†æžä¸Šä¼ æ–‡ä»¶è·¯å¾„(图片和 pdf ä½¿ç”¨é¢„览地址,其他使用下载地址)
     */
    private List<String> analyzeFilePaths;
    /**
     * å¤šæ–‡ä»¶åˆ†æžæ¯æ¬¡æé—®å¯¹åº”的文件路径分组
     */
    private List<List<String>> analyzeFilePathGroups;
    private Date createTime;
    private Date updateTime;
src/main/java/com/ruoyi/ai/schedule/AiSessionCleanupTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.ai.schedule;
import com.ruoyi.ai.context.AiSessionUserContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class AiSessionCleanupTask {
    private final AiSessionUserContext aiSessionUserContext;
    public AiSessionCleanupTask(AiSessionUserContext aiSessionUserContext) {
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Scheduled(cron = "0 0 2 * * ?")
    public void cleanupExpiredSessions() {
        try {
            aiSessionUserContext.cleanExpiredSessions();
        } catch (Exception e) {
            System.err.println("清理过期AI会话失败: " + e.getMessage());
        }
    }
}
src/main/java/com/ruoyi/ai/service/KnowledgeRagService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.ai.service;
import java.util.List;
/**
 * çŸ¥è¯†åº“RAG服务
 * è´Ÿè´£æ–‡ä»¶å‘量化处理和检索
 */
public interface KnowledgeRagService {
    /**
     * å¼‚步处理向量化
     */
    void processVectorAsync(Long vectorId);
    /**
     * åŒæ­¥å¤„理向量化
     */
    void processVector(Long vectorId);
    /**
     * æ£€ç´¢ç›¸å…³å†…容
     * @param namespace å‘½åç©ºé—´
     * @param query æŸ¥è¯¢æ–‡æœ¬
     * @param maxResults æœ€å¤§ç»“果数
     * @return ç›¸å…³å†…容列表
     */
    List<String> searchRelevantContent(String namespace, String query, int maxResults);
    /**
     * åˆ é™¤æŒ‡å®šæ–‡ä»¶çš„向量数据
     */
    void deleteEmbeddings(String namespace, Long storageBlobId);
}
src/main/java/com/ruoyi/ai/service/PurchaseAiService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1069 @@
package com.ruoyi.ai.service;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.ai.assistant.PurchaseAgent;
import com.ruoyi.ai.assistant.PurchaseIntentExecutor;
import com.ruoyi.ai.bean.ChatForm;
import com.ruoyi.ai.bean.PurchaseAiConfirmRequest;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.ai.dto.AiChatMessageDto;
import com.ruoyi.ai.dto.AiChatSessionDto;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.basic.mapper.SupplierManageMapper;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.basic.service.StorageBlobService;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
import com.ruoyi.purchase.service.IPurchaseLedgerService;
import com.ruoyi.purchase.service.PurchaseReturnOrdersService;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import dev.langchain4j.data.image.Image;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.Content;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Base64;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.nio.file.Files;
@Service
public class PurchaseAiService {
    private static final String PURCHASE_FILE_ANALYZE_MEMORY_PREFIX = "purchase-file-analyze::";
    private static final int MAX_FILE_COUNT = 10;
    private static final int MAX_SINGLE_FILE_TEXT_LENGTH = 8000;
    private static final int MAX_TOTAL_FILE_TEXT_LENGTH = 30000;
    private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private final PurchaseAgent purchaseAgent;
    private final PurchaseIntentExecutor purchaseIntentExecutor;
    private final AiSessionUserContext aiSessionUserContext;
    private final MongoChatMemoryStore mongoChatMemoryStore;
    private final AiChatSessionService aiChatSessionService;
    private final AiFileTextExtractor aiFileTextExtractor;
    private final ObjectMapper objectMapper;
    private final IPurchaseLedgerService purchaseLedgerService;
    private final PurchaseReturnOrdersService purchaseReturnOrdersService;
    private final StorageBlobService storageBlobService;
    private final SupplierManageMapper supplierManageMapper;
    private final StreamingChatLanguageModel purchaseVisionStreamingChatModel;
    public PurchaseAiService(PurchaseAgent purchaseAgent,
                                PurchaseIntentExecutor purchaseIntentExecutor,
                                AiSessionUserContext aiSessionUserContext,
                                MongoChatMemoryStore mongoChatMemoryStore,
                                AiChatSessionService aiChatSessionService,
                                AiFileTextExtractor aiFileTextExtractor,
                                 ObjectMapper objectMapper,
                                 IPurchaseLedgerService purchaseLedgerService,
                                 PurchaseReturnOrdersService purchaseReturnOrdersService,
                                 StorageBlobService storageBlobService,
                                 SupplierManageMapper supplierManageMapper,
                                 @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) {
        this.purchaseAgent = purchaseAgent;
        this.purchaseIntentExecutor = purchaseIntentExecutor;
        this.aiSessionUserContext = aiSessionUserContext;
        this.mongoChatMemoryStore = mongoChatMemoryStore;
        this.aiChatSessionService = aiChatSessionService;
        this.aiFileTextExtractor = aiFileTextExtractor;
        this.objectMapper = objectMapper;
        this.purchaseLedgerService = purchaseLedgerService;
        this.purchaseReturnOrdersService = purchaseReturnOrdersService;
        this.storageBlobService = storageBlobService;
        this.supplierManageMapper = supplierManageMapper;
        this.purchaseVisionStreamingChatModel = purchaseVisionStreamingChatModel;
    }
    public Flux<String> chat(ChatForm chatForm, LoginUser loginUser) {
        if (!StringUtils.hasText(chatForm.getMemoryId())) {
            return Flux.just("memoryId不能为空");
        }
        if (!StringUtils.hasText(chatForm.getMessage())) {
            return Flux.just("message不能为空");
        }
        String memoryId = chatForm.getMemoryId();
        String userMessage = chatForm.getMessage();
        aiSessionUserContext.bind(memoryId, loginUser);
        aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
        String directResponse = purchaseIntentExecutor.tryExecute(memoryId, userMessage);
        if (StringUtils.isNotEmpty(directResponse)) {
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(directResponse);
        }
        if (isPurchaseBusinessIntent(userMessage)) {
            String noGuessResponse = buildNoGuessResponse();
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(noGuessResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(noGuessResponse);
        }
        return purchaseAgent.chat(memoryId, userMessage, currentDateForPrompt())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
    public Flux<String> analyzeFiles(MultipartFile[] files,
                                     String message,
                                     String memoryId,
                                     LoginUser loginUser) {
        if (files == null || files.length == 0) {
            return Flux.just("files不能为空");
        }
        if (files.length > MAX_FILE_COUNT) {
            return Flux.just("一次最多分析" + MAX_FILE_COUNT + "个文件");
        }
        String rawMemoryId = StringUtils.hasText(memoryId) ? memoryId : UUID.randomUUID().toString();
        String finalMemoryId = rawMemoryId.startsWith(PURCHASE_FILE_ANALYZE_MEMORY_PREFIX)
                ? rawMemoryId
                : PURCHASE_FILE_ANALYZE_MEMORY_PREFIX + rawMemoryId;
        aiSessionUserContext.bind(finalMemoryId, loginUser);
        String finalMessage = StringUtils.hasText(message)
                ? message
                : "请分析这些采购文件,提取可用于业务处理的数据,并整理成待客户确认的格式";
        List<String> filePaths;
        try {
            List<StorageBlobVO> uploadedFiles = storageBlobService.upload(copyFilesForUpload(files), true);
            filePaths = resolveFileAccessPaths(uploadedFiles);
        } catch (Exception ex) {
            return Flux.just("文件上传失败");
        }
        try {
            mongoChatMemoryStore.appendAnalyzeFileContext(finalMemoryId, finalMessage, filePaths);
        } catch (Exception ex) {
            return Flux.just("会话文件信息保存失败");
        }
        String fileContent;
        try {
            fileContent = buildMultiFileContent(files);
        } catch (IllegalArgumentException ex) {
            return Flux.just(ex.getMessage());
        } catch (IOException ex) {
            return Flux.just("文件读取失败");
        }
        if (!StringUtils.hasText(fileContent)) {
            return Flux.just("未提取到有效文件内容");
        }
        String userPrompt = buildPurchaseFileAnalyzePrompt(finalMessage, fileContent);
        aiChatSessionService.touchSession(finalMemoryId, loginUser, "采购多文件分析: " + finalMessage);
        if (containsImageFile(files)) {
            return chatWithPurchaseVisionModel(finalMemoryId, finalMessage, userPrompt, files)
                    .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
                    .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
        }
        return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt, currentDateForPrompt()))
                .onErrorResume(NoSuchElementException.class, ex -> {
                    mongoChatMemoryStore.deleteMessages(finalMemoryId);
                    return purchaseAgent.chat(finalMemoryId, userPrompt, currentDateForPrompt());
                })
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
    }
    public AjaxResult confirmAnalyzeResult(PurchaseAiConfirmRequest request) {
        if (request == null || !StringUtils.hasText(request.getBusinessType())) {
            return AjaxResult.error("businessType不能为空");
        }
        if (request.getPayload() == null || request.getPayload().isEmpty()) {
            return AjaxResult.error("payload不能为空");
        }
        try {
            String businessType = request.getBusinessType().trim();
            return switch (businessType) {
                case "purchase_ledger" -> processPurchaseLedger(request.getPayload());
                case "purchase_return_order" -> processPurchaseReturnOrder(request.getPayload());
                default -> AjaxResult.error("暂不支持该业务类型: " + businessType);
            };
        } catch (Exception ex) {
            return AjaxResult.error(toCustomerMessage(ex));
        }
    }
    public List<AiChatSessionDto> listSessions(LoginUser loginUser) {
        return aiChatSessionService.listCurrentUserSessions(loginUser);
    }
    public List<AiChatMessageDto> listMessages(String memoryId, LoginUser loginUser) {
        return aiChatSessionService.listCurrentUserMessages(memoryId, loginUser);
    }
    public boolean deleteSession(String memoryId, LoginUser loginUser) {
        aiSessionUserContext.remove(memoryId);
        return aiChatSessionService.deleteCurrentUserSession(memoryId, loginUser);
    }
    private String buildMultiFileContent(MultipartFile[] files) throws IOException {
        StringBuilder builder = new StringBuilder();
        int totalLength = 0;
        for (MultipartFile file : files) {
            String text = aiFileTextExtractor.extractText(file);
            if (!StringUtils.hasText(text)) {
                continue;
            }
            String limitedText = text.length() > MAX_SINGLE_FILE_TEXT_LENGTH
                    ? text.substring(0, MAX_SINGLE_FILE_TEXT_LENGTH)
                    : text;
            if (totalLength + limitedText.length() > MAX_TOTAL_FILE_TEXT_LENGTH) {
                int remain = MAX_TOTAL_FILE_TEXT_LENGTH - totalLength;
                if (remain <= 0) {
                    break;
                }
                limitedText = limitedText.substring(0, remain);
            }
            builder.append("\n--- æ–‡ä»¶: ")
                    .append(file.getOriginalFilename())
                    .append(" ---\n")
                    .append(limitedText)
                    .append('\n');
            totalLength += limitedText.length();
        }
        return builder.toString();
    }
    private boolean containsImageFile(MultipartFile[] files) {
        for (MultipartFile file : files) {
            if (aiFileTextExtractor.isImageFile(file)) {
                return true;
            }
        }
        return false;
    }
    private List<String> resolveFileAccessPaths(List<StorageBlobVO> uploadedFiles) {
        if (StringUtils.isEmpty(uploadedFiles)) {
            return Collections.emptyList();
        }
        List<String> filePaths = new ArrayList<>();
        for (StorageBlobVO uploadedFile : uploadedFiles) {
            if (uploadedFile == null) {
                continue;
            }
            String selectedPath;
            if (shouldUsePreviewPath(uploadedFile)) {
                selectedPath = StringUtils.hasText(uploadedFile.getPreviewURL())
                        ? uploadedFile.getPreviewURL()
                        : uploadedFile.getDownloadURL();
            } else {
                selectedPath = StringUtils.hasText(uploadedFile.getDownloadURL())
                        ? uploadedFile.getDownloadURL()
                        : uploadedFile.getPreviewURL();
            }
            if (StringUtils.hasText(selectedPath)) {
                filePaths.add(selectedPath);
            }
        }
        return filePaths;
    }
    private boolean shouldUsePreviewPath(StorageBlobVO uploadedFile) {
        String contentType = uploadedFile.getContentType();
        if (StringUtils.hasText(contentType)) {
            String normalized = contentType.toLowerCase(Locale.ROOT);
            if (normalized.startsWith("image/") || "application/pdf".equals(normalized)) {
                return true;
            }
        }
        String filename = uploadedFile.getOriginalFilename();
        if (!StringUtils.hasText(filename) || !filename.contains(".")) {
            return false;
        }
        String ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT);
        return StringUtils.inStringIgnoreCase(ext, "png", "jpg", "jpeg", "webp", "bmp", "pdf");
    }
    private List<MultipartFile> copyFilesForUpload(MultipartFile[] files) throws IOException {
        List<MultipartFile> copies = new ArrayList<>();
        for (MultipartFile file : files) {
            copies.add(new InMemoryMultipartFile(
                    file.getName(),
                    file.getOriginalFilename(),
                    file.getContentType(),
                    file.getBytes()
            ));
        }
        return copies;
    }
    private static final class InMemoryMultipartFile implements MultipartFile {
        private final String name;
        private final String originalFilename;
        private final String contentType;
        private final byte[] bytes;
        private InMemoryMultipartFile(String name, String originalFilename, String contentType, byte[] bytes) {
            this.name = name;
            this.originalFilename = originalFilename;
            this.contentType = contentType;
            this.bytes = bytes == null ? new byte[0] : bytes;
        }
        @Override
        public String getName() {
            return name;
        }
        @Override
        public String getOriginalFilename() {
            return originalFilename;
        }
        @Override
        public String getContentType() {
            return contentType;
        }
        @Override
        public boolean isEmpty() {
            return bytes.length == 0;
        }
        @Override
        public long getSize() {
            return bytes.length;
        }
        @Override
        public byte[] getBytes() {
            return bytes.clone();
        }
        @Override
        public InputStream getInputStream() {
            return new ByteArrayInputStream(bytes);
        }
        @Override
        public void transferTo(File dest) throws IOException, IllegalStateException {
            Files.write(dest.toPath(), bytes);
        }
    }
    private Flux<String> chatWithPurchaseVisionModel(String memoryId,
                                                     String userMessage,
                                                     String userPrompt,
                                                     MultipartFile[] files) {
        return Flux.create(sink -> {
            StringBuilder assistantReply = new StringBuilder();
            try {
                List<Content> contents = new ArrayList<>();
                contents.add(TextContent.from(userPrompt));
                for (MultipartFile file : files) {
                    if (!aiFileTextExtractor.isImageFile(file)) {
                        continue;
                    }
                    contents.add(TextContent.from("下面这张图片文件名:" + file.getOriginalFilename()));
                    contents.add(ImageContent.from(Image.builder()
                            .base64Data(Base64.getEncoder().encodeToString(file.getBytes()))
                            .mimeType(resolveImageMimeType(file))
                            .build()));
                }
                List<ChatMessage> messages = List.of(
                        SystemMessage.from("你是采购业务文件分析助手。请从文本和图片中识别采购台账、采购产品明细、付款或退货信息,只输出合法 JSON。"),
                        UserMessage.from(contents)
                );
                safeAppendMessages(memoryId, List.of(UserMessage.from("采购多文件分析: " + userMessage)));
                purchaseVisionStreamingChatModel.chat(messages, new StreamingChatResponseHandler() {
                    @Override
                    public void onPartialResponse(String partialResponse) {
                        if (partialResponse != null) {
                            assistantReply.append(partialResponse);
                            sink.next(partialResponse);
                        }
                    }
                    @Override
                    public void onCompleteResponse(ChatResponse completeResponse) {
                        if (StringUtils.hasText(assistantReply.toString())) {
                            safeAppendMessages(memoryId, List.of(AiMessage.from(assistantReply.toString())));
                        }
                        sink.complete();
                    }
                    @Override
                    public void onError(Throwable error) {
                        sink.error(error);
                    }
                });
            } catch (Exception ex) {
                sink.next("图片文件读取失败,请确认图片格式为 png、jpg、jpeg、webp æˆ– bmp,且大小不超过10MB");
                sink.complete();
            }
        });
    }
    private void safeAppendMessages(String memoryId, List<ChatMessage> messages) {
        if (!StringUtils.hasText(memoryId) || StringUtils.isEmpty(messages)) {
            return;
        }
        try {
            mongoChatMemoryStore.appendMessages(memoryId, messages);
        } catch (Exception ignored) {
        }
    }
    private String resolveImageMimeType(MultipartFile file) {
        String contentType = file.getContentType();
        if (StringUtils.hasText(contentType) && contentType.startsWith("image/")) {
            return contentType;
        }
        String filename = file.getOriginalFilename();
        String ext = "";
        if (StringUtils.hasText(filename) && filename.contains(".")) {
            ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
        }
        return switch (ext) {
            case "jpg", "jpeg" -> "image/jpeg";
            case "webp" -> "image/webp";
            case "bmp" -> "image/bmp";
            default -> "image/png";
        };
    }
    private String currentDateForPrompt() {
        return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
    }
    private boolean isPurchaseBusinessIntent(String message) {
        if (!StringUtils.hasText(message)) {
            return false;
        }
        String text = message.trim();
        boolean hasDomainWord = containsAny(text,
                "采购", "采购台账", "采购单", "采购订单", "供应商", "物料", "入库", "到货", "待付款",
                "付款", "退货", "退料", "发票", "合同");
        boolean hasIntentWord = containsAny(text,
                "查询", "查看", "统计", "分析", "排行", "排名", "列出", "有哪些", "情况", "明细", "详情", "报表");
        return hasDomainWord && hasIntentWord;
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.contains(keyword)) {
                return true;
            }
        }
        return false;
    }
    private String buildNoGuessResponse() {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("success", false);
        result.put("type", "purchase_intent_not_recognized");
        result.put("description", "未识别到可执行的采购查询条件。为保证结果准确,当前不会推测或编造数据,请补充明确时间范围、供应商、采购合同号或物料后再查询。");
        result.put("summary", Map.of());
        result.put("data", Map.of(
                "quickPrompts", List.of(
                        "本月采购金额排名前十的物料有哪些?",
                        "哪些采购订单还未入库?",
                        "最近7天供应商到货异常有哪些?",
                        "帮我统计待付款采购单!",
                        "列出本月采购退货情况"
                )
        ));
        result.put("charts", Map.of());
        return JSON.toJSONString(result);
    }
    private String buildPurchaseFileAnalyzePrompt(String message, String fileContent) {
        return """
                ä½ æ˜¯é‡‡è´­ä¸šåŠ¡æ–‡ä»¶åˆ†æžåŠ©æ‰‹ã€‚è¯·ä¸¥æ ¼æ ¹æ®ç”¨æˆ·ä¸Šä¼ çš„å¤šä¸ªæ–‡ä»¶å’Œç”¨æˆ·è¦æ±‚æå–é‡‡è´­ä¸šåŠ¡æ•°æ®ã€‚
                ç”¨æˆ·è¦æ±‚:
                %s
                è¾“出要求:
                1. åªè¾“出合法 JSON,不要 Markdown,不要额外解释。
                2. JSON é¡¶å±‚字段固定为:
                   - success: boolean
                   - businessType: purchase_ledger  | purchase_return_order | unknown
                   - action: confirm_required
                   - description: ä¸­æ–‡è¯´æ˜Ž
                   - confidence: 0到1的小数
                   - missingFields: ç¼ºå¤±å­—段中文名称数组,面向客户展示,不要输出英文字段名
                   - warnings: é£Žé™©æç¤ºæ•°ç»„
                   - payload: å¾…客户确认的数据,字段名必须使用后端 DTO å­—段名
                   - preview: ç»™å®¢æˆ·ç¡®è®¤ç”¨çš„中文摘要数组
                3. å¦‚果可判断为采购台账,businessType ä½¿ç”¨ purchase_ledger,payload.purchaseLedgers ä¸ºé‡‡è´­è®¢å•/采购台账数组:
                   - purchaseLedgers: é‡‡è´­è®¢å•/采购台账数组,每条记录字段名必须与 PurchaseLedgerDto ä¿æŒä¸€è‡´
                   - äº§å“æ˜Žç»†å¿…须放在每条采购台账记录的 productData å­—段中,productData ç±»åž‹ä¸º List<SalesLedgerProduct>
                   - ä¸è¦ä¼˜å…ˆä½¿ç”¨ payload é¡¶å±‚ productData;顶层 productData ä»…作为旧格式兼容
                   - æ–‡ä»¶é‡Œçš„“采购单号”就是“采购合同号”,统一映射为 purchaseContractNumber
                   - æ–‡ä»¶é‡Œçš„“销售单号”就是“销售合同号”,统一映射为 salesContractNo
                   - æ‰€æœ‰æ—¥æœŸå­—段必须使用 yyyy-MM-dd,例如 2026-04-30;不要输出 4/30/26、2026/4/30、2026å¹´4月30日 æˆ–带时分秒的格式
                   - é‡‡è´­å°è´¦ä¸éœ€è¦åœ¨ payload ä¸­ä¼ å®¡æ‰¹äººï¼Œä¸è¦è¾“出 approveUserIds、approverId
                   - missingFields åªå¡«å†™ä¸šåŠ¡å¿…å¡«ä½†æ— æ³•è¯†åˆ«çš„å­—æ®µï¼Œä¸è¦æŠŠ PurchaseLedgerDto çš„æ‰€æœ‰ç©ºå­—段都列为缺失;缺失项必须写中文,例如“供应商名称”“含税单价”,不要写 supplierId、taxInclusiveUnitPrice
                   - é‡‡è´­å°è´¦ä¸»è¡¨å¿…填字段仅按这些判断: purchaseContractNumber、supplierName æˆ– supplierId
                   - productData æ¯æ¡äº§å“å¿…填字段: productCategory、specificationModel、unit、quantity、taxInclusiveUnitPrice æˆ– taxInclusiveTotalPrice;如果只有含税总价和数量,必须计算 taxInclusiveUnitPrice;如果只有含税单价和数量,必须计算 taxInclusiveTotalPrice
                   - äº§å“å­—段按采购导入接口 PurchaseLedgerProductImportDto å¯¹é½: é‡‡è´­å•号、产品大类、规格型号、单位、数量、税率、含税单价、含税总价、发票类型、是否质检
                   - é‡‡è´­äº§å“ type å›ºå®šä¸º 2
                   - purchaseLedgers æ¯æ¡è®°å½•只使用这些 PurchaseLedgerDto å­—段名:
                     entryDateStart, entryDateEnd, id, purchaseContractNumber, supplierId, supplierName, isWhite, recorderId, recorderName, salesContractNo, salesContractNoId, projectName, entryDate, executionDate, remarks, attachmentMaterials, createdAt, updatedAt, salesLedgerId, hasChildren, Type, productData, tempFileIds, SalesLedgerFiles, phoneNumber, businessPersonId, productId, productModelId, invoiceNumber, invoiceAmount, ticketRegistrationId, contractAmount, receiptPaymentAmount, unReceiptPaymentAmount, type, paymentMethod, approvalStatus, templateName
                   - productData æ¯æ¡äº§å“åªä½¿ç”¨è¿™äº› SalesLedgerProduct å­—段名:
                     productCategory, specificationModel, unit, quantity, taxRate, taxInclusiveUnitPrice, taxInclusiveTotalPrice, taxExclusiveTotalPrice, invoiceType, productId, productModelId, isChecked, type
                4. å¦‚果可判断为付款登记,businessType ä½¿ç”¨ payload.records ä¸ºä»˜æ¬¾ç™»è®°æ•°ç»„,字段尽量包含 purchaseLedgerId、salesLedgerProductId、currentPaymentAmount、paymentMethod、paymentDate。
                5. å¦‚果可判断为采购退货,businessType ä½¿ç”¨ purchase_return_order,payload æŒ‰ PurchaseReturnOrderDto ç»„织,明细放 purchaseReturnOrderProductsDtos。
                6. ç¼ºå°‘业务处理必须字段时,不要编造 ID,把字段放入 missingFields,并仍返回可确认的草稿数据。
                7. æ‰€æœ‰ä¸­æ–‡å†…容直接保留,不要转义成 Unicode。
                æ–‡ä»¶å†…容:
                %s
                """.formatted(message, fileContent);
    }
    private AjaxResult processPurchaseLedger(Map<String, Object> payload) throws Exception {
        if (payload.containsKey("purchaseLedgers")) {
            return processPurchaseLedgerBatch(payload);
        }
        Map<String, Object> normalizedPayload = normalizePurchaseLedgerMap(payload);
        PurchaseLedgerDto dto = objectMapper.convertValue(normalizedPayload, PurchaseLedgerDto.class);
        AjaxResult ledgerResult = validatePurchaseLedger(dto, 0);
        if (ledgerResult != null) {
            return ledgerResult;
        }
        AjaxResult supplierResult = fillSupplierIdByName(dto);
        if (supplierResult != null) {
            return supplierResult;
        }
        AjaxResult productResult = validatePurchaseProducts(dto.getProductData(), 0);
        if (productResult != null) {
            return productResult;
        }
        int result = purchaseLedgerService.addOrEditPurchase(dto);
        return AjaxResult.success("采购台账已处理", result);
    }
    private AjaxResult processPurchaseLedgerBatch(Map<String, Object> payload) throws Exception {
        List<Map<String, Object>> purchaseLedgers = toMapList(payload.get("purchaseLedgers"));
        if (purchaseLedgers.isEmpty()) {
            return AjaxResult.error("purchaseLedgers不能为空");
        }
        List<Map<String, Object>> topLevelProductData = toMapList(payload.get("productData"));
        List<Map<String, Object>> results = new ArrayList<>();
        for (int i = 0; i < purchaseLedgers.size(); i++) {
            Map<String, Object> ledgerMap = normalizePurchaseLedgerMap(purchaseLedgers.get(i));
            PurchaseLedgerDto dto = objectMapper.convertValue(ledgerMap, PurchaseLedgerDto.class);
            AjaxResult ledgerResult = validatePurchaseLedger(dto, i);
            if (ledgerResult != null) {
                return ledgerResult;
            }
            AjaxResult supplierResult = fillSupplierIdByName(dto);
            if (supplierResult != null) {
                return supplierResult;
            }
            List<SalesLedgerProduct> products = dto.getProductData();
            if (products == null || products.isEmpty()) {
                products = matchProductsForLedger(ledgerMap, dto, topLevelProductData, purchaseLedgers.size() == 1);
                dto.setProductData(products);
            }
            AjaxResult productResult = validatePurchaseProducts(products, i);
            if (productResult != null) {
                return productResult;
            }
            int result = purchaseLedgerService.addOrEditPurchase(dto);
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("index", i);
            item.put("purchaseContractNumber", dto.getPurchaseContractNumber());
            item.put("supplierId", dto.getSupplierId());
            item.put("supplierName", dto.getSupplierName());
            item.put("productCount", products.size());
            item.put("result", result);
            results.add(item);
        }
        return AjaxResult.success("采购台账已批量处理", results);
    }
    private List<SalesLedgerProduct> matchProductsForLedger(Map<String, Object> ledgerMap,
                                                            PurchaseLedgerDto dto,
                                                            List<Map<String, Object>> productData,
                                                            boolean onlyOneLedger) {
        List<SalesLedgerProduct> products = new ArrayList<>();
        for (Map<String, Object> productMap : productData) {
            if (onlyOneLedger || productBelongsToLedger(productMap, ledgerMap, dto)) {
                products.add(objectMapper.convertValue(normalizeSalesLedgerProductMap(productMap), SalesLedgerProduct.class));
            }
        }
        return products;
    }
    private boolean productBelongsToLedger(Map<String, Object> productMap, Map<String, Object> ledgerMap, PurchaseLedgerDto dto) {
        Long productPurchaseLedgerId = longValue(productMap, "purchaseLedgerId", "purchaseId", "采购订单id", "采购台账id");
        if (productPurchaseLedgerId != null && dto.getId() != null && productPurchaseLedgerId.equals(dto.getId())) {
            return true;
        }
        Long productSalesLedgerId = longValue(productMap, "salesLedgerId");
        if (productSalesLedgerId != null && dto.getId() != null && productSalesLedgerId.equals(dto.getId())) {
            return true;
        }
        String productContractNo = stringValue(productMap, "purchaseContractNumber", "purchaseContractNo", "采购合同号", "采购单号", "采购订单号");
        if (StringUtils.hasText(productContractNo)
                && StringUtils.hasText(dto.getPurchaseContractNumber())
                && productContractNo.trim().equals(dto.getPurchaseContractNumber().trim())) {
            return true;
        }
        String ledgerContractNo = stringValue(ledgerMap, "purchaseContractNumber", "purchaseContractNo", "采购合同号", "采购单号", "采购订单号");
        if (StringUtils.hasText(productContractNo)
                && StringUtils.hasText(ledgerContractNo)
                && productContractNo.trim().equals(ledgerContractNo.trim())) {
            return true;
        }
        String productSalesContractNo = stringValue(productMap, "salesContractNo", "salesContractNumber", "销售合同号", "销售单号", "销售订单号");
        if (StringUtils.hasText(productSalesContractNo)
                && StringUtils.hasText(dto.getSalesContractNo())
                && productSalesContractNo.trim().equals(dto.getSalesContractNo().trim())) {
            return true;
        }
        String ledgerSalesContractNo = stringValue(ledgerMap, "salesContractNo", "salesContractNumber", "销售合同号", "销售单号", "销售订单号");
        if (StringUtils.hasText(productSalesContractNo)
                && StringUtils.hasText(ledgerSalesContractNo)
                && productSalesContractNo.trim().equals(ledgerSalesContractNo.trim())) {
            return true;
        }
        String productSupplierName = stringValue(productMap, "supplierName", "供应商名称");
        return StringUtils.hasText(productSupplierName)
                && StringUtils.hasText(dto.getSupplierName())
                && productSupplierName.trim().equals(dto.getSupplierName().trim());
    }
    private Map<String, Object> normalizePurchaseLedgerMap(Map<String, Object> source) {
        Map<String, Object> target = new LinkedHashMap<>();
        copyPurchaseLedgerDtoFields(source, target);
        putDtoFieldIfPresent(source, target, "entryDateStart", "录入开始日期", "录入日期开始");
        putDtoFieldIfPresent(source, target, "entryDateEnd", "录入结束日期", "录入日期结束");
        putDtoFieldIfPresent(source, target, "id", "采购台账id", "采购订单id", "主键");
        putDtoFieldIfPresent(source, target, "purchaseContractNumber", "purchaseContractNo", "采购合同号", "采购单号", "采购订单号");
        putDtoFieldIfPresent(source, target, "supplierId", "供应商id", "供应商ID", "供应商名称id", "供应商名称ID");
        putDtoFieldIfPresent(source, target, "supplierName", "供应商", "供应商名称");
        putDtoFieldIfPresent(source, target, "isWhite", "是否白名单");
        putDtoFieldIfPresent(source, target, "recorderId", "录入人id", "录入人ID", "录入人姓名id", "录入人姓名ID");
        putDtoFieldIfPresent(source, target, "recorderName", "录入人", "录入人姓名");
        putDtoFieldIfPresent(source, target, "salesContractNo", "salesContractNumber", "销售合同号", "销售单号", "销售订单号");
        putDtoFieldIfPresent(source, target, "salesContractNoId", "销售合同号id", "销售合同号ID", "销售单号id", "销售单号ID");
        putDtoFieldIfPresent(source, target, "projectName", "项目", "项目名称");
        putDtoFieldIfPresent(source, target, "entryDate", "录入日期");
        putDtoFieldIfPresent(source, target, "executionDate", "签订日期", "合同签订日期");
        putDtoFieldIfPresent(source, target, "remarks", "备注", "说明");
        putDtoFieldIfPresent(source, target, "attachmentMaterials", "附件材料", "附件材料路径或名称");
        putDtoFieldIfPresent(source, target, "createdAt", "创建时间", "记录创建时间");
        putDtoFieldIfPresent(source, target, "updatedAt", "更新时间", "记录最后更新时间");
        putDtoFieldIfPresent(source, target, "salesLedgerId", "销售台账id", "销售台账ID", "关联销售台账主表主键");
        putDtoFieldIfPresent(source, target, "hasChildren", "是否有子级", "是否有明细");
        putDtoFieldIfPresent(source, target, "Type", "台账类型", "业务类型");
        putDtoFieldIfPresent(source, target, "productData", "products", "产品明细", "采购产品明细");
        putDtoFieldIfPresent(source, target, "tempFileIds", "临时文件id", "临时文件ID", "临时文件ids");
        putDtoFieldIfPresent(source, target, "SalesLedgerFiles", "附件列表", "销售台账附件");
        putDtoFieldIfPresent(source, target, "phoneNumber", "业务员手机号", "手机号");
        putDtoFieldIfPresent(source, target, "businessPersonId", "业务员id", "业务员ID");
        putDtoFieldIfPresent(source, target, "productId", "产品id", "产品ID");
        putDtoFieldIfPresent(source, target, "productModelId", "产品规格id", "产品规格ID");
        putDtoFieldIfPresent(source, target, "invoiceNumber", "发票号", "发票号码");
        putDtoFieldIfPresent(source, target, "invoiceAmount", "发票金额", "发票金额(元)");
        putDtoFieldIfPresent(source, target, "ticketRegistrationId", "来票登记id", "来票登记ID");
        putDtoFieldIfPresent(source, target, "contractAmount", "合同金额", "合同金额(产品含税总价)");
        putDtoFieldIfPresent(source, target, "receiptPaymentAmount", "来票金额", "已来票金额", "已来票金额(元)");
        putDtoFieldIfPresent(source, target, "unReceiptPaymentAmount", "未来票金额", "未来票金额(元)");
        putDtoFieldIfPresent(source, target, "type", "文件类型");
        putDtoFieldIfPresent(source, target, "paymentMethod", "付款方式");
        putDtoFieldIfPresent(source, target, "approvalStatus", "审批状态");
        putDtoFieldIfPresent(source, target, "templateName", "模板名称");
        target.remove("approveUserIds");
        target.remove("approverId");
        normalizeNestedProductData(target);
        attachImportStyleProductData(source, target);
        if (target.get("type") == null) {
            target.put("type", 2);
        }
        target.putIfAbsent("entryDate", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
        normalizePurchaseLedgerDateFields(target);
        return target;
    }
    private void attachImportStyleProductData(Map<String, Object> source, Map<String, Object> target) {
        if (target.get("productData") != null) {
            return;
        }
        Map<String, Object> productMap = normalizeSalesLedgerProductMap(source);
        if (hasImportStyleProductData(productMap)) {
            target.put("productData", List.of(productMap));
        }
    }
    private boolean hasImportStyleProductData(Map<String, Object> productMap) {
        return hasMapText(productMap, "productCategory")
                || hasMapText(productMap, "specificationModel")
                || productMap.get("quantity") != null
                || productMap.get("taxInclusiveUnitPrice") != null
                || productMap.get("taxInclusiveTotalPrice") != null;
    }
    private boolean hasMapText(Map<String, Object> map, String key) {
        Object value = map.get(key);
        return value != null && StringUtils.hasText(String.valueOf(value));
    }
    private void normalizeNestedProductData(Map<String, Object> target) {
        Object productDataValue = target.get("productData");
        if (productDataValue == null) {
            return;
        }
        List<Map<String, Object>> productMaps = toMapList(productDataValue);
        List<Map<String, Object>> normalizedProducts = new ArrayList<>();
        for (Map<String, Object> productMap : productMaps) {
            normalizedProducts.add(normalizeSalesLedgerProductMap(productMap));
        }
        target.put("productData", normalizedProducts);
    }
    private Map<String, Object> normalizeSalesLedgerProductMap(Map<String, Object> source) {
        Map<String, Object> target = new LinkedHashMap<>();
        copySalesLedgerProductFields(source, target);
        putDtoFieldIfPresent(source, target, "productCategory", "产品大类", "产品名称", "产品", "品名", "物料名称");
        putDtoFieldIfPresent(source, target, "specificationModel", "规格型号", "型号", "规格", "产品规格");
        putDtoFieldIfPresent(source, target, "unit", "单位");
        putDtoFieldIfPresent(source, target, "quantity", "数量", "采购数量");
        putDtoFieldIfPresent(source, target, "taxRate", "税率");
        putDtoFieldIfPresent(source, target, "taxInclusiveUnitPrice", "含税单价", "单价", "采购单价", "含税价格");
        putDtoFieldIfPresent(source, target, "taxInclusiveTotalPrice", "含税总价", "总价", "采购金额", "金额", "合同金额");
        putDtoFieldIfPresent(source, target, "taxExclusiveTotalPrice", "不含税总价");
        putDtoFieldIfPresent(source, target, "invoiceType", "发票类型", "发票类别");
        putDtoFieldIfPresent(source, target, "productId", "产品id", "产品ID");
        putDtoFieldIfPresent(source, target, "productModelId", "产品规格id", "产品规格ID", "型号id", "型号ID");
        putDtoFieldIfPresent(source, target, "isChecked", "是否质检", "是否质检验", "质检");
        putDtoFieldIfPresent(source, target, "type", "台账类型");
        normalizeProductAmounts(target);
        target.putIfAbsent("type", 2);
        return target;
    }
    private void copySalesLedgerProductFields(Map<String, Object> source, Map<String, Object> target) {
        String[] productFields = {
                "id", "salesLedgerId", "warnNum", "productCategory", "specificationModel", "unit",
                "speculativeTradingName", "quantity", "minStock", "taxRate", "taxInclusiveUnitPrice",
                "taxInclusiveTotalPrice", "taxExclusiveTotalPrice", "invoiceType", "type", "ticketsNum",
                "ticketsAmount", "futureTickets", "futureTicketsAmount", "invoiceNum", "noInvoiceNum",
                "invoiceAmount", "noInvoiceAmount", "productId", "productModelId", "register", "registerDate",
                "approveStatus", "pendingInvoiceTotal", "invoiceTotal", "pendingTicketsTotal", "ticketsTotal",
                "isChecked", "isProduction"
        };
        for (String field : productFields) {
            if (source.containsKey(field)) {
                target.put(field, source.get(field));
            }
        }
    }
    private void normalizeProductAmounts(Map<String, Object> target) {
        BigDecimal quantity = decimalValue(target.get("quantity"));
        BigDecimal unitPrice = decimalValue(target.get("taxInclusiveUnitPrice"));
        BigDecimal totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
        if (unitPrice == null && totalPrice != null && quantity != null && quantity.compareTo(BigDecimal.ZERO) != 0) {
            target.put("taxInclusiveUnitPrice", totalPrice.divide(quantity, 6, RoundingMode.HALF_UP));
        }
        if (totalPrice == null && unitPrice != null && quantity != null) {
            target.put("taxInclusiveTotalPrice", unitPrice.multiply(quantity));
        }
        BigDecimal taxRate = decimalValue(target.get("taxRate"));
        totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
        if (target.get("taxExclusiveTotalPrice") == null && totalPrice != null && taxRate != null) {
            BigDecimal divisor = BigDecimal.ONE.add(taxRate.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP));
            target.put("taxExclusiveTotalPrice", totalPrice.divide(divisor, 2, RoundingMode.HALF_UP));
        }
    }
    private AjaxResult validatePurchaseProducts(List<SalesLedgerProduct> products, int ledgerIndex) {
        if (products == null || products.isEmpty()) {
            return null;
        }
        for (int i = 0; i < products.size(); i++) {
            SalesLedgerProduct product = products.get(i);
            String prefix = "第" + (ledgerIndex + 1) + "个采购台账的第" + (i + 1) + "条产品";
            if (!StringUtils.hasText(product.getProductCategory())) {
                return AjaxResult.error(prefix + "缺少产品名称,请补充后再确认");
            }
            if (!StringUtils.hasText(product.getSpecificationModel())) {
                return AjaxResult.error(prefix + "缺少规格型号,请补充后再确认");
            }
            if (!StringUtils.hasText(product.getUnit())) {
                return AjaxResult.error(prefix + "缺少单位,请补充后再确认");
            }
            if (product.getQuantity() == null) {
                return AjaxResult.error(prefix + "缺少数量");
            }
            if (product.getTaxInclusiveUnitPrice() == null) {
                return AjaxResult.error(prefix + "缺少含税单价,请补充后再确认");
            }
            if (product.getTaxInclusiveTotalPrice() == null) {
                return AjaxResult.error(prefix + "缺少含税总价,请补充后再确认");
            }
        }
        return null;
    }
    private AjaxResult validatePurchaseLedger(PurchaseLedgerDto dto, int ledgerIndex) {
        String prefix = "第" + (ledgerIndex + 1) + "个采购台账";
        if (!StringUtils.hasText(dto.getPurchaseContractNumber())) {
            return AjaxResult.error(prefix + "缺少采购合同号,请补充后再确认");
        }
        if (dto.getSupplierId() == null && !StringUtils.hasText(dto.getSupplierName())) {
            return AjaxResult.error(prefix + "缺少供应商名称,请补充后再确认");
        }
        return null;
    }
    private void normalizePurchaseLedgerDateFields(Map<String, Object> target) {
        normalizeDateField(target, "entryDate");
        normalizeDateField(target, "executionDate");
        normalizeDateField(target, "createdAt");
        normalizeDateField(target, "updatedAt");
    }
    private void normalizeDateField(Map<String, Object> target, String fieldName) {
        Object value = target.get(fieldName);
        if (value == null) {
            return;
        }
        String normalizedDate = normalizeDateValue(value);
        if (StringUtils.hasText(normalizedDate)) {
            target.put(fieldName, normalizedDate);
        }
    }
    private String normalizeDateValue(Object value) {
        if (value instanceof Date date) {
            return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
        }
        if (value instanceof Number number) {
            return LocalDate.of(1899, 12, 30)
                    .plusDays(number.longValue())
                    .format(DateTimeFormatter.ISO_LOCAL_DATE);
        }
        String text = String.valueOf(value).trim();
        if (!StringUtils.hasText(text)) {
            return null;
        }
        if (text.length() >= 10 && text.charAt(4) == '-' && text.charAt(7) == '-') {
            return text.substring(0, 10);
        }
        String normalizedText = text.replace("å¹´", "-")
                .replace("月", "-")
                .replace("日", "")
                .replace(".", "-")
                .replace("/", "-")
                .trim();
        DateTimeFormatter[] formatters = {
                DateTimeFormatter.ofPattern("yyyy-M-d"),
                DateTimeFormatter.ofPattern("M-d-yyyy"),
                DateTimeFormatter.ofPattern("M-d-yy")
        };
        for (DateTimeFormatter formatter : formatters) {
            try {
                return LocalDate.parse(normalizedText, formatter).format(DateTimeFormatter.ISO_LOCAL_DATE);
            } catch (DateTimeParseException ignored) {
                // Try the next supported input pattern.
            }
        }
        return text;
    }
    private void copyPurchaseLedgerDtoFields(Map<String, Object> source, Map<String, Object> target) {
        String[] dtoFields = {
                "entryDateStart", "entryDateEnd", "id", "purchaseContractNumber",
                "supplierId", "supplierName", "isWhite", "recorderId", "recorderName", "salesContractNo",
                "salesContractNoId", "projectName", "entryDate", "executionDate", "remarks", "attachmentMaterials",
                "createdAt", "updatedAt", "salesLedgerId", "hasChildren", "Type", "productData", "tempFileIds",
                "SalesLedgerFiles", "phoneNumber", "businessPersonId", "productId", "productModelId", "invoiceNumber",
                "invoiceAmount", "ticketRegistrationId", "contractAmount", "receiptPaymentAmount",
                "unReceiptPaymentAmount", "type", "paymentMethod", "approvalStatus", "templateName"
        };
        for (String field : dtoFields) {
            if (source.containsKey(field)) {
                target.put(field, source.get(field));
            }
        }
    }
    private void putDtoFieldIfPresent(Map<String, Object> source, Map<String, Object> target, String dtoField, String... aliases) {
        if (target.containsKey(dtoField) && target.get(dtoField) != null) {
            return;
        }
        for (String alias : aliases) {
            Object value = source.get(alias);
            if (value != null && StringUtils.hasText(String.valueOf(value))) {
                target.put(dtoField, value);
                return;
            }
        }
    }
    private List<Map<String, Object>> toMapList(Object value) {
        if (value == null) {
            return List.of();
        }
        return objectMapper.convertValue(value, new TypeReference<List<Map<String, Object>>>() {
        });
    }
    private String stringValue(Map<String, Object> map, String... keys) {
        for (String key : keys) {
            Object value = map.get(key);
            if (value != null && StringUtils.hasText(String.valueOf(value))) {
                return String.valueOf(value);
            }
        }
        return null;
    }
    private Long longValue(Map<String, Object> map, String... keys) {
        String value = stringValue(map, keys);
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            return Long.parseLong(value.trim());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }
    private BigDecimal decimalValue(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof BigDecimal decimal) {
            return decimal;
        }
        if (value instanceof Number number) {
            return new BigDecimal(String.valueOf(number));
        }
        String text = String.valueOf(value)
                .replace(",", "")
                .replace(",", "")
                .replace("元", "")
                .replace("ï¿¥", "")
                .trim();
        if (!StringUtils.hasText(text)) {
            return null;
        }
        try {
            return new BigDecimal(text);
        } catch (NumberFormatException ignored) {
            return null;
        }
    }
    private String toCustomerMessage(Exception ex) {
        String message = ex.getMessage();
        if (!StringUtils.hasText(message)) {
            return "处理失败,请检查确认数据后重试";
        }
        if (message.contains("tax_inclusive_unit_price")) {
            return "处理失败:产品明细缺少含税单价,请补充后再确认";
        }
        if (message.contains("tax_inclusive_total_price")) {
            return "处理失败:产品明细缺少含税总价,请补充后再确认";
        }
        if (message.contains("entryDate")) {
            return "处理失败:录入日期格式不正确,请使用 yyyy-MM-dd,例如 2026-04-30";
        }
        if (message.contains("supplier")) {
            return "处理失败:供应商信息不完整,请确认供应商名称或供应商ID";
        }
        if (message.contains("SQL") || message.contains("java.") || message.contains("Exception")) {
            return "处理失败:确认数据不完整或格式不正确,请检查必填字段后重试";
        }
        return "处理失败:" + message;
    }
    private AjaxResult fillSupplierIdByName(PurchaseLedgerDto dto) {
        if (dto.getSupplierId() != null) {
            return null;
        }
        if (!StringUtils.hasText(dto.getSupplierName())) {
            return AjaxResult.error("供应商ID不能为空;未识别到供应商名称,无法自动匹配供应商ID");
        }
        SupplierManage supplier = supplierManageMapper.selectOne(new LambdaQueryWrapper<SupplierManage>()
                .eq(SupplierManage::getSupplierName, dto.getSupplierName().trim())
                .last("limit 1"));
        if (supplier == null) {
            return AjaxResult.error("未找到供应商:" + dto.getSupplierName() + ",请先维护供应商或手动选择供应商ID");
        }
        dto.setSupplierId(supplier.getId());
        return null;
    }
    private AjaxResult processPurchaseReturnOrder(Map<String, Object> payload) {
        PurchaseReturnOrderDto dto = objectMapper.convertValue(payload, PurchaseReturnOrderDto.class);
        Boolean result = purchaseReturnOrdersService.add(dto);
        return AjaxResult.success("采购退货单已处理", result);
    }
}
src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java
@@ -114,7 +114,10 @@
            return new LinkedList<>();
        }
        List<ChatMessage> messages = mongoChatMemoryStore.getMessages(memoryId);
        return messages.stream().map(this::convertMessage).collect(Collectors.toList());
        List<AiChatMessageDto> messageDtos = messages.stream().map(this::convertMessage).collect(Collectors.toList());
        List<List<String>> analyzeFilePathGroups = mongoChatMemoryStore.getAnalyzeFilePathGroups(memoryId);
        attachAnalyzeFilePaths(messageDtos, analyzeFilePathGroups);
        return messageDtos;
    }
    @Override
@@ -188,4 +191,22 @@
        }
        return new AiChatMessageDto("unknown", String.valueOf(message));
    }
    private void attachAnalyzeFilePaths(List<AiChatMessageDto> messages,
                                        List<List<String>> analyzeFilePathGroups) {
        if (StringUtils.isEmpty(messages) || StringUtils.isEmpty(analyzeFilePathGroups)) {
            return;
        }
        int analyzeIndex = 0;
        for (AiChatMessageDto message : messages) {
            if (!"user".equals(message.getRole()) || analyzeIndex >= analyzeFilePathGroups.size()) {
                continue;
            }
            List<String> filePaths = analyzeFilePathGroups.get(analyzeIndex);
            if (!StringUtils.isEmpty(filePaths)) {
                message.setFilePaths(filePaths);
            }
            analyzeIndex++;
        }
    }
}
src/main/java/com/ruoyi/ai/service/impl/KnowledgeRagServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,342 @@
package com.ruoyi.ai.service.impl;
import com.ruoyi.ai.service.KnowledgeRagService;
import com.ruoyi.approve.pojo.KnowledgeBaseVector;
import com.ruoyi.approve.service.KnowledgeBaseVectorService;
import com.ruoyi.basic.pojo.StorageBlob;
import com.ruoyi.basic.service.StorageBlobService;
import com.ruoyi.common.config.FileProperties;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.store.embedding.EmbeddingStore;
import io.pinecone.clients.Index;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.File;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * çŸ¥è¯†åº“RAG服务实现
 */
@Slf4j
@Service
public class KnowledgeRagServiceImpl implements KnowledgeRagService {
    private final KnowledgeBaseVectorService knowledgeBaseVectorService;
    private final StorageBlobService storageBlobService;
    private final EmbeddingModel embeddingModel;
    private final EmbeddingStore<TextSegment> embeddingStore;
    private final FileProperties fileProperties;
    private final Index pineconeIndex;
    @org.springframework.beans.factory.annotation.Value("${pinecone.namespace:knowledge-base}")
    private String namespace;
    public KnowledgeRagServiceImpl(
            KnowledgeBaseVectorService knowledgeBaseVectorService,
            StorageBlobService storageBlobService,
            EmbeddingModel embeddingModel,
            EmbeddingStore<TextSegment> embeddingStore,
            FileProperties fileProperties,
            Index pineconeIndex) {
        this.knowledgeBaseVectorService = knowledgeBaseVectorService;
        this.storageBlobService = storageBlobService;
        this.embeddingModel = embeddingModel;
        this.embeddingStore = embeddingStore;
        this.fileProperties = fileProperties;
        this.pineconeIndex = pineconeIndex;
    }
    private static final int CHUNK_SIZE = 500;
    private static final int CHUNK_OVERLAP = 100;
    private static final long CHUNK_THRESHOLD_BYTES = 80L * 1024 * 1024;
    private static final int EMBEDDING_MAX_LENGTH = 8000;
    @Override
    @Async("threadPoolTaskExecutor")
    public void processVectorAsync(Long vectorId) {
        log.info("开始异步向量化处理: vectorId={}, thread={}", vectorId, Thread.currentThread().getName());
        processVector(vectorId);
    }
    @Override
    public void processVector(Long vectorId) {
        log.info("开始处理向量化: vectorId={}", vectorId);
        KnowledgeBaseVector vector = knowledgeBaseVectorService.getById(vectorId);
        if (vector == null) {
            log.error("向量记录不存在: {}", vectorId);
            return;
        }
        try {
            knowledgeBaseVectorService.updateVectorStatus(vectorId,
                    KnowledgeBaseVector.STATUS_PROCESSING, null, null);
            StorageBlob blob = storageBlobService.getById(vector.getStorageBlobId());
            if (blob == null) {
                throw new RuntimeException("文件不存在: " + vector.getStorageBlobId());
            }
            File file = getFile(blob);
            log.info("文件路径: {}, æ˜¯å¦å­˜åœ¨: {}", file.getAbsolutePath(), file.exists());
            long fileSize = file.length();
            String content = extractFileContent(file, vector.getFileName());
            log.info("文件内容长度: {}", content != null ? content.length() : 0);
            if (content == null || content.trim().isEmpty()) {
                throw new RuntimeException("文件内容为空");
            }
            List<TextSegment> chunks;
            boolean needChunk = fileSize > CHUNK_THRESHOLD_BYTES || content.length() > EMBEDDING_MAX_LENGTH;
            if (needChunk) {
                log.info("开始切片: fileSize={}, contentLength={}", fileSize, content.length());
                chunks = splitText(content, vector);
                log.info("切片完成,共 {} ä¸ªå—", chunks.size());
            } else {
                log.info("文件较小,不进行切片");
                Map<String, Object> metadata = buildMetadata(vector);
                chunks = List.of(TextSegment.from(content, new dev.langchain4j.data.document.Metadata(metadata)));
            }
            int chunkCount = 0;
            for (TextSegment chunk : chunks) {
                Embedding embedding = embeddingModel.embed(chunk).content();
                embeddingStore.add(embedding, chunk);
                chunkCount++;
            }
            knowledgeBaseVectorService.updateVectorStatus(vectorId,
                    KnowledgeBaseVector.STATUS_COMPLETED, chunkCount, null);
            log.info("向量化处理完成: vectorId={}, chunkCount={}", vectorId, chunkCount);
        } catch (Exception e) {
            log.error("向量化处理失败: vectorId={}", vectorId, e);
            knowledgeBaseVectorService.updateVectorStatus(vectorId,
                    KnowledgeBaseVector.STATUS_FAILED, null, e.getMessage());
        }
    }
    @Override
    public List<String> searchRelevantContent(String namespace, String query, int maxResults) {
        try {
            Embedding queryEmbedding = embeddingModel.embed(query).content();
            EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
                    .queryEmbedding(queryEmbedding)
                    .maxResults(maxResults)
                    .minScore(0.7)
                    .build();
            EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest);
            return searchResult.matches().stream()
                    .map(match -> match.embedded().text())
                    .collect(Collectors.toList());
        } catch (Exception e) {
            log.error("向量检索失败: namespace={}", namespace, e);
            return new ArrayList<>();
        }
    }
    @Override
    public void deleteEmbeddings(String namespace, Long storageBlobId) {
        log.info("删除向量数据: namespace={}, storageBlobId={}", namespace, storageBlobId);
        try {
            Struct filter = Struct.newBuilder()
                    .putFields("storageBlobId", Value.newBuilder()
                            .setStructValue(Struct.newBuilder()
                                    .putFields("$eq", Value.newBuilder()
                                            .setNumberValue(storageBlobId.doubleValue())
                                            .build()))
                            .build())
                    .build();
            List<String> emptyIds = new ArrayList<>();
            pineconeIndex.delete(emptyIds, false, this.namespace, filter);
            log.info("向量删除完成: storageBlobId={}", storageBlobId);
        } catch (Exception e) {
            log.error("删除向量数据失败: namespace={}, storageBlobId={}", namespace, storageBlobId, e);
        }
    }
    private File getFile(StorageBlob blob) {
        String path = blob.getPath();
        if (path != null && !path.isEmpty()) {
            return new File(new File(fileProperties.getPath(), path), blob.getUidFilename());
        }
        return new File(fileProperties.getPath(), blob.getUidFilename());
    }
    private String extractFileContent(File file, String fileName) throws Exception {
        String ext = getFileExtension(fileName);
        if (isPlainText(ext)) {
            return readFileWithEncoding(file);
        }
        if ("docx".equals(ext)) {
            return extractDocx(file);
        }
        if ("xlsx".equals(ext)) {
            return extractXlsx(file);
        }
        if ("xls".equals(ext)) {
            return extractXls(file);
        }
        return readFileWithEncoding(file);
    }
    private String readFileWithEncoding(File file) throws Exception {
        byte[] bytes = Files.readAllBytes(file.toPath());
        String utf8Content = new String(bytes, StandardCharsets.UTF_8);
        if (isValidUtf8(utf8Content)) {
            log.debug("文件编码: UTF-8");
            return utf8Content;
        }
        try {
            Charset gbk = Charset.forName("GBK");
            String gbkContent = new String(bytes, gbk);
            log.debug("文件编码: GBK");
            return gbkContent;
        } catch (Exception e) {
            log.warn("编码检测失败,使用 UTF-8");
            return utf8Content;
        }
    }
    private boolean isValidUtf8(String decoded) {
        // æ£€æŸ¥æ›¿æ¢å­—符 U+FFFD (UTF-8 è§£ç å¤±è´¥æ—¶å‡ºçް)
        if (decoded.contains("�")) {
            return false;
        }
        int invalidCount = 0;
        int checkLen = Math.min(decoded.length(), 1000);
        for (int i = 0; i < checkLen; i++) {
            char c = decoded.charAt(i);
            // æ£€æŸ¥ç§æœ‰ä½¿ç”¨åŒºåŸŸ (U+E000-U+F8FF) æˆ–异常控制字符
            if ((c >= '' && c <= '') || (c < ' ' && c != '\n' && c != '\r' && c != '\t')) {
                invalidCount++;
            }
        }
        return invalidCount < checkLen * 0.05;
    }
    private String getFileExtension(String fileName) {
        if (fileName == null || !fileName.contains(".")) {
            return "";
        }
        return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
    }
    private boolean isPlainText(String ext) {
        return "txt".equals(ext) || "md".equals(ext) || "json".equals(ext)
                || "csv".equals(ext) || "xml".equals(ext) || "yaml".equals(ext)
                || "yml".equals(ext);
    }
    private String extractDocx(File file) throws Exception {
        try (var doc = new org.apache.poi.xwpf.usermodel.XWPFDocument(new java.io.FileInputStream(file));
             var extractor = new org.apache.poi.xwpf.extractor.XWPFWordExtractor(doc)) {
            return extractor.getText();
        }
    }
    private String extractXlsx(File file) throws Exception {
        try (var workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook(file)) {
            return extractWorkbook(workbook);
        }
    }
    private String extractXls(File file) throws Exception {
        try (var workbook = new org.apache.poi.hssf.usermodel.HSSFWorkbook(new java.io.FileInputStream(file))) {
            return extractWorkbook(workbook);
        }
    }
    private String extractWorkbook(org.apache.poi.ss.usermodel.Workbook workbook) {
        StringBuilder text = new StringBuilder();
        var formatter = new org.apache.poi.ss.usermodel.DataFormatter();
        for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
            var sheet = workbook.getSheetAt(i);
            text.append("Sheet: ").append(sheet.getSheetName()).append("\n");
            for (var row : sheet) {
                for (var cell : row) {
                    text.append(formatter.formatCellValue(cell)).append("\t");
                }
                text.append("\n");
            }
        }
        return text.toString();
    }
    private List<TextSegment> splitText(String content, KnowledgeBaseVector vector) {
        List<TextSegment> chunks = new ArrayList<>();
        if (content.length() <= CHUNK_SIZE) {
            Map<String, Object> metadata = buildMetadata(vector);
            chunks.add(TextSegment.from(content, new dev.langchain4j.data.document.Metadata(metadata)));
            return chunks;
        }
        int start = 0;
        int chunkIndex = 0;
        while (start < content.length()) {
            int end = Math.min(start + CHUNK_SIZE, content.length());
            if (end < content.length()) {
                int lastPeriod = content.lastIndexOf('。', end);
                int lastNewline = content.lastIndexOf('\n', end);
                int boundary = Math.max(lastPeriod, lastNewline);
                if (boundary > start + CHUNK_SIZE / 2) {
                    end = boundary + 1;
                }
            }
            String chunkText = content.substring(start, end).trim();
            if (!chunkText.isEmpty()) {
                Map<String, Object> metadata = buildMetadata(vector);
                metadata.put("chunkIndex", chunkIndex);
                chunks.add(TextSegment.from(chunkText, new dev.langchain4j.data.document.Metadata(metadata)));
                chunkIndex++;
            }
            start = end - CHUNK_OVERLAP;
            if (start < 0) start = 0;
            if (start >= content.length() - CHUNK_OVERLAP) break;
        }
        return chunks;
    }
    private Map<String, Object> buildMetadata(KnowledgeBaseVector vector) {
        Map<String, Object> metadata = new HashMap<>();
        metadata.put("knowledgeBaseId", vector.getKnowledgeBaseId());
        metadata.put("storageBlobId", vector.getStorageBlobId());
        metadata.put("fileName", vector.getFileName());
        metadata.put("namespace", vector.getNamespace());
        return metadata;
    }
}
src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
@@ -11,6 +11,8 @@
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.LinkedList;
@@ -24,8 +26,7 @@
    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        Query query = Query.query(Criteria.where("memoryId").is(memoryIdString(memoryId)));
        ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);
        ChatMessages chatMessages = findChatMessages(memoryId);
        if (chatMessages == null || chatMessages.getContent() == null) {
            return new LinkedList<>();
        }
@@ -56,7 +57,75 @@
        updateMessages(memoryId, messages);
    }
    public void appendAnalyzeFileContext(Object memoryId, String userQuestion, List<String> filePaths) {
        String memoryIdValue = memoryIdString(memoryId);
        if (!StringUtils.hasText(memoryIdValue)) {
            return;
        }
        List<String> validFilePaths = new LinkedList<>();
        if (!CollectionUtils.isEmpty(filePaths)) {
            for (String filePath : filePaths) {
                if (StringUtils.hasText(filePath)) {
                    validFilePaths.add(filePath);
                }
            }
        }
        if (!StringUtils.hasText(userQuestion) && validFilePaths.isEmpty()) {
            return;
        }
        Query query = Query.query(Criteria.where("memoryId").is(memoryIdValue));
        Update update = new Update();
        update.set("memoryId", memoryIdValue);
        update.set("updateTime", new Date());
        update.setOnInsert("createTime", new Date());
        if (StringUtils.hasText(userQuestion)) {
            update.push("analyzeUserQuestions", userQuestion);
        }
        if (!validFilePaths.isEmpty()) {
            update.push("analyzeFilePaths").each(validFilePaths.toArray());
            update.push("analyzeFilePathGroups", validFilePaths);
        }
        mongoTemplate.upsert(query, update, ChatMessages.class);
    }
    public List<String> getAnalyzeUserQuestions(Object memoryId) {
        ChatMessages chatMessages = findChatMessages(memoryId);
        if (chatMessages == null || CollectionUtils.isEmpty(chatMessages.getAnalyzeUserQuestions())) {
            return new LinkedList<>();
        }
        return new LinkedList<>(chatMessages.getAnalyzeUserQuestions());
    }
    public List<List<String>> getAnalyzeFilePathGroups(Object memoryId) {
        ChatMessages chatMessages = findChatMessages(memoryId);
        if (chatMessages == null) {
            return new LinkedList<>();
        }
        if (CollectionUtils.isEmpty(chatMessages.getAnalyzeFilePathGroups())) {
            if (CollectionUtils.isEmpty(chatMessages.getAnalyzeFilePaths())) {
                return new LinkedList<>();
            }
            List<List<String>> fallback = new LinkedList<>();
            fallback.add(new LinkedList<>(chatMessages.getAnalyzeFilePaths()));
            return fallback;
        }
        List<List<String>> groups = new LinkedList<>();
        for (List<String> group : chatMessages.getAnalyzeFilePathGroups()) {
            if (CollectionUtils.isEmpty(group)) {
                groups.add(new LinkedList<>());
            } else {
                groups.add(new LinkedList<>(group));
            }
        }
        return groups;
    }
    private String memoryIdString(Object memoryId) {
        return memoryId == null ? "" : memoryId.toString();
    }
    private ChatMessages findChatMessages(Object memoryId) {
        Query query = Query.query(Criteria.where("memoryId").is(memoryIdString(memoryId)));
        return mongoTemplate.findOne(query, ChatMessages.class);
    }
}
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
@@ -22,11 +22,10 @@
import java.io.IOException;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -46,7 +45,7 @@
    private static final int DEFAULT_LIMIT = 10;
    private static final int MAX_LIMIT = 20;
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private final ApproveProcessMapper approveProcessMapper;
@@ -76,12 +75,17 @@
                            @P(value = "审批类型编号,可不传", required = false) Integer approveType,
                            @P(value = "关键字,可匹配流程编号、标题、申请人、当前审批人", required = false) String keyword,
                            @P(value = "返回条数,默认10,最大20", required = false) Integer limit,
                            @P(value = "查询范围,可选值:related、applicant、approver;related è¡¨ç¤ºå½“前用户相关,applicant è¡¨ç¤ºæˆ‘发起的,approver è¡¨ç¤ºå¾…我处理的", required = false) String scope) {
                            @P(value = "查询范围,可选值:related、applicant、approver;related è¡¨ç¤ºå½“前用户相关,applicant è¡¨ç¤ºæˆ‘发起的,approver è¡¨ç¤ºå¾…我处理的", required = false) String scope,
                            @P(value = "开始日期 yyyy-MM-dd,可不传", required = false) String startDate,
                            @P(value = "结束日期 yyyy-MM-dd,可不传", required = false) String endDate,
                            @P(value = "时间范围描述,例如 ä»Šå¤©ã€æœ¬æœˆã€è¿‘30天,可不传", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        Long userId = loginUser.getUserId();
        Integer statusCode = parseStatus(status);
        String normalizedScope = normalizeScope(scope);
        boolean hasDateFilter = StringUtils.hasText(startDate) || StringUtils.hasText(endDate) || StringUtils.hasText(timeRange);
        DateRange dateRange = hasDateFilter ? resolveDateRange(startDate, endDate, timeRange) : null;
        LambdaQueryWrapper<ApproveProcess> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ApproveProcess::getApproveDelete, 0);
@@ -120,6 +124,11 @@
            }
        }
        if (dateRange != null) {
            wrapper.ge(ApproveProcess::getCreateTime, dateRange.start().atStartOfDay())
                    .lt(ApproveProcess::getCreateTime, dateRange.end().plusDays(1).atStartOfDay());
        }
        wrapper.orderByDesc(ApproveProcess::getCreateTime)
                .last("limit " + normalizeLimit(limit));
@@ -156,7 +165,10 @@
                        "statusFilter", StringUtils.hasText(status) ? status : "all",
                        "approveType", approveType == null ? "" : approveType,
                        "keyword", keyword == null ? "" : keyword,
                        "scope", normalizedScope
                        "scope", normalizedScope,
                        "timeRange", dateRange == null ? "all" : dateRange.label(),
                        "startDate", dateRange == null ? "" : dateRange.start().toString(),
                        "endDate", dateRange == null ? "" : dateRange.end().toString()
                ),
                Map.of("columns", todoColumns(), "items", items),
                Map.of());
@@ -727,7 +739,10 @@
    }
    private String formatDate(Date value) {
        return value == null ? "" : DATE_FORMAT.format(value);
        if (value == null) {
            return "";
        }
        return value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().format(DATE_FORMATTER);
    }
    private long countByStatus(List<ApproveProcess> processes, int status) {
@@ -811,8 +826,9 @@
    private Date parseDate(String dateText) {
        try {
            return DATE_FORMAT.parse(dateText);
        } catch (ParseException e) {
            LocalDate localDate = LocalDate.parse(dateText, DATE_FORMATTER);
            return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
        } catch (Exception e) {
            throw new IllegalArgumentException("日期格式必须是 yyyy-MM-dd");
        }
    }
src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2311 @@
package com.ruoyi.ai.tools;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.ruoyi.account.mapper.AccountStatementMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.account.service.impl.AccountingServiceImpl;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.mapper.SupplierManageMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.pojo.DeviceRepair;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
import com.ruoyi.production.mapper.ProductionAccountMapper;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionPlanMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.mapper.ProductionProductOutputMapper;
import com.ruoyi.production.pojo.ProductionAccount;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionPlan;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.pojo.ProductionProductOutput;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
import com.ruoyi.technology.pojo.TechnologyOperation;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Component
public class FinancialAgentTools {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final Pattern RELATIVE_PATTERN = Pattern.compile("(近|最近)?\\s*(\\d+)\\s*(天|周|个月|月|å¹´)");
    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
    private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
    private static final int DEFAULT_LIMIT = 10;
    private static final int MAX_LIMIT = 50;
    private static final BigDecimal DEFAULT_FALLBACK_MATERIAL_COST_RATE = new BigDecimal("0.60");
    private static final BigDecimal DEFAULT_LABOR_COST_RATE = new BigDecimal("0.15");
    private static final BigDecimal DEFAULT_OVERHEAD_COST_RATE = new BigDecimal("0.10");
    private static final BigDecimal SME_RECEIVABLE_RISK_THRESHOLD = new BigDecimal("500000");
    private static final BigDecimal SME_INVENTORY_RISK_THRESHOLD = new BigDecimal("1000000");
    private static final BigDecimal SME_PROFIT_WARNING_RATE = new BigDecimal("0.08");
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final ProductionAccountMapper productionAccountMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionProductOutputMapper productionProductOutputMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final DeviceRepairMapper deviceRepairMapper;
    private final ProcurementRecordMapper procurementRecordMapper;
    private final ProcurementRecordOutMapper procurementRecordOutMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final AccountStatementMapper accountStatementMapper;
    private final CustomerMapper customerMapper;
    private final SupplierManageMapper supplierManageMapper;
    private final ProductModelMapper productModelMapper;
    private final ProductMapper productMapper;
    private final AiSessionUserContext aiSessionUserContext;
    public FinancialAgentTools(SalesLedgerMapper salesLedgerMapper,
                               SalesLedgerProductMapper salesLedgerProductMapper,
                               ProductionAccountMapper productionAccountMapper,
                               ProductionProductMainMapper productionProductMainMapper,
                               ProductionOperationTaskMapper productionOperationTaskMapper,
                               ProductionOrderMapper productionOrderMapper,
                               ProductionPlanMapper productionPlanMapper,
                               ProductionProductOutputMapper productionProductOutputMapper,
                               TechnologyOperationMapper technologyOperationMapper,
                               DeviceLedgerMapper deviceLedgerMapper,
                               DeviceRepairMapper deviceRepairMapper,
                               ProcurementRecordMapper procurementRecordMapper,
                               ProcurementRecordOutMapper procurementRecordOutMapper,
                               StockInventoryMapper stockInventoryMapper,
                               AccountSalesCollectionMapper accountSalesCollectionMapper,
                               AccountPurchasePaymentMapper accountPurchasePaymentMapper,
                               AccountStatementMapper accountStatementMapper,
                               CustomerMapper customerMapper,
                               SupplierManageMapper supplierManageMapper,
                               ProductModelMapper productModelMapper,
                               ProductMapper productMapper,
                               AiSessionUserContext aiSessionUserContext) {
        this.salesLedgerMapper = salesLedgerMapper;
        this.salesLedgerProductMapper = salesLedgerProductMapper;
        this.productionAccountMapper = productionAccountMapper;
        this.productionProductMainMapper = productionProductMainMapper;
        this.productionOperationTaskMapper = productionOperationTaskMapper;
        this.productionOrderMapper = productionOrderMapper;
        this.productionPlanMapper = productionPlanMapper;
        this.productionProductOutputMapper = productionProductOutputMapper;
        this.technologyOperationMapper = technologyOperationMapper;
        this.deviceLedgerMapper = deviceLedgerMapper;
        this.deviceRepairMapper = deviceRepairMapper;
        this.procurementRecordMapper = procurementRecordMapper;
        this.procurementRecordOutMapper = procurementRecordOutMapper;
        this.stockInventoryMapper = stockInventoryMapper;
        this.accountSalesCollectionMapper = accountSalesCollectionMapper;
        this.accountPurchasePaymentMapper = accountPurchasePaymentMapper;
        this.accountStatementMapper = accountStatementMapper;
        this.customerMapper = customerMapper;
        this.supplierManageMapper = supplierManageMapper;
        this.productModelMapper = productModelMapper;
        this.productMapper = productMapper;
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Tool(name = "财务知识检索", value = "按财务经营问题检索业财融合知识片段与指标口径,作为RAG上下文。")
    public String retrieveFinancialKnowledge(@ToolMemoryId String memoryId,
                                             @P(value = "问题或关键词,例如利润下降、库存周转、资金缺口") String question) {
        List<KnowledgeDoc> knowledgeDocs = financeKnowledgeBase();
        String normalized = normalizeForMatch(question);
        List<KnowledgeDoc> ranked = knowledgeDocs.stream()
                .sorted(Comparator.comparingInt((KnowledgeDoc doc) -> keywordHitCount(doc.keywords(), normalized)).reversed())
                .filter(doc -> keywordHitCount(doc.keywords(), normalized) > 0 || !StringUtils.hasText(normalized))
                .limit(5)
                .toList();
        List<Map<String, Object>> items = ranked.stream().map(doc -> {
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("topic", doc.topic());
            map.put("knowledge", doc.knowledge());
            map.put("relatedTables", doc.relatedTables());
            map.put("suggestedQuestions", doc.suggestedQuestions());
            return map;
        }).toList();
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("question", safe(question));
        summary.put("hitCount", items.size());
        summary.put("retrievalMode", "keyword_rag");
        return jsonResponse(true, "financial_rag_knowledge", "已返回财务知识检索结果", summary, Map.of("items", items), Map.of());
    }
    @Tool(name = "智能成本核算", value = "自动核算产品成本、工序成本、人工成本、设备折旧、材料损耗与订单利润。")
    public String calculateIntelligentCost(@ToolMemoryId String memoryId,
                                           @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                           @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                           @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange,
                                           @P(value = "关键词,可匹配合同号/客户/项目", required = false) String keyword,
                                           @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        if (loginUser == null) {
            return jsonResponse(false, "financial_cost_accounting", "用户信息获取失败", Map.of(), Map.of(), Map.of());
        }
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit);
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("orderCount", bundle.orderMetrics().size());
        summary.put("totalRevenue", bundle.totalRevenue());
        summary.put("totalMaterialCost", bundle.totalMaterialCost());
        summary.put("totalLaborCost", bundle.totalLaborCost());
        summary.put("totalDepreciationCost", bundle.totalDepreciationCost());
        summary.put("totalScrapCost", bundle.totalScrapCost());
        summary.put("totalCost", bundle.totalCost());
        summary.put("totalProfit", bundle.totalProfit());
        summary.put("profitRate", toPercent(rate(bundle.totalProfit(), bundle.totalRevenue())));
        List<Map<String, Object>> orderItems = bundle.orderMetrics().stream()
                .map(this::toOrderCostItem)
                .toList();
        List<Map<String, Object>> processItems = bundle.processCostRanking().entrySet().stream()
                .map(entry -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("processName", entry.getKey());
                    map.put("cost", entry.getValue());
                    return map;
                }).toList();
        List<Map<String, Object>> topCustomerItems = buildCustomerProfitTop(bundle.orderMetrics(), 5);
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("costCompositionPieOption",
                buildCostCompositionPie(bundle.totalMaterialCost(), bundle.totalLaborCost(), bundle.totalDepreciationCost(), bundle.totalScrapCost()));
        charts.put("orderProfitBarOption", buildOrderProfitBar(bundle.orderMetrics()));
        charts.put("processCostBarOption", buildProcessCostBar(bundle.processCostRanking()));
        return jsonResponse(true, "financial_cost_accounting", "已完成智能成本核算", summary,
                Map.of(
                        "orders", orderItems,
                        "processCostRanking", processItems,
                        "topCustomers", topCustomerItems
                ),
                charts
        );
    }
    @Tool(name = "订单利润分析", value = "识别低利润/亏损订单,输出原因分析和优化建议。")
    public String analyzeOrderProfit(@ToolMemoryId String memoryId,
                                     @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                     @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                     @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange,
                                     @P(value = "关键词,可匹配合同号/客户/项目", required = false) String keyword,
                                     @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        if (loginUser == null) {
            return jsonResponse(false, "financial_order_profit_analysis", "用户信息获取失败", Map.of(), Map.of(), Map.of());
        }
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit);
        List<OrderProfitMetric> metrics = bundle.orderMetrics();
        List<OrderProfitMetric> riskyOrders = metrics.stream()
                .filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0 || item.profitRate().compareTo(SME_PROFIT_WARNING_RATE) < 0)
                .sorted(Comparator.comparing(OrderProfitMetric::profitRate)
                        .thenComparing(OrderProfitMetric::profit))
                .toList();
        Map<String, BigDecimal> customerProfitMap = new LinkedHashMap<>();
        for (OrderProfitMetric metric : metrics) {
            customerProfitMap.merge(metric.customerName(), metric.profit(), BigDecimal::add);
        }
        Map.Entry<String, BigDecimal> topCustomer = customerProfitMap.entrySet().stream()
                .max(Map.Entry.comparingByValue())
                .orElse(Map.entry("暂无数据", BigDecimal.ZERO));
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("orderCount", metrics.size());
        summary.put("lossOrderCount", metrics.stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count());
        summary.put("lowProfitOrderCount", riskyOrders.size());
        summary.put("avgProfitRate", toPercent(avgRate(metrics)));
        summary.put("topCustomerByProfit", topCustomer.getKey());
        summary.put("topCustomerProfit", topCustomer.getValue());
        List<Map<String, Object>> riskyItems = riskyOrders.stream()
                .limit(normalizeLimit(limit))
                .map(this::toRiskOrderItem)
                .toList();
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("profitDistributionOption", buildProfitDistributionBar(metrics));
        charts.put("lossOrderTrendOption", buildLossOrderTrendLine(metrics));
        charts.put("customerProfitTopOption", buildCustomerProfitBar(customerProfitMap));
        return jsonResponse(true, "financial_order_profit_analysis", "已完成订单利润分析", summary,
                Map.of(
                        "riskOrders", riskyItems,
                        "allOrders", metrics.stream().map(this::toOrderCostItem).toList(),
                        "customerProfitTop", buildCustomerProfitTop(metrics, 10)
                ),
                charts
        );
    }
    @Tool(name = "库存资金分析", value = "分析库存积压、呆滞库存、资金占用与周转率。")
    public String analyzeInventoryCapital(@ToolMemoryId String memoryId,
                                          @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                          @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                          @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange,
                                          @P(value = "关键词,可匹配产品名称/型号", required = false) String keyword,
                                          @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        int finalLimit = normalizeLimit(limit);
        List<StockInventory> inventoryRows = queryStockInventory(loginUser);
        if (inventoryRows.isEmpty()) {
            return jsonResponse(true, "financial_inventory_capital_analysis", "当前无库存数据",
                    rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
        }
        Set<Long> modelIds = inventoryRows.stream()
                .map(StockInventory::getProductModelId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, ProductModel> productModelMap = queryProductModels(modelIds);
        Map<Long, Product> productMap = queryProducts(productModelMap.values());
        Map<Long, BigDecimal> avgUnitCostByModelId = queryAverageUnitCostByModel(loginUser, modelIds);
        OutboundStats outboundStats = queryOutboundStats(loginUser, modelIds, range);
        List<InventoryMetric> metrics = buildInventoryMetrics(inventoryRows, productModelMap, productMap, avgUnitCostByModelId, outboundStats)
                .stream()
                .filter(metric -> matchInventoryKeyword(metric, keyword))
                .sorted(Comparator.comparing(InventoryMetric::inventoryValue).reversed())
                .toList();
        BigDecimal totalInventoryValue = metrics.stream().map(InventoryMetric::inventoryValue).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal stagnantValue = metrics.stream()
                .filter(metric -> metric.stagnantDays() >= 90)
                .map(InventoryMetric::inventoryValue)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        long stagnantCount = metrics.stream().filter(metric -> metric.stagnantDays() >= 90).count();
        long overstockCount = metrics.stream().filter(InventoryMetric::overstock).count();
        BigDecimal totalOutboundCost = outboundStats.totalOutboundCost();
        BigDecimal turnoverDays = totalOutboundCost.compareTo(BigDecimal.ZERO) > 0
                ? totalInventoryValue.multiply(BigDecimal.valueOf(daysBetween(range.start(), range.end()) + 1L))
                .divide(totalOutboundCost, 2, RoundingMode.HALF_UP)
                : BigDecimal.ZERO;
        List<Map<String, Object>> items = metrics.stream()
                .limit(finalLimit)
                .map(this::toInventoryItem)
                .toList();
        Map<String, Object> summary = rangeSummary(range, metrics.size(), keyword);
        summary.put("totalInventoryValue", totalInventoryValue);
        summary.put("stagnantValue", stagnantValue);
        summary.put("stagnantCount", stagnantCount);
        summary.put("overstockCount", overstockCount);
        summary.put("turnoverDays", turnoverDays);
        summary.put("capitalOccupation", totalInventoryValue);
        summary.put("totalOutboundCost", totalOutboundCost);
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("inventoryValueTopOption", buildInventoryTopBar(metrics));
        charts.put("inventoryAgingPieOption", buildInventoryAgingPie(metrics));
        charts.put("inventoryTurnoverGauge", buildTurnoverGauge(turnoverDays));
        return jsonResponse(true, "financial_inventory_capital_analysis", "已完成库存资金分析", summary, Map.of("items", items), charts);
    }
    @Tool(name = "应收应付与现金流预测", value = "预测未来现金流、回款风险、付款压力与资金缺口。")
    public String forecastCashFlow(@ToolMemoryId String memoryId,
                                   @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                   @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                   @P(value = "时间范围描述,如近90天、本年", required = false) String timeRange,
                                   @P(value = "预测月份数,默认3,最大6", required = false) Integer forecastMonths) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近90天");
        int months = forecastMonths == null || forecastMonths <= 0 ? 3 : Math.min(forecastMonths, 6);
        List<AccountSalesCollection> collections = queryCollections(loginUser, range);
        List<AccountPurchasePayment> payments = queryPayments(loginUser, range);
        List<MonthlyCashFlow> monthlyActual = buildMonthlyCashFlow(range, collections, payments);
        List<MonthlyCashFlow> monthlyForecast = forecastMonthlyCashFlow(monthlyActual, months);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        BigDecimal receivableTotal = snapshot.receivableTotal();
        BigDecimal payableTotal = snapshot.payableTotal();
        BigDecimal forecastNetSum = monthlyForecast.stream().map(MonthlyCashFlow::netFlow).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal coverage = receivableTotal.add(maxZero(forecastNetSum));
        BigDecimal fundGap = maxZero(payableTotal.subtract(coverage));
        Map<String, String> customerNameMap = queryCustomerNameMap(snapshot.receivableTop().stream().map(StatementMetric::entityId).collect(Collectors.toSet()));
        Map<String, String> supplierNameMap = querySupplierNameMap(snapshot.payableTop().stream().map(StatementMetric::entityId).collect(Collectors.toSet()));
        List<Map<String, Object>> receivableRiskItems = snapshot.receivableTop().stream().map(item -> toStatementRiskItem(item, customerNameMap, "customer")).toList();
        List<Map<String, Object>> payablePressureItems = snapshot.payableTop().stream().map(item -> toStatementRiskItem(item, supplierNameMap, "supplier")).toList();
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("actualIncomeTotal", collections.stream().map(AccountSalesCollection::getCollectionAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
        summary.put("actualExpenseTotal", payments.stream().map(AccountPurchasePayment::getPaymentAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
        summary.put("receivableBalance", receivableTotal);
        summary.put("payableBalance", payableTotal);
        summary.put("forecastNetSum", forecastNetSum);
        summary.put("fundGap", fundGap);
        summary.put("forecastMonths", months);
        summary.put("collectionRiskLevel", riskLevelByAmount(receivableTotal));
        summary.put("paymentPressureLevel", riskLevelByAmount(payableTotal));
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("cashFlowTrendOption", buildCashflowTrend(monthlyActual, monthlyForecast));
        charts.put("receivablePayableBarOption", buildReceivablePayableBar(receivableTotal, payableTotal));
        charts.put("fundGapGaugeOption", buildFundGapGauge(fundGap));
        return jsonResponse(true, "financial_cashflow_forecast", "已完成应收应付与现金流预测", summary,
                Map.of(
                        "actualMonthly", monthlyActual.stream().map(this::toMonthlyCashFlowItem).toList(),
                        "forecastMonthly", monthlyForecast.stream().map(this::toMonthlyCashFlowItem).toList(),
                        "receivableRiskTop", receivableRiskItems,
                        "payablePressureTop", payablePressureItems
                ),
                charts
        );
    }
    @Tool(name = "经营异常预警", value = "识别成本异常、利润异常、回款异常、订单风险、库存异常。")
    public String detectBusinessAnomalies(@ToolMemoryId String memoryId,
                                          @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                          @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                          @P(value = "时间范围描述,如近30天", required = false) String timeRange,
                                          @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        int finalLimit = normalizeLimit(limit);
        AnalysisBundle currentBundle = buildOrderProfitBundle(loginUser, range, null, Math.max(finalLimit, 30));
        DateRange prevRange = previousSameLengthRange(range);
        AnalysisBundle prevBundle = buildOrderProfitBundle(loginUser, prevRange, null, 50);
        BigDecimal currentCostRate = rate(currentBundle.totalCost(), currentBundle.totalRevenue());
        BigDecimal prevCostRate = rate(prevBundle.totalCost(), prevBundle.totalRevenue());
        BigDecimal costRateDiff = currentCostRate.subtract(prevCostRate);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        List<InventoryMetric> inventoryMetrics = buildInventoryMetrics(
                queryStockInventory(loginUser),
                queryProductModels(Collections.emptySet()),
                Map.of(),
                queryAverageUnitCostByModel(loginUser, Collections.emptySet()),
                queryOutboundStats(loginUser, Collections.emptySet(), range)
        );
        List<Map<String, Object>> anomalyItems = new ArrayList<>();
        if (costRateDiff.compareTo(new BigDecimal("0.10")) > 0) {
            anomalyItems.add(anomalyItem("high", "成本异常", "单位收入成本率较上周期上升超过10%", Map.of(
                    "currentCostRate", toPercent(currentCostRate),
                    "previousCostRate", toPercent(prevCostRate),
                    "delta", toPercent(costRateDiff)
            )));
        }
        long lossCount = currentBundle.orderMetrics().stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count();
        if (lossCount > 0) {
            anomalyItems.add(anomalyItem("high", "利润异常", "检测到亏损订单", Map.of("lossOrderCount", lossCount)));
        }
        if (snapshot.receivableTotal().compareTo(snapshot.payableTotal().multiply(new BigDecimal("1.2"))) > 0) {
            anomalyItems.add(anomalyItem("medium", "回款异常", "应收余额显著高于应付,回款压力偏大", Map.of(
                    "receivableBalance", snapshot.receivableTotal(),
                    "payableBalance", snapshot.payableTotal()
            )));
        }
        long overdueOrderCount = currentBundle.orderMetrics().stream()
                .filter(item -> item.deliveryDate() != null && item.deliveryDate().isBefore(LocalDate.now()) && item.profitRate().compareTo(new BigDecimal("0.10")) < 0)
                .count();
        if (overdueOrderCount > 0) {
            anomalyItems.add(anomalyItem("medium", "订单风险", "存在低利润且交付已逾期订单", Map.of("overdueRiskOrderCount", overdueOrderCount)));
        }
        long stagnantCount = inventoryMetrics.stream().filter(item -> item.stagnantDays() >= 90).count();
        if (stagnantCount > 0) {
            anomalyItems.add(anomalyItem("medium", "库存异常", "存在超过90天未周转库存", Map.of("stagnantCount", stagnantCount)));
        }
        List<Map<String, Object>> topAnomalies = anomalyItems.stream().limit(finalLimit).toList();
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("anomalyCount", topAnomalies.size());
        summary.put("highRiskCount", topAnomalies.stream().filter(item -> "high".equals(item.get("riskLevel"))).count());
        summary.put("mediumRiskCount", topAnomalies.stream().filter(item -> "medium".equals(item.get("riskLevel"))).count());
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("anomalyLevelPieOption", buildAnomalyLevelPie(topAnomalies));
        charts.put("anomalyTypeBarOption", buildAnomalyTypeBar(topAnomalies));
        return jsonResponse(true, "financial_business_anomaly_warning", "已完成经营异常预警分析", summary,
                Map.of("items", topAnomalies), charts);
    }
    @Tool(name = "AI经营驾驶舱", value = "实时展示产值、利润、库存、回款、设备利用率、订单利润率等核心经营指标。")
    public String getBusinessCockpit(@ToolMemoryId String memoryId,
                                     @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                     @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                     @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "本月");
        AnalysisBundle profitBundle = buildOrderProfitBundle(loginUser, range, null, 30);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        List<StockInventory> inventories = queryStockInventory(loginUser);
        BigDecimal inventoryValue = estimateInventoryValue(loginUser, inventories);
        long deviceTotal = countDevices(loginUser);
        long repairingCount = countRepairingDevices(loginUser);
        BigDecimal deviceUtilization = deviceTotal > 0
                ? new BigDecimal(deviceTotal - repairingCount).multiply(ONE_HUNDRED).divide(new BigDecimal(deviceTotal), 2, RoundingMode.HALF_UP)
                : BigDecimal.ZERO;
        BigDecimal outputValue = profitBundle.totalRevenue();
        BigDecimal profitRate = rate(profitBundle.totalProfit(), profitBundle.totalRevenue());
        BigDecimal collectionRate = snapshot.receivableTotal().compareTo(BigDecimal.ZERO) > 0
                ? ONE_HUNDRED.subtract(rate(snapshot.receivableTotal(), snapshot.receivableTotal().add(snapshot.payableTotal())).multiply(ONE_HUNDRED))
                : BigDecimal.ZERO;
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("outputValue", outputValue);
        summary.put("profit", profitBundle.totalProfit());
        summary.put("profitRate", toPercent(profitRate));
        summary.put("inventoryValue", inventoryValue);
        summary.put("receivableBalance", snapshot.receivableTotal());
        summary.put("payableBalance", snapshot.payableTotal());
        summary.put("collectionRate", toPercent(collectionRate.divide(ONE_HUNDRED, 4, RoundingMode.HALF_UP)));
        summary.put("deviceUtilizationRate", deviceUtilization + "%");
        summary.put("orderProfitRate", toPercent(avgRate(profitBundle.orderMetrics())));
        Map<String, Object> indicators = new LinkedHashMap<>();
        indicators.put("产值", outputValue);
        indicators.put("利润", profitBundle.totalProfit());
        indicators.put("库存资金占用", inventoryValue);
        indicators.put("应收余额", snapshot.receivableTotal());
        indicators.put("设备利用率", deviceUtilization + "%");
        indicators.put("订单平均利润率", toPercent(avgRate(profitBundle.orderMetrics())));
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("kpiCardData", indicators);
        charts.put("profitTrendOption", buildOrderProfitBar(profitBundle.orderMetrics()));
        charts.put("receivablePayableBarOption", buildReceivablePayableBar(snapshot.receivableTotal(), snapshot.payableTotal()));
        charts.put("inventoryProfitGaugeOption", buildInventoryProfitGauge(inventoryValue, profitBundle.totalProfit()));
        return jsonResponse(true, "financial_business_cockpit", "已生成AI经营驾驶舱数据", summary,
                Map.of(
                        "orderProfitTop", profitBundle.orderMetrics().stream()
                                .sorted(Comparator.comparing(OrderProfitMetric::profit).reversed())
                                .limit(10)
                                .map(this::toOrderCostItem)
                                .toList(),
                        "indicators", indicators
                ),
                charts
        );
    }
    @Tool(name = "日报周报自动生成", value = "自动输出经营分析日报/周报与风险建议。")
    public String generateOperationReport(@ToolMemoryId String memoryId,
                                          @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                          @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                          @P(value = "时间范围描述,如今天、本周", required = false) String timeRange,
                                          @P(value = "报告类型 daily/weekly", required = false) String reportType) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange,
                "weekly".equalsIgnoreCase(reportType) ? "本周" : "今天");
        String type = "weekly".equalsIgnoreCase(reportType) ? "weekly" : "daily";
        AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, null, 30);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        BigDecimal inventoryValue = estimateInventoryValue(loginUser, queryStockInventory(loginUser));
        long lossCount = bundle.orderMetrics().stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count();
        List<String> conclusions = new ArrayList<>();
        conclusions.add("营收" + bundle.totalRevenue() + ",利润" + bundle.totalProfit() + ",利润率" + toPercent(rate(bundle.totalProfit(), bundle.totalRevenue())) + "。");
        conclusions.add("应收余额" + snapshot.receivableTotal() + ",应付余额" + snapshot.payableTotal() + ",库存资金占用" + inventoryValue + "。");
        if (lossCount > 0) {
            conclusions.add("发现亏损订单" + lossCount + "个,建议优先复核材料损耗和工序人工效率。");
        } else {
            conclusions.add("当前未发现亏损订单,建议持续跟踪低于8%利润率订单。");
        }
        if (snapshot.receivableTotal().compareTo(snapshot.payableTotal()) > 0) {
            conclusions.add("回款压力偏高,建议针对高应收客户执行分层催收与账期优化。");
        } else {
            conclusions.add("资金压力可控,建议保持付款计划与采购节奏联动。");
        }
        List<Map<String, Object>> riskSuggestions = new ArrayList<>();
        if (lossCount > 0) {
            riskSuggestions.add(riskSuggestion("利润风险", "高", "复核亏损订单BOM和工序工资定额,必要时调整报价与交付节奏。"));
        }
        if (snapshot.receivableTotal().compareTo(SME_RECEIVABLE_RISK_THRESHOLD) > 0) {
            riskSuggestions.add(riskSuggestion("回款风险", "中", "对应收TOP客户建立周度回款计划,并设置预警阈值。"));
        }
        if (inventoryValue.compareTo(SME_INVENTORY_RISK_THRESHOLD) > 0) {
            riskSuggestions.add(riskSuggestion("库存风险", "中", "对高金额呆滞库存执行降价、替代和生产消耗策略。"));
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("reportType", type);
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("orderCount", bundle.orderMetrics().size());
        summary.put("lossOrderCount", lossCount);
        summary.put("riskSuggestionCount", riskSuggestions.size());
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("headline", "weekly".equals(type) ? "经营周报" : "经营日报");
        data.put("conclusions", conclusions);
        data.put("riskSuggestions", riskSuggestions);
        data.put("orderProfitTop", bundle.orderMetrics().stream()
                .sorted(Comparator.comparing(OrderProfitMetric::profitRate))
                .limit(10)
                .map(this::toRiskOrderItem)
                .toList());
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("reportProfitBarOption", buildOrderProfitBar(bundle.orderMetrics()));
        charts.put("reportReceivablePayableOption", buildReceivablePayableBar(snapshot.receivableTotal(), snapshot.payableTotal()));
        return jsonResponse(true, "financial_operation_report", "已自动生成经营分析报告", summary, data, charts);
    }
    private AnalysisBundle buildOrderProfitBundle(LoginUser loginUser, DateRange range, String keyword, Integer limit) {
        List<SalesLedger> ledgers = querySalesLedgers(loginUser, range, keyword, limit);
        if (ledgers.isEmpty()) {
            return AnalysisBundle.empty();
        }
        List<Long> ledgerIds = ledgers.stream().map(SalesLedger::getId).filter(Objects::nonNull).toList();
        List<SalesLedgerProduct> ledgerProducts = queryLedgerProducts(loginUser, ledgerIds);
        Map<Long, List<SalesLedgerProduct>> productsByLedgerId = ledgerProducts.stream()
                .collect(Collectors.groupingBy(SalesLedgerProduct::getSalesLedgerId));
        MaterialCostResult materialCostResult = calculateMaterialCost(loginUser, range, ledgerProducts);
        ProductionCostContext productionCostContext = calculateProductionCost(loginUser, range, ledgers, ledgerProducts, materialCostResult.avgUnitCostByModelId());
        BigDecimal totalDepreciation = calculateTotalDepreciation(loginUser);
        BigDecimal totalRevenue = ledgers.stream()
                .map(SalesLedger::getContractAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        Map<Long, BigDecimal> depreciationCostByLedger = allocateDepreciation(ledgers, totalDepreciation, totalRevenue);
        List<OrderProfitMetric> metrics = new ArrayList<>();
        for (SalesLedger ledger : ledgers) {
            BigDecimal revenue = defaultDecimal(ledger.getContractAmount());
            BigDecimal materialCost = materialCostResult.materialCostByLedgerId().getOrDefault(ledger.getId(), fallbackMaterialCost(productsByLedgerId.get(ledger.getId()), revenue));
            BigDecimal laborCost = productionCostContext.laborCostByLedgerId().getOrDefault(ledger.getId(), BigDecimal.ZERO);
            BigDecimal scrapCost = productionCostContext.scrapCostByLedgerId().getOrDefault(ledger.getId(), BigDecimal.ZERO);
            BigDecimal depreciationCost = depreciationCostByLedger.getOrDefault(ledger.getId(), BigDecimal.ZERO);
            BigDecimal totalCost = materialCost.add(laborCost).add(scrapCost).add(depreciationCost);
            BigDecimal profit = revenue.subtract(totalCost);
            BigDecimal profitRate = rate(profit, revenue);
            String riskLevel = profit.compareTo(BigDecimal.ZERO) < 0
                    ? "high"
                    : (profitRate.compareTo(new BigDecimal("0.08")) < 0 ? "medium" : "low");
            List<String> reasons = buildProfitReasons(revenue, materialCost, laborCost, scrapCost, profit, profitRate);
            String suggestion = buildProfitSuggestion(riskLevel, reasons);
            metrics.add(new OrderProfitMetric(
                    ledger.getId(),
                    safe(ledger.getSalesContractNo()),
                    safe(ledger.getCustomerName()),
                    safe(ledger.getProjectName()),
                    toLocalDate(ledger.getEntryDate()),
                    ledger.getDeliveryDate(),
                    revenue,
                    materialCost,
                    laborCost,
                    depreciationCost,
                    scrapCost,
                    totalCost,
                    profit,
                    profitRate,
                    riskLevel,
                    reasons,
                    suggestion
            ));
        }
        metrics.sort(Comparator.comparing(OrderProfitMetric::entryDate, Comparator.nullsLast(Comparator.reverseOrder()))
                .thenComparing(OrderProfitMetric::ledgerId, Comparator.nullsLast(Comparator.reverseOrder())));
        BigDecimal totalMaterialCost = metrics.stream().map(OrderProfitMetric::materialCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalLaborCost = metrics.stream().map(OrderProfitMetric::laborCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalScrapCost = metrics.stream().map(OrderProfitMetric::scrapCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalDepreciationCost = metrics.stream().map(OrderProfitMetric::depreciationCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalCost = metrics.stream().map(OrderProfitMetric::totalCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalProfit = metrics.stream().map(OrderProfitMetric::profit).reduce(BigDecimal.ZERO, BigDecimal::add);
        return new AnalysisBundle(
                metrics,
                productionCostContext.processCostRanking(),
                totalRevenue,
                totalMaterialCost,
                totalLaborCost,
                totalDepreciationCost,
                totalScrapCost,
                totalCost,
                totalProfit
        );
    }
    private MaterialCostResult calculateMaterialCost(LoginUser loginUser, DateRange range, List<SalesLedgerProduct> ledgerProducts) {
        if (ledgerProducts.isEmpty()) {
            return new MaterialCostResult(Map.of(), Map.of());
        }
        List<Long> ledgerProductIds = ledgerProducts.stream().map(SalesLedgerProduct::getId).filter(Objects::nonNull).toList();
        Set<Long> productModelIds = ledgerProducts.stream().map(SalesLedgerProduct::getProductModelId).filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, Long> productIdToLedgerId = ledgerProducts.stream()
                .filter(item -> item.getId() != null && item.getSalesLedgerId() != null)
                .collect(Collectors.toMap(SalesLedgerProduct::getId, SalesLedgerProduct::getSalesLedgerId, (a, b) -> a));
        Map<Long, BigDecimal> avgUnitCostByModelId = queryAverageUnitCostByModel(loginUser, productModelIds);
        LambdaQueryWrapper<ProcurementRecordOut> outWrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(outWrapper, loginUser.getTenantId(), ProcurementRecordOut::getTenantId);
        applyDeptFilter(outWrapper, loginUser.getCurrentDeptId(), ProcurementRecordOut::getDeptId);
        outWrapper.eq(ProcurementRecordOut::getType, 2)
                .in(ProcurementRecordOut::getSalesLedgerProductId, ledgerProductIds);
        if (range.hasDateFilter()) {
            outWrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
                    .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(outWrapper));
        Set<Integer> storageIds = outList.stream()
                .map(ProcurementRecordOut::getProcurementRecordStorageId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Integer, ProcurementRecordStorage> storageMap = storageIds.isEmpty()
                ? Map.of()
                : defaultList(procurementRecordMapper.selectBatchIds(storageIds)).stream()
                .collect(Collectors.toMap(ProcurementRecordStorage::getId, item -> item, (a, b) -> a));
        Map<Long, BigDecimal> materialCostByLedgerId = new HashMap<>();
        for (ProcurementRecordOut out : outList) {
            Long ledgerId = productIdToLedgerId.get(out.getSalesLedgerProductId());
            if (ledgerId == null) {
                continue;
            }
            ProcurementRecordStorage storage = storageMap.get(out.getProcurementRecordStorageId());
            BigDecimal unitPrice = storage == null ? BigDecimal.ZERO : defaultDecimal(storage.getUnitPrice());
            BigDecimal quantity = defaultDecimal(out.getInboundNum());
            BigDecimal cost = quantity.multiply(unitPrice);
            materialCostByLedgerId.merge(ledgerId, cost, BigDecimal::add);
        }
        return new MaterialCostResult(materialCostByLedgerId, avgUnitCostByModelId);
    }
    private ProductionCostContext calculateProductionCost(LoginUser loginUser,
                                                          DateRange range,
                                                          List<SalesLedger> ledgers,
                                                          List<SalesLedgerProduct> ledgerProducts,
                                                          Map<Long, BigDecimal> avgUnitCostByModelId) {
        if (ledgers.isEmpty()) {
            return ProductionCostContext.empty();
        }
        Set<Long> ledgerIds = ledgers.stream().map(SalesLedger::getId).filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, Set<Long>> productModelToLedgerIds = new HashMap<>();
        for (SalesLedgerProduct product : ledgerProducts) {
            if (product.getProductModelId() == null || product.getSalesLedgerId() == null) {
                continue;
            }
            productModelToLedgerIds.computeIfAbsent(product.getProductModelId(), key -> new HashSet<>()).add(product.getSalesLedgerId());
        }
        LambdaQueryWrapper<ProductionPlan> planWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(planWrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        planWrapper.in(ProductionPlan::getSalesLedgerId, ledgerIds);
        List<ProductionPlan> plans = defaultList(productionPlanMapper.selectList(planWrapper));
        Map<Long, Long> planIdToLedgerId = plans.stream()
                .filter(item -> item.getId() != null && item.getSalesLedgerId() != null)
                .collect(Collectors.toMap(ProductionPlan::getId, ProductionPlan::getSalesLedgerId, (a, b) -> a));
        LambdaQueryWrapper<ProductionOrder> orderWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(orderWrapper, loginUser.getCurrentDeptId(), ProductionOrder::getDeptId);
        if (range.hasDateFilter()) {
            orderWrapper.ge(ProductionOrder::getCreateTime, range.start().atStartOfDay().minusMonths(2))
                    .lt(ProductionOrder::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
        }
        List<ProductionOrder> orders = defaultList(productionOrderMapper.selectList(orderWrapper));
        Map<Long, Set<Long>> orderIdToLedgerIds = new HashMap<>();
        for (ProductionOrder order : orders) {
            Set<Long> orderLedgers = new HashSet<>();
            for (Long planId : parseIdList(order.getProductionPlanIds())) {
                Long ledgerId = planIdToLedgerId.get(planId);
                if (ledgerId != null) {
                    orderLedgers.add(ledgerId);
                }
            }
            if (orderLedgers.isEmpty() && order.getProductModelId() != null) {
                orderLedgers.addAll(productModelToLedgerIds.getOrDefault(order.getProductModelId(), Set.of()));
            }
            if (!orderLedgers.isEmpty()) {
                orderIdToLedgerIds.put(order.getId(), orderLedgers);
            }
        }
        if (orderIdToLedgerIds.isEmpty()) {
            return ProductionCostContext.empty();
        }
        LambdaQueryWrapper<ProductionOperationTask> taskWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(taskWrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        taskWrapper.in(ProductionOperationTask::getProductionOrderId, orderIdToLedgerIds.keySet());
        List<ProductionOperationTask> tasks = defaultList(productionOperationTaskMapper.selectList(taskWrapper));
        Map<Long, Long> taskIdToOrderId = tasks.stream()
                .filter(item -> item.getId() != null && item.getProductionOrderId() != null)
                .collect(Collectors.toMap(ProductionOperationTask::getId, ProductionOperationTask::getProductionOrderId, (a, b) -> a));
        if (taskIdToOrderId.isEmpty()) {
            return ProductionCostContext.empty();
        }
        LambdaQueryWrapper<ProductionProductMain> mainWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(mainWrapper, loginUser.getCurrentDeptId(), ProductionProductMain::getDeptId);
        mainWrapper.in(ProductionProductMain::getProductionOperationTaskId, taskIdToOrderId.keySet());
        if (range.hasDateFilter()) {
            mainWrapper.ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay().minusMonths(2))
                    .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
        }
        List<ProductionProductMain> mainList = defaultList(productionProductMainMapper.selectList(mainWrapper));
        Map<Long, Set<Long>> mainIdToLedgers = new HashMap<>();
        for (ProductionProductMain main : mainList) {
            Long orderId = taskIdToOrderId.get(main.getProductionOperationTaskId());
            if (orderId == null) {
                continue;
            }
            Set<Long> ledgerSet = orderIdToLedgerIds.get(orderId);
            if (ledgerSet == null || ledgerSet.isEmpty()) {
                continue;
            }
            mainIdToLedgers.put(main.getId(), ledgerSet);
        }
        if (mainIdToLedgers.isEmpty()) {
            return ProductionCostContext.empty();
        }
        LambdaQueryWrapper<ProductionAccount> accountWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(accountWrapper, loginUser.getCurrentDeptId(), ProductionAccount::getDeptId);
        accountWrapper.in(ProductionAccount::getProductionProductMainId, mainIdToLedgers.keySet());
        if (range.hasDateFilter()) {
            accountWrapper.ge(ProductionAccount::getSchedulingDate, range.start().atStartOfDay())
                    .lt(ProductionAccount::getSchedulingDate, range.end().plusDays(1).atStartOfDay());
        }
        List<ProductionAccount> accountList = defaultList(productionAccountMapper.selectList(accountWrapper));
        Map<String, BigDecimal> salaryQuotaByOperation = defaultList(technologyOperationMapper.selectList(new LambdaQueryWrapper<TechnologyOperation>()
                        .select(TechnologyOperation::getName, TechnologyOperation::getSalaryQuota)))
                .stream()
                .filter(item -> StringUtils.hasText(item.getName()))
                .collect(Collectors.toMap(TechnologyOperation::getName, item -> defaultDecimal(item.getSalaryQuota()), (a, b) -> a));
        Map<Long, BigDecimal> laborCostByLedger = new HashMap<>();
        Map<String, BigDecimal> processCostMap = new HashMap<>();
        for (ProductionAccount account : accountList) {
            Set<Long> ledgerSet = mainIdToLedgers.get(account.getProductionProductMainId());
            if (ledgerSet == null || ledgerSet.isEmpty()) {
                continue;
            }
            BigDecimal cost = estimateLaborCost(account, salaryQuotaByOperation);
            if (cost.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal split = cost.divide(new BigDecimal(ledgerSet.size()), 4, RoundingMode.HALF_UP);
            for (Long ledgerId : ledgerSet) {
                laborCostByLedger.merge(ledgerId, split, BigDecimal::add);
            }
            processCostMap.merge(safe(account.getTechnologyOperationName()), cost, BigDecimal::add);
        }
        LambdaQueryWrapper<ProductionProductOutput> outputWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(outputWrapper, loginUser.getCurrentDeptId(), ProductionProductOutput::getDeptId);
        outputWrapper.in(ProductionProductOutput::getProductionProductMainId, mainIdToLedgers.keySet());
        if (range.hasDateFilter()) {
            outputWrapper.ge(ProductionProductOutput::getCreateTime, range.start().atStartOfDay())
                    .lt(ProductionProductOutput::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        List<ProductionProductOutput> outputList = defaultList(productionProductOutputMapper.selectList(outputWrapper));
        Map<Long, BigDecimal> scrapCostByLedger = new HashMap<>();
        for (ProductionProductOutput output : outputList) {
            Set<Long> ledgerSet = mainIdToLedgers.get(output.getProductionProductMainId());
            if (ledgerSet == null || ledgerSet.isEmpty()) {
                continue;
            }
            BigDecimal scrapQty = defaultDecimal(output.getScrapQty());
            if (scrapQty.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal unitCost = avgUnitCostByModelId.getOrDefault(output.getProductModelId(), BigDecimal.ZERO);
            BigDecimal scrapCost = scrapQty.multiply(unitCost);
            BigDecimal split = scrapCost.divide(new BigDecimal(ledgerSet.size()), 4, RoundingMode.HALF_UP);
            for (Long ledgerId : ledgerSet) {
                scrapCostByLedger.merge(ledgerId, split, BigDecimal::add);
            }
        }
        Map<String, BigDecimal> processCostRanking = processCostMap.entrySet().stream()
                .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
                .limit(10)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
        return new ProductionCostContext(laborCostByLedger, scrapCostByLedger, processCostRanking);
    }
    private List<SalesLedger> querySalesLedgers(LoginUser loginUser, DateRange range, String keyword, Integer limit) {
        LambdaQueryWrapper<SalesLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), SalesLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedger::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(SalesLedger::getSalesContractNo, keyword)
                    .or().like(SalesLedger::getCustomerContractNo, keyword)
                    .or().like(SalesLedger::getCustomerName, keyword)
                    .or().like(SalesLedger::getProjectName, keyword)
                    .or().like(SalesLedger::getSalesman, keyword));
        }
        if (range.hasDateFilter()) {
            wrapper.ge(SalesLedger::getEntryDate, toDate(range.start()))
                    .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end()));
        }
        wrapper.orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId);
        if (limit != null && limit > 0) {
            wrapper.last("limit " + normalizeLimit(limit));
        }
        return defaultList(salesLedgerMapper.selectList(wrapper));
    }
    private List<SalesLedgerProduct> queryLedgerProducts(LoginUser loginUser, List<Long> ledgerIds) {
        if (ledgerIds == null || ledgerIds.isEmpty()) {
            return List.of();
        }
        LambdaQueryWrapper<SalesLedgerProduct> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedgerProduct::getDeptId);
        wrapper.in(SalesLedgerProduct::getSalesLedgerId, ledgerIds)
                .eq(SalesLedgerProduct::getType, 1);
        return defaultList(salesLedgerProductMapper.selectList(wrapper));
    }
    private List<StockInventory> queryStockInventory(LoginUser loginUser) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        return defaultList(stockInventoryMapper.selectList(wrapper));
    }
    private Map<Long, ProductModel> queryProductModels(Set<Long> modelIds) {
        if (modelIds == null || modelIds.isEmpty()) {
            return defaultList(productModelMapper.selectList(null)).stream()
                    .filter(item -> item.getId() != null)
                    .collect(Collectors.toMap(ProductModel::getId, item -> item, (a, b) -> a));
        }
        LambdaQueryWrapper<ProductModel> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(ProductModel::getId, modelIds);
        return defaultList(productModelMapper.selectList(wrapper)).stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(ProductModel::getId, item -> item, (a, b) -> a));
    }
    private Map<Long, Product> queryProducts(Collection<ProductModel> models) {
        Set<Long> productIds = models == null ? Set.of() : models.stream()
                .map(ProductModel::getProductId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (productIds.isEmpty()) {
            return Map.of();
        }
        LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(Product::getId, productIds);
        return defaultList(productMapper.selectList(wrapper)).stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(Product::getId, item -> item, (a, b) -> a));
    }
    private Map<Long, BigDecimal> queryAverageUnitCostByModel(LoginUser loginUser, Set<Long> productModelIds) {
        LambdaQueryWrapper<ProcurementRecordStorage> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementRecordStorage::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementRecordStorage::getDeptId);
        wrapper.in(ProcurementRecordStorage::getType, List.of(1, 2));
        if (productModelIds != null && !productModelIds.isEmpty()) {
            wrapper.in(ProcurementRecordStorage::getProductModelId, productModelIds);
        }
        List<ProcurementRecordStorage> rows = defaultList(procurementRecordMapper.selectList(wrapper));
        Map<Long, BigDecimal> amountByModel = new HashMap<>();
        Map<Long, BigDecimal> qtyByModel = new HashMap<>();
        for (ProcurementRecordStorage row : rows) {
            if (row.getProductModelId() == null) {
                continue;
            }
            BigDecimal qty = defaultDecimal(row.getInboundNum());
            if (qty.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal amount = defaultDecimal(row.getUnitPrice()).multiply(qty);
            amountByModel.merge(row.getProductModelId(), amount, BigDecimal::add);
            qtyByModel.merge(row.getProductModelId(), qty, BigDecimal::add);
        }
        Map<Long, BigDecimal> result = new HashMap<>();
        for (Map.Entry<Long, BigDecimal> entry : amountByModel.entrySet()) {
            BigDecimal qty = qtyByModel.get(entry.getKey());
            if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            result.put(entry.getKey(), entry.getValue().divide(qty, 6, RoundingMode.HALF_UP));
        }
        return result;
    }
    private OutboundStats queryOutboundStats(LoginUser loginUser, Set<Long> productModelIds, DateRange range) {
        LambdaQueryWrapper<ProcurementRecordOut> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementRecordOut::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementRecordOut::getDeptId);
        if (productModelIds != null && !productModelIds.isEmpty()) {
            wrapper.in(ProcurementRecordOut::getProductModelId, productModelIds);
        }
        if (range.hasDateFilter()) {
            wrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
                    .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(wrapper));
        if (outList.isEmpty()) {
            return OutboundStats.empty();
        }
        Set<Integer> storageIds = outList.stream()
                .map(ProcurementRecordOut::getProcurementRecordStorageId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Integer, ProcurementRecordStorage> storageMap = storageIds.isEmpty()
                ? Map.of()
                : defaultList(procurementRecordMapper.selectBatchIds(storageIds)).stream()
                .collect(Collectors.toMap(ProcurementRecordStorage::getId, item -> item, (a, b) -> a));
        Map<Long, BigDecimal> outboundQtyByModel = new HashMap<>();
        Map<Long, LocalDateTime> lastOutboundTimeByModel = new HashMap<>();
        BigDecimal totalOutboundCost = BigDecimal.ZERO;
        for (ProcurementRecordOut out : outList) {
            Long modelId = out.getProductModelId();
            if (modelId == null) {
                continue;
            }
            BigDecimal qty = defaultDecimal(out.getInboundNum());
            outboundQtyByModel.merge(modelId, qty, BigDecimal::add);
            if (out.getCreateTime() != null) {
                LocalDateTime existing = lastOutboundTimeByModel.get(modelId);
                if (existing == null || out.getCreateTime().isAfter(existing)) {
                    lastOutboundTimeByModel.put(modelId, out.getCreateTime());
                }
            }
            ProcurementRecordStorage storage = storageMap.get(out.getProcurementRecordStorageId());
            BigDecimal unitPrice = storage == null ? BigDecimal.ZERO : defaultDecimal(storage.getUnitPrice());
            totalOutboundCost = totalOutboundCost.add(unitPrice.multiply(qty));
        }
        return new OutboundStats(outboundQtyByModel, lastOutboundTimeByModel, totalOutboundCost);
    }
    private List<InventoryMetric> buildInventoryMetrics(List<StockInventory> inventoryRows,
                                                        Map<Long, ProductModel> productModelMap,
                                                        Map<Long, Product> productMap,
                                                        Map<Long, BigDecimal> avgUnitCostByModelId,
                                                        OutboundStats outboundStats) {
        Map<Long, InventoryMetricBuilder> metricBuilderByModel = new HashMap<>();
        for (StockInventory row : inventoryRows) {
            if (row.getProductModelId() == null) {
                continue;
            }
            InventoryMetricBuilder builder = metricBuilderByModel.computeIfAbsent(row.getProductModelId(), InventoryMetricBuilder::new);
            builder.addQuantity(maxZero(defaultDecimal(row.getQualitity()).subtract(defaultDecimal(row.getLockedQuantity()))));
            builder.addLockedQuantity(defaultDecimal(row.getLockedQuantity()));
            builder.addWarnNum(defaultDecimal(row.getWarnNum()));
            if (row.getCreateTime() != null) {
                builder.updateFirstInTime(row.getCreateTime());
            }
        }
        List<InventoryMetric> result = new ArrayList<>();
        LocalDate today = LocalDate.now();
        for (InventoryMetricBuilder builder : metricBuilderByModel.values()) {
            Long modelId = builder.modelId();
            ProductModel model = productModelMap.get(modelId);
            Product product = model == null ? null : productMap.get(model.getProductId());
            BigDecimal unitCost = avgUnitCostByModelId.getOrDefault(modelId, BigDecimal.ZERO);
            BigDecimal value = builder.quantity().multiply(unitCost);
            LocalDateTime lastOutTime = outboundStats.lastOutboundTimeByModel().get(modelId);
            long stagnantDays;
            if (lastOutTime != null) {
                stagnantDays = daysBetween(lastOutTime.toLocalDate(), today);
            } else if (builder.firstInTime() != null) {
                stagnantDays = daysBetween(builder.firstInTime().toLocalDate(), today);
            } else {
                stagnantDays = 0;
            }
            BigDecimal outQty = outboundStats.outboundQtyByModel().getOrDefault(modelId, BigDecimal.ZERO);
            boolean overstock = builder.warnNum().compareTo(BigDecimal.ZERO) > 0
                    && builder.quantity().compareTo(builder.warnNum().multiply(new BigDecimal("3"))) > 0;
            result.add(new InventoryMetric(
                    modelId,
                    product == null ? "未知产品" : safe(product.getProductName()),
                    model == null ? "未知型号" : safe(model.getModel()),
                    builder.quantity(),
                    builder.lockedQuantity(),
                    unitCost,
                    value,
                    outQty,
                    stagnantDays,
                    overstock
            ));
        }
        return result;
    }
    private List<AccountSalesCollection> queryCollections(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId);
        if (range.hasDateFilter()) {
            wrapper.ge(AccountSalesCollection::getCollectionDate, range.start())
                    .le(AccountSalesCollection::getCollectionDate, range.end());
        }
        wrapper.orderByAsc(AccountSalesCollection::getCollectionDate);
        return defaultList(accountSalesCollectionMapper.selectList(wrapper));
    }
    private List<AccountPurchasePayment> queryPayments(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountPurchasePayment> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchasePayment::getDeptId);
        if (range.hasDateFilter()) {
            wrapper.ge(AccountPurchasePayment::getPaymentDate, range.start())
                    .le(AccountPurchasePayment::getPaymentDate, range.end());
        }
        wrapper.orderByAsc(AccountPurchasePayment::getPaymentDate);
        return defaultList(accountPurchasePaymentMapper.selectList(wrapper));
    }
    private List<MonthlyCashFlow> buildMonthlyCashFlow(DateRange range,
                                                       List<AccountSalesCollection> collections,
                                                       List<AccountPurchasePayment> payments) {
        Map<YearMonth, BigDecimal> incomeByMonth = new LinkedHashMap<>();
        Map<YearMonth, BigDecimal> expenseByMonth = new LinkedHashMap<>();
        DateRange monthlyRange = range.hasDateFilter() ? range : inferCashFlowRange(collections, payments);
        if (!monthlyRange.hasDateFilter()) {
            return List.of();
        }
        YearMonth startMonth = YearMonth.from(monthlyRange.start());
        YearMonth endMonth = YearMonth.from(monthlyRange.end());
        for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) {
            incomeByMonth.put(month, BigDecimal.ZERO);
            expenseByMonth.put(month, BigDecimal.ZERO);
        }
        for (AccountSalesCollection row : collections) {
            if (row.getCollectionDate() == null) {
                continue;
            }
            YearMonth month = YearMonth.from(row.getCollectionDate());
            if (incomeByMonth.containsKey(month)) {
                incomeByMonth.put(month, incomeByMonth.get(month).add(defaultDecimal(row.getCollectionAmount())));
            }
        }
        for (AccountPurchasePayment row : payments) {
            if (row.getPaymentDate() == null) {
                continue;
            }
            YearMonth month = YearMonth.from(row.getPaymentDate());
            if (expenseByMonth.containsKey(month)) {
                expenseByMonth.put(month, expenseByMonth.get(month).add(defaultDecimal(row.getPaymentAmount())));
            }
        }
        List<MonthlyCashFlow> result = new ArrayList<>();
        for (YearMonth month : incomeByMonth.keySet()) {
            BigDecimal income = incomeByMonth.get(month);
            BigDecimal expense = expenseByMonth.getOrDefault(month, BigDecimal.ZERO);
            result.add(new MonthlyCashFlow(month.toString(), income, expense, income.subtract(expense)));
        }
        return result;
    }
    private DateRange inferCashFlowRange(List<AccountSalesCollection> collections,
                                         List<AccountPurchasePayment> payments) {
        LocalDate min = null;
        LocalDate max = null;
        for (AccountSalesCollection row : defaultList(collections)) {
            if (row.getCollectionDate() == null) {
                continue;
            }
            min = min == null || row.getCollectionDate().isBefore(min) ? row.getCollectionDate() : min;
            max = max == null || row.getCollectionDate().isAfter(max) ? row.getCollectionDate() : max;
        }
        for (AccountPurchasePayment row : defaultList(payments)) {
            if (row.getPaymentDate() == null) {
                continue;
            }
            min = min == null || row.getPaymentDate().isBefore(min) ? row.getPaymentDate() : min;
            max = max == null || row.getPaymentDate().isAfter(max) ? row.getPaymentDate() : max;
        }
        return min == null || max == null ? new DateRange(null, null, "全部") : new DateRange(min, max, "全部");
    }
    private List<MonthlyCashFlow> forecastMonthlyCashFlow(List<MonthlyCashFlow> actual, int forecastMonths) {
        if (actual.isEmpty()) {
            List<MonthlyCashFlow> defaults = new ArrayList<>();
            YearMonth now = YearMonth.now();
            for (int i = 1; i <= forecastMonths; i++) {
                YearMonth month = now.plusMonths(i);
                defaults.add(new MonthlyCashFlow(month.toString(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
            }
            return defaults;
        }
        List<BigDecimal> series = actual.stream().map(MonthlyCashFlow::netFlow).toList();
        BigDecimal avg = series.stream().reduce(BigDecimal.ZERO, BigDecimal::add)
                .divide(new BigDecimal(series.size()), 4, RoundingMode.HALF_UP);
        BigDecimal slope = BigDecimal.ZERO;
        if (series.size() > 1) {
            slope = series.get(series.size() - 1).subtract(series.get(0))
                    .divide(new BigDecimal(series.size() - 1), 4, RoundingMode.HALF_UP);
        }
        YearMonth lastMonth = YearMonth.parse(actual.get(actual.size() - 1).month());
        List<MonthlyCashFlow> forecast = new ArrayList<>();
        for (int i = 1; i <= forecastMonths; i++) {
            YearMonth month = lastMonth.plusMonths(i);
            BigDecimal net = avg.add(slope.multiply(new BigDecimal(i))).setScale(2, RoundingMode.HALF_UP);
            BigDecimal income = net.compareTo(BigDecimal.ZERO) >= 0 ? net : BigDecimal.ZERO;
            BigDecimal expense = net.compareTo(BigDecimal.ZERO) >= 0 ? BigDecimal.ZERO : net.abs();
            forecast.add(new MonthlyCashFlow(month.toString(), income, expense, net));
        }
        return forecast;
    }
    private StatementSnapshot buildStatementSnapshot(LoginUser loginUser) {
        LambdaQueryWrapper<AccountStatement> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountStatement::getDeptId);
        wrapper.orderByDesc(AccountStatement::getStatementMonth);
        List<AccountStatement> rows = defaultList(accountStatementMapper.selectList(wrapper));
        if (rows.isEmpty()) {
            return StatementSnapshot.empty();
        }
        Map<String, AccountStatement> latestByEntity = new HashMap<>();
        for (AccountStatement row : rows) {
            if (row.getAccountType() == null || row.getCustomerId() == null || !StringUtils.hasText(row.getStatementMonth())) {
                continue;
            }
            String key = row.getAccountType() + "::" + row.getCustomerId();
            AccountStatement existing = latestByEntity.get(key);
            if (existing == null || row.getStatementMonth().compareTo(existing.getStatementMonth()) > 0) {
                latestByEntity.put(key, row);
            }
        }
        BigDecimal receivableTotal = BigDecimal.ZERO;
        BigDecimal payableTotal = BigDecimal.ZERO;
        List<StatementMetric> receivableMetrics = new ArrayList<>();
        List<StatementMetric> payableMetrics = new ArrayList<>();
        for (AccountStatement row : latestByEntity.values()) {
            BigDecimal closing = defaultDecimal(row.getClosingBalance());
            if (Objects.equals(row.getAccountType(), 1)) {
                receivableTotal = receivableTotal.add(closing);
                receivableMetrics.add(new StatementMetric(String.valueOf(row.getCustomerId()), closing,
                        defaultDecimal(row.getCurrentPlan()), defaultDecimal(row.getCurrentActually()), safe(row.getStatementMonth())));
            } else if (Objects.equals(row.getAccountType(), 2)) {
                payableTotal = payableTotal.add(closing);
                payableMetrics.add(new StatementMetric(String.valueOf(row.getCustomerId()), closing,
                        defaultDecimal(row.getCurrentPlan()), defaultDecimal(row.getCurrentActually()), safe(row.getStatementMonth())));
            }
        }
        receivableMetrics.sort(Comparator.comparing(StatementMetric::closingBalance).reversed());
        payableMetrics.sort(Comparator.comparing(StatementMetric::closingBalance).reversed());
        return new StatementSnapshot(
                receivableTotal,
                payableTotal,
                receivableMetrics.stream().limit(10).toList(),
                payableMetrics.stream().limit(10).toList()
        );
    }
    private BigDecimal calculateTotalDepreciation(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
        wrapper.eq(DeviceLedger::getIsDepr, 1);
        List<DeviceLedger> devices = defaultList(deviceLedgerMapper.selectList(wrapper));
        BigDecimal total = BigDecimal.ZERO;
        for (DeviceLedger device : devices) {
            total = total.add(defaultDecimal(AccountingServiceImpl.calculatePreciseDepreciation(device)));
        }
        return total;
    }
    private Map<Long, BigDecimal> allocateDepreciation(List<SalesLedger> ledgers, BigDecimal totalDepreciation, BigDecimal totalRevenue) {
        if (ledgers.isEmpty() || totalDepreciation.compareTo(BigDecimal.ZERO) <= 0) {
            return Map.of();
        }
        Map<Long, BigDecimal> result = new HashMap<>();
        if (totalRevenue.compareTo(BigDecimal.ZERO) <= 0) {
            BigDecimal avg = totalDepreciation.divide(new BigDecimal(ledgers.size()), 4, RoundingMode.HALF_UP);
            for (SalesLedger ledger : ledgers) {
                result.put(ledger.getId(), avg);
            }
            return result;
        }
        for (SalesLedger ledger : ledgers) {
            BigDecimal revenue = defaultDecimal(ledger.getContractAmount());
            BigDecimal ratio = revenue.divide(totalRevenue, 6, RoundingMode.HALF_UP);
            result.put(ledger.getId(), totalDepreciation.multiply(ratio));
        }
        return result;
    }
    private BigDecimal fallbackMaterialCost(List<SalesLedgerProduct> products, BigDecimal revenue) {
        if (products != null && !products.isEmpty()) {
            BigDecimal productAmount = products.stream()
                    .map(SalesLedgerProduct::getTaxExclusiveTotalPrice)
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            if (productAmount.compareTo(BigDecimal.ZERO) > 0) {
                return productAmount;
            }
        }
        BigDecimal materialCost = revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE);
        BigDecimal laborCost = revenue.multiply(DEFAULT_LABOR_COST_RATE);
        BigDecimal overheadCost = revenue.multiply(DEFAULT_OVERHEAD_COST_RATE);
        return materialCost.add(laborCost).add(overheadCost);
    }
    private BigDecimal estimateTotalCost(BigDecimal revenue, List<SalesLedgerProduct> products) {
        if (revenue == null || revenue.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        BigDecimal materialCost = BigDecimal.ZERO;
        if (products != null && !products.isEmpty()) {
            materialCost = products.stream()
                    .map(SalesLedgerProduct::getTaxExclusiveTotalPrice)
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
        }
        if (materialCost.compareTo(BigDecimal.ZERO) <= 0) {
            materialCost = revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE);
        }
        BigDecimal laborCost = revenue.multiply(DEFAULT_LABOR_COST_RATE);
        BigDecimal overheadCost = revenue.multiply(DEFAULT_OVERHEAD_COST_RATE);
        return materialCost.add(laborCost).add(overheadCost);
    }
    private Map<String, String> queryCustomerNameMap(Set<String> idSet) {
        if (idSet == null || idSet.isEmpty()) {
            return Map.of();
        }
        Set<Long> ids = idSet.stream()
                .map(this::toLongOrNull)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (ids.isEmpty()) {
            return Map.of();
        }
        LambdaQueryWrapper<Customer> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(Customer::getId, ids);
        return defaultList(customerMapper.selectList(wrapper)).stream()
                .collect(Collectors.toMap(item -> String.valueOf(item.getId()), Customer::getCustomerName, (a, b) -> a));
    }
    private Map<String, String> querySupplierNameMap(Set<String> idSet) {
        if (idSet == null || idSet.isEmpty()) {
            return Map.of();
        }
        Set<Long> ids = idSet.stream()
                .map(this::toLongOrNull)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (ids.isEmpty()) {
            return Map.of();
        }
        LambdaQueryWrapper<SupplierManage> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(SupplierManage::getId, ids);
        return defaultList(supplierManageMapper.selectList(wrapper)).stream()
                .collect(Collectors.toMap(item -> String.valueOf(item.getId()), SupplierManage::getSupplierName, (a, b) -> a));
    }
    private String riskLevelByAmount(BigDecimal amount) {
        if (amount.compareTo(new BigDecimal("5000000")) >= 0) {
            return "high";
        }
        if (amount.compareTo(new BigDecimal("1000000")) >= 0) {
            return "medium";
        }
        return "low";
    }
    private long countDevices(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
        return deviceLedgerMapper.selectCount(wrapper);
    }
    private long countRepairingDevices(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
        wrapper.in(DeviceRepair::getStatus, List.of(0, 3));
        return deviceRepairMapper.selectCount(wrapper);
    }
    private BigDecimal estimateInventoryValue(LoginUser loginUser, List<StockInventory> inventories) {
        if (inventories == null || inventories.isEmpty()) {
            return BigDecimal.ZERO;
        }
        Set<Long> modelIds = inventories.stream().map(StockInventory::getProductModelId).filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, BigDecimal> costMap = queryAverageUnitCostByModel(loginUser, modelIds);
        BigDecimal total = BigDecimal.ZERO;
        for (StockInventory inventory : inventories) {
            BigDecimal qty = maxZero(defaultDecimal(inventory.getQualitity()).subtract(defaultDecimal(inventory.getLockedQuantity())));
            BigDecimal unit = costMap.getOrDefault(inventory.getProductModelId(), BigDecimal.ZERO);
            total = total.add(qty.multiply(unit));
        }
        return total;
    }
    private DateRange previousSameLengthRange(DateRange range) {
        if (!range.hasDateFilter()) {
            return new DateRange(null, null, "全部");
        }
        long days = daysBetween(range.start(), range.end()) + 1L;
        LocalDate prevEnd = range.start().minusDays(1);
        LocalDate prevStart = prevEnd.minusDays(days - 1L);
        return new DateRange(prevStart, prevEnd, prevStart + "至" + prevEnd);
    }
    private List<String> buildProfitReasons(BigDecimal revenue,
                                            BigDecimal materialCost,
                                            BigDecimal laborCost,
                                            BigDecimal scrapCost,
                                            BigDecimal profit,
                                            BigDecimal profitRate) {
        List<String> reasons = new ArrayList<>();
        BigDecimal materialRate = rate(materialCost, revenue);
        if (materialRate.compareTo(new BigDecimal("0.70")) >= 0) {
            reasons.add("材料成本占比超过70%");
        } else if (materialRate.compareTo(new BigDecimal("0.55")) >= 0) {
            reasons.add("材料成本占比偏高");
        }
        BigDecimal laborRate = rate(laborCost, revenue);
        if (laborRate.compareTo(new BigDecimal("0.20")) >= 0) {
            reasons.add("人工成本占比超过20%");
        } else if (laborRate.compareTo(new BigDecimal("0.12")) >= 0) {
            reasons.add("人工成本增长偏快");
        }
        BigDecimal scrapRate = rate(scrapCost, revenue);
        if (scrapRate.compareTo(new BigDecimal("0.05")) >= 0) {
            reasons.add("报废损耗占比偏高");
        }
        if (profit.compareTo(BigDecimal.ZERO) < 0) {
            reasons.add("订单处于亏损状态");
        } else if (profitRate.compareTo(new BigDecimal("0.08")) < 0) {
            reasons.add("利润率低于8%");
        }
        if (reasons.isEmpty()) {
            reasons.add("成本结构处于合理区间");
        }
        return reasons;
    }
    private String buildProfitSuggestion(String riskLevel, List<String> reasons) {
        if ("high".equals(riskLevel)) {
            return "优先复核BOM用量与工序定额,必要时调整报价和付款条款,并限制超账期交付。";
        }
        if ("medium".equals(riskLevel)) {
            return "建议优化采购批次和工序排产,提升一次合格率并同步执行毛利预警。";
        }
        if (reasons.stream().anyMatch(item -> item.contains("材料"))) {
            return "保持材料采购成本看板,按周跟踪主要材料单价波动。";
        }
        return "维持当前经营节奏,继续跟踪订单利润率和回款效率。";
    }
    private Map<String, Object> toOrderCostItem(OrderProfitMetric metric) {
        Map<String, Object> item = new LinkedHashMap<>();
        item.put("ledgerId", metric.ledgerId());
        item.put("salesContractNo", metric.salesContractNo());
        item.put("customerName", metric.customerName());
        item.put("projectName", metric.projectName());
        item.put("entryDate", formatDate(metric.entryDate()));
        item.put("deliveryDate", formatDate(metric.deliveryDate()));
        item.put("revenue", metric.revenue());
        item.put("materialCost", metric.materialCost());
        item.put("laborCost", metric.laborCost());
        item.put("depreciationCost", metric.depreciationCost());
        item.put("scrapCost", metric.scrapCost());
        item.put("totalCost", metric.totalCost());
        item.put("profit", metric.profit());
        item.put("profitRate", toPercent(metric.profitRate()));
        item.put("riskLevel", metric.riskLevel());
        item.put("reasons", metric.reasons());
        item.put("suggestion", metric.suggestion());
        return item;
    }
    private Map<String, Object> toRiskOrderItem(OrderProfitMetric metric) {
        Map<String, Object> map = toOrderCostItem(metric);
        map.put("priority", "high".equals(metric.riskLevel()) ? "high" : ("medium".equals(metric.riskLevel()) ? "medium" : "low"));
        return map;
    }
    private List<Map<String, Object>> buildCustomerProfitTop(List<OrderProfitMetric> metrics, int topN) {
        Map<String, BigDecimal> customerProfitMap = new HashMap<>();
        Map<String, BigDecimal> customerRevenueMap = new HashMap<>();
        for (OrderProfitMetric metric : metrics) {
            customerProfitMap.merge(metric.customerName(), metric.profit(), BigDecimal::add);
            customerRevenueMap.merge(metric.customerName(), metric.revenue(), BigDecimal::add);
        }
        return customerProfitMap.entrySet().stream()
                .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
                .limit(topN)
                .map(entry -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    BigDecimal revenue = customerRevenueMap.getOrDefault(entry.getKey(), BigDecimal.ZERO);
                    map.put("customerName", entry.getKey());
                    map.put("profit", entry.getValue());
                    map.put("revenue", revenue);
                    map.put("profitRate", toPercent(rate(entry.getValue(), revenue)));
                    return map;
                })
                .toList();
    }
    private Map<String, Object> toInventoryItem(InventoryMetric metric) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("productModelId", metric.modelId());
        map.put("productName", metric.productName());
        map.put("model", metric.modelName());
        map.put("quantity", metric.quantity());
        map.put("lockedQuantity", metric.lockedQuantity());
        map.put("avgUnitCost", metric.avgUnitCost());
        map.put("inventoryValue", metric.inventoryValue());
        map.put("outboundQuantity", metric.outboundQuantity());
        map.put("stagnantDays", metric.stagnantDays());
        map.put("overstock", metric.overstock());
        map.put("riskLevel", metric.stagnantDays() >= 90 ? "high" : (metric.stagnantDays() >= 30 ? "medium" : "low"));
        return map;
    }
    private boolean matchInventoryKeyword(InventoryMetric metric, String keyword) {
        if (!StringUtils.hasText(keyword)) {
            return true;
        }
        return metric.productName().contains(keyword.trim()) || metric.modelName().contains(keyword.trim());
    }
    private Map<String, Object> toMonthlyCashFlowItem(MonthlyCashFlow flow) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("month", flow.month());
        map.put("income", flow.income());
        map.put("expense", flow.expense());
        map.put("netFlow", flow.netFlow());
        return map;
    }
    private Map<String, Object> toStatementRiskItem(StatementMetric metric, Map<String, String> nameMap, String type) {
        BigDecimal actualRate = rate(metric.actualAmount(), metric.planAmount());
        Map<String, Object> map = new LinkedHashMap<>();
        map.put(type + "Id", metric.entityId());
        map.put(type + "Name", safe(nameMap.get(metric.entityId())));
        map.put("statementMonth", metric.statementMonth());
        map.put("closingBalance", metric.closingBalance());
        map.put("planAmount", metric.planAmount());
        map.put("actualAmount", metric.actualAmount());
        map.put("actualRate", toPercent(actualRate));
        map.put("riskLevel", metric.closingBalance().compareTo(new BigDecimal("1000000")) > 0 || actualRate.compareTo(new BigDecimal("0.50")) < 0 ? "high" : "medium");
        return map;
    }
    private Map<String, Object> anomalyItem(String level, String type, String message, Map<String, Object> detail) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("riskLevel", level);
        map.put("type", type);
        map.put("message", message);
        map.put("detail", detail);
        return map;
    }
    private Map<String, Object> riskSuggestion(String type, String level, String suggestion) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("type", type);
        map.put("level", level);
        map.put("suggestion", suggestion);
        return map;
    }
    private Map<String, Object> buildCostCompositionPie(BigDecimal material, BigDecimal labor, BigDecimal depreciation, BigDecimal scrap) {
        List<Map<String, Object>> data = List.of(
                Map.of("name", "材料成本", "value", material),
                Map.of("name", "人工成本", "value", labor),
                Map.of("name", "折旧成本", "value", depreciation),
                Map.of("name", "损耗成本", "value", scrap)
        );
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "成本构成", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("series", List.of(Map.of("name", "成本构成", "type", "pie", "radius", "60%", "data", data)));
        return option;
    }
    private Map<String, Object> buildOrderProfitBar(List<OrderProfitMetric> metrics) {
        List<OrderProfitMetric> top = metrics.stream()
                .sorted(Comparator.comparing(OrderProfitMetric::profit))
                .limit(10)
                .toList();
        List<String> xData = top.stream().map(OrderProfitMetric::salesContractNo).toList();
        List<BigDecimal> yData = top.stream().map(OrderProfitMetric::profit).toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "订单利润分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", xData));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "利润", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildProcessCostBar(Map<String, BigDecimal> processCosts) {
        List<String> xData = new ArrayList<>(processCosts.keySet());
        List<BigDecimal> yData = new ArrayList<>(processCosts.values());
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "工序成本排名", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", xData));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "成本", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildProfitDistributionBar(List<OrderProfitMetric> metrics) {
        List<OrderProfitMetric> sorted = metrics.stream()
                .sorted(Comparator.comparing(OrderProfitMetric::profitRate))
                .limit(15)
                .toList();
        List<String> xData = sorted.stream().map(OrderProfitMetric::salesContractNo).toList();
        List<BigDecimal> yData = sorted.stream().map(metric -> metric.profitRate().multiply(ONE_HUNDRED).setScale(2, RoundingMode.HALF_UP)).toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "订单利润率分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", xData));
        option.put("yAxis", Map.of("type", "value", "name", "%"));
        option.put("series", List.of(Map.of("name", "利润率", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildLossOrderTrendLine(List<OrderProfitMetric> metrics) {
        Map<String, Long> lossByDate = new LinkedHashMap<>();
        List<OrderProfitMetric> sorted = metrics.stream()
                .filter(metric -> metric.entryDate() != null)
                .sorted(Comparator.comparing(OrderProfitMetric::entryDate))
                .toList();
        for (OrderProfitMetric metric : sorted) {
            String day = formatDate(metric.entryDate());
            long inc = metric.profit().compareTo(BigDecimal.ZERO) < 0 ? 1L : 0L;
            lossByDate.merge(day, inc, Long::sum);
        }
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "亏损订单趋势", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", new ArrayList<>(lossByDate.keySet())));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "亏损订单数", "type", "line", "smooth", true, "data", new ArrayList<>(lossByDate.values()))));
        return option;
    }
    private Map<String, Object> buildCustomerProfitBar(Map<String, BigDecimal> customerProfitMap) {
        List<Map.Entry<String, BigDecimal>> top = customerProfitMap.entrySet().stream()
                .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
                .limit(10)
                .toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "客户利润贡献TOP10", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", top.stream().map(Map.Entry::getKey).toList()));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "利润", "type", "bar", "data", top.stream().map(Map.Entry::getValue).toList())));
        return option;
    }
    private Map<String, Object> buildInventoryTopBar(List<InventoryMetric> metrics) {
        List<InventoryMetric> top = metrics.stream()
                .sorted(Comparator.comparing(InventoryMetric::inventoryValue).reversed())
                .limit(10)
                .toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "库存资金占用TOP10", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", top.stream().map(item -> item.productName() + "/" + item.modelName()).toList()));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "资金占用", "type", "bar", "data", top.stream().map(InventoryMetric::inventoryValue).toList())));
        return option;
    }
    private Map<String, Object> buildInventoryAgingPie(List<InventoryMetric> metrics) {
        long normal = metrics.stream().filter(item -> item.stagnantDays() < 30).count();
        long slow = metrics.stream().filter(item -> item.stagnantDays() >= 30 && item.stagnantDays() < 90).count();
        long stagnant = metrics.stream().filter(item -> item.stagnantDays() >= 90).count();
        List<Map<String, Object>> data = List.of(
                Map.of("name", "正常", "value", normal),
                Map.of("name", "缓慢", "value", slow),
                Map.of("name", "呆滞", "value", stagnant)
        );
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "库存库龄分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("series", List.of(Map.of("type", "pie", "radius", "60%", "data", data)));
        return option;
    }
    private Map<String, Object> buildTurnoverGauge(BigDecimal turnoverDays) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "库存周转天数", "left", "center"));
        option.put("series", List.of(Map.of(
                "type", "gauge",
                "min", 0,
                "max", 180,
                "detail", Map.of("formatter", "{value}天"),
                "data", List.of(Map.of("value", turnoverDays, "name", "周转天数"))
        )));
        return option;
    }
    private Map<String, Object> buildCashflowTrend(List<MonthlyCashFlow> actual, List<MonthlyCashFlow> forecast) {
        List<String> labels = new ArrayList<>();
        List<BigDecimal> netActual = new ArrayList<>();
        List<BigDecimal> netForecast = new ArrayList<>();
        for (MonthlyCashFlow point : actual) {
            labels.add(point.month());
            netActual.add(point.netFlow());
            netForecast.add(null);
        }
        for (MonthlyCashFlow point : forecast) {
            labels.add(point.month());
            netActual.add(null);
            netForecast.add(point.netFlow());
        }
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "现金流趋势(实际+预测)", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", labels));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(
                Map.of("name", "实际净现金流", "type", "line", "smooth", true, "data", netActual),
                Map.of("name", "预测净现金流", "type", "line", "smooth", true, "data", netForecast)
        ));
        return option;
    }
    private Map<String, Object> buildReceivablePayableBar(BigDecimal receivable, BigDecimal payable) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "应收应付余额对比", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", List.of("应收", "应付")));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "余额", "type", "bar", "data", List.of(receivable, payable))));
        return option;
    }
    private Map<String, Object> buildFundGapGauge(BigDecimal fundGap) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "资金缺口", "left", "center"));
        option.put("series", List.of(Map.of(
                "type", "gauge",
                "min", 0,
                "max", 10000000,
                "detail", Map.of("formatter", "{value}"),
                "data", List.of(Map.of("value", fundGap, "name", "资金缺口"))
        )));
        return option;
    }
    private Map<String, Object> buildAnomalyLevelPie(List<Map<String, Object>> anomalies) {
        long high = anomalies.stream().filter(item -> "high".equals(item.get("riskLevel"))).count();
        long medium = anomalies.stream().filter(item -> "medium".equals(item.get("riskLevel"))).count();
        long low = anomalies.stream().filter(item -> "low".equals(item.get("riskLevel"))).count();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "异常等级分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("series", List.of(Map.of("type", "pie", "radius", "60%", "data", List.of(
                Map.of("name", "高风险", "value", high),
                Map.of("name", "中风险", "value", medium),
                Map.of("name", "低风险", "value", low)
        ))));
        return option;
    }
    private Map<String, Object> buildAnomalyTypeBar(List<Map<String, Object>> anomalies) {
        Map<String, Long> countByType = new LinkedHashMap<>();
        for (Map<String, Object> anomaly : anomalies) {
            countByType.merge(String.valueOf(anomaly.get("type")), 1L, Long::sum);
        }
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "异常类型分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", new ArrayList<>(countByType.keySet())));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "异常数", "type", "bar", "data", new ArrayList<>(countByType.values()))));
        return option;
    }
    private Map<String, Object> buildInventoryProfitGauge(BigDecimal inventoryValue, BigDecimal profit) {
        BigDecimal ratio = inventoryValue.compareTo(BigDecimal.ZERO) <= 0
                ? BigDecimal.ZERO
                : profit.divide(inventoryValue, 4, RoundingMode.HALF_UP).multiply(ONE_HUNDRED);
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "利润/库存资金比", "left", "center"));
        option.put("series", List.of(Map.of(
                "type", "gauge",
                "min", -100,
                "max", 100,
                "detail", Map.of("formatter", "{value}%"),
                "data", List.of(Map.of("value", ratio.setScale(2, RoundingMode.HALF_UP), "name", "利润资金比"))
        )));
        return option;
    }
    private int normalizeLimit(Integer limit) {
        if (limit == null || limit <= 0) {
            return DEFAULT_LIMIT;
        }
        return Math.min(limit, MAX_LIMIT);
    }
    private DateRange resolveDateRange(String startDate, String endDate, String timeRange, String defaultLabel) {
        LocalDate today = LocalDate.now();
        LocalDate explicitStart = parseLocalDate(startDate);
        LocalDate explicitEnd = parseLocalDate(endDate);
        if (explicitStart != null || explicitEnd != null) {
            LocalDate start = explicitStart != null ? explicitStart : explicitEnd;
            LocalDate end = explicitEnd != null ? explicitEnd : explicitStart;
            if (start.isAfter(end)) {
                LocalDate temp = start;
                start = end;
                end = temp;
            }
            return new DateRange(start, end, start + "至" + end);
        }
        if (!StringUtils.hasText(timeRange)) {
            return new DateRange(null, null, "全部");
        }
        String text = timeRange.trim();
        if (text.contains("今天")) {
            return new DateRange(today, today, "今天");
        }
        if (text.contains("昨天") || text.contains("昨日")) {
            LocalDate day = today.minusDays(1);
            return new DateRange(day, day, "昨天");
        }
        if (text.contains("本周")) {
            LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            return new DateRange(start, today, "本周");
        }
        if (text.contains("上周")) {
            LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            LocalDate start = thisWeekStart.minusWeeks(1);
            LocalDate end = thisWeekStart.minusDays(1);
            return new DateRange(start, end, "上周");
        }
        if (text.contains("本月")) {
            return new DateRange(today.withDayOfMonth(1), today, "本月");
        }
        if (text.contains("上月")) {
            YearMonth lastMonth = YearMonth.from(today).minusMonths(1);
            return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth(), "上月");
        }
        if (text.contains("今年") || text.contains("本年")) {
            return new DateRange(today.withDayOfYear(1), today, "今年");
        }
        Matcher relativeMatcher = RELATIVE_PATTERN.matcher(text);
        if (relativeMatcher.find()) {
            int amount = Integer.parseInt(relativeMatcher.group(2));
            String unit = relativeMatcher.group(3);
            LocalDate start = switch (unit) {
                case "天" -> today.minusDays(Math.max(amount - 1L, 0));
                case "周" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
                case "个月", "月" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
                case "å¹´" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
                default -> today.minusDays(29);
            };
            return new DateRange(start, today, "近" + amount + unit);
        }
        Matcher dateMatcher = DATE_PATTERN.matcher(text);
        if (dateMatcher.find()) {
            LocalDate start = parseLocalDate(dateMatcher.group(1));
            LocalDate end = dateMatcher.find() ? parseLocalDate(dateMatcher.group(1)) : start;
            if (start != null && end != null) {
                if (start.isAfter(end)) {
                    LocalDate temp = start;
                    start = end;
                    end = temp;
                }
                return new DateRange(start, end, start + "至" + end);
            }
        }
        return new DateRange(null, null, "全部");
    }
    private LocalDate parseLocalDate(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        try {
            return LocalDate.parse(text.trim(), DATE_FMT);
        } catch (Exception ignored) {
            return null;
        }
    }
    private Date toDate(LocalDate localDate) {
        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private Date toExclusiveEndDate(LocalDate localDate) {
        return Date.from(localDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private LocalDate toLocalDate(Date date) {
        if (date == null) {
            return null;
        }
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
    }
    private String formatDate(LocalDate date) {
        return date == null ? "" : date.format(DATE_FMT);
    }
    private String displayDate(LocalDate date) {
        return date == null ? "" : date.format(DATE_FMT);
    }
    private long daysBetween(LocalDate start, LocalDate end) {
        if (start == null || end == null || start.isAfter(end)) {
            return 0;
        }
        return end.toEpochDay() - start.toEpochDay();
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private BigDecimal maxZero(BigDecimal value) {
        return value == null || value.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : value;
    }
    private BigDecimal rate(BigDecimal numerator, BigDecimal denominator) {
        if (denominator == null || denominator.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        return defaultDecimal(numerator).divide(denominator, 6, RoundingMode.HALF_UP);
    }
    private String toPercent(BigDecimal decimal) {
        if (decimal == null) {
            return "0.00%";
        }
        BigDecimal rate = decimal.multiply(ONE_HUNDRED).setScale(2, RoundingMode.HALF_UP);
        return rate.toPlainString() + "%";
    }
    private BigDecimal avgRate(List<OrderProfitMetric> metrics) {
        if (metrics == null || metrics.isEmpty()) {
            return BigDecimal.ZERO;
        }
        BigDecimal sum = metrics.stream().map(OrderProfitMetric::profitRate).reduce(BigDecimal.ZERO, BigDecimal::add);
        return sum.divide(new BigDecimal(metrics.size()), 6, RoundingMode.HALF_UP);
    }
    private BigDecimal estimateLaborCost(ProductionAccount account, Map<String, BigDecimal> salaryQuotaByOperation) {
        BigDecimal salaryQuota = salaryQuotaByOperation.getOrDefault(safe(account.getTechnologyOperationName()), BigDecimal.ZERO);
        BigDecimal finishedNum = defaultDecimal(account.getFinishedNum());
        BigDecimal workHours = defaultDecimal(account.getWorkHours());
        if (salaryQuota.compareTo(BigDecimal.ZERO) > 0 && finishedNum.compareTo(BigDecimal.ZERO) > 0) {
            return finishedNum.multiply(salaryQuota);
        }
        if (salaryQuota.compareTo(BigDecimal.ZERO) > 0 && workHours.compareTo(BigDecimal.ZERO) > 0) {
            return workHours.multiply(salaryQuota);
        }
        if (workHours.compareTo(BigDecimal.ZERO) > 0) {
            return workHours;
        }
        return finishedNum;
    }
    private List<Long> parseIdList(String raw) {
        if (!StringUtils.hasText(raw)) {
            return List.of();
        }
        String text = raw.replace("[", "").replace("]", "").replace(" ", "");
        if (!StringUtils.hasText(text)) {
            return List.of();
        }
        List<Long> result = new ArrayList<>();
        for (String part : text.split(",")) {
            if (!StringUtils.hasText(part)) {
                continue;
            }
            try {
                result.add(Long.parseLong(part.trim()));
            } catch (Exception ignored) {
            }
        }
        return result;
    }
    private int keywordHitCount(List<String> keywords, String question) {
        if (!StringUtils.hasText(question) || keywords == null) {
            return 0;
        }
        int count = 0;
        for (String keyword : keywords) {
            if (question.contains(keyword)) {
                count++;
            }
        }
        return count;
    }
    private String normalizeForMatch(String text) {
        if (!StringUtils.hasText(text)) {
            return "";
        }
        return text.replace(",", "")
                .replace(",", "")
                .replace("。", "")
                .replace(".", "")
                .replace("!", "")
                .replace("!", "")
                .replace("?", "")
                .replace("?", "")
                .replace(":", "")
                .replace(":", "")
                .replace(";", "")
                .replace(";", "")
                .replace(" ", "")
                .trim();
    }
    private String safe(Object value) {
        return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ').trim();
    }
    private LoginUser currentLoginUser(String memoryId) {
        LoginUser loginUser = aiSessionUserContext.get(memoryId);
        if (loginUser != null) {
            return loginUser;
        }
        return SecurityUtils.getLoginUser();
    }
    private Map<String, Object> rangeSummary(DateRange range, int count, String keyword) {
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("count", count);
        summary.put("keyword", safe(keyword));
        return summary;
    }
    private Long toLongOrNull(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            return Long.valueOf(value.trim());
        } catch (Exception ignored) {
            return null;
        }
    }
    private <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    private <T> void applyTenantFilter(LambdaQueryWrapper<T> wrapper, Long tenantId, SFunction<T, Long> field) {
        if (tenantId != null) {
            wrapper.eq(field, tenantId);
        }
    }
    private <T> void applyDeptFilter(LambdaQueryWrapper<T> wrapper, Long deptId, SFunction<T, Long> field) {
        if (deptId != null) {
            wrapper.eq(field, deptId);
        }
    }
    private List<KnowledgeDoc> financeKnowledgeBase() {
        return List.of(
                new KnowledgeDoc(
                        "利润下降分析框架",
                        List.of("利润下降", "亏损订单", "毛利率", "净利率"),
                        "先看收入端(订单结构、单价、交付延迟),再看成本端(材料、人工、折旧、损耗),最后看现金端(回款、账期、坏账风险)。",
                        List.of("sales_ledger", "sales_ledger_product", "production_account", "device_ledger", "account_statement"),
                        List.of("为什么本月利润下降?", "哪些订单亏损最严重?", "成本上升来自哪个工序?")
                ),
                new KnowledgeDoc(
                        "库存资金占用诊断",
                        List.of("库存积压", "呆滞库存", "周转率", "资金占用"),
                        "库存资金诊断重点看:库存价值、近30天出库成本、呆滞天数、超储比例,形成去库存与采购节奏联动策略。",
                        List.of("stock_inventory", "procurement_record_storage", "procurement_record_out"),
                        List.of("哪些物料资金占用最高?", "哪些库存超过90天未周转?", "库存周转天数是否异常?")
                ),
                new KnowledgeDoc(
                        "现金流与账款风险",
                        List.of("现金流", "应收", "应付", "回款", "资金缺口"),
                        "现金流判断要结合收款、付款、应收应付余额与预测净流量,重点关注高余额客户和高集中付款供应商。",
                        List.of("account_sales_collection", "account_purchase_payment", "account_statement"),
                        List.of("未来三个月是否有资金缺口?", "哪个客户回款风险最高?", "付款压力最大的是哪些供应商?")
                ),
                new KnowledgeDoc(
                        "业财一体化口径",
                        List.of("业财融合", "业财联动", "口径", "驾驶舱"),
                        "订单利润口径=销售收入-材料成本-人工成本-设备折旧-损耗成本;经营驾驶舱联动订单、生产、库存、设备、账款数据。",
                        List.of("sales_ledger", "production_operation_task", "production_product_main", "device_ledger", "stock_inventory", "account_statement"),
                        List.of("订单利润率如何计算?", "经营驾驶舱核心指标有哪些?")
                )
        );
    }
    private String jsonResponse(boolean success,
                                String type,
                                String description,
                                Map<String, Object> summary,
                                Map<String, Object> data,
                                Map<String, Object> charts) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("success", success);
        result.put("type", type);
        result.put("description", description);
        result.put("summary", summary == null ? Map.of() : summary);
        result.put("data", data == null ? Map.of() : data);
        result.put("charts", charts == null ? Map.of() : charts);
        return JSON.toJSONString(result);
    }
    private record DateRange(LocalDate start, LocalDate end, String label) {
        private boolean hasDateFilter() {
            return start != null && end != null;
        }
    }
    private record OrderProfitMetric(Long ledgerId,
                                     String salesContractNo,
                                     String customerName,
                                     String projectName,
                                     LocalDate entryDate,
                                     LocalDate deliveryDate,
                                     BigDecimal revenue,
                                     BigDecimal materialCost,
                                     BigDecimal laborCost,
                                     BigDecimal depreciationCost,
                                     BigDecimal scrapCost,
                                     BigDecimal totalCost,
                                     BigDecimal profit,
                                     BigDecimal profitRate,
                                     String riskLevel,
                                     List<String> reasons,
                                     String suggestion) {
    }
    private record AnalysisBundle(List<OrderProfitMetric> orderMetrics,
                                  Map<String, BigDecimal> processCostRanking,
                                  BigDecimal totalRevenue,
                                  BigDecimal totalMaterialCost,
                                  BigDecimal totalLaborCost,
                                  BigDecimal totalDepreciationCost,
                                  BigDecimal totalScrapCost,
                                  BigDecimal totalCost,
                                  BigDecimal totalProfit) {
        private static AnalysisBundle empty() {
            return new AnalysisBundle(List.of(), Map.of(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO,
                    BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
        }
    }
    private record MaterialCostResult(Map<Long, BigDecimal> materialCostByLedgerId,
                                      Map<Long, BigDecimal> avgUnitCostByModelId) {
    }
    private record ProductionCostContext(Map<Long, BigDecimal> laborCostByLedgerId,
                                         Map<Long, BigDecimal> scrapCostByLedgerId,
                                         Map<String, BigDecimal> processCostRanking) {
        private static ProductionCostContext empty() {
            return new ProductionCostContext(Map.of(), Map.of(), Map.of());
        }
    }
    private record InventoryMetric(Long modelId,
                                   String productName,
                                   String modelName,
                                   BigDecimal quantity,
                                   BigDecimal lockedQuantity,
                                   BigDecimal avgUnitCost,
                                   BigDecimal inventoryValue,
                                   BigDecimal outboundQuantity,
                                   long stagnantDays,
                                   boolean overstock) {
    }
    private static class InventoryMetricBuilder {
        private final Long modelId;
        private BigDecimal quantity = BigDecimal.ZERO;
        private BigDecimal lockedQuantity = BigDecimal.ZERO;
        private BigDecimal warnNum = BigDecimal.ZERO;
        private LocalDateTime firstInTime;
        private InventoryMetricBuilder(Long modelId) {
            this.modelId = modelId;
        }
        private void addQuantity(BigDecimal quantity) {
            this.quantity = this.quantity.add(quantity);
        }
        private void addLockedQuantity(BigDecimal lockedQuantity) {
            this.lockedQuantity = this.lockedQuantity.add(lockedQuantity);
        }
        private void addWarnNum(BigDecimal warnNum) {
            this.warnNum = this.warnNum.max(warnNum);
        }
        private void updateFirstInTime(LocalDateTime createTime) {
            if (this.firstInTime == null || createTime.isBefore(this.firstInTime)) {
                this.firstInTime = createTime;
            }
        }
        private Long modelId() {
            return modelId;
        }
        private BigDecimal quantity() {
            return quantity;
        }
        private BigDecimal lockedQuantity() {
            return lockedQuantity;
        }
        private BigDecimal warnNum() {
            return warnNum;
        }
        private LocalDateTime firstInTime() {
            return firstInTime;
        }
    }
    private record OutboundStats(Map<Long, BigDecimal> outboundQtyByModel,
                                 Map<Long, LocalDateTime> lastOutboundTimeByModel,
                                 BigDecimal totalOutboundCost) {
        private static OutboundStats empty() {
            return new OutboundStats(Map.of(), Map.of(), BigDecimal.ZERO);
        }
    }
    private record MonthlyCashFlow(String month, BigDecimal income, BigDecimal expense, BigDecimal netFlow) {
    }
    private record StatementMetric(String entityId,
                                   BigDecimal closingBalance,
                                   BigDecimal planAmount,
                                   BigDecimal actualAmount,
                                   String statementMonth) {
    }
    private record StatementSnapshot(BigDecimal receivableTotal,
                                     BigDecimal payableTotal,
                                     List<StatementMetric> receivableTop,
                                     List<StatementMetric> payableTop) {
        private static StatementSnapshot empty() {
            return new StatementSnapshot(BigDecimal.ZERO, BigDecimal.ZERO, List.of(), List.of());
        }
    }
    private record KnowledgeDoc(String topic,
                                List<String> keywords,
                                String knowledge,
                                List<String> relatedTables,
                                List<String> suggestedQuestions) {
    }
}
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1035 @@
package com.ruoyi.ai.tools;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.mapper.DeviceDefectRecordMapper;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.device.pojo.DeviceDefectRecord;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.pojo.DeviceRepair;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.procurementrecord.mapper.ProcurementExceptionRecordMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementExceptionRecord;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionPlanMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionPlan;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityUnqualifiedMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityUnqualified;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInventory;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Component
public class ManufacturingAgentTools {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final int DEFAULT_LIMIT = 10;
    private static final int MAX_LIMIT = 30;
    private static final int DEVICE_REPAIR_STATUS_PENDING = 0;
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final DeviceRepairMapper deviceRepairMapper;
    private final DeviceDefectRecordMapper deviceDefectRecordMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final QualityUnqualifiedMapper qualityUnqualifiedMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final ProcurementExceptionRecordMapper procurementExceptionRecordMapper;
    private final AiSessionUserContext aiSessionUserContext;
    public ManufacturingAgentTools(ProductionPlanMapper productionPlanMapper,
                                   ProductionOrderMapper productionOrderMapper,
                                   ProductionOperationTaskMapper productionOperationTaskMapper,
                                   ProductionProductMainMapper productionProductMainMapper,
                                   DeviceLedgerMapper deviceLedgerMapper,
                                   DeviceRepairMapper deviceRepairMapper,
                                   DeviceDefectRecordMapper deviceDefectRecordMapper,
                                   QualityInspectMapper qualityInspectMapper,
                                   QualityUnqualifiedMapper qualityUnqualifiedMapper,
                                   StockInventoryMapper stockInventoryMapper,
                                   ProcurementExceptionRecordMapper procurementExceptionRecordMapper,
                                   AiSessionUserContext aiSessionUserContext) {
        this.productionPlanMapper = productionPlanMapper;
        this.productionOrderMapper = productionOrderMapper;
        this.productionOperationTaskMapper = productionOperationTaskMapper;
        this.productionProductMainMapper = productionProductMainMapper;
        this.deviceLedgerMapper = deviceLedgerMapper;
        this.deviceRepairMapper = deviceRepairMapper;
        this.deviceDefectRecordMapper = deviceDefectRecordMapper;
        this.qualityInspectMapper = qualityInspectMapper;
        this.qualityUnqualifiedMapper = qualityUnqualifiedMapper;
        this.stockInventoryMapper = stockInventoryMapper;
        this.procurementExceptionRecordMapper = procurementExceptionRecordMapper;
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Tool(name = "查询制造业务域数据", value = "按业务域查询生产现场、计划、工单、设备、质量、物料、异常处理相关数据。")
    public String queryDomain(@ToolMemoryId String memoryId,
                              @P(value = "业务域,site/plan/workorder/device/quality/material/exception") String domain,
                              @P(value = "关键字,可不传", required = false) String keyword,
                              @P(value = "返回条数,默认10,最大30", required = false) Integer limit,
                              @P(value = "开始日期 yyyy-MM-dd,可不传", required = false) String startDate,
                              @P(value = "结束日期 yyyy-MM-dd,可不传", required = false) String endDate,
                              @P(value = "时间范围描述,例如今年、本月、近30天", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        int finalLimit = normalizeLimit(limit);
        DateRange range = resolveDateRange(startDate, endDate, timeRange);
        boolean hasTimeConstraint = hasTimeConstraint(startDate, endDate, timeRange);
        String normalizedDomain = normalizeDomain(domain);
        return switch (normalizedDomain) {
            case "site" -> siteSnapshot(loginUser, range);
            case "plan" -> listProductionPlans(loginUser, keyword, finalLimit, range);
            case "workorder" -> listWorkOrders(loginUser, keyword, finalLimit, range);
            case "device" -> isRepairIntent(keyword, timeRange)
                    ? listDeviceRepairs(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit, range, hasTimeConstraint)
                    : listDevices(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit);
            case "repair" -> listDeviceRepairs(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit, range, hasTimeConstraint);
            case "quality" -> listQualityIssues(loginUser, keyword, finalLimit, range);
            case "material" -> listMaterialInventory(loginUser, keyword, finalLimit);
            case "exception" -> listExceptions(loginUser, keyword, finalLimit, range);
            default -> jsonResponse(false, "manufacturing_query", "不支持的业务域: " + safe(domain), Map.of(), Map.of(), Map.of());
        };
    }
    @Tool(name = "制造预警看板", value = "计算计划、工单、设备、质量、物料、异常处理的预警信息。")
    public String getWarningBoard(@ToolMemoryId String memoryId,
                                  @P(value = "开始日期 yyyy-MM-dd,可不传", required = false) String startDate,
                                  @P(value = "结束日期 yyyy-MM-dd,可不传", required = false) String endDate,
                                  @P(value = "时间范围描述,例如今天、本周、本月、近30天", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange);
        LocalDate today = LocalDate.now();
        long overduePlanCount = countOverduePlans(loginUser, today);
        long overdueWorkOrderCount = countOverdueWorkOrders(loginUser, today);
        long pendingRepairCount = countPendingRepairs(loginUser);
        long qualityOpenCount = countOpenQualityIssues(loginUser, range);
        long lowStockCount = countLowStock(loginUser);
        long exceptionCount = countExceptionRecords(loginUser, range);
        List<Map<String, Object>> warningItems = new ArrayList<>();
        if (overduePlanCount > 0) {
            warningItems.add(warningItem("high", "计划逾期", overduePlanCount, "有生产计划超过需求日期仍未完成"));
        }
        if (overdueWorkOrderCount > 0) {
            warningItems.add(warningItem("high", "工单逾期", overdueWorkOrderCount, "有工单计划结束日期已过仍未完工"));
        }
        if (pendingRepairCount > 0) {
            warningItems.add(warningItem("medium", "设备待维修", pendingRepairCount, "存在待维修/维修中的设备"));
        }
        if (qualityOpenCount > 0) {
            warningItems.add(warningItem("high", "质量未闭环", qualityOpenCount, "存在未处理完成的不合格记录"));
        }
        if (lowStockCount > 0) {
            warningItems.add(warningItem("medium", "物料低库存", lowStockCount, "库存数量低于或等于预警阈值"));
        }
        if (exceptionCount > 0) {
            warningItems.add(warningItem("medium", "异常记录", exceptionCount, "时间范围内存在异常处理记录"));
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("warningCount", warningItems.size());
        summary.put("overduePlanCount", overduePlanCount);
        summary.put("overdueWorkOrderCount", overdueWorkOrderCount);
        summary.put("pendingRepairCount", pendingRepairCount);
        summary.put("qualityOpenCount", qualityOpenCount);
        summary.put("lowStockCount", lowStockCount);
        summary.put("exceptionCount", exceptionCount);
        return jsonResponse(true, "manufacturing_warning", "已返回制造预警看板。", summary,
                Map.of("items", warningItems), Map.of());
    }
    @Tool(name = "制造经营分析", value = "按时间范围输出制造关键指标,支持查、问、分析场景。")
    public String analyzeFactory(@ToolMemoryId String memoryId,
                                 @P(value = "开始日期 yyyy-MM-dd,可不传", required = false) String startDate,
                                 @P(value = "结束日期 yyyy-MM-dd,可不传", required = false) String endDate,
                                 @P(value = "时间范围描述,例如本月、近30天", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange);
        long planTotal = countPlans(loginUser, range);
        long planCompleted = countPlansByStatus(loginUser, range, 2);
        long workOrderTotal = countWorkOrders(loginUser, range);
        long workOrderCompleted = countWorkOrdersByStatus(loginUser, range, 2);
        long workOrderInProgress = countWorkOrdersByStatus(loginUser, range, 1);
        long outputCount = countOutputs(loginUser, range);
        long deviceTotal = countDevices(loginUser);
        long pendingRepairCount = countPendingRepairs(loginUser);
        long qualityInspectTotal = countQualityInspect(loginUser, range);
        long qualityNgCount = countOpenQualityIssues(loginUser, range);
        long materialSkuCount = countInventorySku(loginUser);
        long lowStockCount = countLowStock(loginUser);
        long exceptionCount = countExceptionRecords(loginUser, range);
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("planTotal", planTotal);
        summary.put("planCompleted", planCompleted);
        summary.put("planCompletionRate", toRate(planCompleted, planTotal));
        summary.put("workOrderTotal", workOrderTotal);
        summary.put("workOrderCompleted", workOrderCompleted);
        summary.put("workOrderInProgress", workOrderInProgress);
        summary.put("workOrderCompletionRate", toRate(workOrderCompleted, workOrderTotal));
        summary.put("outputCount", outputCount);
        summary.put("deviceTotal", deviceTotal);
        summary.put("pendingRepairCount", pendingRepairCount);
        summary.put("qualityInspectTotal", qualityInspectTotal);
        summary.put("qualityNgCount", qualityNgCount);
        summary.put("qualityIssueRate", toRate(qualityNgCount, qualityInspectTotal));
        summary.put("materialSkuCount", materialSkuCount);
        summary.put("lowStockCount", lowStockCount);
        summary.put("exceptionCount", exceptionCount);
        List<Map<String, Object>> coreMetrics = List.of(
                metric("计划完成率", toRate(planCompleted, planTotal)),
                metric("工单完成率", toRate(workOrderCompleted, workOrderTotal)),
                metric("质量异常率", toRate(qualityNgCount, qualityInspectTotal)),
                metric("低库存占比", toRate(lowStockCount, materialSkuCount))
        );
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("domainBarOption", buildDomainBarOption(summary));
        charts.put("qualityPieOption", buildQualityPieOption(qualityInspectTotal, qualityNgCount));
        return jsonResponse(true, "manufacturing_analysis", "已返回制造分析结果。", summary,
                Map.of("coreMetrics", coreMetrics), charts);
    }
    @Tool(name = "生成制造办理建议", value = "根据用户问题输出可执行的办理动作建议,包括目标业务接口、必填字段和示例。")
    public String planActions(@ToolMemoryId String memoryId,
                              @P("用户诉求原文") String userQuery) {
        LoginUser loginUser = currentLoginUser(memoryId);
        List<Map<String, Object>> actionCards = new ArrayList<>();
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "工单", "派工", "作业")) {
            actionCards.add(actionCard(
                    "workorder_assign",
                    "工单派工",
                    "POST",
                    "/productionOperationTask/assign",
                    List.of("id", "userIds"),
                    Map.of("id", 10001, "userIds", "12,13"),
                    "将工单分配给指定人员,适用于现场调度。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "设备", "ç»´ä¿®", "故障")) {
            actionCards.add(actionCard(
                    "device_repair_create",
                    "创建设备维修单",
                    "POST",
                    "/device/repair",
                    List.of("deviceLedgerId", "deviceName", "repairName", "remark"),
                    Map.of("deviceLedgerId", 1001, "deviceName", "空压机A-01", "repairName", "张三", "remark", "异响并伴随温升"),
                    "新建维修单,进入设备异常处理闭环。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "质量", "不合格", "闭环")) {
            actionCards.add(actionCard(
                    "quality_unqualified_deal",
                    "处理不合格单",
                    "POST",
                    "/quality/qualityUnqualified/deal",
                    List.of("id", "dealResult", "dealName"),
                    Map.of("id", 3001, "dealResult", "返工后复检", "dealName", "李四"),
                    "对不合格记录执行处置并闭环。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "物料", "库存", "补料")) {
            actionCards.add(actionCard(
                    "material_inbound",
                    "补充库存",
                    "POST",
                    "/stockInventory/addstockInventory",
                    List.of("productModelId", "batchNo", "qualitity"),
                    Map.of("productModelId", 5001, "batchNo", "B2026051601", "qualitity", 120),
                    "当低库存预警触发时,增加库存数量。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "异常", "采购异常", "来料异常")) {
            actionCards.add(actionCard(
                    "procurement_exception_add",
                    "登记异常记录",
                    "POST",
                    "/procurementExceptionRecord/add",
                    List.of("purchaseLedgerId", "exceptionReason", "exceptionNum"),
                    Map.of("purchaseLedgerId", 888, "exceptionReason", "到料短缺", "exceptionNum", 24),
                    "登记采购/来料异常,便于后续追踪和分析。"));
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("actionCount", actionCards.size());
        summary.put("userId", loginUser.getUserId());
        summary.put("tenantId", loginUser.getTenantId());
        return jsonResponse(true, "manufacturing_action_plan", "已生成办理建议,请前端引导用户确认后调用目标业务接口。",
                summary, Map.of("actionCards", actionCards), Map.of());
    }
    private String siteSnapshot(LoginUser loginUser, DateRange range) {
        long planTotal = countPlans(loginUser, range);
        long workOrderTotal = countWorkOrders(loginUser, range);
        long outputCount = countOutputs(loginUser, range);
        long deviceTotal = countDevices(loginUser);
        long pendingRepairCount = countPendingRepairs(loginUser);
        long qualityOpenCount = countOpenQualityIssues(loginUser, range);
        long lowStockCount = countLowStock(loginUser);
        long exceptionCount = countExceptionRecords(loginUser, range);
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("planTotal", planTotal);
        summary.put("workOrderTotal", workOrderTotal);
        summary.put("outputCount", outputCount);
        summary.put("deviceTotal", deviceTotal);
        summary.put("pendingRepairCount", pendingRepairCount);
        summary.put("qualityOpenCount", qualityOpenCount);
        summary.put("lowStockCount", lowStockCount);
        summary.put("exceptionCount", exceptionCount);
        return jsonResponse(true, "manufacturing_site_snapshot", "已返回生产现场概览。", summary, Map.of(), Map.of());
    }
    private String listProductionPlans(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.ge(ProductionPlan::getRequiredDate, range.start()).le(ProductionPlan::getRequiredDate, range.end());
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(ProductionPlan::getMpsNo, keyword)
                    .or().like(ProductionPlan::getRemark, keyword)
                    .or().like(ProductionPlan::getSource, keyword));
        }
        wrapper.orderByDesc(ProductionPlan::getRequiredDate, ProductionPlan::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(productionPlanMapper.selectList(wrapper)).stream()
                .map(this::toPlanItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_plan_list", "已返回生产计划列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listWorkOrders(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
                .le(ProductionOperationTask::getPlanEndTime, range.end());
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(ProductionOperationTask::getWorkOrderNo, keyword)
                    .or().like(ProductionOperationTask::getUserIds, keyword));
        }
        wrapper.orderByDesc(ProductionOperationTask::getPlanEndTime, ProductionOperationTask::getId)
                .last("limit " + limit);
        List<Map<String, Object>> items = defaultList(productionOperationTaskMapper.selectList(wrapper)).stream()
                .map(this::toWorkOrderItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_workorder_list", "已返回工单列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listDevices(LoginUser loginUser, String keyword, int limit) {
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(DeviceLedger::getDeviceName, keyword)
                    .or().like(DeviceLedger::getDeviceModel, keyword)
                    .or().like(DeviceLedger::getDeviceBrand, keyword));
        }
        wrapper.orderByDesc(DeviceLedger::getId).last("limit " + limit);
        Map<Long, Long> pendingRepairMap = pendingRepairCountByDevice(loginUser);
        List<Map<String, Object>> items = defaultList(deviceLedgerMapper.selectList(wrapper)).stream()
                .map(item -> toDeviceItem(item, pendingRepairMap.getOrDefault(item.getId(), 0L)))
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_device_list", "已返回设备列表。",
                Map.of("count", items.size(), "keyword", safe(keyword)), Map.of("items", items), Map.of());
    }
    private String listDeviceRepairs(LoginUser loginUser, String keyword, int limit, DateRange range, boolean hasTimeConstraint) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        Long currentDeptId = loginUser.getCurrentDeptId();
        if (currentDeptId != null) {
            wrapper.and(w -> w.eq(DeviceRepair::getDeptId, currentDeptId).or().isNull(DeviceRepair::getDeptId));
        }
        if (hasTimeConstraint) {
            wrapper.ge(DeviceRepair::getCreateTime, range.start().atStartOfDay())
                    .lt(DeviceRepair::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        if (StringUtils.hasText(keyword)) {
            List<Long> matchedDeviceIds = findDeviceLedgerIdsByKeyword(loginUser, keyword);
            wrapper.and(w -> {
                w.like(DeviceRepair::getDeviceName, keyword)
                        .or().like(DeviceRepair::getDeviceModel, keyword)
                        .or().like(DeviceRepair::getRemark, keyword)
                        .or().like(DeviceRepair::getRepairName, keyword)
                        .or().like(DeviceRepair::getMaintenanceName, keyword);
                if (!matchedDeviceIds.isEmpty()) {
                    w.or().in(DeviceRepair::getDeviceLedgerId, matchedDeviceIds);
                }
            });
        }
        wrapper.orderByDesc(DeviceRepair::getCreateTime, DeviceRepair::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(deviceRepairMapper.selectList(wrapper)).stream()
                .map(this::toDeviceRepairItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_device_repair_list", "已返回设备维修记录。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listQualityIssues(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<QualityUnqualified> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityUnqualified::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityUnqualified::getDeptId);
        wrapper.ge(QualityUnqualified::getCheckTime, toDate(range.start()))
                .lt(QualityUnqualified::getCheckTime, toExclusiveEndDate(range.end()));
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(QualityUnqualified::getProductName, keyword)
                    .or().like(QualityUnqualified::getDefectivePhenomena, keyword)
                    .or().like(QualityUnqualified::getDealResult, keyword));
        }
        wrapper.orderByDesc(QualityUnqualified::getCheckTime, QualityUnqualified::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(qualityUnqualifiedMapper.selectList(wrapper)).stream()
                .map(this::toQualityItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_quality_list", "已返回质量异常列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listMaterialInventory(LoginUser loginUser, String keyword, int limit) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(StockInventory::getBatchNo, keyword)
                    .or().like(StockInventory::getProductModelId, keyword));
        }
        wrapper.orderByDesc(StockInventory::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(stockInventoryMapper.selectList(wrapper)).stream()
                .map(this::toMaterialItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_material_list", "已返回物料库存列表。",
                Map.of("count", items.size(), "keyword", safe(keyword)), Map.of("items", items), Map.of());
    }
    private String listExceptions(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<ProcurementExceptionRecord> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementExceptionRecord::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementExceptionRecord::getDeptId);
        wrapper.ge(ProcurementExceptionRecord::getCreateTime, range.start().atStartOfDay())
                .lt(ProcurementExceptionRecord::getCreateTime, range.end().plusDays(1).atStartOfDay());
        if (StringUtils.hasText(keyword)) {
            wrapper.like(ProcurementExceptionRecord::getExceptionReason, keyword);
        }
        wrapper.orderByDesc(ProcurementExceptionRecord::getCreateTime, ProcurementExceptionRecord::getId)
                .last("limit " + limit);
        List<Map<String, Object>> items = defaultList(procurementExceptionRecordMapper.selectList(wrapper)).stream()
                .map(this::toExceptionItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_exception_list", "已返回异常处理列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private long countPlans(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.ge(ProductionPlan::getRequiredDate, range.start()).le(ProductionPlan::getRequiredDate, range.end());
        return productionPlanMapper.selectCount(wrapper);
    }
    private long countPlansByStatus(LoginUser loginUser, DateRange range, int status) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.ge(ProductionPlan::getRequiredDate, range.start())
                .le(ProductionPlan::getRequiredDate, range.end())
                .eq(ProductionPlan::getStatus, status);
        return productionPlanMapper.selectCount(wrapper);
    }
    private long countWorkOrders(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
                .le(ProductionOperationTask::getPlanEndTime, range.end());
        return productionOperationTaskMapper.selectCount(wrapper);
    }
    private long countWorkOrdersByStatus(LoginUser loginUser, DateRange range, int status) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
                .le(ProductionOperationTask::getPlanEndTime, range.end())
                .eq(ProductionOperationTask::getStatus, status);
        return productionOperationTaskMapper.selectCount(wrapper);
    }
    private long countOutputs(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProductionProductMain> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionProductMain::getDeptId);
        wrapper.ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay())
                .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay());
        return productionProductMainMapper.selectCount(wrapper);
    }
    private long countDevices(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
        return deviceLedgerMapper.selectCount(wrapper);
    }
    private long countPendingRepairs(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
        wrapper.eq(DeviceRepair::getStatus, DEVICE_REPAIR_STATUS_PENDING);
        return deviceRepairMapper.selectCount(wrapper);
    }
    private long countQualityInspect(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<QualityInspect> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityInspect::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityInspect::getDeptId);
        wrapper.ge(QualityInspect::getCheckTime, toDate(range.start()))
                .lt(QualityInspect::getCheckTime, toExclusiveEndDate(range.end()));
        return qualityInspectMapper.selectCount(wrapper);
    }
    private long countOpenQualityIssues(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<QualityUnqualified> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityUnqualified::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityUnqualified::getDeptId);
        wrapper.ge(QualityUnqualified::getCheckTime, toDate(range.start()))
                .lt(QualityUnqualified::getCheckTime, toExclusiveEndDate(range.end()))
                .ne(QualityUnqualified::getInspectState, 2);
        return qualityUnqualifiedMapper.selectCount(wrapper);
    }
    private long countInventorySku(LoginUser loginUser) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        return stockInventoryMapper.selectCount(wrapper);
    }
    private long countLowStock(LoginUser loginUser) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        wrapper.isNotNull(StockInventory::getWarnNum);
        List<StockInventory> stocks = defaultList(stockInventoryMapper.selectList(wrapper));
        return stocks.stream()
                .filter(this::isLowStock)
                .count();
    }
    private long countExceptionRecords(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProcurementExceptionRecord> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementExceptionRecord::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementExceptionRecord::getDeptId);
        wrapper.ge(ProcurementExceptionRecord::getCreateTime, range.start().atStartOfDay())
                .lt(ProcurementExceptionRecord::getCreateTime, range.end().plusDays(1).atStartOfDay());
        return procurementExceptionRecordMapper.selectCount(wrapper);
    }
    private long countOverduePlans(LoginUser loginUser, LocalDate today) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.lt(ProductionPlan::getRequiredDate, today).ne(ProductionPlan::getStatus, 2);
        return productionPlanMapper.selectCount(wrapper);
    }
    private long countOverdueWorkOrders(LoginUser loginUser, LocalDate today) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.lt(ProductionOperationTask::getPlanEndTime, today).ne(ProductionOperationTask::getStatus, 2);
        return productionOperationTaskMapper.selectCount(wrapper);
    }
    private Map<Long, Long> pendingRepairCountByDevice(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
        wrapper.eq(DeviceRepair::getStatus, DEVICE_REPAIR_STATUS_PENDING);
        return defaultList(deviceRepairMapper.selectList(wrapper)).stream()
                .filter(item -> item.getDeviceLedgerId() != null)
                .collect(Collectors.groupingBy(DeviceRepair::getDeviceLedgerId, Collectors.counting()));
    }
    private Map<String, Object> toPlanItem(ProductionPlan item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("mpsNo", safe(item.getMpsNo()));
        map.put("requiredDate", formatDate(item.getRequiredDate()));
        map.put("promisedDeliveryDate", formatDate(item.getPromisedDeliveryDate()));
        map.put("qtyRequired", item.getQtyRequired());
        map.put("quantityIssued", item.getQuantityIssued());
        map.put("status", item.getStatus());
        map.put("source", safe(item.getSource()));
        map.put("remark", safe(item.getRemark()));
        return map;
    }
    private Map<String, Object> toWorkOrderItem(ProductionOperationTask item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("workOrderNo", safe(item.getWorkOrderNo()));
        map.put("productionOrderId", item.getProductionOrderId());
        map.put("planStartTime", formatDate(item.getPlanStartTime()));
        map.put("planEndTime", formatDate(item.getPlanEndTime()));
        map.put("actualStartTime", formatDate(item.getActualStartTime()));
        map.put("actualEndTime", formatDate(item.getActualEndTime()));
        map.put("planQuantity", item.getPlanQuantity());
        map.put("completeQuantity", item.getCompleteQuantity());
        map.put("status", item.getStatus());
        map.put("userIds", safe(item.getUserIds()));
        return map;
    }
    private Map<String, Object> toDeviceItem(DeviceLedger item, long pendingRepairCount) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("deviceName", safe(item.getDeviceName()));
        map.put("deviceModel", safe(item.getDeviceModel()));
        map.put("deviceBrand", safe(item.getDeviceBrand()));
        map.put("status", safe(item.getStatus()));
        map.put("storageLocation", safe(item.getStorageLocation()));
        map.put("supplierName", safe(item.getSupplierName()));
        map.put("pendingRepairCount", pendingRepairCount);
        return map;
    }
    private Map<String, Object> toDeviceRepairItem(DeviceRepair item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("deviceLedgerId", item.getDeviceLedgerId());
        map.put("deviceName", safe(item.getDeviceName()));
        map.put("deviceModel", safe(item.getDeviceModel()));
        map.put("repairTime", formatDate(item.getRepairTime()));
        map.put("repairName", safe(item.getRepairName()));
        map.put("maintenanceName", safe(item.getMaintenanceName()));
        map.put("maintenanceTime", formatDateTime(item.getMaintenanceTime()));
        map.put("maintenanceResult", safe(item.getMaintenanceResult()));
        map.put("acceptanceName", safe(item.getAcceptanceName()));
        map.put("acceptanceTime", formatDateTime(item.getAcceptanceTime()));
        map.put("status", item.getStatus());
        map.put("remark", safe(item.getRemark()));
        map.put("createTime", formatDateTime(item.getCreateTime()));
        return map;
    }
    private Map<String, Object> toQualityItem(QualityUnqualified item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("checkTime", formatDate(item.getCheckTime()));
        map.put("inspectState", item.getInspectState());
        map.put("productId", item.getProductId());
        map.put("productName", safe(item.getProductName()));
        map.put("model", safe(item.getModel()));
        map.put("quantity", item.getQuantity());
        map.put("defectivePhenomena", safe(item.getDefectivePhenomena()));
        map.put("dealResult", safe(item.getDealResult()));
        map.put("dealName", safe(item.getDealName()));
        map.put("dealTime", formatDate(item.getDealTime()));
        return map;
    }
    private Map<String, Object> toMaterialItem(StockInventory item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("productModelId", item.getProductModelId());
        map.put("batchNo", safe(item.getBatchNo()));
        map.put("qualitity", item.getQualitity());
        map.put("lockedQuantity", item.getLockedQuantity());
        map.put("warnNum", item.getWarnNum());
        map.put("lowStock", isLowStock(item));
        map.put("remark", safe(item.getRemark()));
        return map;
    }
    private Map<String, Object> toExceptionItem(ProcurementExceptionRecord item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("purchaseLedgerId", item.getPurchaseLedgerId());
        map.put("exceptionReason", safe(item.getExceptionReason()));
        map.put("exceptionNum", item.getExceptionNum());
        map.put("createTime", formatDateTime(item.getCreateTime()));
        return map;
    }
    private boolean isLowStock(StockInventory item) {
        BigDecimal quantity = item.getQualitity();
        BigDecimal warnNum = item.getWarnNum();
        if (quantity == null || warnNum == null) {
            return false;
        }
        return quantity.compareTo(warnNum) <= 0;
    }
    private Map<String, Object> warningItem(String level, String title, long count, String detail) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("level", level);
        map.put("title", title);
        map.put("count", count);
        map.put("detail", detail);
        return map;
    }
    private Map<String, Object> actionCard(String code,
                                           String name,
                                           String method,
                                           String targetApi,
                                           List<String> requiredFields,
                                           Map<String, Object> examplePayload,
                                           String description) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("code", code);
        map.put("name", name);
        map.put("method", method);
        map.put("targetApi", targetApi);
        map.put("requiredFields", requiredFields);
        map.put("examplePayload", examplePayload);
        map.put("description", description);
        return map;
    }
    private Map<String, Object> metric(String label, String value) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("label", label);
        map.put("value", value);
        return map;
    }
    private Map<String, Object> rangeSummary(DateRange range, int count, String keyword) {
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("count", count);
        summary.put("keyword", safe(keyword));
        return summary;
    }
    private Map<String, Object> buildDomainBarOption(Map<String, Object> summary) {
        List<String> xData = List.of("计划", "工单", "设备", "质量", "物料", "异常");
        List<Number> yData = List.of(
                numberValue(summary.get("planTotal")),
                numberValue(summary.get("workOrderTotal")),
                numberValue(summary.get("deviceTotal")),
                numberValue(summary.get("qualityNgCount")),
                numberValue(summary.get("lowStockCount")),
                numberValue(summary.get("exceptionCount"))
        );
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "制造域关键数量", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", xData));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "数量", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildQualityPieOption(long inspectTotal, long ngCount) {
        long passCount = Math.max(inspectTotal - ngCount, 0);
        List<Map<String, Object>> data = List.of(
                Map.of("name", "不合格", "value", ngCount),
                Map.of("name", "非不合格", "value", passCount)
        );
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "质量结果分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("series", List.of(Map.of("name", "质量", "type", "pie", "radius", "60%", "data", data)));
        return option;
    }
    private int numberValue(Object value) {
        if (value instanceof Number number) {
            return number.intValue();
        }
        return 0;
    }
    private String toRate(long numerator, long denominator) {
        if (denominator <= 0) {
            return "0.00%";
        }
        BigDecimal rate = new BigDecimal(numerator)
                .multiply(new BigDecimal("100"))
                .divide(new BigDecimal(denominator), 2, RoundingMode.HALF_UP);
        return rate.toPlainString() + "%";
    }
    private String normalizeDomain(String domain) {
        if (!StringUtils.hasText(domain)) {
            return "";
        }
        String value = domain.trim().toLowerCase();
        return switch (value) {
            case "生产现场", "site", "factory", "workshop" -> "site";
            case "计划", "plan", "schedule" -> "plan";
            case "工单", "workorder", "work_order", "task" -> "workorder";
            case "设备", "device", "equipment" -> "device";
            case "ç»´ä¿®", "repair", "maintenance" -> "repair";
            case "质量", "quality", "qc" -> "quality";
            case "物料", "material", "inventory", "stock" -> "material";
            case "异常", "exception", "abnormal" -> "exception";
            default -> value;
        };
    }
    private boolean isRepairIntent(String keyword, String userQuery) {
        String query = safe(userQuery);
        return containsAny(safe(keyword), "ç»´ä¿®", "报修", "检修", "维护")
                || containsAny(query, "ç»´ä¿®", "报修", "检修", "维护");
    }
    private String normalizeDeviceQueryKeyword(String keyword, String userQuery) {
        String source = StringUtils.hasText(keyword) ? keyword : userQuery;
        if (!StringUtils.hasText(source)) {
            return null;
        }
        String cleaned = source
                .replace("查询", "")
                .replace("查看", "")
                .replace("帮我", "")
                .replace("请", "")
                .replace("查", "")
                .replace("设备", "")
                .replace("维修记录", "")
                .replace("维修情况", "")
                .replace("报修记录", "")
                .replace("报修情况", "")
                .replace("ç»´ä¿®", "")
                .replace("报修", "")
                .replace("情况", "")
                .replace("记录", "")
                .replace("信息", "")
                .replace("的", "")
                .replace("一下", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
    private List<Long> findDeviceLedgerIdsByKeyword(LoginUser loginUser, String keyword) {
        if (!StringUtils.hasText(keyword)) {
            return List.of();
        }
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        Long currentDeptId = loginUser.getCurrentDeptId();
        if (currentDeptId != null) {
            wrapper.and(w -> w.eq(DeviceLedger::getDeptId, currentDeptId).or().isNull(DeviceLedger::getDeptId));
        }
        wrapper.and(w -> w.like(DeviceLedger::getDeviceName, keyword)
                .or().like(DeviceLedger::getDeviceModel, keyword)
                .or().like(DeviceLedger::getDeviceBrand, keyword));
        wrapper.orderByDesc(DeviceLedger::getId).last("limit 200");
        return defaultList(deviceLedgerMapper.selectList(wrapper)).stream()
                .map(DeviceLedger::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }
    private boolean hasTimeConstraint(String startDate, String endDate, String userQuery) {
        if (StringUtils.hasText(startDate) || StringUtils.hasText(endDate)) {
            return true;
        }
        if (!StringUtils.hasText(userQuery)) {
            return false;
        }
        String text = userQuery.trim();
        return containsAny(text, "今天", "昨天", "本周", "上周", "本月", "上月", "今年", "去年", "近", "最近");
    }
    private DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
        LocalDate today = LocalDate.now();
        LocalDate start = parseLocalDate(startDate);
        LocalDate end = parseLocalDate(endDate);
        if (start != null || end != null) {
            LocalDate s = start != null ? start : end;
            LocalDate e = end != null ? end : start;
            if (s.isAfter(e)) {
                LocalDate temp = s;
                s = e;
                e = temp;
            }
            return new DateRange(s, e, s + "至" + e);
        }
        if (!StringUtils.hasText(timeRange)) {
            return new DateRange(today.minusDays(29), today, "近30天");
        }
        String text = timeRange.trim();
        if (text.contains("今天")) {
            return new DateRange(today, today, "今天");
        }
        if (text.contains("本周")) {
            LocalDate startOfWeek = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            return new DateRange(startOfWeek, today, "本周");
        }
        if (text.contains("本月")) {
            return new DateRange(today.withDayOfMonth(1), today, "本月");
        }
        if (text.contains("本年") || text.contains("今年")) {
            return new DateRange(today.withDayOfYear(1), today, "今年");
        }
        if (text.contains("去年")) {
            LocalDate firstDay = today.minusYears(1).withDayOfYear(1);
            LocalDate lastDay = today.minusYears(1).withMonth(12).withDayOfMonth(31);
            return new DateRange(firstDay, lastDay, "去年");
        }
        if (text.contains("上月")) {
            LocalDate startOfLastMonth = today.minusMonths(1).withDayOfMonth(1);
            return new DateRange(startOfLastMonth, startOfLastMonth.withDayOfMonth(startOfLastMonth.lengthOfMonth()), "上月");
        }
        java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(近|最近)(\\d+)(天|周|个月|月|å¹´)").matcher(text);
        if (matcher.find()) {
            int amount = Integer.parseInt(matcher.group(2));
            String unit = matcher.group(3);
            LocalDate relativeStart = switch (unit) {
                case "天" -> today.minusDays(Math.max(amount - 1L, 0));
                case "周" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
                case "个月", "月" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
                case "å¹´" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
                default -> today.minusDays(29);
            };
            return new DateRange(relativeStart, today, "近" + amount + unit);
        }
        return new DateRange(today.minusDays(29), today, "近30天");
    }
    private LocalDate parseLocalDate(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        return LocalDate.parse(text.trim(), DATE_FMT);
    }
    private Date toDate(LocalDate date) {
        return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private Date toExclusiveEndDate(LocalDate date) {
        return Date.from(date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private String formatDate(LocalDate date) {
        return date == null ? "" : DATE_FMT.format(date);
    }
    private String formatDate(Date date) {
        if (date == null) {
            return "";
        }
        return DATE_FMT.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
    }
    private String formatDateTime(LocalDateTime time) {
        if (time == null) {
            return "";
        }
        return time.truncatedTo(ChronoUnit.SECONDS).toString().replace('T', ' ');
    }
    private int normalizeLimit(Integer limit) {
        if (limit == null || limit <= 0) {
            return DEFAULT_LIMIT;
        }
        return Math.min(limit, MAX_LIMIT);
    }
    private boolean containsAny(String text, String... values) {
        for (String value : values) {
            if (text.contains(value)) {
                return true;
            }
        }
        return false;
    }
    private <T> void applyTenantFilter(LambdaQueryWrapper<T> wrapper, Long tenantId, SFunction<T, Long> field) {
        if (tenantId != null) {
            wrapper.eq(field, tenantId);
        }
    }
    private <T> void applyDeptFilter(LambdaQueryWrapper<T> wrapper, Long deptId, SFunction<T, Long> field) {
        if (deptId != null) {
            wrapper.eq(field, deptId);
        }
    }
    private LoginUser currentLoginUser(String memoryId) {
        LoginUser loginUser = aiSessionUserContext.get(memoryId);
        if (loginUser != null) {
            return loginUser;
        }
        return SecurityUtils.getLoginUser();
    }
    private String safe(Object value) {
        return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ');
    }
    private <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    private String jsonResponse(boolean success,
                                String type,
                                String description,
                                Map<String, Object> summary,
                                Map<String, Object> data,
                                Map<String, Object> charts) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("success", success);
        result.put("type", type);
        result.put("description", description);
        result.put("summary", summary == null ? Map.of() : summary);
        result.put("data", data == null ? Map.of() : data);
        result.put("charts", charts == null ? Map.of() : charts);
        return JSON.toJSONString(result);
    }
    private record DateRange(LocalDate start, LocalDate end, String label) {
    }
}
在上述文件截断后对比
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java src/main/java/com/ruoyi/approve/bean/dto/ApprovalInstanceDto.java src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateDto.java src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeApproverDto.java src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeDto.java src/main/java/com/ruoyi/approve/bean/dto/FinReimbursementDto.java src/main/java/com/ruoyi/approve/bean/vo/ApprovalInstanceVo.java src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeApproverVo.java src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeVo.java src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateVo.java src/main/java/com/ruoyi/approve/bean/vo/ApproveGetAndUpdateVo.java src/main/java/com/ruoyi/approve/bean/vo/ApproveProcessVO.java src/main/java/com/ruoyi/approve/bean/vo/FinReimbursementVo.java src/main/java/com/ruoyi/approve/controller/ApprovalInstanceController.java src/main/java/com/ruoyi/approve/controller/ApprovalInstanceNodeController.java src/main/java/com/ruoyi/approve/controller/ApprovalRecordController.java src/main/java/com/ruoyi/approve/controller/ApprovalTaskController.java src/main/java/com/ruoyi/approve/controller/ApprovalTemplateController.java src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeApproverController.java src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeController.java src/main/java/com/ruoyi/approve/controller/FinReimbursementController.java src/main/java/com/ruoyi/approve/controller/FinReimbursementDetailController.java src/main/java/com/ruoyi/approve/controller/FinReimbursementTravelController.java src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java src/main/java/com/ruoyi/approve/dto/KnowledgeBaseVectorVO.java src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceMapper.java src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceNodeMapper.java src/main/java/com/ruoyi/approve/mapper/ApprovalRecordMapper.java src/main/java/com/ruoyi/approve/mapper/ApprovalTaskMapper.java src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateMapper.java src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeApproverMapper.java src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeMapper.java src/main/java/com/ruoyi/approve/mapper/FinReimbursementDetailMapper.java src/main/java/com/ruoyi/approve/mapper/FinReimbursementMapper.java src/main/java/com/ruoyi/approve/mapper/FinReimbursementTravelMapper.java src/main/java/com/ruoyi/approve/mapper/KnowledgeBaseVectorMapper.java src/main/java/com/ruoyi/approve/pojo/ApprovalInstance.java src/main/java/com/ruoyi/approve/pojo/ApprovalInstanceNode.java src/main/java/com/ruoyi/approve/pojo/ApprovalRecord.java src/main/java/com/ruoyi/approve/pojo/ApprovalTask.java src/main/java/com/ruoyi/approve/pojo/ApprovalTemplate.java src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNode.java src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNodeApprover.java src/main/java/com/ruoyi/approve/pojo/ApproveProcess.java src/main/java/com/ruoyi/approve/pojo/FinReimbursement.java src/main/java/com/ruoyi/approve/pojo/FinReimbursementDetail.java src/main/java/com/ruoyi/approve/pojo/FinReimbursementTravel.java src/main/java/com/ruoyi/approve/pojo/KnowledgeBase.java src/main/java/com/ruoyi/approve/pojo/KnowledgeBaseVector.java src/main/java/com/ruoyi/approve/service/ApprovalInstanceNodeService.java src/main/java/com/ruoyi/approve/service/ApprovalInstanceService.java src/main/java/com/ruoyi/approve/service/ApprovalRecordService.java src/main/java/com/ruoyi/approve/service/ApprovalTaskService.java src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeApproverService.java src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeService.java src/main/java/com/ruoyi/approve/service/ApprovalTemplateService.java src/main/java/com/ruoyi/approve/service/FinReimbursementDetailService.java src/main/java/com/ruoyi/approve/service/FinReimbursementService.java src/main/java/com/ruoyi/approve/service/FinReimbursementTravelService.java src/main/java/com/ruoyi/approve/service/KnowledgeBaseVectorService.java src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceNodeServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/ApprovalRecordServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/ApprovalTaskServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeApproverServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/ApproveBusinessStatusService.java src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/FinReimbursementDetailServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/FinReimbursementTravelServiceImpl.java src/main/java/com/ruoyi/approve/service/impl/KnowledgeBaseVectorServiceImpl.java src/main/java/com/ruoyi/approve/utils/ApproveProcessConfigNodeUtils.java src/main/java/com/ruoyi/basic/controller/CustomerController.java src/main/java/com/ruoyi/basic/dto/ProductModelDto.java src/main/java/com/ruoyi/basic/dto/ProductModelExportDto.java src/main/java/com/ruoyi/basic/dto/StorageBlobVO.java src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java src/main/java/com/ruoyi/basic/mapper/CustomerMapper.java src/main/java/com/ruoyi/basic/mapper/ProductModelMapper.java src/main/java/com/ruoyi/basic/mapper/SupplierManageMapper.java src/main/java/com/ruoyi/basic/pojo/ProductModel.java src/main/java/com/ruoyi/basic/service/ICustomerService.java src/main/java/com/ruoyi/basic/service/ISupplierService.java src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java src/main/java/com/ruoyi/basic/service/impl/ProductModelServiceImpl.java src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java src/main/java/com/ruoyi/basic/service/impl/SupplierServiceImpl.java src/main/java/com/ruoyi/basic/utils/FileUtil.java src/main/java/com/ruoyi/basic/vo/CustomerVo.java src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsController.java src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeDeptController.java src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeUserController.java src/main/java/com/ruoyi/collaborativeApproval/controller/SealApplicationManagementController.java src/main/java/com/ruoyi/collaborativeApproval/dto/EnterpriseNewsDto.java src/main/java/com/ruoyi/collaborativeApproval/dto/SealApplicationManagementDTO.java src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsMapper.java src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeDeptMapper.java src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeUserMapper.java src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNews.java src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeDept.java src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeUser.java src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeDeptService.java src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeUserService.java src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsService.java src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeDeptServiceImpl.java src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeUserServiceImpl.java src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsServiceImpl.java src/main/java/com/ruoyi/collaborativeApproval/service/impl/MeetingServiceImpl.java src/main/java/com/ruoyi/collaborativeApproval/service/impl/SealApplicationManagementServiceImpl.java src/main/java/com/ruoyi/collaborativeApproval/vo/EnterpriseNewsVo.java src/main/java/com/ruoyi/collaborativeApproval/vo/SearchMeetingUseVo.java src/main/java/com/ruoyi/common/enums/ApprovalStatusEnum.java src/main/java/com/ruoyi/common/enums/EnterpriseNewsStatusEnum.java src/main/java/com/ruoyi/common/enums/SalesQuotationStatusEnum.java src/main/java/com/ruoyi/common/enums/ShippingStatusEnum.java src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java src/main/java/com/ruoyi/common/enums/StockInUnQualifiedRecordTypeEnum.java (已删除) src/main/java/com/ruoyi/common/enums/StockOutQualifiedRecordTypeEnum.java src/main/java/com/ruoyi/common/enums/StockOutUnQualifiedRecordTypeEnum.java (已删除) src/main/java/com/ruoyi/common/enums/TypeEnums.java src/main/java/com/ruoyi/common/utils/DateUtils.java src/main/java/com/ruoyi/common/utils/OrderUtils.java src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java src/main/java/com/ruoyi/customervisits/service/impl/CustomerVisitsServiceImpl.java src/main/java/com/ruoyi/device/controller/DeviceLedgerController.java src/main/java/com/ruoyi/device/controller/DeviceRepairController.java src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java src/main/java/com/ruoyi/device/execl/DeviceRepairExeclDto.java src/main/java/com/ruoyi/device/mapper/DeviceLedgerMapper.java src/main/java/com/ruoyi/device/pojo/DeviceRepair.java src/main/java/com/ruoyi/device/pojo/MaintenanceTask.java src/main/java/com/ruoyi/device/service/IDeviceLedgerService.java src/main/java/com/ruoyi/device/service/IDeviceRepairService.java src/main/java/com/ruoyi/device/service/impl/DeviceLedgerServiceImpl.java src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskServiceImpl.java src/main/java/com/ruoyi/framework/config/ApplicationConfig.java src/main/java/com/ruoyi/framework/security/LoginUser.java src/main/java/com/ruoyi/home/controller/HomeController.java src/main/java/com/ruoyi/home/dto/StatisticsReceivablePayableDto.java src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java src/main/java/com/ruoyi/inspectiontask/pojo/InspectionTask.java src/main/java/com/ruoyi/inspectiontask/pojo/TimingTask.java src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java src/main/java/com/ruoyi/mock/controller/DataCheckController.java src/main/java/com/ruoyi/mock/dto/DataCheckRequest.java src/main/java/com/ruoyi/mock/dto/DataGenerateRequest.java src/main/java/com/ruoyi/mock/prompt/MockDataPrompt.java src/main/java/com/ruoyi/mock/service/DataCheckService.java src/main/java/com/ruoyi/mock/service/DataGenerateService.java src/main/java/com/ruoyi/mock/service/impl/DataCheckServiceImpl.java src/main/java/com/ruoyi/mock/service/impl/DataGenerateServiceImpl.java src/main/java/com/ruoyi/mock/vo/DataCheckResult.java src/main/java/com/ruoyi/mock/vo/DataGenerateResult.java 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/bean/dto/InventoryInformationDto.java src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementAddDto.java src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementDto.java src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementManagementUpdateDto.java src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementPageDto.java src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementPageDtoCopy.java src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutAdd.java src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementRecordOutPageDto.java src/main/java/com/ruoyi/procurementrecord/bean/dto/ProcurementUpdateDto.java src/main/java/com/ruoyi/procurementrecord/bean/dto/ReturnManagementDto.java src/main/java/com/ruoyi/procurementrecord/bean/dto/ReturnSaleProductDto.java src/main/java/com/ruoyi/procurementrecord/bean/vo/ShippingInfoVo.java src/main/java/com/ruoyi/procurementrecord/bean/vo/ShippingProductVo.java src/main/java/com/ruoyi/procurementrecord/controller/ProcurementExceptionRecordController.java src/main/java/com/ruoyi/procurementrecord/controller/ProcurementRecordController.java src/main/java/com/ruoyi/procurementrecord/controller/ProcurementRecordOutController.java src/main/java/com/ruoyi/procurementrecord/controller/ReturnManagementController.java src/main/java/com/ruoyi/procurementrecord/dto/Details.java (已删除) src/main/java/com/ruoyi/procurementrecord/dto/InventoryInformationDto.java (已删除) src/main/java/com/ruoyi/procurementrecord/dto/ProcurementAddDto.java (已删除) src/main/java/com/ruoyi/procurementrecord/dto/ProcurementDto.java (已删除) src/main/java/com/ruoyi/procurementrecord/dto/ProcurementManagementUpdateDto.java (已删除) src/main/java/com/ruoyi/procurementrecord/dto/ProcurementPageDto.java (已删除) src/main/java/com/ruoyi/procurementrecord/dto/ProcurementPageDtoCopy.java (已删除) src/main/java/com/ruoyi/procurementrecord/dto/ProcurementRecordOutAdd.java (已删除) src/main/java/com/ruoyi/procurementrecord/dto/ProcurementRecordOutPageDto.java (已删除) src/main/java/com/ruoyi/procurementrecord/dto/ProcurementUpdateDto.java (已删除) src/main/java/com/ruoyi/procurementrecord/dto/ReturnManagementDto.java (已删除) src/main/java/com/ruoyi/procurementrecord/dto/ReturnSaleProductDto.java (已删除) src/main/java/com/ruoyi/procurementrecord/mapper/ProcurementRecordMapper.java src/main/java/com/ruoyi/procurementrecord/mapper/ProcurementRecordOutMapper.java src/main/java/com/ruoyi/procurementrecord/mapper/ReturnManagementMapper.java src/main/java/com/ruoyi/procurementrecord/mapper/ReturnSaleProductMapper.java src/main/java/com/ruoyi/procurementrecord/pojo/CustomStorage.java src/main/java/com/ruoyi/procurementrecord/pojo/ReturnManagement.java src/main/java/com/ruoyi/procurementrecord/pojo/ReturnSaleProduct.java src/main/java/com/ruoyi/procurementrecord/service/ProcurementRecordOutService.java src/main/java/com/ruoyi/procurementrecord/service/ProcurementRecordService.java src/main/java/com/ruoyi/procurementrecord/service/ReturnManagementService.java src/main/java/com/ruoyi/procurementrecord/service/ReturnSaleProductService.java src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordOutServiceImpl.java src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordServiceImpl.java src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnManagementServiceImpl.java src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnSaleProductServiceImpl.java src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java src/main/java/com/ruoyi/production/bean/dto/ProductionAccountDto.java src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java src/main/java/com/ruoyi/production/bean/dto/ProductionPlanDto.java src/main/java/com/ruoyi/production/bean/dto/ProductionPlanImportDto.java src/main/java/com/ruoyi/production/bean/dto/ProductionProductMainDto.java src/main/java/com/ruoyi/production/bean/vo/ProductionAccountVo.java src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java src/main/java/com/ruoyi/production/bean/vo/ProductionOrderWorkOrderDetailVo.java src/main/java/com/ruoyi/production/controller/ProductionOrderController.java src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java src/main/java/com/ruoyi/production/controller/ProductionPlanController.java src/main/java/com/ruoyi/production/enums/ProductOrderStatusEnum.java src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java src/main/java/com/ruoyi/production/pojo/ProductionAccount.java src/main/java/com/ruoyi/production/pojo/ProductionOrder.java src/main/java/com/ruoyi/production/pojo/ProductionOrderRoutingOperation.java src/main/java/com/ruoyi/production/pojo/ProductionProductInput.java src/main/java/com/ruoyi/production/pojo/ProductionProductMain.java src/main/java/com/ruoyi/production/pojo/ProductionProductOutput.java src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java src/main/java/com/ruoyi/production/service/ProductionPlanService.java src/main/java/com/ruoyi/production/service/impl/ProductionAccountServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickRecordServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationParamServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionProductInputServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java src/main/java/com/ruoyi/production/service/impl/ProductionProductOutputServiceImpl.java src/main/java/com/ruoyi/production/service/impl/SalesLedgerProductionAccountingServiceImpl.java src/main/java/com/ruoyi/production/util/TaskPlanQuantityUtil.java src/main/java/com/ruoyi/project/common/CaptchaController.java src/main/java/com/ruoyi/project/monitor/controller/CacheController.java src/main/java/com/ruoyi/project/monitor/controller/ServerController.java src/main/java/com/ruoyi/project/monitor/controller/SysJobController.java src/main/java/com/ruoyi/project/monitor/controller/SysJobLogController.java src/main/java/com/ruoyi/project/monitor/controller/SysLogininforController.java src/main/java/com/ruoyi/project/monitor/controller/SysOperlogController.java src/main/java/com/ruoyi/project/monitor/controller/SysUserOnlineController.java src/main/java/com/ruoyi/project/system/controller/SysConfigController.java src/main/java/com/ruoyi/project/system/controller/SysDeptController.java src/main/java/com/ruoyi/project/system/controller/SysDictDataController.java src/main/java/com/ruoyi/project/system/controller/SysDictTypeController.java src/main/java/com/ruoyi/project/system/controller/SysLoginController.java src/main/java/com/ruoyi/project/system/controller/SysMenuController.java src/main/java/com/ruoyi/project/system/controller/SysNoticeController.java src/main/java/com/ruoyi/project/system/controller/SysPostController.java src/main/java/com/ruoyi/project/system/controller/SysProfileController.java src/main/java/com/ruoyi/project/system/controller/SysRegisterController.java src/main/java/com/ruoyi/project/system/controller/SysRoleController.java src/main/java/com/ruoyi/project/system/controller/SysUserController.java src/main/java/com/ruoyi/project/system/domain/SysUser.java src/main/java/com/ruoyi/project/system/mapper/SysUserDeptMapper.java src/main/java/com/ruoyi/project/system/mapper/SysUserMapper.java src/main/java/com/ruoyi/project/system/service/ISysUserService.java src/main/java/com/ruoyi/project/system/service/impl/SysNoticeServiceImpl.java src/main/java/com/ruoyi/project/system/service/impl/SysUserServiceImpl.java src/main/java/com/ruoyi/project/tool/gen/controller/GenController.java src/main/java/com/ruoyi/projectManagement/controller/RolesController.java src/main/java/com/ruoyi/projectManagement/pojo/Roles.java src/main/java/com/ruoyi/projectManagement/service/impl/PlanServiceImpl.java src/main/java/com/ruoyi/projectManagement/service/impl/handle/InfoStageHandleService.java src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java src/main/java/com/ruoyi/purchase/controller/InvoicePurchaseController.java (已删除) src/main/java/com/ruoyi/purchase/controller/PaymentRegistrationController.java (已删除) src/main/java/com/ruoyi/purchase/controller/PurchaseLedgerController.java src/main/java/com/ruoyi/purchase/controller/PurchaseReturnOrdersController.java src/main/java/com/ruoyi/purchase/controller/TicketRegistrationController.java (已删除) src/main/java/com/ruoyi/purchase/dto/InvoicePurchaseDto.java (已删除) src/main/java/com/ruoyi/purchase/dto/InvoicePurchaseReportDto.java (已删除) src/main/java/com/ruoyi/purchase/dto/PaymentRegistrationDto.java (已删除) src/main/java/com/ruoyi/purchase/dto/ProductRecordDto.java (已删除) src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java src/main/java/com/ruoyi/purchase/dto/PurchaseReturnOrderHasAllInfoDto.java src/main/java/com/ruoyi/purchase/dto/PurchaseReturnOrderProductsDto.java src/main/java/com/ruoyi/purchase/dto/SimpleReturnOrderGroupDto.java src/main/java/com/ruoyi/purchase/dto/TicketRegistrationDto.java (已删除) src/main/java/com/ruoyi/purchase/dto/VatDto.java src/main/java/com/ruoyi/purchase/mapper/InvoicePurchaseMapper.java (已删除) src/main/java/com/ruoyi/purchase/mapper/PaymentRegistrationMapper.java (已删除) src/main/java/com/ruoyi/purchase/mapper/ProductRecordMapper.java (已删除) src/main/java/com/ruoyi/purchase/mapper/PurchaseLedgerMapper.java src/main/java/com/ruoyi/purchase/mapper/PurchaseReturnOrdersMapper.java src/main/java/com/ruoyi/purchase/mapper/TicketRegistrationMapper.java (已删除) src/main/java/com/ruoyi/purchase/pojo/InvoicePurchase.java (已删除) src/main/java/com/ruoyi/purchase/pojo/PaymentRegistration.java (已删除) src/main/java/com/ruoyi/purchase/pojo/ProductRecord.java (已删除) src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java src/main/java/com/ruoyi/purchase/pojo/PurchaseReturnOrderProducts.java src/main/java/com/ruoyi/purchase/pojo/PurchaseReturnOrders.java src/main/java/com/ruoyi/purchase/pojo/SalesLedgerProductTemplate.java src/main/java/com/ruoyi/purchase/pojo/TicketRegistration.java (已删除) src/main/java/com/ruoyi/purchase/service/IInvoicePurchaseService.java (已删除) src/main/java/com/ruoyi/purchase/service/IPaymentRegistrationService.java (已删除) src/main/java/com/ruoyi/purchase/service/IProductRecordService.java (已删除) src/main/java/com/ruoyi/purchase/service/IPurchaseLedgerService.java src/main/java/com/ruoyi/purchase/service/ITicketRegistrationService.java (已删除) src/main/java/com/ruoyi/purchase/service/PurchaseReportService.java src/main/java/com/ruoyi/purchase/service/PurchaseReturnOrdersService.java src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java (已删除) src/main/java/com/ruoyi/purchase/service/impl/PaymentRegistrationServiceImpl.java (已删除) src/main/java/com/ruoyi/purchase/service/impl/ProductRecordServiceImpl.java (已删除) src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java src/main/java/com/ruoyi/purchase/service/impl/PurchaseReportServiceImpl.java src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java (已删除) src/main/java/com/ruoyi/purchase/vo/PurchaseReportVo.java src/main/java/com/ruoyi/purchase/vo/PurchaseReturnDetailsVo.java src/main/java/com/ruoyi/purchase/vo/PurchaseReturnOrderProductsDetailVo.java src/main/java/com/ruoyi/purchase/vo/PurchaseReturnOrderVo.java (已删除) src/main/java/com/ruoyi/purchase/vo/PurchaseStockInProductVo.java src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java src/main/java/com/ruoyi/quality/controller/QualityInspectController.java src/main/java/com/ruoyi/quality/controller/QualityInspectParamController.java src/main/java/com/ruoyi/quality/controller/QualityReportController.java src/main/java/com/ruoyi/quality/controller/QualityTestStandardBindingController.java src/main/java/com/ruoyi/quality/controller/QualityTestStandardController.java src/main/java/com/ruoyi/quality/controller/QualityTestStandardParamController.java src/main/java/com/ruoyi/quality/controller/QualityUnqualifiedController.java src/main/java/com/ruoyi/quality/pojo/QualityInspect.java src/main/java/com/ruoyi/quality/pojo/QualityUnqualified.java src/main/java/com/ruoyi/quality/service/IQualityInspectService.java src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java src/main/java/com/ruoyi/quality/service/impl/QualityUnqualifiedServiceImpl.java src/main/java/com/ruoyi/quality/utils/QualityInspectHelper.java src/main/java/com/ruoyi/sales/controller/InvoiceLedgerController.java (已删除) src/main/java/com/ruoyi/sales/controller/InvoiceRegistrationController.java (已删除) src/main/java/com/ruoyi/sales/controller/MetricStatisticsController.java src/main/java/com/ruoyi/sales/controller/PaymentShippingController.java src/main/java/com/ruoyi/sales/controller/ReceiptPaymentController.java (已删除) src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java src/main/java/com/ruoyi/sales/controller/SalesLedgerProductController.java src/main/java/com/ruoyi/sales/controller/ShipmentApprovalController.java (已删除) src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java src/main/java/com/ruoyi/sales/dto/InvoiceLedgerDto.java (已删除) src/main/java/com/ruoyi/sales/dto/InvoiceRegistrationDto.java (已删除) src/main/java/com/ruoyi/sales/dto/InvoiceRegistrationProductDto.java (已删除) src/main/java/com/ruoyi/sales/dto/ReceiptPaymentDto.java (已删除) src/main/java/com/ruoyi/sales/dto/ReceiptPaymentExeclDto.java (已删除) src/main/java/com/ruoyi/sales/dto/ReceiptPaymentRecordDto.java (已删除) src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java src/main/java/com/ruoyi/sales/dto/SalesLedgerImportDto.java src/main/java/com/ruoyi/sales/dto/SalesLedgerProductImportDto.java src/main/java/com/ruoyi/sales/dto/SalesQuotationDto.java src/main/java/com/ruoyi/sales/dto/ShippingApproveDto.java src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java src/main/java/com/ruoyi/sales/excel/InvoiceLedgerExcelDto.java src/main/java/com/ruoyi/sales/excel/InvoiceRegisAndProductExcelDto.java (已删除) src/main/java/com/ruoyi/sales/mapper/InvoiceLedgerFileMapper.java (已删除) src/main/java/com/ruoyi/sales/mapper/InvoiceLedgerMapper.java (已删除) src/main/java/com/ruoyi/sales/mapper/InvoiceRegistrationMapper.java (已删除) src/main/java/com/ruoyi/sales/mapper/InvoiceRegistrationProductMapper.java (已删除) src/main/java/com/ruoyi/sales/mapper/ReceiptPaymentMapper.java (已删除) src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java src/main/java/com/ruoyi/sales/mapper/SalesLedgerProductMapper.java src/main/java/com/ruoyi/sales/mapper/ShipmentApprovalMapper.java (已删除) src/main/java/com/ruoyi/sales/mapper/ShippingInfoMapper.java src/main/java/com/ruoyi/sales/mapper/ShippingProductDetailMapper.java src/main/java/com/ruoyi/sales/pojo/InvoiceLedger.java (已删除) src/main/java/com/ruoyi/sales/pojo/InvoiceLedgerFile.java (已删除) src/main/java/com/ruoyi/sales/pojo/InvoiceRegistration.java (已删除) src/main/java/com/ruoyi/sales/pojo/InvoiceRegistrationProduct.java (已删除) src/main/java/com/ruoyi/sales/pojo/PaymentShipping.java src/main/java/com/ruoyi/sales/pojo/ReceiptPayment.java (已删除) src/main/java/com/ruoyi/sales/pojo/SalesLedger.java src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java src/main/java/com/ruoyi/sales/pojo/SalesQuotation.java src/main/java/com/ruoyi/sales/pojo/SalesQuotationProduct.java src/main/java/com/ruoyi/sales/pojo/ShipmentApproval.java (已删除) src/main/java/com/ruoyi/sales/service/ISalesLedgerProductService.java src/main/java/com/ruoyi/sales/service/InvoiceLedgerService.java (已删除) src/main/java/com/ruoyi/sales/service/InvoiceRegistrationService.java (已删除) src/main/java/com/ruoyi/sales/service/ReceiptPaymentService.java (已删除) src/main/java/com/ruoyi/sales/service/ShipmentApprovalService.java (已删除) src/main/java/com/ruoyi/sales/service/ShippingInfoService.java src/main/java/com/ruoyi/sales/service/impl/InvoiceLedgerServiceImpl.java (已删除) src/main/java/com/ruoyi/sales/service/impl/InvoiceRegistrationServiceImpl.java (已删除) src/main/java/com/ruoyi/sales/service/impl/ReceiptPaymentServiceImpl.java (已删除) src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java src/main/java/com/ruoyi/sales/service/impl/ShipmentApprovalServiceImpl.java (已删除) src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java src/main/java/com/ruoyi/staff/controller/PersonalAttendanceLocationConfigController.java src/main/java/com/ruoyi/staff/controller/PersonalShiftController.java src/main/java/com/ruoyi/staff/controller/StaffOnJobController.java src/main/java/com/ruoyi/staff/controller/StaffSalaryMainController.java src/main/java/com/ruoyi/staff/dto/StaffLeaveDto.java src/main/java/com/ruoyi/staff/dto/StaffOnJobDto.java src/main/java/com/ruoyi/staff/dto/StaffSalaryCalculateDto.java src/main/java/com/ruoyi/staff/excel/StaffSalaryExportData.java src/main/java/com/ruoyi/staff/init/DataInitializer.java src/main/java/com/ruoyi/staff/mapper/SchemeApplicableStaffMapper.java src/main/java/com/ruoyi/staff/mapper/StaffOnJobMapper.java src/main/java/com/ruoyi/staff/mapper/StaffSalaryMainMapper.java src/main/java/com/ruoyi/staff/pojo/StaffOnJob.java src/main/java/com/ruoyi/staff/pojo/StaffSalaryDetail.java src/main/java/com/ruoyi/staff/service/IStaffOnJobService.java src/main/java/com/ruoyi/staff/service/StaffSalaryMainService.java src/main/java/com/ruoyi/staff/service/impl/PersonalAttendanceRecordsServiceImpl.java src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java src/main/java/com/ruoyi/staff/service/impl/StaffLeaveServiceImpl.java src/main/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java src/main/java/com/ruoyi/stock/controller/StockInRecordController.java src/main/java/com/ruoyi/stock/controller/StockInventoryController.java src/main/java/com/ruoyi/stock/controller/StockOutRecordController.java src/main/java/com/ruoyi/stock/controller/StockUninventoryController.java src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java src/main/java/com/ruoyi/stock/dto/StockOutRecordDto.java src/main/java/com/ruoyi/stock/execl/StockInRecordExportData.java src/main/java/com/ruoyi/stock/execl/StockInventoryExportData.java src/main/java/com/ruoyi/stock/execl/StockOutRecordExportData.java src/main/java/com/ruoyi/stock/execl/StockUnInventoryExportData.java src/main/java/com/ruoyi/stock/mapper/StockInRecordMapper.java src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java src/main/java/com/ruoyi/stock/mapper/StockOutRecordMapper.java src/main/java/com/ruoyi/stock/pojo/StockInRecord.java src/main/java/com/ruoyi/stock/pojo/StockOutRecord.java src/main/java/com/ruoyi/stock/service/StockInRecordService.java src/main/java/com/ruoyi/stock/service/StockInventoryService.java src/main/java/com/ruoyi/stock/service/StockOutRecordService.java src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java src/main/java/com/ruoyi/stock/service/impl/StockUninventoryServiceImpl.java src/main/java/com/ruoyi/technology/bean/vo/TechnologyBomStructureVo.java src/main/java/com/ruoyi/technology/controller/TechnologyBomController.java src/main/java/com/ruoyi/technology/controller/TechnologyOperationController.java src/main/java/com/ruoyi/technology/controller/TechnologyOperationParamController.java src/main/java/com/ruoyi/technology/pojo/TechnologyOperation.java src/main/java/com/ruoyi/technology/pojo/TechnologyParam.java src/main/java/com/ruoyi/technology/pojo/TechnologyRouting.java src/main/java/com/ruoyi/technology/pojo/TechnologyRoutingOperation.java src/main/java/com/ruoyi/technology/service/TechnologyBomService.java src/main/java/com/ruoyi/technology/service/TechnologyOperationService.java src/main/java/com/ruoyi/technology/service/impl/TechnologyBomServiceImpl.java src/main/java/com/ruoyi/technology/service/impl/TechnologyOperationParamServiceImpl.java src/main/java/com/ruoyi/technology/service/impl/TechnologyOperationServiceImpl.java src/main/java/com/ruoyi/technology/service/impl/TechnologyParamServiceImpl.java src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java src/main/java/com/ruoyi/warehouse/controller/DocumentationFileController.java src/main/java/com/ruoyi/warehouse/mapper/DocumentationFileMapper.java src/main/java/com/ruoyi/warehouse/mapper/DocumentationMapper.java src/main/java/com/ruoyi/warehouse/service/DocumentationFileService.java src/main/resources/application-btw.yml src/main/resources/application-ckgm.yml src/main/resources/application-dev.yml src/main/resources/application-hqjc.yml src/main/resources/application-hthb.yml src/main/resources/application-jfhy.yml src/main/resources/application-khyy.yml src/main/resources/application-lsy.yml src/main/resources/application-myhb-pro.yml src/main/resources/application-nhxcl.yml src/main/resources/application-qfsw.yml src/main/resources/application-qlmc-pro.yml src/main/resources/application-rfsy.yml src/main/resources/application-xhks.yml src/main/resources/application-xlgs.yml src/main/resources/application-yss.yml src/main/resources/application-zdjc.yml src/main/resources/application.yml src/main/resources/approve-todo-agent-prompt.txt src/main/resources/financial-agent-prompt.txt src/main/resources/logback.xml src/main/resources/manufacturing-agent-prompt.txt src/main/resources/mapper/account/AccountExpenseMapper.xml (已删除) src/main/resources/mapper/account/AccountFileMapper.xml (已删除) src/main/resources/mapper/account/AccountIncomeMapper.xml (已删除) src/main/resources/mapper/account/AccountStatementMapper.xml src/main/resources/mapper/account/BorrowInfoMapper.xml (已删除) src/main/resources/mapper/account/SalesRefundAmountOrderMapper.xml src/main/resources/mapper/account/financial/AccountSubjectMapper.xml src/main/resources/mapper/account/financial/FinVoucherEntryMapper.xml src/main/resources/mapper/account/purchase/AccountPaymentApplicationMapper.xml src/main/resources/mapper/account/purchase/AccountPurchaseInvoiceMapper.xml src/main/resources/mapper/account/purchase/AccountPurchasePaymentMapper.xml src/main/resources/mapper/account/sales/AccountInvoiceApplicationMapper.xml src/main/resources/mapper/account/sales/AccountSalesCollectionMapper.xml src/main/resources/mapper/account/sales/AccountSalesInvoiceMapper.xml src/main/resources/mapper/aftersalesservice/AfterSalesNearExpiryMapper.xml src/main/resources/mapper/aftersalesservice/AfterSalesServiceMapper.xml src/main/resources/mapper/approve/ApprovalInstanceMapper.xml src/main/resources/mapper/approve/ApprovalInstanceNodeMapper.xml src/main/resources/mapper/approve/ApprovalRecordMapper.xml src/main/resources/mapper/approve/ApprovalTaskMapper.xml src/main/resources/mapper/approve/ApprovalTemplateMapper.xml src/main/resources/mapper/approve/ApprovalTemplateNodeApproverMapper.xml src/main/resources/mapper/approve/ApprovalTemplateNodeMapper.xml src/main/resources/mapper/approve/ApproveProcessMapper.xml src/main/resources/mapper/approve/FinReimbursementDetailMapper.xml src/main/resources/mapper/approve/FinReimbursementMapper.xml src/main/resources/mapper/approve/FinReimbursementTravelMapper.xml src/main/resources/mapper/approve/KnowledgeBaseMapper.xml src/main/resources/mapper/basic/CustomerMapper.xml src/main/resources/mapper/basic/ProductModelMapper.xml src/main/resources/mapper/basic/SupplierManageMapper.xml src/main/resources/mapper/collaborativeApproval/EnterpriseNewsMapper.xml src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeDeptMapper.xml src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeUserMapper.xml src/main/resources/mapper/collaborativeApproval/NoticeMapper.xml src/main/resources/mapper/collaborativeApproval/RulesRegulationsManagementMapper.xml src/main/resources/mapper/collaborativeApproval/SealApplicationManagementMapper.xml src/main/resources/mapper/device/DeviceLedgerMapper.xml src/main/resources/mapper/device/DeviceMaintenanceMapper.xml src/main/resources/mapper/device/DeviceRepairMapper.xml src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerRecordMapper.xml src/main/resources/mapper/measuringinstrumentledger/SparePartsMapper.xml src/main/resources/mapper/measuringinstrumentledger/SparePartsRequisitionRecordMapper.xml 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 src/main/resources/mapper/procurementrecord/ProcurementRecordOutMapper.xml src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml src/main/resources/mapper/procurementrecord/ReturnSaleProductMapper.xml src/main/resources/mapper/production/ProductionAccountMapper.xml src/main/resources/mapper/production/ProductionOperationTaskMapper.xml src/main/resources/mapper/production/ProductionOrderMapper.xml src/main/resources/mapper/production/ProductionOrderRoutingOperationMapper.xml src/main/resources/mapper/production/ProductionPlanMapper.xml src/main/resources/mapper/production/ProductionProductMainMapper.xml src/main/resources/mapper/purchase/InvoicePurchaseMapper.xml (已删除) src/main/resources/mapper/purchase/PaymentRegistrationMapper.xml (已删除) src/main/resources/mapper/purchase/ProductRecordMapper.xml (已删除) src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml src/main/resources/mapper/purchase/PurchaseReturnOrderProductsMapper.xml src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml src/main/resources/mapper/quality/QualityInspectMapper.xml src/main/resources/mapper/quality/QualityTestStandardMapper.xml src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml src/main/resources/mapper/sales/InvoiceLedgerMapper.xml (已删除) src/main/resources/mapper/sales/InvoiceRegistrationMapper.xml (已删除) src/main/resources/mapper/sales/InvoiceRegistrationProductMapper.xml (已删除) src/main/resources/mapper/sales/ReceiptPaymentMapper.xml (已删除) src/main/resources/mapper/sales/SalesLedgerMapper.xml src/main/resources/mapper/sales/SalesLedgerProductMapper.xml src/main/resources/mapper/sales/SalesQuotationMapper.xml src/main/resources/mapper/sales/ShipmentApprovalMapper.xml (已删除) src/main/resources/mapper/sales/ShippingInfoMapper.xml src/main/resources/mapper/sales/ShippingProductDetailMapper.xml src/main/resources/mapper/staff/PersonalAttendanceRecordsMapper.xml src/main/resources/mapper/staff/PersonalShiftMapper.xml src/main/resources/mapper/staff/SchemeApplicableStaffMapper.xml src/main/resources/mapper/staff/StaffLeaveMapper.xml src/main/resources/mapper/staff/StaffOnJobMapper.xml src/main/resources/mapper/staff/StaffSalaryMainMapper.xml src/main/resources/mapper/stock/StockInRecordMapper.xml src/main/resources/mapper/stock/StockInventoryMapper.xml src/main/resources/mapper/stock/StockOutRecordMapper.xml src/main/resources/mapper/system/SysDeptMapper.xml src/main/resources/mapper/system/SysDictTypeMapper.xml src/main/resources/mapper/system/SysNoticeMapper.xml src/main/resources/mapper/system/SysPostMapper.xml src/main/resources/mapper/system/SysRoleMapper.xml src/main/resources/mapper/system/SysUserMapper.xml src/main/resources/mapper/technology/TechnologyBomStructureMapper.xml src/main/resources/mapper/technology/TechnologyOperationMapper.xml src/main/resources/mapper/technology/TechnologyOperationParamMapper.xml src/main/resources/mapper/technology/TechnologyRoutingOperationMapper.xml src/main/resources/mapper/warehouse/DocumentationBorrowManagementMapper.xml src/main/resources/mapper/warehouse/DocumentationMapper.xml src/main/resources/mapper/warehouse/DocumentationReturnManagementMapper.xml src/main/resources/mapper/warehouse/WarehouseGoodsShelvesRowcolMapper.xml src/main/resources/purchase-agent-prompt.txt src/main/resources/sales-agent-prompt.txt src/main/resources/static/销售台账导入模板.xlsx