liyong
5 天以前 9d66bfbfcda297f628e6a857e343f98422f4534a
Merge remote-tracking branch 'refs/remotes/origin/dev_New_pro' into dev_New_pro_OA

# Conflicts:
# src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java
# src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java
已添加84个文件
已重命名23个文件
已修改113个文件
已删除81个文件
27984 ■■■■ 文件已修改
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_财务升级AI模块前端变更联调文档.md 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/financial-ai-front-integration.md 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/CodeGenerator.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/StatementAccountDto.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectImportDto.java 2 ●●● 补丁 | 查看 | 原始文档 | 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 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseReturnDto.java 10 ●●●●● 补丁 | 查看 | 原始文档 | 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 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/sales/SalesReturnDto.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/StatementAccountVo.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/financial/AccountSubjectVo.java 4 ●●●● 补丁 | 查看 | 原始文档 | 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 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseReturnVo.java 2 ●●● 补丁 | 查看 | 原始文档 | 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 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/sales/SalesReturnVo.java 2 ●●● 补丁 | 查看 | 原始文档 | 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/BorrowInfoController.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/financial/AccountSubjectController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/purchase/AccounPurchaseController.java 12 ●●●● 补丁 | 查看 | 原始文档 | 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 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/sales/AccountSalesCollectionController.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/sales/AccountSalesController.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/sales/AccountSalesInvoiceController.java 72 ●●●●● 补丁 | 查看 | 原始文档 | 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 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/BorrowInfoMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/financial/AccountSubjectMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | 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 32 ●●●●● 补丁 | 查看 | 原始文档 | 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 6 ●●●● 补丁 | 查看 | 原始文档 | 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/BorrowInfoService.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/financial/AccountPurchaseService.java 10 ●●●● 补丁 | 查看 | 原始文档 | 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 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/BorrowInfoServiceImpl.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | 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 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/sales/AccountInvoiceApplicationServiceImpl.java 97 ●●●●● 补丁 | 查看 | 原始文档 | 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 12 ●●●● 补丁 | 查看 | 原始文档 | 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 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/sales/AccountInvoiceApplicationService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | 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 10 ●●●● 补丁 | 查看 | 原始文档 | 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 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java 3 ●●●● 补丁 | 查看 | 原始文档 | 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/FinancialAgentConfig.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/SalesAgentConfig.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/FinancialAiController.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | 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/service/PurchaseAiService.java 88 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java 2226 ●●●●● 补丁 | 查看 | 原始文档 | 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/basic/enums/RecordTypeEnum.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/CustomerMapper.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/SupplierManageMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/ProductModel.java 6 ●●●●● 补丁 | 查看 | 原始文档 | 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 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/SupplierServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/controller/HomeController.java 586 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java 171 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/ReturnManagementController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/mapper/ReturnManagementMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnManagementServiceImpl.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java 152 ●●●● 补丁 | 查看 | 原始文档 | 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 310 ●●●● 补丁 | 查看 | 原始文档 | 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 584 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/tool/gen/controller/GenController.java 508 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java 51 ●●●● 补丁 | 查看 | 原始文档 | 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 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/PurchaseReturnOrdersController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | 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 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/TicketRegistrationDto.java 93 ●●●●● 补丁 | 查看 | 原始文档 | 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 4 ●●●● 补丁 | 查看 | 原始文档 | 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 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java 8 ●●●●● 补丁 | 查看 | 原始文档 | 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 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/ITicketRegistrationService.java 37 ●●●●● 补丁 | 查看 | 原始文档 | 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 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java 464 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java 48 ●●●● 补丁 | 查看 | 原始文档 | 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 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityUnqualifiedController.java 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityUnqualified.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | 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/ReceiptPaymentController.java 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/SalesLedgerProductController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | 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/SalesLedgerImportDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesLedgerProductImportDto.java 7 ●●●● 补丁 | 查看 | 原始文档 | 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/SalesLedgerProductMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | 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/ReceiptPayment.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java 78 ●●●●● 补丁 | 查看 | 原始文档 | 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/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 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 93 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockOutRecordDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/mapper/StockInRecordMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/mapper/StockOutRecordMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | 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/service/DocumentationFileService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/approve-todo-agent-prompt.txt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/financial-agent-prompt.txt 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/manufacturing-agent-prompt.txt 2 ●●●●● 补丁 | 查看 | 原始文档 | 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 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/BorrowInfoMapper.xml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/financial/AccountSubjectMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPaymentApplicationMapper.xml 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPurchaseInvoiceMapper.xml 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPurchasePaymentMapper.xml 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountInvoiceApplicationMapper.xml 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountSalesCollectionMapper.xml 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountSalesInvoiceMapper.xml 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/ProductModelMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/SupplierManageMapper.xml 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderMapper.xml 81 ●●●●● 补丁 | 查看 | 原始文档 | 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 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | 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/SalesLedgerProductMapper.xml 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInRecordMapper.xml 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockOutRecordMapper.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | 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 | 历史
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_²ÆÎñÉý¼¶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/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. æœ€åŽè¡¥ä¼šè¯åŽ†å²ä¸Žåˆ é™¤èƒ½åŠ›ï¼Œå½¢æˆå®Œæ•´å¯¹è¯é—­çŽ¯
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/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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/AccountSubjectDto.java ÐÞ¸Ä
@@ -1,6 +1,6 @@
package com.ruoyi.account.bean.dto;
package com.ruoyi.account.bean.dto.financial;
import com.ruoyi.account.pojo.AccountSubject;
import com.ruoyi.account.pojo.financial.AccountSubject;
import lombok.Data;
import lombok.EqualsAndHashCode;
src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectImportDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/AccountSubjectImportDto.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.account.bean.dto;
package com.ruoyi.account.bean.dto.financial;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/PurchaseInboundDto.java ÐÞ¸Ä
@@ -1,11 +1,11 @@
package com.ruoyi.account.bean.dto;
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.util.Date;
import java.time.LocalDate;
@Data
@Schema(name = "PurchaseInboundDto", description = "财务管理--采购入库台账(传参)")
@@ -14,16 +14,18 @@
    @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 Date startDate;
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseReturnDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/PurchaseReturnDto.java ÐÞ¸Ä
@@ -1,11 +1,11 @@
package com.ruoyi.account.bean.dto;
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.util.Date;
import java.time.LocalDate;
@Data
@Schema(name = "PurchaseReturnDto", description = "财务管理--采购退货台账(传参)")
@@ -14,16 +14,18 @@
    @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 Date startDate;
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
    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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/SalesOutboundDto.java ÐÞ¸Ä
@@ -1,11 +1,11 @@
package com.ruoyi.account.bean.dto;
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.util.Date;
import java.time.LocalDate;
@Data
@Schema(name = "SalesOutboundDto", description = "财务管理--销售出库台账(传参)")
@@ -14,16 +14,18 @@
    @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 Date startDate;
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
    private LocalDate endDate;
}
src/main/java/com/ruoyi/account/bean/dto/sales/SalesReturnDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/SalesReturnDto.java ÐÞ¸Ä
@@ -1,11 +1,11 @@
package com.ruoyi.account.bean.dto;
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.util.Date;
import java.time.LocalDate;
@Data
@Schema(name = "SalesReturnDto", description = "财务管理--销售退货台账(传参)")
@@ -14,16 +14,18 @@
    @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 Date startDate;
    private LocalDate startDate;
    @Schema(description = "结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
    private LocalDate endDate;
}
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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java ÐÞ¸Ä
@@ -1,6 +1,6 @@
package com.ruoyi.account.bean.vo;
package com.ruoyi.account.bean.vo.financial;
import com.ruoyi.account.pojo.AccountSubject;
import com.ruoyi.account.pojo.financial.AccountSubject;
import lombok.Data;
import java.util.ArrayList;
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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/vo/PurchaseInboundVo.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.account.bean.vo;
package com.ruoyi.account.bean.vo.purchase;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.fasterxml.jackson.annotation.JsonFormat;
@@ -7,7 +7,7 @@
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.time.LocalDate;
@Data
@Schema(name = "PurchaseInboundVo", description = "财务管理--采购入库台账(返回)")
@@ -28,7 +28,7 @@
    @Schema(description = "入库日期")
    @Excel(name = "入库日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date InboundDate;
    private LocalDate InboundDate;
    @Schema(description = "产品名称")
    @Excel(name = "产品名称")
src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseReturnVo.java
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/vo/PurchaseReturnVo.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.account.bean.vo;
package com.ruoyi.account.bean.vo.purchase;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.fasterxml.jackson.annotation.JsonFormat;
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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/vo/SalesOutboundVo.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.account.bean.vo;
package com.ruoyi.account.bean.vo.sales;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.fasterxml.jackson.annotation.JsonFormat;
@@ -7,7 +7,7 @@
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.time.LocalDate;
@Data
@Schema(name = "SalesOutboundVo", description = "财务管理--销售出库台账(返回)")
@@ -28,7 +28,7 @@
    @Schema(description = "出库日期")
    @Excel(name = "出库日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date shippingDate;
    private LocalDate shippingDate;
    @Schema(description = "产品名称")
    @Excel(name = "产品名称")
@@ -42,6 +42,9 @@
    @Excel(name = "金额")
    private BigDecimal outboundAmount;
    @Schema(description = "税率")
    private BigDecimal taxRate;
    @Schema(description = "发货编号")
    @Excel(name = "发货编号")
    private String shippingNo;
src/main/java/com/ruoyi/account/bean/vo/sales/SalesReturnVo.java
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/vo/SalesReturnVo.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.account.bean.vo;
package com.ruoyi.account.bean.vo.sales;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.fasterxml.jackson.annotation.JsonFormat;
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/BorrowInfoController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/controller/financial/AccountSubjectController.java
ÎļþÃû´Ó src/main/java/com/ruoyi/account/controller/AccountSubjectController.java ÐÞ¸Ä
@@ -1,10 +1,10 @@
package com.ruoyi.account.controller;
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.AccountSubjectDto;
import com.ruoyi.account.bean.vo.AccountSubjectVo;
import com.ruoyi.account.service.AccountSubjectService;
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;
src/main/java/com/ruoyi/account/controller/purchase/AccounPurchaseController.java
ÎļþÃû´Ó src/main/java/com/ruoyi/account/controller/AccounPurchaseController.java ÐÞ¸Ä
@@ -1,12 +1,12 @@
package com.ruoyi.account.controller;
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.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.PurchaseReturnVo;
import com.ruoyi.account.service.AccountPurchaseService;
import com.ruoyi.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;
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,86 @@
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;
/**
 * <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("/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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/controller/AccountSalesController.java ÐÞ¸Ä
@@ -1,12 +1,12 @@
package com.ruoyi.account.controller;
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.SalesOutboundDto;
import com.ruoyi.account.bean.dto.SalesReturnDto;
import com.ruoyi.account.bean.vo.SalesOutboundVo;
import com.ruoyi.account.bean.vo.SalesReturnVo;
import com.ruoyi.account.service.AccountSalesService;
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;
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/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,25 @@
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 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);
}
src/main/java/com/ruoyi/account/mapper/BorrowInfoMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/mapper/financial/AccountSubjectMapper.java
ÎļþÃû´Ó src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java ÐÞ¸Ä
@@ -1,7 +1,7 @@
package com.ruoyi.account.mapper;
package com.ruoyi.account.mapper.financial;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.account.pojo.AccountSubject;
import com.ruoyi.account.pojo.financial.AccountSubject;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
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,32 @@
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);
}
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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/pojo/AccountSubject.java ÐÞ¸Ä
@@ -1,4 +1,4 @@
package com.ruoyi.account.pojo;
package com.ruoyi.account.pojo.financial;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
@@ -85,7 +85,7 @@
     */
    @ApiModelProperty("创建人")
    @TableField(fill = FieldFill.INSERT)
    private String createUser;
    private Integer createUser;
    /**
     * åˆ›å»ºæ—¶é—´
@@ -99,7 +99,7 @@
     */
    @ApiModelProperty("修改人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String updateUser;
    private Integer updateUser;
    /**
     * ä¿®æ”¹æ—¶é—´
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/BorrowInfoService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/financial/AccountPurchaseService.java
ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/AccountPurchaseService.java ÐÞ¸Ä
@@ -1,11 +1,11 @@
package com.ruoyi.account.service;
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.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.PurchaseReturnVo;
import com.ruoyi.account.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;
/**
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
@@ -6,8 +6,6 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.DeviceTypeDetail;
import com.ruoyi.account.bean.dto.DeviceTypeDistributionVO;
import com.ruoyi.account.mapper.BorrowInfoMapper;
import com.ruoyi.account.pojo.BorrowInfo;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.framework.web.domain.AjaxResult;
@@ -38,7 +36,6 @@
public class AccountingServiceImpl {
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final BorrowInfoMapper borrowInfoMapper;
    private final CustomStorageMapper customStorageMapper;
    private final ProcurementRecordMapper procurementRecordMapper;
    private final ProcurementRecordOutMapper procurementRecordOutMapper;
@@ -75,17 +72,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);
src/main/java/com/ruoyi/account/service/impl/BorrowInfoServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java
ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java ÐÞ¸Ä
@@ -1,15 +1,15 @@
package com.ruoyi.account.service.impl;
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.AccountSubjectDto;
import com.ruoyi.account.bean.dto.AccountSubjectImportDto;
import com.ruoyi.account.bean.vo.AccountSubjectVo;
import com.ruoyi.account.mapper.AccountSubjectMapper;
import com.ruoyi.account.pojo.AccountSubject;
import com.ruoyi.account.service.AccountSubjectService;
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;
src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java
@@ -8,10 +8,10 @@
import com.ruoyi.account.bean.dto.financial.FinVoucherEntryDto;
import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto;
import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo;
import com.ruoyi.account.mapper.AccountSubjectMapper;
import com.ruoyi.account.mapper.financial.AccountSubjectMapper;
import com.ruoyi.account.mapper.financial.FinVoucherEntryMapper;
import com.ruoyi.account.mapper.financial.FinVoucherMapper;
import com.ruoyi.account.pojo.AccountSubject;
import com.ruoyi.account.pojo.financial.AccountSubject;
import com.ruoyi.account.pojo.financial.FinVoucher;
import com.ruoyi.account.pojo.financial.FinVoucherEntry;
import com.ruoyi.account.service.financial.FinVoucherService;
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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/impl/AccountPurchaseServiceImpl.java ÐÞ¸Ä
@@ -1,12 +1,12 @@
package com.ruoyi.account.service.impl;
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.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.PurchaseReturnVo;
import com.ruoyi.account.service.AccountPurchaseService;
import com.ruoyi.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;
src/main/java/com/ruoyi/account/service/impl/sales/AccountInvoiceApplicationServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
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);
    }
    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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/impl/AccountSalesServiceImpl.java ÐÞ¸Ä
@@ -1,12 +1,12 @@
package com.ruoyi.account.service.impl;
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.SalesOutboundDto;
import com.ruoyi.account.bean.dto.SalesReturnDto;
import com.ruoyi.account.bean.vo.SalesOutboundVo;
import com.ruoyi.account.bean.vo.SalesReturnVo;
import com.ruoyi.account.service.AccountSalesService;
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;
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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/AccountSubjectService.java ÐÞ¸Ä
@@ -1,10 +1,10 @@
package com.ruoyi.account.service;
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.AccountSubjectDto;
import com.ruoyi.account.bean.vo.AccountSubjectVo;
import com.ruoyi.account.pojo.AccountSubject;
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;
src/main/java/com/ruoyi/account/service/sales/AccountInvoiceApplicationService.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.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);
}
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
ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/AccountSalesService.java ÐÞ¸Ä
@@ -1,11 +1,11 @@
package com.ruoyi.account.service;
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.SalesOutboundDto;
import com.ruoyi.account.bean.dto.SalesReturnDto;
import com.ruoyi.account.bean.vo.SalesOutboundVo;
import com.ruoyi.account.bean.vo.SalesReturnVo;
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;
/**
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,246 @@
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 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, limit);
        }
        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)) {
            DateRange range = monthRange();
            return financialAgentTools.getBusinessCockpit(memoryId, range.startDate(), range.endDate(), range.label());
        }
        if ("查询近30天亏损订单".equals(normalized) || "哪个订单亏损".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, 20);
        }
        if ("生成本周经营周报".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, 20);
        }
        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(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, 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(2));
            return recentDaysRange(days);
        }
        return new DateRange(null, null, "近30天");
    }
    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, "近30天");
        }
        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(" ", "")
                .trim();
    }
    private String extractKeyword(String text) {
        String cleaned = text
                .replace("查询", "")
                .replace("查看", "")
                .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/ManufacturingAgent.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 ManufacturingAgent {
    @SystemMessage(fromResource = "manufacturing-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/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/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/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/controller/FinancialAiController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
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);
        }
        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);
    }
}
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java
@@ -24,12 +24,18 @@
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;
@@ -76,7 +82,7 @@
            return Flux.just(directResponse);
        }
        return manufacturingAgent.chat(memoryId, userMessage)
        return manufacturingAgent.chat(memoryId, userMessage, currentDateForPrompt())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
@@ -99,4 +105,8 @@
        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/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/service/PurchaseAiService.java
@@ -1,5 +1,6 @@
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;
@@ -20,8 +21,6 @@
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;
@@ -48,7 +47,6 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Base64;
import java.util.Arrays;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@@ -71,6 +69,8 @@
    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;
@@ -80,7 +80,6 @@
    private final AiFileTextExtractor aiFileTextExtractor;
    private final ObjectMapper objectMapper;
    private final IPurchaseLedgerService purchaseLedgerService;
    private final IPaymentRegistrationService paymentRegistrationService;
    private final PurchaseReturnOrdersService purchaseReturnOrdersService;
    private final StorageBlobService storageBlobService;
    private final SupplierManageMapper supplierManageMapper;
@@ -94,7 +93,6 @@
                                AiFileTextExtractor aiFileTextExtractor,
                                 ObjectMapper objectMapper,
                                 IPurchaseLedgerService purchaseLedgerService,
                                 IPaymentRegistrationService paymentRegistrationService,
                                 PurchaseReturnOrdersService purchaseReturnOrdersService,
                                 StorageBlobService storageBlobService,
                                 SupplierManageMapper supplierManageMapper,
@@ -107,7 +105,6 @@
        this.aiFileTextExtractor = aiFileTextExtractor;
        this.objectMapper = objectMapper;
        this.purchaseLedgerService = purchaseLedgerService;
        this.paymentRegistrationService = paymentRegistrationService;
        this.purchaseReturnOrdersService = purchaseReturnOrdersService;
        this.storageBlobService = storageBlobService;
        this.supplierManageMapper = supplierManageMapper;
@@ -138,7 +135,17 @@
            return Flux.just(directResponse);
        }
        return purchaseAgent.chat(memoryId, userMessage)
        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));
    }
@@ -200,10 +207,10 @@
                    .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
        }
        return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt))
        return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt, currentDateForPrompt()))
                .onErrorResume(NoSuchElementException.class, ex -> {
                    mongoChatMemoryStore.deleteMessages(finalMemoryId);
                    return purchaseAgent.chat(finalMemoryId, userPrompt);
                    return purchaseAgent.chat(finalMemoryId, userPrompt, currentDateForPrompt());
                })
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
@@ -221,7 +228,6 @@
            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);
            };
@@ -471,6 +477,51 @@
        };
    }
    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 """
                ä½ æ˜¯é‡‡è´­ä¸šåŠ¡æ–‡ä»¶åˆ†æžåŠ©æ‰‹ã€‚è¯·ä¸¥æ ¼æ ¹æ®ç”¨æˆ·ä¸Šä¼ çš„å¤šä¸ªæ–‡ä»¶å’Œç”¨æˆ·è¦æ±‚æå–é‡‡è´­ä¸šåŠ¡æ•°æ®ã€‚
@@ -482,7 +533,7 @@
                1. åªè¾“出合法 JSON,不要 Markdown,不要额外解释。
                2. JSON é¡¶å±‚字段固定为:
                   - success: boolean
                   - businessType: purchase_ledger | payment_registration | purchase_return_order | unknown
                   - businessType: purchase_ledger  | purchase_return_order | unknown
                   - action: confirm_required
                   - description: ä¸­æ–‡è¯´æ˜Ž
                   - confidence: 0到1的小数
@@ -507,7 +558,7 @@
                     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。
                4. å¦‚果可判断为付款登记,businessType ä½¿ç”¨ payload.records ä¸ºä»˜æ¬¾ç™»è®°æ•°ç»„,字段尽量包含 purchaseLedgerId、salesLedgerProductId、currentPaymentAmount、paymentMethod、paymentDate。
                5. å¦‚果可判断为采购退货,businessType ä½¿ç”¨ purchase_return_order,payload æŒ‰ PurchaseReturnOrderDto ç»„织,明细放 purchaseReturnOrderProductsDtos。
                6. ç¼ºå°‘业务处理必须字段时,不要编造 ID,把字段放入 missingFields,并仍返回可确认的草稿数据。
                7. æ‰€æœ‰ä¸­æ–‡å†…容直接保留,不要转义成 Unicode。
@@ -1008,19 +1059,6 @@
        }
        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) {
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
@@ -76,12 +76,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 +125,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 +166,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());
src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2226 @@
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.65");
    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);
        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", range.start().toString());
        summary.put("endDate", range.end().toString());
        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);
        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(new BigDecimal("0.08")) < 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", range.start().toString());
        summary.put("endDate", range.end().toString());
        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", range.start().toString());
        summary.put("endDate", range.end().toString());
        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", range.start().toString());
        summary.put("endDate", range.end().toString());
        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", range.start().toString());
        summary.put("endDate", range.end().toString());
        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(new BigDecimal("1000000")) > 0) {
            riskSuggestions.add(riskSuggestion("回款风险", "中", "对应收TOP客户建立周度回款计划,并设置预警阈值。"));
        }
        if (inventoryValue.compareTo(new BigDecimal("3000000")) > 0) {
            riskSuggestions.add(riskSuggestion("库存风险", "中", "对高金额呆滞库存执行降价、替代和生产消耗策略。"));
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("reportType", type);
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        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)
                .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);
        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())
                .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())
                .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())
                .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));
        }
        wrapper.ge(SalesLedger::getEntryDate, toDate(range.start()))
                .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end()))
                .orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId)
                .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);
        }
        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);
        wrapper.ge(AccountSalesCollection::getCollectionDate, range.start())
                .le(AccountSalesCollection::getCollectionDate, range.end())
                .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);
        wrapper.ge(AccountPurchasePayment::getPaymentDate, range.start())
                .le(AccountPurchasePayment::getPaymentDate, range.end())
                .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<>();
        YearMonth startMonth = YearMonth.from(range.start());
        YearMonth endMonth = YearMonth.from(range.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 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;
            }
        }
        return revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE);
    }
    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) {
        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)) {
            if ("今天".equals(defaultLabel)) {
                return new DateRange(today, today, "今天");
            }
            if ("本周".equals(defaultLabel)) {
                LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
                return new DateRange(start, today, "本周");
            }
            if ("本月".equals(defaultLabel)) {
                return new DateRange(today.withDayOfMonth(1), today, "本月");
            }
            if ("近90天".equals(defaultLabel)) {
                return new DateRange(today.minusDays(89), today, "近90天");
            }
            return new DateRange(today.minusDays(29), today, defaultLabel);
        }
        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(today.minusDays(29), today, "近30天");
    }
    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 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", range.start().toString());
        summary.put("endDate", range.end().toString());
        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 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/PurchaseAgentTools.java
@@ -2,23 +2,29 @@
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.account.mapper.purchase.AccountPaymentApplicationMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchaseInvoiceMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.pojo.purchase.AccountPaymentApplication;
import com.ruoyi.account.pojo.purchase.AccountPurchaseInvoice;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.purchase.mapper.InvoicePurchaseMapper;
import com.ruoyi.purchase.mapper.PaymentRegistrationMapper;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.purchase.pojo.InvoicePurchase;
import com.ruoyi.purchase.pojo.PaymentRegistration;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
import com.ruoyi.procurementrecord.mapper.InboundManagementMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.pojo.InboundManagement;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.pojo.StockInRecord;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
@@ -27,48 +33,52 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Comparator;
import java.util.*;
import java.util.stream.Collectors;
@Component
public class PurchaseAgentTools {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private static final int DEFAULT_LIMIT = 10;
    private static final int MAX_LIMIT = 30;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final PaymentRegistrationMapper paymentRegistrationMapper;
    private final InvoicePurchaseMapper invoicePurchaseMapper;
    private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final ProcurementRecordMapper procurementRecordMapper;
    private final InboundManagementMapper inboundManagementMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final AccountPaymentApplicationMapper accountPaymentApplicationMapper;
    private final AccountPurchaseInvoiceMapper accountPurchaseInvoiceMapper;
    private final StockInRecordMapper stockInRecordMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final AiSessionUserContext aiSessionUserContext;
    public PurchaseAgentTools(PurchaseLedgerMapper purchaseLedgerMapper,
                              PaymentRegistrationMapper paymentRegistrationMapper,
                              InvoicePurchaseMapper invoicePurchaseMapper,
                              PurchaseReturnOrdersMapper purchaseReturnOrdersMapper,
                              SalesLedgerProductMapper salesLedgerProductMapper,
                              ProcurementRecordMapper procurementRecordMapper,
                              InboundManagementMapper inboundManagementMapper,
                              AccountPurchasePaymentMapper accountPurchasePaymentMapper,
                              AccountPaymentApplicationMapper accountPaymentApplicationMapper,
                              AccountPurchaseInvoiceMapper accountPurchaseInvoiceMapper,
                              StockInRecordMapper stockInRecordMapper,
                              QualityInspectMapper qualityInspectMapper,
                              AiSessionUserContext aiSessionUserContext) {
        this.purchaseLedgerMapper = purchaseLedgerMapper;
        this.paymentRegistrationMapper = paymentRegistrationMapper;
        this.invoicePurchaseMapper = invoicePurchaseMapper;
        this.purchaseReturnOrdersMapper = purchaseReturnOrdersMapper;
        this.salesLedgerProductMapper = salesLedgerProductMapper;
        this.procurementRecordMapper = procurementRecordMapper;
        this.inboundManagementMapper = inboundManagementMapper;
        this.accountPurchasePaymentMapper = accountPurchasePaymentMapper;
        this.accountPaymentApplicationMapper = accountPaymentApplicationMapper;
        this.accountPurchaseInvoiceMapper = accountPurchaseInvoiceMapper;
        this.stockInRecordMapper = stockInRecordMapper;
        this.qualityInspectMapper = qualityInspectMapper;
        this.aiSessionUserContext = aiSessionUserContext;
    }
@@ -130,8 +140,8 @@
        DateRange range = resolveDateRange(startDate, endDate, timeRange);
        List<PurchaseLedger> ledgers = queryLedgers(loginUser, range);
        List<PaymentRegistration> payments = queryPayments(loginUser, range);
        List<InvoicePurchase> invoices = queryInvoices(loginUser, range);
        List<AccountPurchasePayment> payments = queryPayments(loginUser, range);
        List<AccountPurchaseInvoice> invoices = queryInvoices(loginUser, range);
        List<PurchaseReturnOrders> returns = queryReturns(loginUser, range);
        BigDecimal contractAmount = ledgers.stream()
@@ -139,11 +149,11 @@
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal paymentAmount = payments.stream()
                .map(PaymentRegistration::getCurrentPaymentAmount)
                .map(AccountPurchasePayment::getPaymentAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal invoiceAmount = invoices.stream()
                .map(InvoicePurchase::getInvoiceAmount)
                .map(this::invoiceAmountOf)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal returnAmount = returns.stream()
@@ -279,15 +289,37 @@
                                           @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, null);
        List<Map<String, Object>> items = queryLedgers(loginUser, range).stream()
        List<PurchaseLedger> matchedLedgers = queryLedgers(loginUser, range).stream()
                .filter(ledger -> matchLedgerKeyword(ledger, keyword))
                .map(ledger -> toPendingPaymentItem(loginUser, ledger))
                .collect(Collectors.toList());
        Map<Long, BigDecimal> paidAmountByLedgerId = sumPaymentAmountByLedgerId(loginUser, matchedLedgers.stream()
                .map(PurchaseLedger::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        List<Map<String, Object>> items = matchedLedgers.stream()
                .map(ledger -> toPendingPaymentItem(ledger, paidAmountByLedgerId.getOrDefault(ledger.getId(), BigDecimal.ZERO)))
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(item -> (BigDecimal) item.get("pendingAmount"), Comparator.reverseOrder()))
                .limit(normalizeLimit(limit))
                .collect(Collectors.toList());
        BigDecimal totalContractAmount = items.stream()
                .map(item -> asBigDecimal(item.get("contractAmount")))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalPaidAmount = items.stream()
                .map(item -> asBigDecimal(item.get("paidAmount")))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalPendingAmount = items.stream()
                .map(item -> asBigDecimal(item.get("pendingAmount")))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        Map<String, Object> summary = rangeSummary(range, items.size());
        summary.put("pendingOrderCount", items.size());
        summary.put("totalContractAmount", totalContractAmount);
        summary.put("totalPaidAmount", totalPaidAmount);
        summary.put("totalPendingAmount", totalPendingAmount);
        return jsonResponse(true, "purchase_pending_payment_list", "已返回待付款采购单。",
                rangeSummary(range, items.size()), Map.of("items", items), Map.of());
                summary, Map.of("items", items), Map.of());
    }
    @Tool(name = "查询采购退货情况", value = "按时间范围查询采购退货单列表和退货金额。")
@@ -406,27 +438,58 @@
        return map;
    }
    private Map<String, Object> toPendingPaymentItem(LoginUser loginUser, PurchaseLedger ledger) {
    private Map<String, Object> toPendingPaymentItem(PurchaseLedger ledger, BigDecimal paidAmount) {
        BigDecimal contractAmount = defaultDecimal(ledger.getContractAmount());
        BigDecimal paidAmount = sumPaymentAmount(loginUser, ledger.getId());
        BigDecimal pendingAmount = contractAmount.subtract(paidAmount);
        BigDecimal safePaidAmount = defaultDecimal(paidAmount);
        BigDecimal pendingAmount = contractAmount.subtract(safePaidAmount);
        if (pendingAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return null;
        }
        Map<String, Object> item = toLedgerItem(ledger);
        item.put("paidAmount", paidAmount);
        item.put("paidAmount", safePaidAmount);
        item.put("pendingAmount", pendingAmount);
        return item;
    }
    private BigDecimal sumPaymentAmount(LoginUser loginUser, Long purchaseLedgerId) {
        LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId);
        wrapper.eq(PaymentRegistration::getPurchaseLedgerId, purchaseLedgerId);
        return defaultList(paymentRegistrationMapper.selectList(wrapper)).stream()
                .map(PaymentRegistration::getCurrentPaymentAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    private Map<Long, BigDecimal> sumPaymentAmountByLedgerId(LoginUser loginUser, List<Long> purchaseLedgerIds) {
        if (purchaseLedgerIds == null || purchaseLedgerIds.isEmpty()) {
            return Map.of();
        }
        List<AccountPurchasePayment> payments = queryPayments(loginUser);
        if (payments.isEmpty()) {
            return Map.of();
        }
        Map<Integer, AccountPaymentApplication> applicationById = queryPaymentApplications(payments);
        if (applicationById.isEmpty()) {
            return Map.of();
        }
        Map<Long, StockInRecord> stockInRecordById = queryStockInRecords(applicationById.values());
        Map<Long, Long> purchaseLedgerIdByQualityInspectId = queryPurchaseLedgerIdByQualityInspectId(stockInRecordById.values());
        Set<Long> targetLedgerIdSet = new HashSet<>(purchaseLedgerIds);
        Map<Long, BigDecimal> result = new HashMap<>();
        for (AccountPurchasePayment payment : payments) {
            if (payment.getAccountPaymentApplicationId() == null) {
                continue;
            }
            AccountPaymentApplication application = applicationById.get(payment.getAccountPaymentApplicationId());
            if (application == null) {
                continue;
            }
            Set<Long> ledgerIds = resolvePurchaseLedgerIds(application, stockInRecordById, purchaseLedgerIdByQualityInspectId);
            if (ledgerIds.isEmpty()) {
                continue;
            }
            BigDecimal amount = defaultDecimal(payment.getPaymentAmount());
            for (Long ledgerId : ledgerIds) {
                if (targetLedgerIdSet.contains(ledgerId)) {
                    result.merge(ledgerId, amount, BigDecimal::add);
                }
            }
        }
        return result;
    }
    private Map<String, Object> toReturnItem(PurchaseReturnOrders item) {
@@ -446,21 +509,141 @@
        return value == null ? BigDecimal.ZERO : value;
    }
    private List<PaymentRegistration> queryPayments(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId);
        wrapper.ge(PaymentRegistration::getPaymentDate, toDate(range.start()))
                .lt(PaymentRegistration::getPaymentDate, toExclusiveEndDate(range.end()));
        return defaultList(paymentRegistrationMapper.selectList(wrapper));
    private BigDecimal asBigDecimal(Object value) {
        if (value == null) {
            return BigDecimal.ZERO;
        }
        if (value instanceof BigDecimal decimal) {
            return decimal;
        }
        if (value instanceof Number number) {
            return new BigDecimal(String.valueOf(number));
        }
        try {
            return new BigDecimal(String.valueOf(value));
        } catch (Exception ignored) {
            return BigDecimal.ZERO;
        }
    }
    private List<InvoicePurchase> queryInvoices(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<InvoicePurchase> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), InvoicePurchase::getTenantId);
        wrapper.ge(InvoicePurchase::getIssueDate, range.start())
                .le(InvoicePurchase::getIssueDate, range.end());
        return defaultList(invoicePurchaseMapper.selectList(wrapper));
    private BigDecimal invoiceAmountOf(AccountPurchaseInvoice invoice) {
        if (invoice == null) {
            return BigDecimal.ZERO;
        }
        BigDecimal amount = defaultDecimal(invoice.getTaxInclusivePrice());
        if (amount.compareTo(BigDecimal.ZERO) > 0) {
            return amount;
        }
        return defaultDecimal(invoice.getTaxExclusivelPrice()).add(defaultDecimal(invoice.getTaxPrice()));
    }
    private List<AccountPurchasePayment> queryPayments(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountPurchasePayment> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchasePayment::getDeptId);
        wrapper.ge(AccountPurchasePayment::getPaymentDate, range.start())
                .le(AccountPurchasePayment::getPaymentDate, range.end())
                .orderByDesc(AccountPurchasePayment::getPaymentDate, AccountPurchasePayment::getId);
        return defaultList(accountPurchasePaymentMapper.selectList(wrapper));
    }
    private List<AccountPurchasePayment> queryPayments(LoginUser loginUser) {
        LambdaQueryWrapper<AccountPurchasePayment> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchasePayment::getDeptId);
        return defaultList(accountPurchasePaymentMapper.selectList(wrapper));
    }
    private List<AccountPurchaseInvoice> queryInvoices(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountPurchaseInvoice> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchaseInvoice::getDeptId);
        wrapper.ge(AccountPurchaseInvoice::getIssueDate, range.start())
                .le(AccountPurchaseInvoice::getIssueDate, range.end())
                .orderByDesc(AccountPurchaseInvoice::getIssueDate, AccountPurchaseInvoice::getId);
        return defaultList(accountPurchaseInvoiceMapper.selectList(wrapper));
    }
    private Map<Integer, AccountPaymentApplication> queryPaymentApplications(List<AccountPurchasePayment> payments) {
        List<Integer> ids = payments.stream()
                .map(AccountPurchasePayment::getAccountPaymentApplicationId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (ids.isEmpty()) {
            return Map.of();
        }
        return defaultList(accountPaymentApplicationMapper.selectBatchIds(ids)).stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(AccountPaymentApplication::getId, item -> item, (a, b) -> a));
    }
    private Map<Long, StockInRecord> queryStockInRecords(Collection<AccountPaymentApplication> applications) {
        Set<Long> stockInRecordIds = new HashSet<>();
        for (AccountPaymentApplication application : applications) {
            stockInRecordIds.addAll(parseLongIds(application.getStockInRecordIds()));
        }
        if (stockInRecordIds.isEmpty()) {
            return Map.of();
        }
        return defaultList(stockInRecordMapper.selectBatchIds(stockInRecordIds)).stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(StockInRecord::getId, item -> item, (a, b) -> a));
    }
    private Map<Long, Long> queryPurchaseLedgerIdByQualityInspectId(Collection<StockInRecord> stockInRecords) {
        Set<Long> qualityInspectIds = stockInRecords.stream()
                .filter(Objects::nonNull)
                .filter(item -> item.getRecordId() != null && "10".equals(safe(item.getRecordType()).trim()))
                .map(StockInRecord::getRecordId)
                .collect(Collectors.toSet());
        if (qualityInspectIds.isEmpty()) {
            return Map.of();
        }
        return defaultList(qualityInspectMapper.selectBatchIds(qualityInspectIds)).stream()
                .filter(item -> item.getId() != null && item.getPurchaseLedgerId() != null)
                .collect(Collectors.toMap(QualityInspect::getId, QualityInspect::getPurchaseLedgerId, (a, b) -> a));
    }
    private Set<Long> resolvePurchaseLedgerIds(AccountPaymentApplication application,
                                               Map<Long, StockInRecord> stockInRecordById,
                                               Map<Long, Long> purchaseLedgerIdByQualityInspectId) {
        Set<Long> result = new LinkedHashSet<>();
        for (Long stockInRecordId : parseLongIds(application.getStockInRecordIds())) {
            StockInRecord stockInRecord = stockInRecordById.get(stockInRecordId);
            if (stockInRecord == null || stockInRecord.getRecordId() == null) {
                continue;
            }
            if (stockInRecord.getApprovalStatus() != null && stockInRecord.getApprovalStatus() != 1) {
                continue;
            }
            String recordType = safe(stockInRecord.getRecordType()).trim();
            if ("7".equals(recordType)) {
                result.add(stockInRecord.getRecordId());
            } else if ("10".equals(recordType)) {
                Long purchaseLedgerId = purchaseLedgerIdByQualityInspectId.get(stockInRecord.getRecordId());
                if (purchaseLedgerId != null) {
                    result.add(purchaseLedgerId);
                }
            }
        }
        return result;
    }
    private List<Long> parseLongIds(String raw) {
        if (!StringUtils.hasText(raw)) {
            return List.of();
        }
        List<Long> result = new ArrayList<>();
        for (String part : raw.split(",")) {
            if (!StringUtils.hasText(part)) {
                continue;
            }
            try {
                result.add(Long.parseLong(part.trim()));
            } catch (Exception ignored) {
            }
        }
        return result;
    }
    private List<PurchaseReturnOrders> queryReturns(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<PurchaseReturnOrders> wrapper = new LambdaQueryWrapper<>();
@@ -484,7 +667,7 @@
    }
    private DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
        LocalDate today = LocalDate.now();
        LocalDate today = LocalDate.now(CHINA_ZONE_ID);
        LocalDate start = parseLocalDate(startDate);
        LocalDate end = parseLocalDate(endDate);
        if (start != null || end != null) {
@@ -501,6 +684,22 @@
            return new DateRange(today.minusDays(29), today, "近30天");
        }
        String text = timeRange.trim();
        if (text.contains("今天")) {
            return new DateRange(today, today, "今天");
        }
        if (text.contains("昨天")) {
            LocalDate yesterday = today.minusDays(1);
            return new DateRange(yesterday, yesterday, "昨天");
        }
        if (text.contains("本周")) {
            LocalDate startOfWeek = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            return new DateRange(startOfWeek, today, "本周");
        }
        if (text.contains("上周")) {
            LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            LocalDate startOfLastWeek = thisWeekStart.minusWeeks(1);
            return new DateRange(startOfLastWeek, startOfLastWeek.plusDays(6), "上周");
        }
        if (text.contains("今年") || text.contains("本年")) {
            return new DateRange(today.withDayOfYear(1), today, "今年");
        }
@@ -538,7 +737,11 @@
        if (!StringUtils.hasText(text)) {
            return null;
        }
        return LocalDate.parse(text.trim(), DATE_FMT);
        try {
            return LocalDate.parse(text.trim(), DATE_FMT);
        } catch (Exception ignored) {
            return null;
        }
    }
    private Date toDate(LocalDate localDate) {
src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1633 @@
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.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.basic.dto.CustomerDto;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.vo.CustomerVo;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesQuotationMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesQuotation;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import com.ruoyi.stock.pojo.StockOutRecord;
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.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Component
public class SalesAgentTools {
    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 BigDecimal ONE_HUNDRED = new BigDecimal("100");
    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 final CustomerMapper customerMapper;
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesQuotationMapper salesQuotationMapper;
    private final ShippingInfoMapper shippingInfoMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final StockOutRecordMapper stockOutRecordMapper;
    private final AiSessionUserContext aiSessionUserContext;
    public SalesAgentTools(CustomerMapper customerMapper,
                           SalesLedgerMapper salesLedgerMapper,
                           SalesQuotationMapper salesQuotationMapper,
                           ShippingInfoMapper shippingInfoMapper,
                           AccountSalesCollectionMapper accountSalesCollectionMapper,
                           StockOutRecordMapper stockOutRecordMapper,
                           AiSessionUserContext aiSessionUserContext) {
        this.customerMapper = customerMapper;
        this.salesLedgerMapper = salesLedgerMapper;
        this.salesQuotationMapper = salesQuotationMapper;
        this.shippingInfoMapper = shippingInfoMapper;
        this.accountSalesCollectionMapper = accountSalesCollectionMapper;
        this.stockOutRecordMapper = stockOutRecordMapper;
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Tool(name = "查询客户档案", value = "按私海/公海类型和关键词查询客户档案列表")
    public String listCustomerProfiles(@ToolMemoryId String memoryId,
                                       @P(value = "客户池类型,可选 private/public", required = false) String seaType,
                                       @P(value = "关键词,可匹配客户名称/联系人/电话", required = false) String keyword,
                                       @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        CustomerDto customerDto = new CustomerDto();
        customerDto.setType(normalizeSeaType(seaType));
        customerDto.setUsageStatus(1L);
        List<CustomerVo> rows = defaultList(customerMapper.list(customerDto, loginUser.getUserId()));
        List<CustomerVo> filtered = rows.stream()
                .filter(item -> matchCustomerKeyword(item, keyword))
                .sorted(Comparator.comparing(CustomerVo::getId, Comparator.nullsLast(Comparator.reverseOrder())))
                .limit(normalizeLimit(limit))
                .collect(Collectors.toList());
        List<Map<String, Object>> items = filtered.stream().map(item -> {
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("id", item.getId());
            map.put("customerName", safe(item.getCustomerName()));
            map.put("customerType", safe(item.getCustomerType()));
            map.put("contactPerson", safe(item.getContactPerson()));
            map.put("contactPhone", safe(item.getContactPhone()));
            map.put("companyPhone", safe(item.getCompanyPhone()));
            map.put("maintainer", safe(item.getMaintainer()));
            map.put("maintenanceTime", formatDate(item.getMaintenanceTime()));
            map.put("usageUserName", safe(item.getUsageUserName()));
            map.put("seaType", customerSeaTypeName(item.getType()));
            map.put("isAssigned", item.getIsAssigned());
            return map;
        }).collect(Collectors.toList());
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("count", items.size());
        summary.put("seaType", seaType == null ? "all" : seaType);
        summary.put("keyword", safe(keyword));
        summary.put("userId", loginUser.getUserId());
        return jsonResponse(true, "sales_customer_profile_list", "已返回客户档案列表", summary, Map.of("items", items), Map.of());
    }
    @Tool(name = "查询销售报价", value = "按关键词和时间范围查询销售报价单")
    public String listSalesQuotations(@ToolMemoryId String memoryId,
                                      @P(value = "关键词,可匹配报价单号/客户/业务员/状态", required = false) String keyword,
                                      @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                      @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                      @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, null);
        LambdaQueryWrapper<SalesQuotation> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), SalesQuotation::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesQuotation::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(SalesQuotation::getQuotationNo, keyword)
                    .or().like(SalesQuotation::getCustomer, keyword)
                    .or().like(SalesQuotation::getSalesperson, keyword)
                    .or().like(SalesQuotation::getStatus, keyword));
        }
        wrapper.ge(SalesQuotation::getQuotationDate, range.start())
                .le(SalesQuotation::getQuotationDate, range.end())
                .orderByDesc(SalesQuotation::getQuotationDate, SalesQuotation::getId)
                .last("limit " + normalizeLimit(limit));
        List<SalesQuotation> rows = defaultList(salesQuotationMapper.selectList(wrapper));
        BigDecimal quotationAmountTotal = rows.stream()
                .map(SalesQuotation::getTotalAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        List<Map<String, Object>> items = rows.stream().map(item -> {
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("id", item.getId());
            map.put("quotationNo", safe(item.getQuotationNo()));
            map.put("customer", safe(item.getCustomer()));
            map.put("salesperson", safe(item.getSalesperson()));
            map.put("quotationDate", formatDate(item.getQuotationDate()));
            map.put("validDate", formatDate(item.getValidDate()));
            map.put("status", safe(item.getStatus()));
            map.put("paymentMethod", safe(item.getPaymentMethod()));
            map.put("deliveryPeriod", safe(item.getDeliveryPeriod()));
            map.put("totalAmount", item.getTotalAmount());
            return map;
        }).collect(Collectors.toList());
        Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
        summary.put("quotationAmountTotal", quotationAmountTotal);
        return jsonResponse(true, "sales_quotation_list", "已返回销售报价列表", summary, Map.of("items", items), Map.of());
    }
    @Tool(name = "查询销售台账", value = "按关键词和时间范围查询销售台账,并返回开票回款与发货状态")
    public String listSalesLedgers(@ToolMemoryId String memoryId,
                                   @P(value = "关键词,可匹配销售合同号/客户合同号/客户/项目", required = false) String keyword,
                                   @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                   @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                   @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, null);
        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));
        }
        wrapper.ge(SalesLedger::getEntryDate, toDate(range.start()))
                .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end()))
                .orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId)
                .last("limit " + normalizeLimit(limit));
        List<SalesLedger> rows = defaultList(salesLedgerMapper.selectList(wrapper));
        if (rows.isEmpty()) {
            return jsonResponse(true, "sales_ledger_list", "未查询到符合条件的销售台账", rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
        }
        List<Long> ledgerIds = rows.stream().map(SalesLedger::getId).filter(Objects::nonNull).collect(Collectors.toList());
        Map<Long, BigDecimal> invoiceAmountByLedgerId = sumInvoiceAmounts(ledgerIds);
        Map<Long, BigDecimal> receiptAmountByLedgerId = sumReceiptAmounts(loginUser, ledgerIds);
        Map<Long, List<ShippingInfo>> shippingByLedgerId = queryShippingsByLedgerIds(loginUser, ledgerIds).stream()
                .collect(Collectors.groupingBy(ShippingInfo::getSalesLedgerId));
        BigDecimal contractAmountTotal = BigDecimal.ZERO;
        BigDecimal invoicedAmountTotal = BigDecimal.ZERO;
        BigDecimal receivedAmountTotal = BigDecimal.ZERO;
        BigDecimal pendingAmountTotal = BigDecimal.ZERO;
        List<Map<String, Object>> items = new ArrayList<>();
        for (SalesLedger ledger : rows) {
            BigDecimal contractAmount = defaultDecimal(ledger.getContractAmount());
            BigDecimal invoicedAmount = invoiceAmountByLedgerId.getOrDefault(ledger.getId(), BigDecimal.ZERO);
            BigDecimal receivedAmount = receiptAmountByLedgerId.getOrDefault(ledger.getId(), BigDecimal.ZERO);
            BigDecimal unbilledAmount = maxZero(contractAmount.subtract(invoicedAmount));
            BigDecimal pendingAmount = maxZero(invoicedAmount.subtract(receivedAmount));
            contractAmountTotal = contractAmountTotal.add(contractAmount);
            invoicedAmountTotal = invoicedAmountTotal.add(invoicedAmount);
            receivedAmountTotal = receivedAmountTotal.add(receivedAmount);
            pendingAmountTotal = pendingAmountTotal.add(pendingAmount);
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("id", ledger.getId());
            item.put("salesContractNo", safe(ledger.getSalesContractNo()));
            item.put("customerContractNo", safe(ledger.getCustomerContractNo()));
            item.put("customerName", safe(ledger.getCustomerName()));
            item.put("projectName", safe(ledger.getProjectName()));
            item.put("salesman", safe(ledger.getSalesman()));
            item.put("entryDate", formatDate(ledger.getEntryDate()));
            item.put("executionDate", formatDate(ledger.getExecutionDate()));
            item.put("deliveryDate", formatDate(ledger.getDeliveryDate()));
            item.put("contractAmount", contractAmount);
            item.put("invoicedAmount", invoicedAmount);
            item.put("receivedAmount", receivedAmount);
            item.put("unbilledAmount", unbilledAmount);
            item.put("pendingAmount", pendingAmount);
            item.put("shippingStatus", calcLedgerShippingStatus(shippingByLedgerId.get(ledger.getId())));
            items.add(item);
        }
        Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
        summary.put("contractAmountTotal", contractAmountTotal);
        summary.put("invoicedAmountTotal", invoicedAmountTotal);
        summary.put("receivedAmountTotal", receivedAmountTotal);
        summary.put("pendingAmountTotal", pendingAmountTotal);
        return jsonResponse(true, "sales_ledger_list", "已返回销售台账列表", summary, Map.of("items", items), Map.of());
    }
    @Tool(name = "查询销售退货", value = "按时间范围和关键词查询销售退货记录")
    public String listSalesReturns(@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 keyword,
                                   @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, null);
        LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(AccountSalesCollection::getCollectionNumber, keyword)
                    .or().like(AccountSalesCollection::getCollectionMethod, keyword)
                    .or().like(AccountSalesCollection::getRemark, keyword));
        }
        wrapper.ge(AccountSalesCollection::getCollectionDate, range.start())
                .le(AccountSalesCollection::getCollectionDate, range.end())
                .orderByDesc(AccountSalesCollection::getCollectionDate, AccountSalesCollection::getId)
                .last("limit " + normalizeLimit(limit));
        List<AccountSalesCollection> rows = defaultList(accountSalesCollectionMapper.selectList(wrapper));
        BigDecimal returnAmount = rows.stream()
                .map(AccountSalesCollection::getCollectionAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        List<Map<String, Object>> items = rows.stream().map(item -> {
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("id", item.getId());
            map.put("refundId", safe(item.getCollectionNumber()));
            map.put("collectionNumber", safe(item.getCollectionNumber()));
            map.put("paymentMethod", safe(item.getCollectionMethod()));
            map.put("actualAmount", item.getCollectionAmount());
            map.put("collectionAmount", item.getCollectionAmount());
            map.put("customerId", item.getCustomerId());
            map.put("remark", safe(item.getRemark()));
            map.put("createTime", formatDate(item.getCollectionDate()));
            return map;
        }).collect(Collectors.toList());
        Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
        summary.put("returnAmount", returnAmount);
        return jsonResponse(true, "sales_return_list", "已返回销售退货记录", summary, Map.of("items", items), Map.of());
    }
    @Tool(name = "查询客户往来", value = "按时间范围和关键词查询客户回款往来明细")
    public String listCustomerInteractions(@ToolMemoryId String memoryId,
                                           @P(value = "关键词,可匹配客户名称/销售合同号/项目名", required = false) String keyword,
                                           @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                           @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                           @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, null);
        List<AccountSalesCollection> collections = queryCollections(loginUser, range);
        if (collections.isEmpty()) {
            return jsonResponse(true, "sales_customer_interaction_list", "no_customer_interactions", rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
        }
        Map<Integer, Set<Long>> ledgerIdsByCollectionId = mapCollectionLedgerIds(loginUser, collections);
        Set<Long> ledgerIds = ledgerIdsByCollectionId.values().stream()
                .flatMap(Collection::stream)
                .collect(Collectors.toSet());
        Map<Long, SalesLedger> ledgerMap = defaultList(salesLedgerMapper.selectBatchIds(ledgerIds)).stream()
                .filter(ledger -> tenantMatched(ledger.getTenantId(), loginUser.getTenantId()))
                .collect(Collectors.toMap(SalesLedger::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
        int finalLimit = normalizeLimit(limit);
        List<Map<String, Object>> items = new ArrayList<>();
        for (AccountSalesCollection collection : collections) {
            Set<Long> relatedLedgerIds = ledgerIdsByCollectionId.get(collection.getId());
            if (relatedLedgerIds == null || relatedLedgerIds.isEmpty()) {
                if (!matchInteractionKeyword(collection, null, keyword)) {
                    continue;
                }
                items.add(toInteractionItem(collection, null));
                if (items.size() >= finalLimit) {
                    break;
                }
                continue;
            }
            for (Long ledgerId : relatedLedgerIds) {
                SalesLedger ledger = ledgerMap.get(ledgerId);
                if (ledger == null || !matchInteractionKeyword(collection, ledger, keyword)) {
                    continue;
                }
                items.add(toInteractionItem(collection, ledger));
                if (items.size() >= finalLimit) {
                    break;
                }
            }
            if (items.size() >= finalLimit) {
                break;
            }
        }
        BigDecimal totalReceiptAmount = items.stream()
                .map(item -> asBigDecimal(item.get("receiptPaymentAmount")))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
        summary.put("totalReceiptAmount", totalReceiptAmount);
        summary.put("customerCount", items.stream()
                .map(item -> String.valueOf(item.get("customerName")))
                .filter(StringUtils::hasText)
                .distinct()
                .count());
        return jsonResponse(true, "sales_customer_interaction_list", "ok", summary, Map.of("items", items), Map.of());
    }
    @Tool(name = "查询发货台账", value = "按关键词和时间范围查询发货台账")
    public String listShippingLedgers(@ToolMemoryId String memoryId,
                                      @P(value = "关键词,可匹配发货单号/快递单号/物流公司/车牌号", required = false) String keyword,
                                      @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                      @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                      @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, null);
        LambdaQueryWrapper<ShippingInfo> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ShippingInfo::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ShippingInfo::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(ShippingInfo::getShippingNo, keyword)
                    .or().like(ShippingInfo::getExpressNumber, keyword)
                    .or().like(ShippingInfo::getExpressCompany, keyword)
                    .or().like(ShippingInfo::getShippingCarNumber, keyword)
                    .or().like(ShippingInfo::getStatus, keyword));
        }
        wrapper.ge(ShippingInfo::getShippingDate, toDate(range.start()))
                .lt(ShippingInfo::getShippingDate, toExclusiveEndDate(range.end()))
                .orderByDesc(ShippingInfo::getShippingDate, ShippingInfo::getId)
                .last("limit " + normalizeLimit(limit));
        List<ShippingInfo> rows = defaultList(shippingInfoMapper.selectList(wrapper));
        if (rows.isEmpty()) {
            return jsonResponse(true, "sales_shipping_list", "未查询到发货台账记录", rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
        }
        List<Long> ledgerIds = rows.stream().map(ShippingInfo::getSalesLedgerId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        Map<Long, SalesLedger> ledgerMap = defaultList(salesLedgerMapper.selectBatchIds(ledgerIds)).stream()
                .filter(ledger -> tenantMatched(ledger.getTenantId(), loginUser.getTenantId()))
                .collect(Collectors.toMap(SalesLedger::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
        long shippedCount = rows.stream().filter(item -> isShippedStatus(item.getStatus())).count();
        List<Map<String, Object>> items = rows.stream().map(item -> {
            SalesLedger ledger = ledgerMap.get(item.getSalesLedgerId());
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("id", item.getId());
            map.put("salesLedgerId", item.getSalesLedgerId());
            map.put("salesContractNo", ledger == null ? "" : safe(ledger.getSalesContractNo()));
            map.put("customerName", ledger == null ? "" : safe(ledger.getCustomerName()));
            map.put("shippingNo", safe(item.getShippingNo()));
            map.put("status", safe(item.getStatus()));
            map.put("shippingDate", formatDate(item.getShippingDate()));
            map.put("type", safe(item.getType()));
            map.put("shippingCarNumber", safe(item.getShippingCarNumber()));
            map.put("expressCompany", safe(item.getExpressCompany()));
            map.put("expressNumber", safe(item.getExpressNumber()));
            return map;
        }).collect(Collectors.toList());
        Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
        summary.put("shippingCount", rows.size());
        summary.put("shippedCount", shippedCount);
        summary.put("pendingCount", Math.max(rows.size() - shippedCount, 0));
        return jsonResponse(true, "sales_shipping_list", "已返回发货台账记录", summary, Map.of("items", items), Map.of());
    }
    @Tool(name = "查询销售指标统计", value = "按时间范围统计销售合同、报价、发货、回款等关键指标")
    public String getSalesDashboard(@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);
        List<SalesLedger> ledgers = querySalesLedgers(loginUser, range);
        List<SalesQuotation> quotations = querySalesQuotations(loginUser, range);
        List<ShippingInfo> shippings = queryShippings(loginUser, range);
        BigDecimal contractAmountTotal = ledgers.stream()
                .map(SalesLedger::getContractAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal quotationAmountTotal = quotations.stream()
                .map(SalesQuotation::getTotalAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        long shippingCount = shippings.size();
        long shippedCount = shippings.stream().filter(item -> isShippedStatus(item.getStatus())).count();
        String shipRate = toRate(shippedCount, shippingCount);
        List<Map<String, Object>> topCustomers = buildTopCustomers(ledgers);
        TrendData trendData = buildContractTrendData(ledgers, 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("orderCount", ledgers.size());
        summary.put("quotationCount", quotations.size());
        summary.put("shippingCount", shippingCount);
        summary.put("shippedCount", shippedCount);
        summary.put("shipRate", shipRate);
        summary.put("contractAmountTotal", contractAmountTotal);
        summary.put("quotationAmountTotal", quotationAmountTotal);
        summary.put("receivedAmountTotal", BigDecimal.ZERO);
        summary.put("pendingAmountTotal", BigDecimal.ZERO);
        Map<String, Object> charts = new LinkedHashMap<>();
//        charts.put("amountBarOption", buildAmountBarOption(contractAmountTotal, quotationAmountTotal, receivedAmountTotal, pendingAmountTotal));
        charts.put("amountBarOption", buildAmountBarOption(contractAmountTotal, quotationAmountTotal, BigDecimal.ONE, BigDecimal.ONE));
        charts.put("shippingPieOption", buildShippingPieOption(shippedCount, Math.max(shippingCount - shippedCount, 0)));
        charts.put("customerTopBarOption", buildCustomerTopBarOption(topCustomers));
        charts.put("contractTrendLineOption", buildContractTrendLineOption(trendData.labels(), trendData.values()));
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("topCustomers", topCustomers);
        data.put("contractTrend", trendData.toItemList());
        return jsonResponse(true, "sales_dashboard", "已返回销售指标统计", summary, data, charts);
    }
    @Tool(name = "客户流失风险分析", value = "按客户维度评估流失风险,输出风险分级、原因和建议优先级")
    public String analyzeCustomerChurnRisk(@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 = "关键词,可匹配客户名称", required = false) String keyword,
                                           @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, StringUtils.hasText(timeRange) ? timeRange : "近180天");
        List<CustomerRiskMetric> metrics = buildCustomerRiskMetrics(loginUser, range, keyword);
        if (metrics.isEmpty()) {
            return jsonResponse(true, "sales_customer_churn_risk", "当前范围内未查询到可分析的客户数据",
                    rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
        }
        List<CustomerRiskMetric> sorted = metrics.stream()
                .sorted(Comparator.comparing(CustomerRiskMetric::getRiskScore).reversed()
                        .thenComparing(CustomerRiskMetric::getPendingAmount, Comparator.reverseOrder()))
                .limit(normalizeLimit(limit))
                .collect(Collectors.toList());
        long highCount = sorted.stream().filter(item -> "high".equals(item.getRiskLevel())).count();
        long mediumCount = sorted.stream().filter(item -> "medium".equals(item.getRiskLevel())).count();
        long lowCount = sorted.stream().filter(item -> "low".equals(item.getRiskLevel())).count();
        List<Map<String, Object>> items = sorted.stream().map(this::toRiskItem).collect(Collectors.toList());
        Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
        summary.put("highRiskCount", highCount);
        summary.put("mediumRiskCount", mediumCount);
        summary.put("lowRiskCount", lowCount);
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("riskLevelPieOption", buildRiskLevelPieOption(highCount, mediumCount, lowCount));
        charts.put("riskScoreBarOption", buildRiskScoreBarOption(sorted));
        return jsonResponse(true, "sales_customer_churn_risk", "已完成客户流失风险分析", summary, Map.of("items", items), charts);
    }
    @Tool(name = "回款与报价策略建议", value = "基于客户风险、回款和报价情况生成可执行的跟进策略")
    public String suggestCollectionAndQuotationStrategy(@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 = "关键词,可匹配客户名称", required = false) String keyword,
                                                        @P(value = "返回条数,默认10,最大30", required = false) Integer limit,
                                                        @P(value = "是否优先高风险客户,true è¡¨ç¤ºé«˜é£Žé™©ä¼˜å…ˆ", required = false) Boolean prioritizeHighRisk) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, StringUtils.hasText(timeRange) ? timeRange : "近90天");
        List<CustomerRiskMetric> metrics = buildCustomerRiskMetrics(loginUser, range, keyword);
        if (metrics.isEmpty()) {
            return jsonResponse(true, "sales_collection_quote_strategy", "当前范围内未查询到可生成策略的客户数据",
                    rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
        }
        boolean highRiskFirst = Boolean.TRUE.equals(prioritizeHighRisk);
        Comparator<CustomerRiskMetric> sortComparator;
        if (highRiskFirst) {
            sortComparator = Comparator
                    .comparingInt((CustomerRiskMetric metric) -> riskLevelRank(metric.getRiskLevel())).reversed()
                    .thenComparing(CustomerRiskMetric::getRiskScore, Comparator.reverseOrder())
                    .thenComparing(CustomerRiskMetric::getPendingAmount, Comparator.reverseOrder());
        } else {
            sortComparator = Comparator
                    .comparing(CustomerRiskMetric::getPendingAmount, Comparator.reverseOrder())
                    .thenComparing(CustomerRiskMetric::getRiskScore, Comparator.reverseOrder());
        }
        List<CustomerRiskMetric> sorted = metrics.stream()
                .sorted(sortComparator)
                .limit(normalizeLimit(limit))
                .collect(Collectors.toList());
        List<Map<String, Object>> items = sorted.stream().map(this::toStrategyItem).collect(Collectors.toList());
        long highPriorityCount = items.stream().filter(item -> "high".equals(item.get("priority"))).count();
        long mediumPriorityCount = items.stream().filter(item -> "medium".equals(item.get("priority"))).count();
        long lowPriorityCount = items.stream().filter(item -> "low".equals(item.get("priority"))).count();
        Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
        summary.put("highPriorityCount", highPriorityCount);
        summary.put("mediumPriorityCount", mediumPriorityCount);
        summary.put("lowPriorityCount", lowPriorityCount);
        summary.put("prioritizeHighRisk", highRiskFirst);
        summary.put("priorityMode", highRiskFirst ? "high_risk_first" : "pending_amount_first");
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("pendingAmountBarOption", buildPendingAmountBarOption(sorted));
        charts.put("priorityPieOption", buildPriorityPieOption(highPriorityCount, mediumPriorityCount, lowPriorityCount));
        return jsonResponse(true, "sales_collection_quote_strategy", "已生成回款与报价策略建议", summary, Map.of("items", items), charts);
    }
    private List<CustomerRiskMetric> buildCustomerRiskMetrics(LoginUser loginUser, DateRange range, String keyword) {
        List<SalesLedger> ledgers = querySalesLedgers(loginUser, range).stream()
                .filter(item -> matchLedgerCustomerKeyword(item, keyword))
                .collect(Collectors.toList());
        if (ledgers.isEmpty()) {
            return List.of();
        }
        Map<String, CustomerRiskMetric> metricMap = new LinkedHashMap<>();
        for (SalesLedger ledger : ledgers) {
            String customerName = StringUtils.hasText(ledger.getCustomerName()) ? ledger.getCustomerName().trim() : "未知客户";
            CustomerRiskMetric metric = metricMap.computeIfAbsent(customerName, CustomerRiskMetric::new);
            metric.setOrderCount(metric.getOrderCount() + 1);
            metric.setContractAmount(metric.getContractAmount().add(defaultDecimal(ledger.getContractAmount())));
            metric.setTopSingleOrderAmount(metric.getTopSingleOrderAmount().max(defaultDecimal(ledger.getContractAmount())));
            LocalDate entryDate = toLocalDate(ledger.getEntryDate());
            if (entryDate != null && (metric.getLastOrderDate() == null || entryDate.isAfter(metric.getLastOrderDate()))) {
                metric.setLastOrderDate(entryDate);
            }
            if (ledger.getId() != null) {
                metric.getLedgerIds().add(ledger.getId());
                if (ledger.getDeliveryDate() != null) {
                    metric.getDeliveryDateByLedgerId().put(ledger.getId(), ledger.getDeliveryDate());
                }
            }
        }
        List<Long> allLedgerIds = metricMap.values().stream()
                .flatMap(metric -> metric.getLedgerIds().stream())
                .distinct()
                .collect(Collectors.toList());
        Map<Long, BigDecimal> receiptAmountByLedgerId = sumReceiptAmounts(loginUser, allLedgerIds);
        Map<Long, List<ShippingInfo>> shippingByLedgerId = queryShippingsByLedgerIds(loginUser, allLedgerIds).stream()
                .collect(Collectors.groupingBy(ShippingInfo::getSalesLedgerId));
        List<SalesQuotation> quotations = querySalesQuotations(loginUser, range);
        for (SalesQuotation quotation : quotations) {
            String customerName = safe(quotation.getCustomer());
            CustomerRiskMetric metric = metricMap.get(customerName);
            if (metric == null) {
                continue;
            }
            metric.setQuoteCount(metric.getQuoteCount() + 1);
            metric.setQuoteAmount(metric.getQuoteAmount().add(defaultDecimal(quotation.getTotalAmount())));
        }
        LocalDate today = LocalDate.now();
        for (CustomerRiskMetric metric : metricMap.values()) {
            BigDecimal receivedAmount = BigDecimal.ZERO;
            long overdueDeliveryCount = 0;
            for (Long ledgerId : metric.getLedgerIds()) {
                receivedAmount = receivedAmount.add(receiptAmountByLedgerId.getOrDefault(ledgerId, BigDecimal.ZERO));
                LocalDate deliveryDate = metric.getDeliveryDateByLedgerId().get(ledgerId);
                if (deliveryDate != null && deliveryDate.isBefore(today) && !isLedgerFullyShipped(ledgerId, shippingByLedgerId)) {
                    overdueDeliveryCount++;
                }
            }
            metric.setReceivedAmount(receivedAmount);
            metric.setPendingAmount(maxZero(metric.getContractAmount().subtract(receivedAmount)));
            if (metric.getContractAmount().compareTo(BigDecimal.ZERO) > 0) {
                metric.setPendingRate(metric.getPendingAmount()
                        .divide(metric.getContractAmount(), 4, RoundingMode.HALF_UP));
            } else {
                metric.setPendingRate(BigDecimal.ZERO);
            }
            metric.setOverdueDeliveryCount(overdueDeliveryCount);
            if (metric.getLastOrderDate() == null) {
                metric.setDaysSinceLastOrder(999);
            } else {
                metric.setDaysSinceLastOrder(Math.max(today.toEpochDay() - metric.getLastOrderDate().toEpochDay(), 0));
            }
            evaluateRiskMetric(metric);
        }
        return new ArrayList<>(metricMap.values());
    }
    private void evaluateRiskMetric(CustomerRiskMetric metric) {
        int score = 0;
        List<String> reasons = new ArrayList<>();
        if (metric.getDaysSinceLastOrder() >= 90) {
            score += 35;
            reasons.add("近90天无新增订单");
        } else if (metric.getDaysSinceLastOrder() >= 60) {
            score += 25;
            reasons.add("近60天订单活跃度下降");
        } else if (metric.getDaysSinceLastOrder() >= 30) {
            score += 12;
            reasons.add("近30天订单波动偏弱");
        }
        if (metric.getPendingRate().compareTo(new BigDecimal("0.60")) >= 0) {
            score += 30;
            reasons.add("待回款占比高于60%");
        } else if (metric.getPendingRate().compareTo(new BigDecimal("0.30")) >= 0) {
            score += 20;
            reasons.add("待回款占比高于30%");
        } else if (metric.getPendingRate().compareTo(new BigDecimal("0.10")) >= 0) {
            score += 10;
            reasons.add("存在待回款风险");
        }
        if (metric.getOverdueDeliveryCount() > 0) {
            score += Math.min((int) metric.getOverdueDeliveryCount() * 6, 20);
            reasons.add("存在交期逾期订单");
        }
        if (metric.getOrderCount() <= 1) {
            score += 8;
            reasons.add("订单基数偏低");
        }
        if (metric.getQuoteCount() > 0 && metric.getOrderCount() == 0) {
            score += 10;
            reasons.add("报价未形成订单转化");
        }
        score = Math.min(score, 100);
        metric.setRiskScore(score);
        if (score >= 70) {
            metric.setRiskLevel("high");
        } else if (score >= 40) {
            metric.setRiskLevel("medium");
        } else {
            metric.setRiskLevel("low");
        }
        metric.setRiskReasons(reasons);
    }
    private Map<String, Object> toRiskItem(CustomerRiskMetric metric) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("customerName", metric.getCustomerName());
        map.put("riskLevel", metric.getRiskLevel());
        map.put("riskScore", metric.getRiskScore());
        map.put("contractAmount", metric.getContractAmount());
        map.put("receivedAmount", metric.getReceivedAmount());
        map.put("pendingAmount", metric.getPendingAmount());
        map.put("pendingRate", toPercent(metric.getPendingRate()));
        map.put("orderCount", metric.getOrderCount());
        map.put("quoteCount", metric.getQuoteCount());
        map.put("overdueDeliveryCount", metric.getOverdueDeliveryCount());
        map.put("daysSinceLastOrder", metric.getDaysSinceLastOrder());
        map.put("lastOrderDate", formatDate(metric.getLastOrderDate()));
        map.put("riskReasons", metric.getRiskReasons());
        return map;
    }
    private Map<String, Object> toStrategyItem(CustomerRiskMetric metric) {
        String priority = strategyPriority(metric);
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("customerName", metric.getCustomerName());
        map.put("riskLevel", metric.getRiskLevel());
        map.put("riskScore", metric.getRiskScore());
        map.put("priority", priority);
        map.put("pendingAmount", metric.getPendingAmount());
        map.put("pendingRate", toPercent(metric.getPendingRate()));
        map.put("quoteCount", metric.getQuoteCount());
        map.put("orderCount", metric.getOrderCount());
        map.put("quoteConversionRate", toRate(metric.getOrderCount(), Math.max(metric.getQuoteCount(), 1)));
        map.put("collectionStrategy", buildCollectionStrategy(metric));
        map.put("quotationStrategy", buildQuotationStrategy(metric));
        map.put("nextAction", buildNextAction(priority));
        map.put("topSingleOrderAmount", metric.getTopSingleOrderAmount());
        return map;
    }
    private String buildCollectionStrategy(CustomerRiskMetric metric) {
        if (metric.getPendingAmount().compareTo(BigDecimal.ZERO) <= 0) {
            return "保持正常月度对账与回款确认,维持客户回款节奏。";
        }
        if (metric.getPendingRate().compareTo(new BigDecimal("0.60")) >= 0) {
            return "优先锁定回款计划,按周拆分回款节点并绑定发货条件,避免新增信用敞口。";
        }
        if (metric.getPendingRate().compareTo(new BigDecimal("0.30")) >= 0) {
            return "建议执行双周催收机制,同步财务与业务联合跟进重点合同。";
        }
        return "保持正常催收节奏,按合同节点提前3天提醒客户付款。";
    }
    private String buildQuotationStrategy(CustomerRiskMetric metric) {
        if ("high".equals(metric.getRiskLevel())) {
            return "报价优先保毛利与回款条款,减少超长账期,必要时采用分阶段报价。";
        }
        if (metric.getQuoteCount() > 0 && metric.getOrderCount() < metric.getQuoteCount()) {
            return "优化报价结构,建议提供基础版+升级版组合报价,提高转化率。";
        }
        if (metric.getOrderCount() <= 1) {
            return "加强需求挖掘,围绕客户场景补充增值项与交付保障条款。";
        }
        return "保持当前报价策略,重点围绕交期和服务能力做差异化呈现。";
    }
    private String buildNextAction(String priority) {
        return switch (priority) {
            case "high" -> "48小时内完成客户回访,确认回款计划并复核报价有效期。";
            case "medium" -> "本周内完成客户需求复盘,更新报价版本并同步回款节点。";
            default -> "保持月度例行跟进,持续追踪客户采购计划变化。";
        };
    }
    private String strategyPriority(CustomerRiskMetric metric) {
        if ("high".equals(metric.getRiskLevel()) || metric.getPendingRate().compareTo(new BigDecimal("0.50")) >= 0) {
            return "high";
        }
        if ("medium".equals(metric.getRiskLevel()) || metric.getPendingRate().compareTo(new BigDecimal("0.30")) >= 0) {
            return "medium";
        }
        return "low";
    }
    private int riskLevelRank(String riskLevel) {
        if ("high".equals(riskLevel)) {
            return 3;
        }
        if ("medium".equals(riskLevel)) {
            return 2;
        }
        return 1;
    }
    private List<Map<String, Object>> buildTopCustomers(List<SalesLedger> ledgers) {
        Map<String, BigDecimal> grouped = new LinkedHashMap<>();
        for (SalesLedger ledger : ledgers) {
            String customerName = StringUtils.hasText(ledger.getCustomerName()) ? ledger.getCustomerName().trim() : "未知客户";
            grouped.merge(customerName, defaultDecimal(ledger.getContractAmount()), BigDecimal::add);
        }
        return grouped.entrySet().stream()
                .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
                .limit(5)
                .map(entry -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("customerName", entry.getKey());
                    map.put("contractAmount", entry.getValue());
                    return map;
                })
                .collect(Collectors.toList());
    }
    private TrendData buildContractTrendData(List<SalesLedger> ledgers, DateRange range) {
        Map<String, BigDecimal> amountByMonth = new LinkedHashMap<>();
        YearMonth startMonth = YearMonth.from(range.start());
        YearMonth endMonth = YearMonth.from(range.end());
        for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) {
            amountByMonth.put(month.toString(), BigDecimal.ZERO);
        }
        for (SalesLedger ledger : ledgers) {
            LocalDate entryDate = toLocalDate(ledger.getEntryDate());
            if (entryDate == null) {
                continue;
            }
            String monthKey = YearMonth.from(entryDate).toString();
            if (!amountByMonth.containsKey(monthKey)) {
                continue;
            }
            amountByMonth.put(monthKey, amountByMonth.get(monthKey).add(defaultDecimal(ledger.getContractAmount())));
        }
        return new TrendData(new ArrayList<>(amountByMonth.keySet()), new ArrayList<>(amountByMonth.values()));
    }
    private List<SalesLedger> querySalesLedgers(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<SalesLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), SalesLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedger::getDeptId);
        if (range != null) {
            wrapper.ge(SalesLedger::getEntryDate, toDate(range.start()))
                    .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end()));
        }
        return defaultList(salesLedgerMapper.selectList(wrapper));
    }
    private List<SalesQuotation> querySalesQuotations(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<SalesQuotation> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), SalesQuotation::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesQuotation::getDeptId);
        if (range != null) {
            wrapper.ge(SalesQuotation::getQuotationDate, range.start())
                    .le(SalesQuotation::getQuotationDate, range.end());
        }
        return defaultList(salesQuotationMapper.selectList(wrapper));
    }
    private List<ShippingInfo> queryShippings(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ShippingInfo> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ShippingInfo::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ShippingInfo::getDeptId);
        if (range != null) {
            wrapper.ge(ShippingInfo::getShippingDate, toDate(range.start()))
                    .lt(ShippingInfo::getShippingDate, toExclusiveEndDate(range.end()));
        }
        return defaultList(shippingInfoMapper.selectList(wrapper));
    }
    private List<ShippingInfo> queryShippingsByLedgerIds(LoginUser loginUser, List<Long> ledgerIds) {
        if (ledgerIds == null || ledgerIds.isEmpty()) {
            return List.of();
        }
        LambdaQueryWrapper<ShippingInfo> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ShippingInfo::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ShippingInfo::getDeptId);
        wrapper.in(ShippingInfo::getSalesLedgerId, ledgerIds);
        return defaultList(shippingInfoMapper.selectList(wrapper));
    }
    private Map<Long, BigDecimal> sumInvoiceAmounts(List<Long> ledgerIds) {
        if (ledgerIds == null || ledgerIds.isEmpty()) {
            return Map.of();
        }
        Map<Long, BigDecimal> result = new HashMap<>();
        return result;
    }
    private Map<Long, BigDecimal> sumReceiptAmounts(LoginUser loginUser, List<Long> ledgerIds) {
        if (ledgerIds == null || ledgerIds.isEmpty()) {
            return Map.of();
        }
        List<SalesLedger> ledgers = defaultList(salesLedgerMapper.selectBatchIds(ledgerIds)).stream()
                .filter(ledger -> tenantMatched(ledger.getTenantId(), loginUser.getTenantId()))
                .collect(Collectors.toList());
        if (ledgers.isEmpty()) {
            return Map.of();
        }
        Set<Integer> customerIds = ledgers.stream()
                .map(SalesLedger::getCustomerId)
                .filter(Objects::nonNull)
                .map(Long::intValue)
                .collect(Collectors.toSet());
        if (customerIds.isEmpty()) {
            return Map.of();
        }
        LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId);
        wrapper.in(AccountSalesCollection::getCustomerId, customerIds);
        List<AccountSalesCollection> collections = defaultList(accountSalesCollectionMapper.selectList(wrapper));
        if (collections.isEmpty()) {
            return Map.of();
        }
        Map<Integer, Set<Long>> ledgerIdsByCollectionId = mapCollectionLedgerIds(loginUser, collections);
        Map<Long, List<Long>> ledgerIdsByCustomerId = ledgers.stream()
                .filter(item -> item.getId() != null && item.getCustomerId() != null)
                .collect(Collectors.groupingBy(item -> item.getCustomerId().longValue(),
                        Collectors.mapping(SalesLedger::getId, Collectors.toList())));
        Set<Long> targetLedgerIdSet = new HashSet<>(ledgerIds);
        Map<Long, BigDecimal> result = new HashMap<>();
        for (AccountSalesCollection collection : collections) {
            BigDecimal amount = defaultDecimal(collection.getCollectionAmount());
            if (amount.compareTo(BigDecimal.ZERO) == 0) {
                continue;
            }
            Set<Long> relatedLedgerIds = ledgerIdsByCollectionId.getOrDefault(collection.getId(), Set.of());
            if (!relatedLedgerIds.isEmpty()) {
                for (Long ledgerId : relatedLedgerIds) {
                    if (targetLedgerIdSet.contains(ledgerId)) {
                        result.merge(ledgerId, amount, BigDecimal::add);
                    }
                }
                continue;
            }
            if (collection.getCustomerId() == null) {
                continue;
            }
            List<Long> customerLedgerIds = ledgerIdsByCustomerId.get(collection.getCustomerId().longValue());
            if (customerLedgerIds == null || customerLedgerIds.isEmpty()) {
                continue;
            }
            for (Long ledgerId : customerLedgerIds) {
                if (targetLedgerIdSet.contains(ledgerId)) {
                    result.merge(ledgerId, amount, BigDecimal::add);
                }
            }
        }
        return result;
    }
    private List<AccountSalesCollection> queryCollections(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId);
        if (range != null) {
            wrapper.ge(AccountSalesCollection::getCollectionDate, range.start())
                    .le(AccountSalesCollection::getCollectionDate, range.end());
        }
        wrapper.orderByDesc(AccountSalesCollection::getCollectionDate, AccountSalesCollection::getId);
        return defaultList(accountSalesCollectionMapper.selectList(wrapper));
    }
    private Map<Integer, Set<Long>> mapCollectionLedgerIds(LoginUser loginUser, List<AccountSalesCollection> collections) {
        Map<Integer, Set<Long>> result = new HashMap<>();
        if (collections == null || collections.isEmpty()) {
            return result;
        }
        Map<Integer, List<Long>> stockOutRecordIdsByCollection = new HashMap<>();
        Set<Long> allStockOutRecordIds = new HashSet<>();
        for (AccountSalesCollection collection : collections) {
            if (collection.getId() == null) {
                continue;
            }
            List<Long> stockOutRecordIds = parseLongIds(collection.getStockOutRecordIds());
            if (stockOutRecordIds.isEmpty()) {
                continue;
            }
            stockOutRecordIdsByCollection.put(collection.getId(), stockOutRecordIds);
            allStockOutRecordIds.addAll(stockOutRecordIds);
        }
        if (allStockOutRecordIds.isEmpty()) {
            return result;
        }
        List<StockOutRecord> stockOutRecords = defaultList(stockOutRecordMapper.selectList(new LambdaQueryWrapper<StockOutRecord>()
                .in(StockOutRecord::getId, allStockOutRecordIds)));
        if (stockOutRecords.isEmpty()) {
            return result;
        }
        Map<Long, StockOutRecord> stockOutRecordMap = stockOutRecords.stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(StockOutRecord::getId, item -> item, (a, b) -> a));
        Set<Long> shippingIds = stockOutRecords.stream()
                .filter(this::isSalesOutboundRecord)
                .map(StockOutRecord::getRecordId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (shippingIds.isEmpty()) {
            return result;
        }
        LambdaQueryWrapper<ShippingInfo> shippingWrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(shippingWrapper, loginUser.getTenantId(), ShippingInfo::getTenantId);
        applyDeptFilter(shippingWrapper, loginUser.getCurrentDeptId(), ShippingInfo::getDeptId);
        shippingWrapper.in(ShippingInfo::getId, shippingIds);
        Map<Long, Long> ledgerIdByShippingId = defaultList(shippingInfoMapper.selectList(shippingWrapper)).stream()
                .filter(item -> item.getId() != null && item.getSalesLedgerId() != null)
                .collect(Collectors.toMap(ShippingInfo::getId, ShippingInfo::getSalesLedgerId, (a, b) -> a));
        for (Map.Entry<Integer, List<Long>> entry : stockOutRecordIdsByCollection.entrySet()) {
            Set<Long> ledgerIds = new LinkedHashSet<>();
            for (Long stockOutRecordId : entry.getValue()) {
                StockOutRecord stockOutRecord = stockOutRecordMap.get(stockOutRecordId);
                if (!isSalesOutboundRecord(stockOutRecord)) {
                    continue;
                }
                Long ledgerId = ledgerIdByShippingId.get(stockOutRecord.getRecordId());
                if (ledgerId != null) {
                    ledgerIds.add(ledgerId);
                }
            }
            if (!ledgerIds.isEmpty()) {
                result.put(entry.getKey(), ledgerIds);
            }
        }
        return result;
    }
    private boolean isSalesOutboundRecord(StockOutRecord stockOutRecord) {
        if (stockOutRecord == null || !StringUtils.hasText(stockOutRecord.getRecordType())) {
            return false;
        }
        if (stockOutRecord.getApprovalStatus() != null && stockOutRecord.getApprovalStatus() != 1) {
            return false;
        }
        return "13".equals(stockOutRecord.getRecordType().trim());
    }
    private List<Long> parseLongIds(String raw) {
        if (!StringUtils.hasText(raw)) {
            return List.of();
        }
        List<Long> result = new ArrayList<>();
        for (String part : raw.split(",")) {
            if (!StringUtils.hasText(part)) {
                continue;
            }
            try {
                result.add(Long.parseLong(part.trim()));
            } catch (Exception ignored) {
            }
        }
        return result;
    }
    private boolean matchInteractionKeyword(AccountSalesCollection collection, SalesLedger ledger, String keyword) {
        if (!StringUtils.hasText(keyword)) {
            return true;
        }
        String text = keyword.trim();
        if (safe(collection.getCollectionNumber()).contains(text)
                || safe(collection.getCollectionMethod()).contains(text)
                || safe(collection.getRemark()).contains(text)) {
            return true;
        }
        if (ledger == null) {
            return false;
        }
        return safe(ledger.getSalesContractNo()).contains(text)
                || safe(ledger.getCustomerName()).contains(text)
                || safe(ledger.getProjectName()).contains(text);
    }
    private Map<String, Object> toInteractionItem(AccountSalesCollection collection, SalesLedger ledger) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", collection.getId());
        map.put("salesLedgerId", ledger == null ? null : ledger.getId());
        map.put("salesContractNo", ledger == null ? "" : safe(ledger.getSalesContractNo()));
        map.put("customerName", ledger == null ? "" : safe(ledger.getCustomerName()));
        map.put("projectName", ledger == null ? "" : safe(ledger.getProjectName()));
        map.put("receiptPaymentDate", formatDate(collection.getCollectionDate()));
        map.put("receiptPaymentAmount", collection.getCollectionAmount());
        map.put("receiptPaymentType", safe(collection.getCollectionMethod()));
        map.put("collectionNumber", safe(collection.getCollectionNumber()));
        map.put("registrant", collection.getCreateUser());
        map.put("remark", safe(collection.getRemark()));
        return map;
    }
    private boolean isLedgerFullyShipped(Long ledgerId, Map<Long, List<ShippingInfo>> shippingByLedgerId) {
        List<ShippingInfo> shippingInfos = shippingByLedgerId.get(ledgerId);
        if (shippingInfos == null || shippingInfos.isEmpty()) {
            return false;
        }
        return shippingInfos.stream().allMatch(item -> isShippedStatus(item.getStatus()));
    }
    private String calcLedgerShippingStatus(List<ShippingInfo> shippingInfos) {
        if (shippingInfos == null || shippingInfos.isEmpty()) {
            return "未发货";
        }
        long shippedCount = shippingInfos.stream().filter(item -> isShippedStatus(item.getStatus())).count();
        if (shippedCount == 0) {
            return "待发货";
        }
        if (shippedCount == shippingInfos.size()) {
            return "已发货";
        }
        return "部分发货";
    }
    private boolean isShippedStatus(String status) {
        return StringUtils.hasText(status) && status.contains("已发货");
    }
    private boolean matchCustomerKeyword(CustomerVo customer, String keyword) {
        if (!StringUtils.hasText(keyword)) {
            return true;
        }
        String text = keyword.trim();
        return safe(customer.getCustomerName()).contains(text)
                || safe(customer.getContactPerson()).contains(text)
                || safe(customer.getContactPhone()).contains(text)
                || safe(customer.getCompanyPhone()).contains(text)
                || safe(customer.getUsageUserName()).contains(text);
    }
    private boolean matchLedgerCustomerKeyword(SalesLedger ledger, String keyword) {
        if (!StringUtils.hasText(keyword)) {
            return true;
        }
        String text = keyword.trim();
        return safe(ledger.getCustomerName()).contains(text)
                || safe(ledger.getSalesContractNo()).contains(text)
                || safe(ledger.getProjectName()).contains(text);
    }
    private Integer normalizeSeaType(String seaType) {
        if (!StringUtils.hasText(seaType)) {
            return null;
        }
        String value = seaType.trim().toLowerCase(Locale.ROOT);
        return switch (value) {
            case "private", "私海", "0" -> 0;
            case "public", "公海", "1" -> 1;
            default -> null;
        };
    }
    private String customerSeaTypeName(Integer type) {
        if (type == null) {
            return "未知";
        }
        return type == 1 ? "公海" : "私海";
    }
    private int normalizeLimit(Integer limit) {
        if (limit == null || limit <= 0) {
            return DEFAULT_LIMIT;
        }
        return Math.min(limit, MAX_LIMIT);
    }
    private boolean tenantMatched(Long dataTenantId, Long userTenantId) {
        if (userTenantId == null) {
            return true;
        }
        return Objects.equals(dataTenantId, userTenantId);
    }
    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 DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
        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(today.minusDays(29), today, "近30天");
        }
        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, "今年");
        }
        if (text.contains("去年")) {
            LocalDate start = today.minusYears(1).withDayOfYear(1);
            LocalDate end = today.minusYears(1).withMonth(12).withDayOfMonth(31);
            return new DateRange(start, end, "去年");
        }
        Matcher relativeMatcher = 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(today.minusDays(29), today, "近30天");
    }
    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(Date date) {
        LocalDate localDate = toLocalDate(date);
        return formatDate(localDate);
    }
    private String formatDate(LocalDate date) {
        return date == null ? "" : date.format(DATE_FMT);
    }
    private String formatDateTime(LocalDateTime time) {
        return time == null ? "" : time.toString().replace('T', ' ');
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private BigDecimal asBigDecimal(Object value) {
        if (value == null) {
            return BigDecimal.ZERO;
        }
        if (value instanceof BigDecimal decimal) {
            return decimal;
        }
        if (value instanceof Number number) {
            return new BigDecimal(String.valueOf(number));
        }
        try {
            return new BigDecimal(String.valueOf(value));
        } catch (Exception ignored) {
            return BigDecimal.ZERO;
        }
    }
    private BigDecimal maxZero(BigDecimal value) {
        return value == null || value.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : value;
    }
    private String toRate(long numerator, long denominator) {
        if (denominator <= 0) {
            return "0.00%";
        }
        BigDecimal rate = new BigDecimal(numerator)
                .multiply(ONE_HUNDRED)
                .divide(new BigDecimal(denominator), 2, RoundingMode.HALF_UP);
        return rate.toPlainString() + "%";
    }
    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 String safe(Object value) {
        return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ').trim();
    }
    private <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    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> buildAmountBarOption(BigDecimal contractAmount,
                                                      BigDecimal quotationAmount,
                                                      BigDecimal receivedAmount,
                                                      BigDecimal pendingAmount) {
        List<String> xData = List.of("合同额", "报价额", "回款额", "待回款");
        List<BigDecimal> yData = List.of(contractAmount, quotationAmount, receivedAmount, pendingAmount);
        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> buildShippingPieOption(long shippedCount, long pendingCount) {
        List<Map<String, Object>> data = List.of(
                Map.of("name", "已发货", "value", shippedCount),
                Map.of("name", "未发货", "value", pendingCount)
        );
        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> buildCustomerTopBarOption(List<Map<String, Object>> topCustomers) {
        List<String> xData = new ArrayList<>();
        List<BigDecimal> yData = new ArrayList<>();
        for (Map<String, Object> item : topCustomers) {
            xData.add(String.valueOf(item.get("customerName")));
            yData.add((BigDecimal) item.get("contractAmount"));
        }
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "客户合同额TOP5", "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> buildContractTrendLineOption(List<String> labels, List<BigDecimal> 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", labels));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "合同额", "type", "line", "smooth", true, "data", values)));
        return option;
    }
    private Map<String, Object> buildRiskLevelPieOption(long highCount, long mediumCount, long lowCount) {
        List<Map<String, Object>> data = List.of(
                Map.of("name", "高风险", "value", highCount),
                Map.of("name", "中风险", "value", mediumCount),
                Map.of("name", "低风险", "value", lowCount)
        );
        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> buildRiskScoreBarOption(List<CustomerRiskMetric> metrics) {
        List<String> xData = metrics.stream().map(CustomerRiskMetric::getCustomerName).collect(Collectors.toList());
        List<Integer> yData = metrics.stream().map(CustomerRiskMetric::getRiskScore).collect(Collectors.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", "max", 100));
        option.put("series", List.of(Map.of("name", "风险分值", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildPendingAmountBarOption(List<CustomerRiskMetric> metrics) {
        List<String> xData = metrics.stream().map(CustomerRiskMetric::getCustomerName).collect(Collectors.toList());
        List<BigDecimal> yData = metrics.stream().map(CustomerRiskMetric::getPendingAmount).collect(Collectors.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> buildPriorityPieOption(long high, long medium, long low) {
        List<Map<String, Object>> data = List.of(
                Map.of("name", "高优先级", "value", high),
                Map.of("name", "中优先级", "value", medium),
                Map.of("name", "低优先级", "value", low)
        );
        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 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 record TrendData(List<String> labels, List<BigDecimal> values) {
        private List<Map<String, Object>> toItemList() {
            List<Map<String, Object>> items = new LinkedList<>();
            for (int i = 0; i < labels.size(); i++) {
                Map<String, Object> item = new LinkedHashMap<>();
                item.put("month", labels.get(i));
                item.put("amount", values.get(i));
                items.add(item);
            }
            return items;
        }
    }
    private static class CustomerRiskMetric {
        private final String customerName;
        private final List<Long> ledgerIds = new ArrayList<>();
        private final Map<Long, LocalDate> deliveryDateByLedgerId = new HashMap<>();
        private BigDecimal contractAmount = BigDecimal.ZERO;
        private BigDecimal receivedAmount = BigDecimal.ZERO;
        private BigDecimal pendingAmount = BigDecimal.ZERO;
        private BigDecimal pendingRate = BigDecimal.ZERO;
        private BigDecimal quoteAmount = BigDecimal.ZERO;
        private BigDecimal topSingleOrderAmount = BigDecimal.ZERO;
        private int orderCount;
        private int quoteCount;
        private LocalDate lastOrderDate;
        private long daysSinceLastOrder;
        private long overdueDeliveryCount;
        private int riskScore;
        private String riskLevel = "low";
        private List<String> riskReasons = new ArrayList<>();
        private CustomerRiskMetric(String customerName) {
            this.customerName = customerName;
        }
        private String getCustomerName() {
            return customerName;
        }
        private List<Long> getLedgerIds() {
            return ledgerIds;
        }
        private Map<Long, LocalDate> getDeliveryDateByLedgerId() {
            return deliveryDateByLedgerId;
        }
        private BigDecimal getContractAmount() {
            return contractAmount;
        }
        private void setContractAmount(BigDecimal contractAmount) {
            this.contractAmount = contractAmount;
        }
        private BigDecimal getReceivedAmount() {
            return receivedAmount;
        }
        private void setReceivedAmount(BigDecimal receivedAmount) {
            this.receivedAmount = receivedAmount;
        }
        private BigDecimal getPendingAmount() {
            return pendingAmount;
        }
        private void setPendingAmount(BigDecimal pendingAmount) {
            this.pendingAmount = pendingAmount;
        }
        private BigDecimal getPendingRate() {
            return pendingRate;
        }
        private void setPendingRate(BigDecimal pendingRate) {
            this.pendingRate = pendingRate;
        }
        private BigDecimal getQuoteAmount() {
            return quoteAmount;
        }
        private void setQuoteAmount(BigDecimal quoteAmount) {
            this.quoteAmount = quoteAmount;
        }
        private BigDecimal getTopSingleOrderAmount() {
            return topSingleOrderAmount;
        }
        private void setTopSingleOrderAmount(BigDecimal topSingleOrderAmount) {
            this.topSingleOrderAmount = topSingleOrderAmount;
        }
        private int getOrderCount() {
            return orderCount;
        }
        private void setOrderCount(int orderCount) {
            this.orderCount = orderCount;
        }
        private int getQuoteCount() {
            return quoteCount;
        }
        private void setQuoteCount(int quoteCount) {
            this.quoteCount = quoteCount;
        }
        private LocalDate getLastOrderDate() {
            return lastOrderDate;
        }
        private void setLastOrderDate(LocalDate lastOrderDate) {
            this.lastOrderDate = lastOrderDate;
        }
        private long getDaysSinceLastOrder() {
            return daysSinceLastOrder;
        }
        private void setDaysSinceLastOrder(long daysSinceLastOrder) {
            this.daysSinceLastOrder = daysSinceLastOrder;
        }
        private long getOverdueDeliveryCount() {
            return overdueDeliveryCount;
        }
        private void setOverdueDeliveryCount(long overdueDeliveryCount) {
            this.overdueDeliveryCount = overdueDeliveryCount;
        }
        private int getRiskScore() {
            return riskScore;
        }
        private void setRiskScore(int riskScore) {
            this.riskScore = riskScore;
        }
        private String getRiskLevel() {
            return riskLevel;
        }
        private void setRiskLevel(String riskLevel) {
            this.riskLevel = riskLevel;
        }
        private List<String> getRiskReasons() {
            return riskReasons;
        }
        private void setRiskReasons(List<String> riskReasons) {
            this.riskReasons = riskReasons;
        }
    }
}
src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java
@@ -57,11 +57,8 @@
    SALES_QUOTATION("sales_quotation"),
    SALES_LEDGER_PRODUCT("sales_ledger_product"),
    PURCHASE_LEDGER_FILE("purchase_ledger_file"),
    RECEIPT_PAYMENT("receipt_payment"),
    PAYMENT_SHIPPING("payment_shipping"),
    INVOICE_REGISTRATION_PRODUCT("invoice_registration_product"),
    LOSS("loss"),
    INVOICE_REGISTRATION("invoice_registration"),
    INVOICE_LEDGER_FILE("invoice_ledger_file"),
    INVOICE_LEDGER("invoice_ledger"),
    COMMON_FILE("common_file"),
@@ -86,15 +83,11 @@
    QUALITY_INSPECT_PARAM("quality_inspect_param"),
    QUALITY_INSPECT("quality_inspect"),
    // Purchase
    TICKET_REGISTRATION("ticket_registration"),
    PURCHASE_RETURN_ORDER_PRODUCTS("purchase_return_order_products"),
    PURCHASE_RETURN_ORDERS("purchase_return_orders"),
    SALES_LEDGER_PRODUCT_TEMPLATE("sales_ledger_product_template"),
    PURCHASE_LEDGER("purchase_ledger"),
    PURCHASE_LEDGER_TEMPLATE("purchase_ledger_template"),
    PRODUCT_RECORD("product_record"),
    PAYMENT_REGISTRATION("payment_registration"),
    INVOICE_PURCHASE("invoice_purchase"),
    // Project Management
    SHIPPING_ADDRESS("shipping_address"),
    ROLES("roles"),
@@ -199,8 +192,6 @@
    AFTER_SALES_SERVICE_FILE("after_sales_service_file"),
    AFTER_SALES_NEAR_EXPIRY("after_sales_near_expiry"),
    // Account
    ACCOUNT_INCOME("account_income"),
    BORROW_INFO("borrow_info"),
    SALES_REFUND_AMOUNT_ORDER("sales_refund_amount_order"),
    SALES_RECEIPT_RETURN("sales_receipt_return"),
    ACCOUNT_EXPENSE("account_expense"),
@@ -208,8 +199,9 @@
    FIN_VOUCHER("fin_voucher"),
    ACCOUNT_FILE("account_file"),
    ENTERPRISE_NEWS("enterprise_news"),
    APPROVAL_INSTANCE("approval_instance");
    APPROVAL_INSTANCE("approval_instance"),
    ACCOUNT_INVOICE_APPLICATION("account_invoice_application"),
    ACCOUNT_PURCHASE_INVOICE("account_purchase_invoice");
    private final String type;
    RecordTypeEnum(String type) { this.type = type; }
src/main/java/com/ruoyi/basic/mapper/CustomerMapper.java
@@ -6,6 +6,8 @@
import com.ruoyi.basic.dto.CustomerDto;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.vo.CustomerVo;
import com.ruoyi.sales.vo.CustomerTransactionsDetailsVo;
import com.ruoyi.sales.vo.CustomerTransactionsVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -13,7 +15,7 @@
/**
 * å®¢æˆ·æ¡£æ¡ˆMapper接口
 *
 *
 * @author ruoyi
 * @date 2025-05-07
 */
@@ -22,7 +24,7 @@
{
    /**
     * æŸ¥è¯¢å®¢æˆ·æ¡£æ¡ˆ
     *
     *
     * @param id å®¢æˆ·æ¡£æ¡ˆä¸»é”®
     * @return å®¢æˆ·æ¡£æ¡ˆ
     */
@@ -30,7 +32,7 @@
    /**
     * æŸ¥è¯¢å®¢æˆ·æ¡£æ¡ˆåˆ—表
     *
     *
     * @param customer å®¢æˆ·æ¡£æ¡ˆ
     * @return å®¢æˆ·æ¡£æ¡ˆé›†åˆ
     */
@@ -38,7 +40,7 @@
    /**
     * æ–°å¢žå®¢æˆ·æ¡£æ¡ˆ
     *
     *
     * @param customer å®¢æˆ·æ¡£æ¡ˆ
     * @return ç»“æžœ
     */
@@ -46,7 +48,7 @@
    /**
     * ä¿®æ”¹å®¢æˆ·æ¡£æ¡ˆ
     *
     *
     * @param customer å®¢æˆ·æ¡£æ¡ˆ
     * @return ç»“æžœ
     */
@@ -54,7 +56,7 @@
    /**
     * åˆ é™¤å®¢æˆ·æ¡£æ¡ˆ
     *
     *
     * @param id å®¢æˆ·æ¡£æ¡ˆä¸»é”®
     * @return ç»“æžœ
     */
@@ -62,7 +64,7 @@
    /**
     * æ‰¹é‡åˆ é™¤å®¢æˆ·æ¡£æ¡ˆ
     *
     *
     * @param ids éœ€è¦åˆ é™¤çš„æ•°æ®ä¸»é”®é›†åˆ
     * @return ç»“æžœ
     */
@@ -71,4 +73,8 @@
    IPage<CustomerVo> listPage(Page<CustomerDto> page, @Param("c") CustomerDto customer, @Param("loginUserId") Long loginUserId);
    List<CustomerVo> list(@Param("c") CustomerDto customer, @Param("loginUserId") Long loginUserId);
}
    IPage<CustomerTransactionsVo> customewTransactions(Page page, @Param("customerName") String customerName);
    IPage<CustomerTransactionsDetailsVo> customewTransactionsDetails(Page page, @Param("customerId") Long customerId);
}
src/main/java/com/ruoyi/basic/mapper/SupplierManageMapper.java
@@ -6,6 +6,8 @@
import com.ruoyi.basic.dto.SupplierManageDto;
import com.ruoyi.basic.excel.SupplierManageExcelDto;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.purchase.vo.SupplierTransactionsDetailsVo;
import com.ruoyi.purchase.vo.SupplierTransactionsVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -17,4 +19,8 @@
    IPage<SupplierManage> supplierListPage(Page page, @Param("supplierManageDto") SupplierManageDto supplierManageDto);
    List<SupplierManageExcelDto> supplierExportList(@Param("supplierManageDto") SupplierManageDto supplierManageDto);
    IPage<SupplierTransactionsVo> supplierTransactions(Page page, @Param("supplierName") String supplierName);
    IPage<SupplierTransactionsDetailsVo> supplierTransactionsDetails(Page page, @Param("supplierId") Long supplierId);
}
src/main/java/com/ruoyi/basic/pojo/ProductModel.java
@@ -46,12 +46,6 @@
    @Excel(name = "单位")
    private String unit;
    /**
     * ç”Ÿäº§ç‚’机
     */
    @Excel(name = "生产炒机")
    private String speculativeTradingName;
    @Schema(description = "租户ID")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
src/main/java/com/ruoyi/basic/service/ICustomerService.java
@@ -7,6 +7,8 @@
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.vo.CustomerVo;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.sales.vo.CustomerTransactionsDetailsVo;
import com.ruoyi.sales.vo.CustomerTransactionsVo;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@@ -93,4 +95,20 @@
    void together(CustomerDto customerDto);
    Boolean back(Long id);
    /**
     * æŸ¥è¯¢å®¢æˆ·å¾€æ¥åˆ—表
     * @param page
     * @param customerName
     * @return
     */
    IPage<CustomerTransactionsVo> customewTransactions(Page page, String customerName);
    /**
     * æŸ¥è¯¢å®¢æˆ·å¾€æ¥æ˜Žç»†åˆ—表
     * @param page
     * @param customerId
     * @return
     */
    IPage<CustomerTransactionsDetailsVo> customewTransactionsDetails(Page page, Long customerId);
}
src/main/java/com/ruoyi/basic/service/ISupplierService.java
@@ -5,10 +5,11 @@
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.basic.dto.SupplierManageDto;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.purchase.vo.SupplierTransactionsDetailsVo;
import com.ruoyi.purchase.vo.SupplierTransactionsVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
public interface ISupplierService extends IService<SupplierManage> {
@@ -57,4 +58,20 @@
    void supplierExport(HttpServletResponse response, SupplierManageDto supplierManageDto);
    Boolean importData(MultipartFile file);
    /**
     * ä¾›åº”商往来
     * @param page
     * @param supplierName
     * @return
     */
    IPage<SupplierTransactionsVo> supplierTransactions(Page page, String supplierName);
    /**
     * ä¾›åº”商往来详情
     * @param page
     * @param supplierId
     * @return
     */
    IPage<SupplierTransactionsDetailsVo> supplierTransactionsDetails(Page page, Long supplierId);
}
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java
@@ -23,6 +23,8 @@
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.vo.CustomerTransactionsDetailsVo;
import com.ruoyi.sales.vo.CustomerTransactionsVo;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
@@ -379,6 +381,16 @@
        return this.updateById(customer);
    }
    @Override
    public IPage<CustomerTransactionsVo> customewTransactions(Page page, String customerName) {
        return customerMapper.customewTransactions(page, customerName);
    }
    @Override
    public IPage<CustomerTransactionsDetailsVo> customewTransactionsDetails(Page page, Long customerId) {
        return customerMapper.customewTransactionsDetails(page, customerId);
    }
    /**
     * ä¸‹åˆ’线命名转驼峰命名
     */
src/main/java/com/ruoyi/basic/service/impl/SupplierServiceImpl.java
@@ -14,6 +14,8 @@
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.purchase.vo.SupplierTransactionsDetailsVo;
import com.ruoyi.purchase.vo.SupplierTransactionsVo;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
@@ -130,4 +132,14 @@
        }
        return false;
    }
    @Override
    public IPage<SupplierTransactionsVo> supplierTransactions(Page page, String supplierName) {
        return supplierMapper.supplierTransactions(page,supplierName);
    }
    @Override
    public IPage<SupplierTransactionsDetailsVo> supplierTransactionsDetails(Page page, Long supplierId) {
        return supplierMapper.supplierTransactionsDetails(page,supplierId);
    }
}
src/main/java/com/ruoyi/home/controller/HomeController.java
@@ -1,14 +1,21 @@
package com.ruoyi.home.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.device.pojo.DeviceRepair;
import com.ruoyi.dto.MapDto;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.home.annotation.DefaultType;
import com.ruoyi.home.dto.*;
import com.ruoyi.home.service.HomeService;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionProductOutputMapper;
import com.ruoyi.production.pojo.ProductionOrder;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
@@ -17,9 +24,21 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
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.*;
/**
 * @author :yys
@@ -32,293 +51,698 @@
public class HomeController extends BaseController {
    private final HomeService homeService;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionProductOutputMapper productionProductOutputMapper;
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final DeviceRepairMapper deviceRepairMapper;
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final Integer ORDER_STATUS_WAIT = 1;
    private static final Integer ORDER_STATUS_RUNNING = 2;
    private static final Integer ORDER_STATUS_COMPLETED = 3;
    private static final Integer ORDER_STATUS_PAUSED = 4;
    /********************************************************基础类*****************************************************/
    @GetMapping("/todos")
    @Log(title = "待办事项", businessType = BusinessType.OTHER)
    @Operation(summary = "待办事项")
    public AjaxResult todos(ApproveProcess req) throws ParseException {
    public R todos() throws ParseException {
        List<ApproveProcess> approveProcessList = homeService.todos();
        return AjaxResult.success(approveProcessList);
        return R.ok(approveProcessList);
    }
    @GetMapping("/approveAndDeviceTodos")
    @Operation(summary = "审批协同,设备报修待办事项")
    public AjaxResult approveAndDeviceTodos(){
    public R approveAndDeviceTodos(){
        Map<String, Object> map = homeService.approveAndDeviceTodos();
        return AjaxResult.success(map);
        return R.ok(map);
    }
    @GetMapping("/noticesCount")
    @Operation(summary = "未过期的公告数量")
    public AjaxResult noticesCount(){
    public R noticesCount(){
        Long count = homeService.noticesCount();
        return AjaxResult.success(count);
        return R.ok(count);
    }
    @GetMapping("/deptStaffDistribution")
    @Operation(summary = "各部门人员分布")
    public AjaxResult deptStaffDistribution() {
    public R deptStaffDistribution() {
        DeptStaffDistributionDto dto = homeService.deptStaffDistribution();
        return AjaxResult.success(dto);
        return R.ok(dto);
    }
    @GetMapping("/summaryStatistics")
    @Operation(summary = "员工-客户-供应商总数")
    public AjaxResult summaryStatistics() {
    public R summaryStatistics() {
        HomeSummaryDto homeSummaryDto = homeService.summaryStatistics();
        return AjaxResult.success(homeSummaryDto);
        return R.ok(homeSummaryDto);
    }
    /********************************************************营销采购类**************************************************/
    @GetMapping("/supplierPurchaseRanking")
    @Operation(summary = "供应商采购排名")
    public AjaxResult supplierPurchaseRanking(@DefaultType Integer type) {
    public R supplierPurchaseRanking(@DefaultType Integer type) {
        List<SupplierPurchaseRankingDto> list = homeService.supplierPurchaseRanking(type);
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/customerRevenueAnalysis")
    @Operation(summary = "客户营收贡献数值分析")
    public AjaxResult customerRevenueAnalysis(Long customerId, @DefaultType Integer type) {
    public R customerRevenueAnalysis(Long customerId, @DefaultType Integer type) {
        CustomerRevenueAnalysisDto dto = homeService.customerRevenueAnalysis(customerId, type);
        return AjaxResult.success(dto);
        return R.ok(dto);
    }
    @GetMapping("/customerContributionRanking")
    @Operation(summary = "客户金额贡献排名")
    public AjaxResult customerContributionRanking(@DefaultType Integer type) {
    public R customerContributionRanking(@DefaultType Integer type) {
        List<CustomerContributionRankingDto> list = homeService.customerContributionRanking(type);
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/productSalesAnalysis")
    @Operation(summary = "各产品销售金额分析")
    public AjaxResult productSalesAnalysis() {
    public R productSalesAnalysis() {
        List<MapDto> list = homeService.productSalesAnalysis();
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/rawMaterialPurchaseAmountRatio")
    @Operation(summary = "原材料采购金额占比")
    public AjaxResult rawMaterialPurchaseAmountRatio(){
    public R rawMaterialPurchaseAmountRatio(){
        List<MapDto> list = homeService.rawMaterialPurchaseAmountRatio();
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/business")
    @Log(title = "销售-采购-库存数据", businessType = BusinessType.OTHER)
    @Operation(summary = "销售-采购-库存数据")
    public AjaxResult business(HomeBusinessDto req) {
    public R business() {
        HomeBusinessDto homeBusinessDto = homeService.business();
        return AjaxResult.success(homeBusinessDto);
        return R.ok(homeBusinessDto);
    }
    @GetMapping("/analysisCustomerContractAmounts")
    @Log(title = "客户合同金额分析", businessType = BusinessType.OTHER)
    @Operation(summary = "客户合同金额分析")
    public AjaxResult analysisCustomerContractAmounts(AnalysisCustomerContractAmountsDto req) {
    public R analysisCustomerContractAmounts() {
        AnalysisCustomerContractAmountsDto analysisCustomerContractAmounts = homeService.analysisCustomerContractAmounts();
        return AjaxResult.success(analysisCustomerContractAmounts);
        return R.ok(analysisCustomerContractAmounts);
    }
    /********************************************************生产类*****************************************************/
    @GetMapping("/inputOutputAnalysis")
    @Operation(summary = "投入产出分析")
    public AjaxResult inputOutputAnalysis(@DefaultType Integer type){
    public R inputOutputAnalysis(@DefaultType Integer type){
      List<InputOutputAnalysisDto> list = homeService.inputOutputAnalysis(type);
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/processOutputAnalysis")
    @Operation(summary = "工序产出分析")
    public AjaxResult processOutputAnalysis(@DefaultType Integer type){
    public R processOutputAnalysis(@DefaultType Integer type){
        List<MapDto> list = homeService.processOutputAnalysis(type);
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/workOrderEfficiencyAnalysis")
    @Operation(summary = "工单执行效率分析")
    public AjaxResult workOrderEfficiencyAnalysis(@DefaultType Integer type){
    public R workOrderEfficiencyAnalysis(@DefaultType Integer type){
        List<WorkOrderEfficiencyDto> list = homeService.workOrderEfficiencyAnalysis(type);
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/productionAccountingAnalysis")
    @Operation(summary = "生产核算分析")
    public AjaxResult productionAccountingAnalysis(@DefaultType Integer type){
    public R productionAccountingAnalysis(@DefaultType Integer type){
        List<ProductionAccountingDto> list   = homeService.productionAccountingAnalysis(type);
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/orderCount")
    @Operation(summary = "订单数")
    public AjaxResult orderCount(){
        return AjaxResult.success(homeService.orderCount());
    public R orderCount(){
        return R.ok(homeService.orderCount());
    }
    @GetMapping("/progressStatistics")
    @Operation(summary = "各生产订单的完成进度统计")
    public AjaxResult progressStatistics(){
    public R progressStatistics(){
        ProductionProgressDto productionProgressDto = homeService.productionProgress();
        return AjaxResult.success(productionProgressDto);
        return R.ok(productionProgressDto);
    }
    @GetMapping("/workInProcessTurnover")
    @Operation(summary = "在制品周转情况")
    public AjaxResult workInProcessTurnover(){
    public R workInProcessTurnover(){
        ProductionTurnoverDto productionTurnoverDto = homeService.workInProcessTurnover();
        return AjaxResult.success(productionTurnoverDto);
        return R.ok(productionTurnoverDto);
    }
    @GetMapping("/processDataProductionStatistics")
    @Operation(summary = "工序数据生产统计数据")
    public AjaxResult processDataProductionStatistics(@DefaultType Integer type,@RequestParam(required = false) List<Long> processIds) {
    public R processDataProductionStatistics(@DefaultType Integer type,@RequestParam(required = false) List<Long> processIds) {
        List<processDataProductionStatisticsDto> list = homeService.processDataProductionStatistics(type, processIds);
        return AjaxResult.success(list);
        return R.ok(list);
    }
    /********************************************************质量类*****************************************************/
    @GetMapping("/productionOverview")
    @Operation(summary = "Production Overview")
    public R productionOverview() {
        LocalDate today = LocalDate.now();
        Map<String, BigDecimal> totalStats = loadOutputStats(LocalDate.of(2000, 1, 1), today.plusDays(1));
        BigDecimal totalOutput = totalStats.get("quantity");
        BigDecimal totalScrap = totalStats.get("scrapQty");
        BigDecimal yieldRate = calcRate(totalOutput, totalOutput.add(totalScrap));
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("totalOutput", scale(totalOutput));
        result.put("totalScrap", scale(totalScrap));
        result.put("yieldRate", scale(yieldRate));
        return R.ok(result);
    }
    @GetMapping("/productionRealtimeBoard")
    @Operation(summary = "Production Realtime Board")
    public R productionRealtimeBoard() {
        LocalDate today = LocalDate.now();
        LocalDate yesterday = today.minusDays(1);
        BigDecimal todayDeviceOee = calcDeviceOee(today);
        BigDecimal yesterdayDeviceOee = calcDeviceOee(yesterday);
        BigDecimal todayOrderAchievementRate = calcOrderAchievementRate(today);
        BigDecimal yesterdayOrderAchievementRate = calcOrderAchievementRate(yesterday);
        BigDecimal todayDefectRate = calcDefectRate(today);
        BigDecimal yesterdayDefectRate = calcDefectRate(yesterday);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("deviceOee", buildRealtimeMetric(todayDeviceOee, todayDeviceOee.subtract(yesterdayDeviceOee)));
        result.put("orderAchievementRate", buildRealtimeMetric(todayOrderAchievementRate, todayOrderAchievementRate.subtract(yesterdayOrderAchievementRate)));
        result.put("defectRate", buildRealtimeMetric(todayDefectRate, todayDefectRate.subtract(yesterdayDefectRate)));
        return R.ok(result);
    }
    @GetMapping("/productionOrderProgress")
    @Operation(summary = "Production Order Progress")
    public R productionOrderProgress(@RequestParam(defaultValue = "all") String tab,
                                     @RequestParam(required = false) String status,
                                     @RequestParam(required = false) String bizDate,
                                     @RequestParam(defaultValue = "1") Long pageNum,
                                     @RequestParam(defaultValue = "10") Long pageSize) {
        LocalDate queryDate = parseDateOrNull(bizDate);
        if (!isBlank(bizDate) && queryDate == null) {
            return R.fail("bizDate格式错误,请使用yyyy-MM-dd");
        }
        Integer statusFromParam = parseOrderStatus(status);
        if (!isBlank(status) && statusFromParam == null && !"all".equalsIgnoreCase(status.trim())) {
            return R.fail("status参数不合法,可选值:all/waiting/inProgress/completed/paused æˆ– 1/2/3/4");
        }
        Integer queryStatus = resolveOrderStatus(status, tab);
        long safePageNum = pageNum == null || pageNum < 1 ? 1 : pageNum;
        long safePageSize = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 50);
        long offset = (safePageNum - 1) * safePageSize;
        LocalDateTime startTime = queryDate == null ? null : queryDate.atStartOfDay();
        LocalDateTime endTime = queryDate == null ? null : queryDate.plusDays(1).atStartOfDay();
        List<Map<String, Object>> rawRows = productionOrderMapper.selectHomeOrderProgressPage(queryStatus, offset, safePageSize, startTime, endTime);
        List<Map<String, Object>> records = new ArrayList<>();
        if (rawRows != null) {
            for (Map<String, Object> rawRow : rawRows) {
                records.add(buildOrderProgressRow(rawRow));
            }
        }
        long waitingCount = 0L;
        long inProgressCount = 0L;
        long completedCount = 0L;
        long pausedCount = 0L;
        List<Map<String, Object>> statusCountRows = productionOrderMapper.countHomeOrderProgressByStatus(startTime, endTime);
        if (statusCountRows != null) {
            for (Map<String, Object> countRow : statusCountRows) {
                Integer statusKey = toInteger(countRow.get("status"));
                long cnt = toLong(countRow.get("cnt"));
                if (Objects.equals(statusKey, ORDER_STATUS_WAIT)) {
                    waitingCount = cnt;
                } else if (Objects.equals(statusKey, ORDER_STATUS_RUNNING)) {
                    inProgressCount = cnt;
                } else if (Objects.equals(statusKey, ORDER_STATUS_COMPLETED)) {
                    completedCount = cnt;
                } else if (Objects.equals(statusKey, ORDER_STATUS_PAUSED)) {
                    pausedCount = cnt;
                }
            }
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("tab", mapOrderTab(queryStatus));
        result.put("status", mapOrderStatus(queryStatus));
        result.put("bizDate", queryDate == null ? null : queryDate.format(DATE_FORMATTER));
        result.put("total", toLong(productionOrderMapper.countHomeOrderProgress(queryStatus, startTime, endTime)));
        result.put("pageNum", safePageNum);
        result.put("pageSize", safePageSize);
        result.put("waitingCount", waitingCount);
        result.put("inProgressCount", inProgressCount);
        result.put("completedCount", completedCount);
        result.put("pausedCount", pausedCount);
        result.put("records", records);
        return R.ok(result);
    }
    @GetMapping("/todayProductionPlan")
    @Operation(summary = "Today Production Plan")
    public R todayProductionPlan(@RequestParam(defaultValue = "4") Long limit,
                                 @RequestParam(required = false) String planDate) {
        LocalDate queryDate = parseDateOrNull(planDate);
        if (!isBlank(planDate) && queryDate == null) {
            return R.fail("planDate格式错误,请使用yyyy-MM-dd");
        }
        long safeLimit = limit == null || limit < 1 ? 4 : Math.min(limit, 20);
        LocalDateTime planStart = queryDate == null ? null : queryDate.atStartOfDay();
        LocalDateTime planEnd = queryDate == null ? null : queryDate.plusDays(1).atStartOfDay();
        List<Map<String, Object>> records = new ArrayList<>();
        List<Map<String, Object>> rawRows = productionOrderMapper.selectHomeTodayProductionPlan(safeLimit, planStart, planEnd);
        if (rawRows != null) {
            for (Map<String, Object> rawRow : rawRows) {
                Map<String, Object> row = new LinkedHashMap<>();
                Integer rowStatus = toInteger(rawRow.get("status"));
                row.put("orderNo", rawRow.get("orderNo"));
                row.put("productName", rawRow.get("productName"));
                row.put("plannedQuantity", scale(toBigDecimal(rawRow.get("plannedQuantity"))));
                row.put("dueDate", rawRow.get("dueDate"));
                row.put("status", rowStatus);
                row.put("statusLabel", mapOrderStatusLabel(rowStatus));
                records.add(row);
            }
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("planDate", queryDate == null ? null : queryDate.format(DATE_FORMATTER));
        result.put("total", toLong(productionOrderMapper.countHomeTodayProductionPlan(planStart, planEnd)));
        result.put("records", records);
        return R.ok(result);
    }
    @GetMapping("/rawMaterialDetection")
    @Operation(summary = "原材料检测")
    public AjaxResult rawMaterialDetection(@DefaultType Integer type){
        return AjaxResult.success(homeService.rawMaterialDetection(type));
    public R rawMaterialDetection(@DefaultType Integer type){
        return R.ok(homeService.rawMaterialDetection(type));
    }
    @GetMapping("/processDetection")
    @Operation(summary = "过程检测")
    public AjaxResult processDetection(@DefaultType Integer type){
        return AjaxResult.success(homeService.processDetection(type));
    public R processDetection(@DefaultType Integer type){
        return R.ok(homeService.processDetection(type));
    }
    @GetMapping("/factoryDetection")
    @Operation(summary = "成品出厂检测")
    public AjaxResult factoryDetection(@DefaultType Integer type){
        return AjaxResult.success(homeService.factoryDetection(type));
    public R factoryDetection(@DefaultType Integer type){
        return R.ok(homeService.factoryDetection(type));
    }
    @GetMapping("/qualityInspectionCount")
    @Operation(summary = "质量检验数量")
    public AjaxResult qualityInspectionCount(){
    public R qualityInspectionCount(){
        QualityInspectionCountDto qualityInspectionCountDto = homeService.qualityInspectionCount();
        return AjaxResult.success(qualityInspectionCountDto);
        return R.ok(qualityInspectionCountDto);
    }
    @GetMapping("/nonComplianceWarning")
    @Operation(summary = "不合格预警")
    public AjaxResult nonComplianceWarning(){
    public R nonComplianceWarning(){
        NonComplianceWarningDto nonComplianceWarningDto = homeService.nonComplianceWarning();
        return AjaxResult.success(nonComplianceWarningDto);
        return R.ok(nonComplianceWarningDto);
    }
    @GetMapping("/completedInspectionCount")
    @Operation(summary = "完成检验数")
    public AjaxResult completedInspectionCount(){
    public R completedInspectionCount(){
        List<CompletedInspectionCountDto> list = homeService.completedInspectionCount();
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/unqualifiedProductRanking")
    @Operation(summary = "不合格产品排名")
    public AjaxResult unqualifiedProductRanking(){
    public R unqualifiedProductRanking(){
        List<UnqualifiedProductRankDto> list = homeService.unqualifiedProductRanking();
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/unqualifiedProductProcessingAnalysis")
    @Operation(summary = "不合格检品处理分析")
    public AjaxResult unqualifiedProductProcessingAnalysis(){
    public R unqualifiedProductProcessingAnalysis(){
        List<MapDto> list = homeService.unqualifiedProductProcessingAnalysis();
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/qualityStatistics")
    @Log(title = "质量分析", businessType = BusinessType.OTHER)
    @Operation(summary = "质量分析")
    public AjaxResult qualityStatistics(QualityStatisticsDto req) {
    public R qualityStatistics() {
        QualityStatisticsDto qualityStatisticsDto = homeService.qualityStatistics();
        return AjaxResult.success(qualityStatisticsDto);
        return R.ok(qualityStatisticsDto);
    }
    @GetMapping("/qualityInspectionStatistics")
    @Operation(summary = "质量统计")
    public AjaxResult qualityInspectionStatistics(@DefaultType Integer type) {
    public R qualityInspectionStatistics(@DefaultType Integer type) {
       QualityStatisticsDto  dto = homeService.qualityInspectionStatistics(type);
        return AjaxResult.success(dto);
        return R.ok(dto);
    }
    /********************************************************财务类*****************************************************/
    @GetMapping("/incomeExpenseAnalysis")
    @Operation(summary = "支收对比分析")
    public AjaxResult incomeExpenseAnalysis(@DefaultType Integer type) {
    public R incomeExpenseAnalysis(@DefaultType Integer type) {
        List<Map<String, Object>> result = homeService.incomeExpenseAnalysis(type);
        return AjaxResult.success(result);
        return R.ok(result);
    }
    @GetMapping("/profitTrendAnalysis")
    @Operation(summary = "利润趋势分析")
    public AjaxResult profitTrendAnalysis(){
    public R profitTrendAnalysis(){
        List<MapDto> list = homeService.profitTrendAnalysis();
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/expenseCompositionAnalysis")
    @Operation(summary = "构成分析")
    public AjaxResult expenseCompositionAnalysis(@DefaultType Integer type) {
    public R expenseCompositionAnalysis(@DefaultType Integer type) {
        List<MapDto> list = homeService.expenseCompositionAnalysis(type);
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/monthlyIncome")
    @Operation(summary = "月度收入")
    public AjaxResult monthlyIncome(){
    public R monthlyIncome(){
        MonthlyIncomeDto dto = homeService.monthlyIncome();
        return AjaxResult.success(dto);
        return R.ok(dto);
    }
    @GetMapping("/monthlyExpenditure")
    @Operation(summary = "月度支出")
    public AjaxResult monthlyExpenditure(){
    public R monthlyExpenditure(){
        MonthlyExpenditureDto dto = homeService.monthlyExpenditure();
        return AjaxResult.success(dto);
        return R.ok(dto);
    }
    @GetMapping("/statisticsReceivablePayable")
    @Log(title = "应收应付统计", businessType = BusinessType.OTHER)
    @Operation(summary = "应收应付统计")
    public AjaxResult statisticsReceivablePayable(StatisticsReceivablePayableDto req, @DefaultType Integer type ) {
    public R statisticsReceivablePayable(@DefaultType Integer type ) {
        StatisticsReceivablePayableDto statisticsReceivablePayable = homeService.statisticsReceivablePayable(type);
        return AjaxResult.success(statisticsReceivablePayable);
        return R.ok(statisticsReceivablePayable);
    }
    /********************************************************仓储类*****************************************************/
    @GetMapping("/productCategoryDistribution")
    @Operation(summary = "产品大类分布")
    public AjaxResult productCategoryDistribution() {
    public R productCategoryDistribution() {
        ProductCategoryDistributionDto dto = homeService.productCategoryDistribution();
        return AjaxResult.success(dto);
        return R.ok(dto);
    }
    @GetMapping("/salesPurchaseStorageProductCount")
    @Operation(summary = "销售-采购-储存产品数")
    public AjaxResult salesPurchaseStorageProductCount(){
    public R salesPurchaseStorageProductCount(){
        List<MapDto> list = homeService.salesPurchaseStorageProductCount();
        return AjaxResult.success(list);
        return R.ok(list);
    }
    @GetMapping("/productInOutAnalysis")
    @Operation(summary = "产品出入库分析")
    public AjaxResult productInOutAnalysis(@DefaultType Integer type){
    public R productInOutAnalysis(@DefaultType Integer type){
        List<Map<String, Object>> result = homeService.productInOutAnalysis(type);
        return AjaxResult.success(result);
        return R.ok(result);
    }
    @GetMapping("/productTurnoverDays")
    @Operation(summary = "产品周转天数")
    public AjaxResult productTurnoverDays(){
    public R productTurnoverDays(){
        List<MapDto> list = homeService.productTurnoverDays();
        return AjaxResult.success(list);
        return R.ok(list);
    }
    private Map<String, Object> buildOrderProgressRow(Map<String, Object> rawRow) {
        Map<String, Object> row = new LinkedHashMap<>();
        Integer rowStatus = toInteger(rawRow.get("status"));
        row.put("orderNo", rawRow.get("orderNo"));
        row.put("productName", rawRow.get("productName"));
        row.put("plannedQuantity", scale(toBigDecimal(rawRow.get("plannedQuantity"))));
        row.put("completedQuantity", scale(toBigDecimal(rawRow.get("completedQuantity"))));
        row.put("completionRate", scale(toBigDecimal(rawRow.get("completionRate"))));
        row.put("dueDate", rawRow.get("dueDate"));
        row.put("status", rowStatus);
        row.put("statusLabel", mapOrderStatusLabel(rowStatus));
        return row;
    }
    private Integer resolveOrderStatus(String status, String tab) {
        if (!isBlank(status)) {
            return parseOrderStatus(status);
        }
        return parseOrderStatus(tab);
    }
    private Integer parseOrderStatus(String rawStatus) {
        if (isBlank(rawStatus)) {
            return null;
        }
        String normalized = rawStatus.trim().toLowerCase();
        if ("all".equals(normalized)) {
            return null;
        }
        if ("1".equals(normalized) || "waiting".equals(normalized) || "wait".equals(normalized)) {
            return ORDER_STATUS_WAIT;
        }
        if ("2".equals(normalized) || "inprogress".equals(normalized) || "running".equals(normalized)) {
            return ORDER_STATUS_RUNNING;
        }
        if ("3".equals(normalized) || "completed".equals(normalized)) {
            return ORDER_STATUS_COMPLETED;
        }
        if ("4".equals(normalized) || "paused".equals(normalized)) {
            return ORDER_STATUS_PAUSED;
        }
        return null;
    }
    private String mapOrderTab(Integer status) {
        if (Objects.equals(status, ORDER_STATUS_RUNNING)) {
            return "inProgress";
        }
        if (Objects.equals(status, ORDER_STATUS_COMPLETED)) {
            return "completed";
        }
        if (Objects.equals(status, ORDER_STATUS_PAUSED)) {
            return "paused";
        }
        if (Objects.equals(status, ORDER_STATUS_WAIT)) {
            return "waiting";
        }
        return "all";
    }
    private String mapOrderStatus(Integer status) {
        if (Objects.equals(status, ORDER_STATUS_WAIT)) {
            return "waiting";
        }
        if (Objects.equals(status, ORDER_STATUS_RUNNING)) {
            return "inProgress";
        }
        if (Objects.equals(status, ORDER_STATUS_COMPLETED)) {
            return "completed";
        }
        if (Objects.equals(status, ORDER_STATUS_PAUSED)) {
            return "paused";
        }
        return "all";
    }
    private String mapOrderStatusLabel(Integer status) {
        if (Objects.equals(status, ORDER_STATUS_WAIT)) {
            return "待开始";
        }
        if (Objects.equals(status, ORDER_STATUS_RUNNING)) {
            return "进行中";
        }
        if (Objects.equals(status, ORDER_STATUS_COMPLETED)) {
            return "已完成";
        }
        if (Objects.equals(status, ORDER_STATUS_PAUSED)) {
            return "已暂停";
        }
        return "未知";
    }
    private Map<String, BigDecimal> loadOutputStats(LocalDate startDate, LocalDate endDateExclusive) {
        String start = startDate.atStartOfDay().format(DATE_TIME_FORMATTER);
        String end = endDateExclusive.atStartOfDay().format(DATE_TIME_FORMATTER);
        BigDecimal quantity = BigDecimal.ZERO;
        BigDecimal scrapQty = BigDecimal.ZERO;
        List<Map<String, Object>> rows = productionProductOutputMapper.selectDailyOutputStats(start, end);
        if (rows != null) {
            for (Map<String, Object> row : rows) {
                quantity = quantity.add(toBigDecimal(row.get("quantity")));
                scrapQty = scrapQty.add(toBigDecimal(row.get("scrapQty")));
            }
        }
        Map<String, BigDecimal> stats = new LinkedHashMap<>();
        stats.put("quantity", quantity);
        stats.put("scrapQty", scrapQty);
        return stats;
    }
    private BigDecimal calcDeviceOee(LocalDate day) {
        long totalDeviceCount = deviceLedgerMapper.selectCount(new LambdaQueryWrapper<>());
        if (totalDeviceCount <= 0) {
            return BigDecimal.ZERO;
        }
        Date start = Date.from(day.atStartOfDay(ZoneId.systemDefault()).toInstant());
        Date end = Date.from(day.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
        List<DeviceRepair> repairList = deviceRepairMapper.selectList(new LambdaQueryWrapper<DeviceRepair>()
                .select(DeviceRepair::getDeviceLedgerId)
                .ge(DeviceRepair::getRepairTime, start)
                .lt(DeviceRepair::getRepairTime, end)
                .in(DeviceRepair::getStatus, 0, 3));
        long repairingDeviceCount = repairList == null ? 0 : repairList.stream()
                .map(DeviceRepair::getDeviceLedgerId)
                .filter(Objects::nonNull)
                .distinct()
                .count();
        long availableDeviceCount = Math.max(totalDeviceCount - repairingDeviceCount, 0);
        return new BigDecimal(availableDeviceCount)
                .multiply(new BigDecimal("100"))
                .divide(new BigDecimal(totalDeviceCount), 2, RoundingMode.HALF_UP);
    }
    private BigDecimal calcOrderAchievementRate(LocalDate day) {
        List<ProductionOrder> orderList = productionOrderMapper.selectList(new LambdaQueryWrapper<ProductionOrder>()
                .select(ProductionOrder::getQuantity, ProductionOrder::getCompleteQuantity)
                .ge(ProductionOrder::getCreateTime, day.atStartOfDay())
                .lt(ProductionOrder::getCreateTime, day.plusDays(1).atStartOfDay())
                .ne(ProductionOrder::getStatus, ORDER_STATUS_PAUSED));
        BigDecimal totalQuantity = BigDecimal.ZERO;
        BigDecimal totalCompleteQuantity = BigDecimal.ZERO;
        if (orderList != null) {
            for (ProductionOrder order : orderList) {
                totalQuantity = totalQuantity.add(zeroIfNull(order.getQuantity()));
                totalCompleteQuantity = totalCompleteQuantity.add(zeroIfNull(order.getCompleteQuantity()));
            }
        }
        return calcRate(totalCompleteQuantity, totalQuantity);
    }
    private BigDecimal calcDefectRate(LocalDate day) {
        Map<String, BigDecimal> stats = loadOutputStats(day, day.plusDays(1));
        BigDecimal quantity = stats.get("quantity");
        BigDecimal scrapQty = stats.get("scrapQty");
        return calcRate(scrapQty, quantity.add(scrapQty));
    }
    private Map<String, Object> buildRealtimeMetric(BigDecimal value, BigDecimal change) {
        Map<String, Object> metric = new LinkedHashMap<>();
        metric.put("value", scale(value));
        metric.put("compareYesterday", scale(change));
        return metric;
    }
    private BigDecimal calcRate(BigDecimal numerator, BigDecimal denominator) {
        if (denominator == null || denominator.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        return zeroIfNull(numerator)
                .multiply(new BigDecimal("100"))
                .divide(denominator, 2, RoundingMode.HALF_UP);
    }
    private BigDecimal toBigDecimal(Object value) {
        if (value == null) {
            return BigDecimal.ZERO;
        }
        if (value instanceof BigDecimal) {
            return (BigDecimal) value;
        }
        if (value instanceof Number) {
            return BigDecimal.valueOf(((Number) value).doubleValue());
        }
        try {
            return new BigDecimal(String.valueOf(value));
        } catch (Exception ex) {
            return BigDecimal.ZERO;
        }
    }
    private Integer toInteger(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof Integer) {
            return (Integer) value;
        }
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        try {
            return Integer.valueOf(String.valueOf(value));
        } catch (Exception ex) {
            return null;
        }
    }
    private long toLong(Object value) {
        if (value == null) {
            return 0L;
        }
        if (value instanceof Long) {
            return (Long) value;
        }
        if (value instanceof Number) {
            return ((Number) value).longValue();
        }
        try {
            return Long.parseLong(String.valueOf(value));
        } catch (Exception ex) {
            return 0L;
        }
    }
    private LocalDate parseDateOrNull(String rawDate) {
        if (isBlank(rawDate)) {
            return null;
        }
        try {
            return LocalDate.parse(rawDate.trim(), DATE_FORMATTER);
        } catch (DateTimeParseException ex) {
            return null;
        }
    }
    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
    private BigDecimal zeroIfNull(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private BigDecimal scale(BigDecimal value) {
        return zeroIfNull(value).setScale(2, RoundingMode.HALF_UP);
    }
}
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java
@@ -3,9 +3,8 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.ruoyi.account.mapper.AccountExpenseMapper;
import com.ruoyi.account.mapper.AccountIncomeMapper;
import com.ruoyi.account.pojo.AccountExpense;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.approve.mapper.ApproveProcessMapper;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.basic.mapper.CustomerMapper;
@@ -30,18 +29,14 @@
import com.ruoyi.production.mapper.*;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.purchase.mapper.PaymentRegistrationMapper;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PaymentRegistration;
import com.ruoyi.purchase.pojo.PurchaseLedger;
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.sales.mapper.ReceiptPaymentMapper;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.ReceiptPayment;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.staff.mapper.StaffOnJobMapper;
@@ -83,10 +78,6 @@
    private final ApproveProcessMapper approveProcessMapper;
    private final ReceiptPaymentMapper receiptPaymentMapper;
    private final PaymentRegistrationMapper paymentRegistrationMapper;
    private final SysDeptMapper sysDeptMapper;
    private final NoticeMapper noticeMapper;
@@ -111,9 +102,8 @@
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final AccountExpenseMapper accountExpenseMapper;
    private final AccountIncomeMapper accountIncomeMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final ProductionAccountMapper productionAccountMapper;
@@ -141,12 +131,8 @@
                            salesLedgers.stream().map(SalesLedger::getId).collect(Collectors.toList()));
            List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper
                    .selectList(salesLedgerProductMapperLambdaQueryWrapper);
            // æœªå¼€ç¥¨é‡‘额
            BigDecimal noInvoiceAmountTotal = salesLedgerProducts.stream().map(SalesLedgerProduct::getNoInvoiceAmount)
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            homeBusinessDto.setMonthSaleMoney(contractAmount.setScale(2, RoundingMode.HALF_UP).toString());
            homeBusinessDto.setMonthSaleHaveMoney(noInvoiceAmountTotal.setScale(2, RoundingMode.HALF_UP).toString());
            homeBusinessDto.setMonthSaleHaveMoney(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP).toString());
        }
        // åˆ›å»ºLambdaQueryWrapper
        LambdaQueryWrapper<PurchaseLedger> queryWrapper = new LambdaQueryWrapper<>();
@@ -169,14 +155,8 @@
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            //  å¾…付款总金额
            BigDecimal unReceiptPaymentAmount = salesLedgerProductsCopy.stream()
                    .map(SalesLedgerProduct::getPendingTicketsTotal)
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            homeBusinessDto.setMonthPurchaseMoney(receiveAmount.setScale(2, RoundingMode.HALF_UP).toString());
            homeBusinessDto.setMonthPurchaseHaveMoney(unReceiptPaymentAmount.setScale(2, RoundingMode.HALF_UP).toString());
            homeBusinessDto.setMonthPurchaseHaveMoney(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP).toString());
        }
        // ç»Ÿè®¡åº“å­˜
        BigDecimal stockQuantityTotal = stockInventoryMapper.selectTotal();
@@ -414,6 +394,7 @@
     */
    @Override
    public StatisticsReceivablePayableDto statisticsReceivablePayable(Integer type) {
        StatisticsReceivablePayableDto statisticsReceivablePayableDto = new StatisticsReceivablePayableDto();
        LocalDate today = LocalDate.now();
        LocalDate startDate = null;
        LocalDate endDate = null;
@@ -440,48 +421,14 @@
                break;
        }
        // åº”æ”¶
        List<SalesLedger> salesLedgers = salesLedgerMapper.selectList(new LambdaQueryWrapper<SalesLedger>()
                // .ge(SalesLedger::getEntryDate, startDate)
                // .lt(SalesLedger::getEntryDate, endDate)
        );
        // BigDecimal receivableMoney =
        // salesLedgers.stream().map(SalesLedger::getContractAmount).reduce(BigDecimal.ZERO,
        // BigDecimal::add);
        BigDecimal receivableMoney = sumAmount(salesLedgers, SalesLedger::getContractAmount);
        // åº”付
        List<PurchaseLedger> procurementRecords = purchaseLedgerMapper
                .selectList(new LambdaQueryWrapper<PurchaseLedger>()
                        // .ge(PurchaseLedger::getEntryDate, startDate)
                        // .lt(PurchaseLedger::getEntryDate, endDate)
                );
        // BigDecimal payableMoney =
        // procurementRecords.stream().map(PurchaseLedger::getContractAmount).reduce(BigDecimal.ZERO,
        // BigDecimal::add);
        BigDecimal payableMoney = sumAmount(procurementRecords, PurchaseLedger::getContractAmount);
        // é¢„æ”¶
        List<ReceiptPayment> receiptPayments = receiptPaymentMapper.selectList(new LambdaQueryWrapper<ReceiptPayment>()
                // .ge(ReceiptPayment::getReceiptPaymentDate, startDate)
                // .lt(ReceiptPayment::getReceiptPaymentDate, endDate)
        );
        // BigDecimal advanceMoney =
        // receiptPayments.stream().map(ReceiptPayment::getReceiptPaymentAmount).reduce(BigDecimal.ZERO,
        // BigDecimal::add);
        BigDecimal advanceMoney = sumAmount(receiptPayments, ReceiptPayment::getReceiptPaymentAmount);
        // é¢„付
        List<PaymentRegistration> paymentRegistrations = paymentRegistrationMapper
                .selectList(new LambdaQueryWrapper<PaymentRegistration>()
                        // .ge(PaymentRegistration::getPaymentDate, startDate)
                        // .lt(PaymentRegistration::getPaymentDate, endDate)
                );
        // BigDecimal prepayMoney =
        // paymentRegistrations.stream().map(PaymentRegistration::getCurrentPaymentAmount).reduce(BigDecimal.ZERO,
        // BigDecimal::add);
        BigDecimal prepayMoney = sumAmount(paymentRegistrations, PaymentRegistration::getCurrentPaymentAmount);
        StatisticsReceivablePayableDto statisticsReceivablePayableDto = new StatisticsReceivablePayableDto();
        statisticsReceivablePayableDto.setPayableMoney(payableMoney.subtract(prepayMoney));
        statisticsReceivablePayableDto.setReceivableMoney(receivableMoney.subtract(advanceMoney));
        statisticsReceivablePayableDto.setAdvanceMoney(advanceMoney);
        statisticsReceivablePayableDto.setPrepayMoney(prepayMoney);
        return statisticsReceivablePayableDto;
    }
@@ -1164,13 +1111,9 @@
        String endStr = endDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        // 2. æŸ¥è¯¢æ•°æ®
        List<IncomeExpenseAnalysisDto> incomeList = accountIncomeMapper.selectIncomeStats(startStr, endStr, dateFormat);
        List<IncomeExpenseAnalysisDto> incomeList = accountSalesCollectionMapper.selectIncomeStats(startStr, endStr, dateFormat);
        List<IncomeExpenseAnalysisDto> expenseList = accountPurchasePaymentMapper.selectPayment(startStr, endStr, dateFormat);
        // List<IncomeExpenseAnalysisDto> purchaseList =
        // purchaseLedgerMapper.selectPurchaseStats(startStr, endStr, dateFormat);
        List<IncomeExpenseAnalysisDto> expenseList = accountExpenseMapper.selectAccountExpenseStats(startStr, endStr,
                dateFormat);
        // 3. è½¬ Map(自动合并)
        Map<String, BigDecimal> incomeMap = incomeList.stream()
@@ -1179,11 +1122,7 @@
                        IncomeExpenseAnalysisDto::getAmount,
                        BigDecimal::add));
        // Map<String, BigDecimal> purchaseMap = purchaseList.stream()
        // .collect(Collectors.toMap(
        // IncomeExpenseAnalysisDto::getDateStr,
        // IncomeExpenseAnalysisDto::getAmount,
        // BigDecimal::add));
        Map<String, BigDecimal> expenseMap = expenseList.stream()
                .collect(Collectors.toMap(
@@ -1197,18 +1136,12 @@
        for (String dateStr : xAxis) {
            Map<String, Object> item = new HashMap<>();
            item.put("date", dateStr);
            // æ”¶å…¥
            BigDecimal income = incomeMap.getOrDefault(dateStr, BigDecimal.ZERO);
            item.put("income", income.setScale(2, RoundingMode.HALF_UP));
            // æ”¯å‡º = é‡‡è´­ + è´¢åŠ¡æ”¯å‡º
            // BigDecimal purchase = purchaseMap.getOrDefault(dateStr, BigDecimal.ZERO);
            // æ”¯å‡º
            BigDecimal expense = expenseMap.getOrDefault(dateStr, BigDecimal.ZERO);
            // BigDecimal totalExpense = purchase.add(expense);
            BigDecimal totalExpense = expense;
            item.put("expense", totalExpense.setScale(2, RoundingMode.HALF_UP));
            item.put("expense", expense.setScale(2, RoundingMode.HALF_UP));
            result.add(item);
        }
@@ -1235,8 +1168,8 @@
        String startStr = startDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        String endStr = endDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        List<IncomeExpenseAnalysisDto> incomeList = accountIncomeMapper.selectIncomeStats(startStr, endStr, dateFormat);
        List<IncomeExpenseAnalysisDto> expenseList = accountExpenseMapper.selectAccountExpenseStats(startStr, endStr, dateFormat);
        List<IncomeExpenseAnalysisDto> incomeList = accountSalesCollectionMapper.selectIncomeStats(startStr, endStr, dateFormat);
        List<IncomeExpenseAnalysisDto> expenseList = accountPurchasePaymentMapper.selectPayment(startStr, endStr, dateFormat);
        Map<String, BigDecimal> incomeMap = incomeList.stream().collect(Collectors
                .toMap(IncomeExpenseAnalysisDto::getDateStr, IncomeExpenseAnalysisDto::getAmount, BigDecimal::add));
@@ -1272,11 +1205,6 @@
            rawMaterialDto.setName("原材料");
            rawMaterialDto.setValue(rawMaterialAmount != null ? rawMaterialAmount.toString() : "0");
            result.add(rawMaterialDto);
            List<MapDto> expenseList = accountExpenseMapper.selectExpenseComposition(null, null);
            if (expenseList != null) {
                result.addAll(expenseList);
            }
        }
        BigDecimal total = BigDecimal.ZERO;
@@ -1327,34 +1255,6 @@
            return dto;
        }
        BigDecimal collected = products.stream()
                .map(SalesLedgerProduct::getInvoiceTotal)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        dto.setMonthlyIncome(collected);
        BigDecimal overdue = products.stream()
                .map(SalesLedgerProduct::getPendingInvoiceTotal)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        dto.setOverdueNum(overdue);
        BigDecimal total = collected.add(overdue);
        if (total.compareTo(BigDecimal.ZERO) > 0) {
            String collectionRate = collected.divide(total, 4, RoundingMode.HALF_UP)
                    .multiply(new BigDecimal("100"))
                    .setScale(2, RoundingMode.HALF_UP)
                    .toString();
            dto.setCollectionRate(collectionRate);
            String overdueRate = overdue.divide(total, 4, RoundingMode.HALF_UP)
                    .multiply(new BigDecimal("100"))
                    .setScale(2, RoundingMode.HALF_UP)
                    .toString();
            dto.setOverdueRate(overdueRate);
        }
        return dto;
    }
@@ -1386,38 +1286,14 @@
                if (p.getTaxInclusiveTotalPrice() != null) {
                    rawMaterialCost = rawMaterialCost.add(p.getTaxInclusiveTotalPrice());
                }
                if (p.getTicketsTotal() != null) {
                    paidAmount = paidAmount.add(p.getTicketsTotal());
                }
                if (p.getPendingTicketsTotal() != null) {
                    pendingAmount = pendingAmount.add(p.getPendingTicketsTotal());
                }
            }
        }
        // å…¶ä»–费用
        LambdaQueryWrapper<AccountExpense> expenseWrapper = new LambdaQueryWrapper<>();
        expenseWrapper.ge(AccountExpense::getExpenseDate,
                java.sql.Date.valueOf(currentMonth.atDay(1)));
        expenseWrapper.le(AccountExpense::getExpenseDate,
                java.sql.Date.valueOf(currentMonth.atEndOfMonth()));
        List<AccountExpense> expenses = accountExpenseMapper.selectList(expenseWrapper);
        BigDecimal otherExpense = BigDecimal.ZERO;
        if (!CollectionUtils.isEmpty(expenses)) {
            for (AccountExpense e : expenses) {
                if (e.getExpenseMoney() != null) {
                    otherExpense = otherExpense.add(e.getExpenseMoney());
                }
            }
        }
        // æœˆåº¦æ€»æ”¯å‡º
        // BigDecimal monthlyExpenditure = rawMaterialCost.add(otherExpense);
        BigDecimal monthlyExpenditure = otherExpense;
        BigDecimal monthlyExpenditure = BigDecimal.ZERO;
        dto.setMonthlyExpenditure(monthlyExpenditure);
        // å·²ä»˜æ¬¾ Ã·ï¼ˆå·²ä»˜æ¬¾ + å¾…付款)
@@ -1440,13 +1316,6 @@
        List<SalesLedgerProduct> salesProducts = salesLedgerProductMapper.selectList(salesWrapper);
        BigDecimal revenue = BigDecimal.ZERO;
        if (!CollectionUtils.isEmpty(salesProducts)) {
            for (SalesLedgerProduct s : salesProducts) {
                if (s.getInvoiceAmount() != null) {
                    revenue = revenue.add(s.getInvoiceAmount());
                }
            }
        }
        // æ¯›åˆ©æ¶¦ & åˆ©æ¶¦çއ
        if (revenue.compareTo(BigDecimal.ZERO) > 0) {
src/main/java/com/ruoyi/procurementrecord/controller/ReturnManagementController.java
@@ -3,11 +3,16 @@
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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.pojo.AccountStatementDetails;
import com.ruoyi.account.service.AccountStatementDetailsService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.bean.vo.ShippingInfoVo;
import com.ruoyi.procurementrecord.pojo.ReturnManagement;
import com.ruoyi.procurementrecord.pojo.ReturnSaleProduct;
import com.ruoyi.procurementrecord.service.ReturnManagementService;
import com.ruoyi.procurementrecord.service.ReturnSaleProductService;
@@ -31,6 +36,7 @@
    private ReturnManagementService returnManagementService;
    private ReturnSaleProductService returnSaleProductService;
    private final AccountStatementDetailsService accountStatementDetailsService;
    @GetMapping("/listPage")
    @Operation(summary = "销售退货-查询")
@@ -67,6 +73,14 @@
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult del(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) return error("请选择至少一条数据");
        //如果该销售退货已经生成对账单则无法删除
        List<ReturnManagement> returnManagements = returnManagementService.listByIds(ids);
        List<String> strings = returnManagements.stream().map(ReturnManagement::getReturnNo).toList();
        List<AccountStatementDetails> accountStatementDetails = accountStatementDetailsService.list(Wrappers.<AccountStatementDetails>lambdaQuery()
                .in(AccountStatementDetails::getReceiptNumber, strings));
        if (CollectionUtils.isNotEmpty(accountStatementDetails)){
            throw new ServiceException("该销售退货单已经生成对账单,无法删除");
        }
        returnSaleProductService.remove(new QueryWrapper<ReturnSaleProduct>()
                .lambda()
                .in(ReturnSaleProduct::getReturnManagementId, ids));
src/main/java/com/ruoyi/procurementrecord/mapper/ReturnManagementMapper.java
@@ -3,8 +3,8 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.SalesReturnDto;
import com.ruoyi.account.bean.vo.SalesReturnVo;
import com.ruoyi.account.bean.dto.sales.SalesReturnDto;
import com.ruoyi.account.bean.vo.sales.SalesReturnVo;
import com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.pojo.ReturnManagement;
import org.apache.ibatis.annotations.Param;
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnManagementServiceImpl.java
@@ -4,8 +4,6 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.bean.dto.SalesRefundAmountOrderDto;
import com.ruoyi.account.mapper.AccountExpenseMapper;
import com.ruoyi.account.pojo.AccountExpense;
import com.ruoyi.account.service.SalesRefundAmountOrderService;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.OrderUtils;
@@ -31,7 +29,6 @@
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
@@ -49,7 +46,6 @@
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesRefundAmountOrderService salesRefundAmountOrderService;
    private final StockUtils stockUtils;
    private final AccountExpenseMapper accountExpenseMapper;
    @Override
    public IPage<ReturnManagementDto> listPage(Page page, ReturnManagementDto returnManagement) {
@@ -127,19 +123,6 @@
        salesRefundAmountOrder.setNotRefundedAmount(salesRefundAmountOrder.getRefundedAmount());
        // åˆ†æ‰¹é€€æ¬¾
//        salesRefundAmountOrderService.addSalesRefundAmountOrderDto(salesRefundAmountOrder);
        // å’Œè´¢åŠ¡è”åŠ¨ï¼Œæ–°å¢žæ”¯å‡º
        AccountExpense accountExpense = new AccountExpense();
        accountExpense.setBusinessType(3);
        accountExpense.setExpenseMoney(byId.getRefundAmount());
        accountExpense.setBusinessId(byId.getId());
        accountExpense.setExpenseDate(new Date());
        accountExpense.setExpenseMethod("3");
        accountExpense.setExpenseType("5");
        accountExpense.setExpenseDescribed("销售退货退款");
        accountExpense.setNote(byId.getReturnReason());
        accountExpense.setInputUser(SecurityUtils.getLoginUser().getNickName());
        accountExpense.setInputTime(new Date());
        accountExpenseMapper.insert(accountExpense);
        return true;
    }
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java
@@ -44,4 +44,7 @@
    @Schema(description = "结束日期")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
    @Schema(description = "是否生产")
    private Integer isProduction;
}
src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java
@@ -12,6 +12,7 @@
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
 * <p>
@@ -39,4 +40,24 @@
    Integer countPending(@Param("startDate") String startDate, @Param("endDate") String endDate);
    List<Map<String, Object>> selectHomeOrderProgressPage(@Param("status") Integer status,
                                                          @Param("offset") Long offset,
                                                          @Param("size") Long size,
                                                          @Param("startTime") LocalDateTime startTime,
                                                          @Param("endTime") LocalDateTime endTime);
    Long countHomeOrderProgress(@Param("status") Integer status,
                                @Param("startTime") LocalDateTime startTime,
                                @Param("endTime") LocalDateTime endTime);
    List<Map<String, Object>> countHomeOrderProgressByStatus(@Param("startTime") LocalDateTime startTime,
                                                              @Param("endTime") LocalDateTime endTime);
    List<Map<String, Object>> selectHomeTodayProductionPlan(@Param("size") Long size,
                                                             @Param("planStart") LocalDateTime planStart,
                                                             @Param("planEnd") LocalDateTime planEnd);
    Long countHomeTodayProductionPlan(@Param("planStart") LocalDateTime planStart,
                                      @Param("planEnd") LocalDateTime planEnd);
}
src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java
@@ -8,6 +8,8 @@
    R addRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation);
    R updateRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation);
    R deleteRouteItem(Long id);
    int sortRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation);
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
@@ -23,6 +23,7 @@
import com.ruoyi.production.pojo.ProductionOrderRoutingOperationParam;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.service.ProductionBomStructureService;
import com.ruoyi.production.util.TaskPlanQuantityUtil;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
import com.ruoyi.technology.mapper.TechnologyOperationParamMapper;
import com.ruoyi.technology.mapper.TechnologyParamMapper;
@@ -262,7 +263,7 @@
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionOrderRoutingOperation::getId, item -> item, (left, right) -> left));
        // Keep task plan quantities aligned with the same order BOM snapshot demand used during snapshot creation.
        Map<String, BigDecimal> demandedQuantityMap = buildOperationDemandedQuantityMap(structureList, rootProductModelId);
        Map<String, BigDecimal> demandedQuantityMap = TaskPlanQuantityUtil.buildOperationDemandedQuantityMap(structureList, rootProductModelId);
        for (ProductionOperationTask task : taskList) {
            if (task == null || task.getId() == null || task.getProductionOrderRoutingOperationId() == null) {
                continue;
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java
@@ -4,21 +4,15 @@
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingOperationMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingOperationParamMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperation;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperationParam;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.util.TaskPlanQuantityUtil;
import com.ruoyi.technology.mapper.*;
import com.ruoyi.production.pojo.*;
import com.ruoyi.production.service.ProductionOrderRoutingOperationService;
import com.ruoyi.production.service.ProductionProductMainService;
import com.ruoyi.technology.mapper.TechnologyOperationParamMapper;
import com.ruoyi.technology.mapper.TechnologyParamMapper;
import com.ruoyi.technology.pojo.TechnologyOperationParam;
import com.ruoyi.technology.pojo.TechnologyParam;
import com.ruoyi.technology.pojo.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -26,8 +20,7 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
@Service
@Transactional(rollbackFor = Exception.class)
@@ -42,6 +35,11 @@
    private final TechnologyOperationParamMapper technologyOperationParamMapper;
    private final TechnologyParamMapper technologyParamMapper;
    private final ProductionOrderRoutingOperationParamMapper productionOrderRoutingOperationParamMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderRoutingMapper productionOrderRoutingMapper;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
    @Override
    public R addRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation) {
@@ -102,6 +100,109 @@
            productionOperationTaskMapper.insert(productionOperationTask);
        }
        return R.ok();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R updateRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation) {
        Long operationId = productionOrderRoutingOperation.getId();
        // æ›´æ–°å·¥è‰ºè·¯çº¿å·¥åº
        productionOrderRoutingOperationMapper.updateById(productionOrderRoutingOperation);
        // é‡æ–°æŸ¥è¯¢å®Œæ•´è®°å½•(前端可能没有传递所有字段,如 productionOrderId)
        ProductionOrderRoutingOperation updatedOperation = productionOrderRoutingOperationMapper.selectById(operationId);
        if (updatedOperation == null) {
            throw new ServiceException("工艺路线工序不存在");
        }
        // æŸ¥è¯¢æ˜¯å¦å­˜åœ¨å·¥å•
        ProductionOperationTask productionOperationTask = productionOperationTaskMapper.selectOne(
                new LambdaQueryWrapper<ProductionOperationTask>()
                        .eq(ProductionOperationTask::getProductionOrderRoutingOperationId, operationId)
                        .last("limit 1"));
        // æ ¹æ®æ˜¯å¦éœ€è¦ç”Ÿäº§è¿›è¡Œå¤„理
        Boolean isProduction = updatedOperation.getIsProduction();
        if (Boolean.TRUE.equals(isProduction)) {
            // éœ€è¦ç”Ÿäº§ï¼šæ£€æŸ¥å·¥å•是否存在,不存在则生成
            if (productionOperationTask == null) {
                ProductionOperationTask task = new ProductionOperationTask();
                task.setProductionOrderRoutingOperationId(updatedOperation.getId());
                task.setProductionOrderId(updatedOperation.getProductionOrderId());
                // èŽ·å–ç”Ÿäº§è®¢å•
                ProductionOrder productionOrder = productionOrderMapper.selectById(updatedOperation.getProductionOrderId());
                if (productionOrder == null) {
                    throw new ServiceException("生产订单不存在");
                }
                // èŽ·å–è®¢å•BOM
                ProductionOrderBom orderBom = productionOrderBomMapper.selectOne(
                        Wrappers.<ProductionOrderBom>lambdaQuery()
                                .eq(ProductionOrderBom::getProductionOrderId, productionOrder.getId()));
                // ç¡®å®šæ ¹äº§å“è§„æ ¼ID
                Long rootProductModelId = orderBom != null && orderBom.getProductModelId() != null
                        ? orderBom.getProductModelId()
                        : productionOrder.getProductModelId();
                // èŽ·å–BOM结构列表
                List<ProductionBomStructure> orderBomStructureList = orderBom == null || orderBom.getId() == null
                        ? Collections.emptyList()
                        : productionBomStructureMapper.selectList(
                        Wrappers.<ProductionBomStructure>lambdaQuery()
                                .eq(ProductionBomStructure::getProductionOrderBomId, orderBom.getId())
                                .orderByAsc(ProductionBomStructure::getId));
                // æž„建工序需求量映射
                Map<String, BigDecimal> operationDemandedQuantityMap =
                        TaskPlanQuantityUtil.buildOperationDemandedQuantityMap(orderBomStructureList, rootProductModelId);
                // èŽ·å–å·¥è‰ºè·¯çº¿å·¥åºï¼ˆç”¨äºŽè®¡ç®—è®¡åˆ’æ•°é‡ï¼‰
                TechnologyRoutingOperation sourceOperation = technologyRoutingOperationMapper.selectById(
                        updatedOperation.getTechnologyRoutingOperationId());
                // å°†åŽŸæ¥çš„ç§æœ‰æ–¹æ³•æ›¿æ¢ä¸ºè°ƒç”¨å·¥å…·ç±»
                BigDecimal planQuantity = TaskPlanQuantityUtil.resolveTaskPlanQuantity(
                        sourceOperation,
                        operationDemandedQuantityMap,
                        productionOrder,
                        rootProductModelId);
                task.setPlanQuantity(planQuantity);
                task.setCompleteQuantity(BigDecimal.ZERO);
                task.setWorkOrderNo(generateNextTaskNo());
                task.setStatus(2);
                productionOperationTaskMapper.insert(task);
            }
        } else {
            // ä¸éœ€è¦ç”Ÿäº§ï¼šæ£€æŸ¥å·¥å•是否存在
            if (productionOperationTask != null) {
                validateTaskCanRemove(productionOperationTask);
                // æ²¡æœ‰æŠ¥å·¥ï¼Œåˆ™åˆ é™¤å·¥å•
                productionOperationTaskMapper.deleteById(productionOperationTask.getId());
            }
        }
        return R.ok();
    }
    private void validateTaskCanRemove(ProductionOperationTask task) {
        if (task == null || task.getId() == null) {
            return;
        }
        if (defaultDecimal(task.getCompleteQuantity()).compareTo(BigDecimal.ZERO) > 0) {
            throw new ServiceException("工序已产生报工记录,无法根据 BOM å˜æ›´åˆ é™¤å¯¹åº”工序快照");
        }
        long reportCount = productionProductMainMapper.selectCount(
                Wrappers.<ProductionProductMain>lambdaQuery()
                        .eq(ProductionProductMain::getProductionOperationTaskId, task.getId()));
        if (reportCount > 0) {
            throw new ServiceException("工序已产生报工记录,无法根据 BOM å˜æ›´åˆ é™¤å¯¹åº”工单");
        }
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    @Override
@@ -183,10 +284,23 @@
        return 0;
    }
    @Override
    public Boolean updateRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation) {
        //根据是否生产判断是否需要删除对应的工单
        return null;
    private String generateNextTaskNo() {
        // ç”Ÿæˆä¸‹ä¸€ä¸ªç”Ÿäº§å·¥å•号
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String prefix = "GD" + datePrefix;
        ProductionOperationTask lastTask = productionOperationTaskMapper.selectOne(
                Wrappers.<ProductionOperationTask>lambdaQuery()
                        .likeRight(ProductionOperationTask::getWorkOrderNo, prefix)
                        .orderByDesc(ProductionOperationTask::getWorkOrderNo)
                        .last("limit 1"));
        int sequence = 1;
        if (lastTask != null && lastTask.getWorkOrderNo() != null && lastTask.getWorkOrderNo().startsWith(prefix)) {
            try {
                sequence = Integer.parseInt(lastTask.getWorkOrderNo().substring(prefix.length())) + 1;
            } catch (NumberFormatException ignored) {
                sequence = 1;
            }
        }
        return prefix + String.format("%03d", sequence);
    }
}
src/main/java/com/ruoyi/production/util/TaskPlanQuantityUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,137 @@
package com.ruoyi.production.util;
import com.ruoyi.production.pojo.ProductionBomStructure;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperation;
import com.ruoyi.technology.pojo.TechnologyRoutingOperation;
import lombok.experimental.UtilityClass;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
/**
 * å·¥å•计划数量计算工具类
 */
@UtilityClass
public class TaskPlanQuantityUtil {
    /**
     * è®¡ç®—工单计划数量(使用 TechnologyRoutingOperation)
     */
    public BigDecimal resolveTaskPlanQuantity(TechnologyRoutingOperation sourceOperation,
                                              Map<String, BigDecimal> operationDemandedQuantityMap,
                                              ProductionOrder productionOrder,
                                              Long rootProductModelId) {
        if (sourceOperation == null || operationDemandedQuantityMap == null || operationDemandedQuantityMap.isEmpty()) {
            return defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
        }
        Long outputProductModelId = sourceOperation.getProductModelId() != null
                ? sourceOperation.getProductModelId()
                : rootProductModelId;
        String key = buildOperationDemandedQuantityKey(sourceOperation.getTechnologyOperationId(), outputProductModelId);
        BigDecimal planQuantity = operationDemandedQuantityMap.get(key);
        return planQuantity != null ? planQuantity : defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
    }
    /**
     * è®¡ç®—工单计划数量(使用 ProductionOrderRoutingOperation)
     */
    public BigDecimal resolveTaskPlanQuantity(ProductionOrderRoutingOperation routingOperation,
                                              Map<String, BigDecimal> demandedQuantityMap,
                                              BigDecimal orderQuantity,
                                              Long rootProductModelId) {
        if (routingOperation == null || demandedQuantityMap == null || demandedQuantityMap.isEmpty()) {
            return orderQuantity;
        }
        Long outputProductModelId = routingOperation.getProductModelId() != null
                ? routingOperation.getProductModelId()
                : rootProductModelId;
        String key = buildOperationDemandedQuantityKey(routingOperation.getTechnologyOperationId(), outputProductModelId);
        BigDecimal planQuantity = demandedQuantityMap.get(key);
        return planQuantity != null ? planQuantity : orderQuantity;
    }
    /**
     * æž„建工序需求量映射表
     */
    public Map<String, BigDecimal> buildOperationDemandedQuantityMap(List<ProductionBomStructure> bomStructures, Long rootProductModelId) {
        if (bomStructures == null || bomStructures.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, ProductionBomStructure> structureById = new HashMap<>();
        for (ProductionBomStructure item : bomStructures) {
            if (item != null && item.getId() != null) {
                structureById.put(item.getId(), item);
            }
        }
        Map<String, BigDecimal> demandedQuantityMap = new HashMap<>();
        Set<String> mergedOutputNodeKeySet = new HashSet<>();
        for (ProductionBomStructure bomStructure : bomStructures) {
            if (bomStructure == null || bomStructure.getTechnologyOperationId() == null) {
                continue;
            }
            ProductionBomStructure outputNode = resolveOperationOutputNode(bomStructure, structureById);
            Long outputProductModelId = resolveOutputProductModelId(outputNode, rootProductModelId);
            if (outputProductModelId == null) {
                continue;
            }
            String mergedOutputNodeKey = buildOperationOutputNodeKey(bomStructure.getTechnologyOperationId(),
                    outputNode == null ? null : outputNode.getId(), outputProductModelId);
            if (!mergedOutputNodeKeySet.add(mergedOutputNodeKey)) {
                continue;
            }
            BigDecimal demandedQuantity = defaultDecimal(outputNode == null ? null : outputNode.getDemandedQuantity());
            String key = buildOperationDemandedQuantityKey(bomStructure.getTechnologyOperationId(), outputProductModelId);
            demandedQuantityMap.merge(key, demandedQuantity, BigDecimal::add);
        }
        return demandedQuantityMap;
    }
    /**
     * æž„建工序需求量key
     */
    public String buildOperationDemandedQuantityKey(Long operationId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId);
    }
    /**
     * æž„建输出节点key
     */
    public String buildOperationOutputNodeKey(Long operationId, Long outputNodeId, Long outputProductModelId) {
        return String.valueOf(operationId) + "#" + String.valueOf(outputNodeId) + "#" + String.valueOf(outputProductModelId);
    }
    /**
     * è§£æžå·¥åºè¾“出节点
     */
    public ProductionBomStructure resolveOperationOutputNode(ProductionBomStructure bomStructure,
                                                             Map<Long, ProductionBomStructure> structureById) {
        if (bomStructure == null) {
            return null;
        }
        if (bomStructure.getParentId() == null) {
            return bomStructure;
        }
        ProductionBomStructure parent = structureById.get(bomStructure.getParentId());
        return parent != null ? parent : bomStructure;
    }
    /**
     * è§£æžè¾“出产品规格ID
     */
    public Long resolveOutputProductModelId(ProductionBomStructure outputNode, Long rootProductModelId) {
        if (outputNode == null) {
            return rootProductModelId;
        }
        return outputNode.getProductModelId() != null ? outputNode.getProductModelId() : rootProductModelId;
    }
    /**
     * é»˜è®¤BigDecimal值
     */
    public BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
}
src/main/java/com/ruoyi/project/common/CaptchaController.java
@@ -1,98 +1,98 @@
package com.ruoyi.project.common;
import com.google.code.kaptcha.Producer;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.sign.Base64;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.framework.redis.RedisCache;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.service.ISysConfigService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
 * éªŒè¯ç æ“ä½œå¤„理
 *
 * @author ruoyi
 */
@RestController
@RequiredArgsConstructor
public class CaptchaController
{
    @Resource(name = "captchaProducer")
    private Producer captchaProducer;
    @Resource(name = "captchaProducerMath")
    private Producer captchaProducerMath;
    private final RedisCache redisCache;
    // éªŒè¯ç ç±»åž‹
    @Value("${ruoyi.captchaType}")
    private String captchaType;
    private final ISysConfigService configService;
    /**
     * ç”ŸæˆéªŒè¯ç 
     */
    @GetMapping("/captchaImage")
    public AjaxResult getCode(HttpServletResponse response) throws IOException
    {
        AjaxResult ajax = AjaxResult.success();
        boolean captchaEnabled = configService.selectCaptchaEnabled();
        ajax.put("captchaEnabled", captchaEnabled);
        if (!captchaEnabled)
        {
            return ajax;
        }
        // ä¿å­˜éªŒè¯ç ä¿¡æ¯
        String uuid = IdUtils.simpleUUID();
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
        String capStr = null, code = null;
        BufferedImage image = null;
        // ç”ŸæˆéªŒè¯ç 
        if ("math".equals(captchaType))
        {
            String capText = captchaProducerMath.createText();
            capStr = capText.substring(0, capText.lastIndexOf("@"));
            code = capText.substring(capText.lastIndexOf("@") + 1);
            image = captchaProducerMath.createImage(capStr);
        }
        else if ("char".equals(captchaType))
        {
            capStr = code = captchaProducer.createText();
            image = captchaProducer.createImage(capStr);
        }
        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
        // è½¬æ¢æµä¿¡æ¯å†™å‡º
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try
        {
            ImageIO.write(image, "jpg", os);
        }
        catch (IOException e)
        {
            return AjaxResult.error(e.getMessage());
        }
        ajax.put("uuid", uuid);
        ajax.put("img", Base64.encode(os.toByteArray()));
        return ajax;
    }
}
package com.ruoyi.project.common;
import com.google.code.kaptcha.Producer;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.sign.Base64;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.framework.redis.RedisCache;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.service.ISysConfigService;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
 * éªŒè¯ç æ“ä½œå¤„理
 *
 * @author ruoyi
 */
@RestController
@RequiredArgsConstructor
public class CaptchaController
{
    @Resource(name = "captchaProducer")
    private Producer captchaProducer;
    @Resource(name = "captchaProducerMath")
    private Producer captchaProducerMath;
    private final RedisCache redisCache;
    // éªŒè¯ç ç±»åž‹
    @Value("${ruoyi.captchaType}")
    private String captchaType;
    private final ISysConfigService configService;
    /**
     * ç”ŸæˆéªŒè¯ç 
     */
    @GetMapping("/captchaImage")
    public AjaxResult getCode(HttpServletResponse response) throws IOException
    {
        AjaxResult ajax = AjaxResult.success();
        boolean captchaEnabled = configService.selectCaptchaEnabled();
        ajax.put("captchaEnabled", captchaEnabled);
        if (!captchaEnabled)
        {
            return ajax;
        }
        // ä¿å­˜éªŒè¯ç ä¿¡æ¯
        String uuid = IdUtils.simpleUUID();
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
        String capStr = null, code = null;
        BufferedImage image = null;
        // ç”ŸæˆéªŒè¯ç 
        if ("math".equals(captchaType))
        {
            String capText = captchaProducerMath.createText();
            capStr = capText.substring(0, capText.lastIndexOf("@"));
            code = capText.substring(capText.lastIndexOf("@") + 1);
            image = captchaProducerMath.createImage(capStr);
        }
        else if ("char".equals(captchaType))
        {
            capStr = code = captchaProducer.createText();
            image = captchaProducer.createImage(capStr);
        }
        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
        // è½¬æ¢æµä¿¡æ¯å†™å‡º
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try
        {
            ImageIO.write(image, "jpg", os);
        }
        catch (IOException e)
        {
            return AjaxResult.error(e.getMessage());
        }
        ajax.put("uuid", uuid);
        ajax.put("img", Base64.encode(os.toByteArray()));
        return ajax;
    }
}
src/main/java/com/ruoyi/project/monitor/controller/CacheController.java
@@ -1,111 +1,111 @@
package com.ruoyi.project.monitor.controller;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.monitor.domain.SysCache;
import lombok.AllArgsConstructor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
 * ç¼“存监控
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/cache")
@AllArgsConstructor
public class CacheController
{
    private RedisTemplate<String, String> redisTemplate;
    private final static List<SysCache> caches = new ArrayList<SysCache>();
    {
        caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
        caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
        caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
        caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
        caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
        caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
        caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @GetMapping()
    public AjaxResult getInfo() throws Exception
    {
        Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
        Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
        Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
        Map<String, Object> result = new HashMap<>(3);
        result.put("info", info);
        result.put("dbSize", dbSize);
        List<Map<String, String>> pieList = new ArrayList<>();
        commandStats.stringPropertyNames().forEach(key -> {
            Map<String, String> data = new HashMap<>(2);
            String property = commandStats.getProperty(key);
            data.put("name", StringUtils.removeStart(key, "cmdstat_"));
            data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
            pieList.add(data);
        });
        result.put("commandStats", pieList);
        return AjaxResult.success(result);
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @GetMapping("/getNames")
    public AjaxResult cache()
    {
        return AjaxResult.success(caches);
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @GetMapping("/getKeys/{cacheName}")
    public AjaxResult getCacheKeys(@PathVariable String cacheName)
    {
        Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
        return AjaxResult.success(new TreeSet<>(cacheKeys));
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @GetMapping("/getValue/{cacheName}/{cacheKey}")
    public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
    {
        String cacheValue = redisTemplate.opsForValue().get(cacheKey);
        SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
        return AjaxResult.success(sysCache);
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @DeleteMapping("/clearCacheName/{cacheName}")
    public AjaxResult clearCacheName(@PathVariable String cacheName)
    {
        Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
        redisTemplate.delete(cacheKeys);
        return AjaxResult.success();
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @DeleteMapping("/clearCacheKey/{cacheKey}")
    public AjaxResult clearCacheKey(@PathVariable String cacheKey)
    {
        redisTemplate.delete(cacheKey);
        return AjaxResult.success();
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @DeleteMapping("/clearCacheAll")
    public AjaxResult clearCacheAll()
    {
        Collection<String> cacheKeys = redisTemplate.keys("*");
        redisTemplate.delete(cacheKeys);
        return AjaxResult.success();
    }
}
package com.ruoyi.project.monitor.controller;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.monitor.domain.SysCache;
import lombok.AllArgsConstructor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
 * ç¼“存监控
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/cache")
@AllArgsConstructor
public class CacheController
{
    private RedisTemplate<String, String> redisTemplate;
    private final static List<SysCache> caches = new ArrayList<SysCache>();
    {
        caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
        caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
        caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
        caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
        caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
        caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
        caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @GetMapping()
    public AjaxResult getInfo() throws Exception
    {
        Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
        Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
        Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
        Map<String, Object> result = new HashMap<>(3);
        result.put("info", info);
        result.put("dbSize", dbSize);
        List<Map<String, String>> pieList = new ArrayList<>();
        commandStats.stringPropertyNames().forEach(key -> {
            Map<String, String> data = new HashMap<>(2);
            String property = commandStats.getProperty(key);
            data.put("name", StringUtils.removeStart(key, "cmdstat_"));
            data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
            pieList.add(data);
        });
        result.put("commandStats", pieList);
        return AjaxResult.success(result);
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @GetMapping("/getNames")
    public AjaxResult cache()
    {
        return AjaxResult.success(caches);
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @GetMapping("/getKeys/{cacheName}")
    public AjaxResult getCacheKeys(@PathVariable String cacheName)
    {
        Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
        return AjaxResult.success(new TreeSet<>(cacheKeys));
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @GetMapping("/getValue/{cacheName}/{cacheKey}")
    public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
    {
        String cacheValue = redisTemplate.opsForValue().get(cacheKey);
        SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
        return AjaxResult.success(sysCache);
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @DeleteMapping("/clearCacheName/{cacheName}")
    public AjaxResult clearCacheName(@PathVariable String cacheName)
    {
        Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
        redisTemplate.delete(cacheKeys);
        return AjaxResult.success();
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @DeleteMapping("/clearCacheKey/{cacheKey}")
    public AjaxResult clearCacheKey(@PathVariable String cacheKey)
    {
        redisTemplate.delete(cacheKey);
        return AjaxResult.success();
    }
    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
    @DeleteMapping("/clearCacheAll")
    public AjaxResult clearCacheAll()
    {
        Collection<String> cacheKeys = redisTemplate.keys("*");
        redisTemplate.delete(cacheKeys);
        return AjaxResult.success();
    }
}
src/main/java/com/ruoyi/project/monitor/controller/ServerController.java
@@ -1,27 +1,27 @@
package com.ruoyi.project.monitor.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.Server;
/**
 * æœåŠ¡å™¨ç›‘æŽ§
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/server")
public class ServerController
{
    @PreAuthorize("@ss.hasPermi('monitor:server:list')")
    @GetMapping()
    public AjaxResult getInfo() throws Exception
    {
        Server server = new Server();
        server.copyTo();
        return AjaxResult.success(server);
    }
}
package com.ruoyi.project.monitor.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.Server;
/**
 * æœåŠ¡å™¨ç›‘æŽ§
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/server")
public class ServerController
{
    @PreAuthorize("@ss.hasPermi('monitor:server:list')")
    @GetMapping()
    public AjaxResult getInfo() throws Exception
    {
        Server server = new Server();
        server.copyTo();
        return AjaxResult.success(server);
    }
}
src/main/java/com/ruoyi/project/monitor/controller/SysJobController.java
@@ -1,186 +1,186 @@
package com.ruoyi.project.monitor.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.exception.job.TaskException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.job.CronUtils;
import com.ruoyi.common.utils.job.ScheduleUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.monitor.domain.SysJob;
import com.ruoyi.project.monitor.service.ISysJobService;
/**
 * è°ƒåº¦ä»»åŠ¡ä¿¡æ¯æ“ä½œå¤„ç†
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/job")
@AllArgsConstructor
public class SysJobController extends BaseController
{
    private ISysJobService jobService;
    /**
     * æŸ¥è¯¢å®šæ—¶ä»»åŠ¡åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysJob sysJob)
    {
        startPage();
        List<SysJob> list = jobService.selectJobList(sysJob);
        return getDataTable(list);
    }
    /**
     * å¯¼å‡ºå®šæ—¶ä»»åŠ¡åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:export')")
    @Log(title = "定时任务", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysJob sysJob)
    {
        List<SysJob> list = jobService.selectJobList(sysJob);
        ExcelUtil<SysJob> util = new ExcelUtil<SysJob>(SysJob.class);
        util.exportExcel(response, list, "定时任务");
    }
    /**
     * èŽ·å–å®šæ—¶ä»»åŠ¡è¯¦ç»†ä¿¡æ¯
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:query')")
    @GetMapping(value = "/{jobId}")
    public AjaxResult getInfo(@PathVariable("jobId") Long jobId)
    {
        return success(jobService.selectJobById(jobId));
    }
    /**
     * æ–°å¢žå®šæ—¶ä»»åŠ¡
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:add')")
    @Log(title = "定时任务", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException
    {
        if (!CronUtils.isValid(job.getCronExpression()))
        {
            return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确");
        }
        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
        {
            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS }))
        {
            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
        {
            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR))
        {
            return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规");
        }
        else if (!ScheduleUtils.whiteList(job.getInvokeTarget()))
        {
            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
        }
        job.setCreateBy(getUsername());
        return toAjax(jobService.insertJob(job));
    }
    /**
     * ä¿®æ”¹å®šæ—¶ä»»åŠ¡
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:edit')")
    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException
    {
        if (!CronUtils.isValid(job.getCronExpression()))
        {
            return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确");
        }
        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
        {
            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS }))
        {
            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
        {
            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR))
        {
            return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规");
        }
        else if (!ScheduleUtils.whiteList(job.getInvokeTarget()))
        {
            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
        }
        job.setUpdateBy(getUsername());
        return toAjax(jobService.updateJob(job));
    }
    /**
     * å®šæ—¶ä»»åŠ¡çŠ¶æ€ä¿®æ”¹
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
    @PutMapping("/changeStatus")
    public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException
    {
        SysJob newJob = jobService.selectJobById(job.getJobId());
        newJob.setStatus(job.getStatus());
        return toAjax(jobService.changeStatus(newJob));
    }
    /**
     * å®šæ—¶ä»»åŠ¡ç«‹å³æ‰§è¡Œä¸€æ¬¡
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
    @PutMapping("/run")
    public AjaxResult run(@RequestBody SysJob job) throws SchedulerException
    {
        boolean result = jobService.run(job);
        return result ? success() : error("任务不存在或已过期!");
    }
    /**
     * åˆ é™¤å®šæ—¶ä»»åŠ¡
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
    @Log(title = "定时任务", businessType = BusinessType.DELETE)
    @DeleteMapping("/{jobIds}")
    public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException
    {
        jobService.deleteJobByIds(jobIds);
        return success();
    }
}
package com.ruoyi.project.monitor.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.exception.job.TaskException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.job.CronUtils;
import com.ruoyi.common.utils.job.ScheduleUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.monitor.domain.SysJob;
import com.ruoyi.project.monitor.service.ISysJobService;
/**
 * è°ƒåº¦ä»»åŠ¡ä¿¡æ¯æ“ä½œå¤„ç†
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/job")
@AllArgsConstructor
public class SysJobController extends BaseController
{
    private ISysJobService jobService;
    /**
     * æŸ¥è¯¢å®šæ—¶ä»»åŠ¡åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysJob sysJob)
    {
        startPage();
        List<SysJob> list = jobService.selectJobList(sysJob);
        return getDataTable(list);
    }
    /**
     * å¯¼å‡ºå®šæ—¶ä»»åŠ¡åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:export')")
    @Log(title = "定时任务", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysJob sysJob)
    {
        List<SysJob> list = jobService.selectJobList(sysJob);
        ExcelUtil<SysJob> util = new ExcelUtil<SysJob>(SysJob.class);
        util.exportExcel(response, list, "定时任务");
    }
    /**
     * èŽ·å–å®šæ—¶ä»»åŠ¡è¯¦ç»†ä¿¡æ¯
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:query')")
    @GetMapping(value = "/{jobId}")
    public AjaxResult getInfo(@PathVariable("jobId") Long jobId)
    {
        return success(jobService.selectJobById(jobId));
    }
    /**
     * æ–°å¢žå®šæ—¶ä»»åŠ¡
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:add')")
    @Log(title = "定时任务", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@RequestBody SysJob job) throws SchedulerException, TaskException
    {
        if (!CronUtils.isValid(job.getCronExpression()))
        {
            return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确");
        }
        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
        {
            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS }))
        {
            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
        {
            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR))
        {
            return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规");
        }
        else if (!ScheduleUtils.whiteList(job.getInvokeTarget()))
        {
            return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
        }
        job.setCreateBy(getUsername());
        return toAjax(jobService.insertJob(job));
    }
    /**
     * ä¿®æ”¹å®šæ—¶ä»»åŠ¡
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:edit')")
    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@RequestBody SysJob job) throws SchedulerException, TaskException
    {
        if (!CronUtils.isValid(job.getCronExpression()))
        {
            return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确");
        }
        else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI))
        {
            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS }))
        {
            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[] { Constants.HTTP, Constants.HTTPS }))
        {
            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
        }
        else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR))
        {
            return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规");
        }
        else if (!ScheduleUtils.whiteList(job.getInvokeTarget()))
        {
            return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
        }
        job.setUpdateBy(getUsername());
        return toAjax(jobService.updateJob(job));
    }
    /**
     * å®šæ—¶ä»»åŠ¡çŠ¶æ€ä¿®æ”¹
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
    @PutMapping("/changeStatus")
    public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException
    {
        SysJob newJob = jobService.selectJobById(job.getJobId());
        newJob.setStatus(job.getStatus());
        return toAjax(jobService.changeStatus(newJob));
    }
    /**
     * å®šæ—¶ä»»åŠ¡ç«‹å³æ‰§è¡Œä¸€æ¬¡
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
    @PutMapping("/run")
    public AjaxResult run(@RequestBody SysJob job) throws SchedulerException
    {
        boolean result = jobService.run(job);
        return result ? success() : error("任务不存在或已过期!");
    }
    /**
     * åˆ é™¤å®šæ—¶ä»»åŠ¡
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
    @Log(title = "定时任务", businessType = BusinessType.DELETE)
    @DeleteMapping("/{jobIds}")
    public AjaxResult remove(@PathVariable Long[] jobIds) throws SchedulerException
    {
        jobService.deleteJobByIds(jobIds);
        return success();
    }
}
src/main/java/com/ruoyi/project/monitor/controller/SysJobLogController.java
@@ -1,93 +1,93 @@
package com.ruoyi.project.monitor.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.monitor.domain.SysJobLog;
import com.ruoyi.project.monitor.service.ISysJobLogService;
/**
 * è°ƒåº¦æ—¥å¿—操作处理
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/jobLog")
@AllArgsConstructor
public class SysJobLogController extends BaseController
{
    private ISysJobLogService jobLogService;
    /**
     * æŸ¥è¯¢å®šæ—¶ä»»åŠ¡è°ƒåº¦æ—¥å¿—åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysJobLog sysJobLog)
    {
        startPage();
        List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
        return getDataTable(list);
    }
    /**
     * å¯¼å‡ºå®šæ—¶ä»»åŠ¡è°ƒåº¦æ—¥å¿—åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:export')")
    @Log(title = "任务调度日志", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysJobLog sysJobLog)
    {
        List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
        ExcelUtil<SysJobLog> util = new ExcelUtil<SysJobLog>(SysJobLog.class);
        util.exportExcel(response, list, "调度日志");
    }
    /**
     * æ ¹æ®è°ƒåº¦ç¼–号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:query')")
    @GetMapping(value = "/{jobLogId}")
    public AjaxResult getInfo(@PathVariable Long jobLogId)
    {
        return success(jobLogService.selectJobLogById(jobLogId));
    }
    /**
     * åˆ é™¤å®šæ—¶ä»»åŠ¡è°ƒåº¦æ—¥å¿—
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
    @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE)
    @DeleteMapping("/{jobLogIds}")
    public AjaxResult remove(@PathVariable Long[] jobLogIds)
    {
        return toAjax(jobLogService.deleteJobLogByIds(jobLogIds));
    }
    /**
     * æ¸…空定时任务调度日志
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
    @Log(title = "调度日志", businessType = BusinessType.CLEAN)
    @DeleteMapping("/clean")
    public AjaxResult clean()
    {
        jobLogService.cleanJobLog();
        return success();
    }
}
package com.ruoyi.project.monitor.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.monitor.domain.SysJobLog;
import com.ruoyi.project.monitor.service.ISysJobLogService;
/**
 * è°ƒåº¦æ—¥å¿—操作处理
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/jobLog")
@AllArgsConstructor
public class SysJobLogController extends BaseController
{
    private ISysJobLogService jobLogService;
    /**
     * æŸ¥è¯¢å®šæ—¶ä»»åŠ¡è°ƒåº¦æ—¥å¿—åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysJobLog sysJobLog)
    {
        startPage();
        List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
        return getDataTable(list);
    }
    /**
     * å¯¼å‡ºå®šæ—¶ä»»åŠ¡è°ƒåº¦æ—¥å¿—åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:export')")
    @Log(title = "任务调度日志", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysJobLog sysJobLog)
    {
        List<SysJobLog> list = jobLogService.selectJobLogList(sysJobLog);
        ExcelUtil<SysJobLog> util = new ExcelUtil<SysJobLog>(SysJobLog.class);
        util.exportExcel(response, list, "调度日志");
    }
    /**
     * æ ¹æ®è°ƒåº¦ç¼–号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:query')")
    @GetMapping(value = "/{jobLogId}")
    public AjaxResult getInfo(@PathVariable Long jobLogId)
    {
        return success(jobLogService.selectJobLogById(jobLogId));
    }
    /**
     * åˆ é™¤å®šæ—¶ä»»åŠ¡è°ƒåº¦æ—¥å¿—
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
    @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE)
    @DeleteMapping("/{jobLogIds}")
    public AjaxResult remove(@PathVariable Long[] jobLogIds)
    {
        return toAjax(jobLogService.deleteJobLogByIds(jobLogIds));
    }
    /**
     * æ¸…空定时任务调度日志
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:remove')")
    @Log(title = "调度日志", businessType = BusinessType.CLEAN)
    @DeleteMapping("/clean")
    public AjaxResult clean()
    {
        jobLogService.cleanJobLog();
        return success();
    }
}
src/main/java/com/ruoyi/project/monitor/controller/SysLogininforController.java
@@ -1,70 +1,70 @@
package com.ruoyi.project.monitor.controller;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.security.service.SysPasswordService;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.monitor.domain.SysLogininfor;
import com.ruoyi.project.monitor.service.ISysLogininforService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * ç³»ç»Ÿè®¿é—®è®°å½•
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/logininfor")
@AllArgsConstructor
public class SysLogininforController extends BaseController {
    private ISysLogininforService logininforService;
    private SysPasswordService passwordService;
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysLogininfor logininfor) {
        startPage();
        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
        return getDataTable(list);
    }
    @Log(title = "登录日志", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysLogininfor logininfor) {
        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
        ExcelUtil<SysLogininfor> util = new ExcelUtil<SysLogininfor>(SysLogininfor.class);
        util.exportExcel(response, list, "登录日志");
    }
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
    @Log(title = "登录日志", businessType = BusinessType.DELETE)
    @DeleteMapping("/{infoIds}")
    public AjaxResult remove(@PathVariable Long[] infoIds) {
        return toAjax(logininforService.deleteLogininforByIds(infoIds));
    }
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
    @Log(title = "登录日志", businessType = BusinessType.CLEAN)
    @DeleteMapping("/clean")
    public AjaxResult clean() {
        logininforService.cleanLogininfor();
        return success();
    }
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
    @Log(title = "账户解锁", businessType = BusinessType.OTHER)
    @GetMapping("/unlock/{userName}")
    public AjaxResult unlock(@PathVariable("userName") String userName) {
        passwordService.clearLoginRecordCache(userName);
        return success();
    }
}
package com.ruoyi.project.monitor.controller;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.security.service.SysPasswordService;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.monitor.domain.SysLogininfor;
import com.ruoyi.project.monitor.service.ISysLogininforService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * ç³»ç»Ÿè®¿é—®è®°å½•
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/logininfor")
@AllArgsConstructor
public class SysLogininforController extends BaseController {
    private ISysLogininforService logininforService;
    private SysPasswordService passwordService;
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysLogininfor logininfor) {
        startPage();
        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
        return getDataTable(list);
    }
    @Log(title = "登录日志", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysLogininfor logininfor) {
        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
        ExcelUtil<SysLogininfor> util = new ExcelUtil<SysLogininfor>(SysLogininfor.class);
        util.exportExcel(response, list, "登录日志");
    }
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
    @Log(title = "登录日志", businessType = BusinessType.DELETE)
    @DeleteMapping("/{infoIds}")
    public AjaxResult remove(@PathVariable Long[] infoIds) {
        return toAjax(logininforService.deleteLogininforByIds(infoIds));
    }
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
    @Log(title = "登录日志", businessType = BusinessType.CLEAN)
    @DeleteMapping("/clean")
    public AjaxResult clean() {
        logininforService.cleanLogininfor();
        return success();
    }
    @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
    @Log(title = "账户解锁", businessType = BusinessType.OTHER)
    @GetMapping("/unlock/{userName}")
    public AjaxResult unlock(@PathVariable("userName") String userName) {
        passwordService.clearLoginRecordCache(userName);
        return success();
    }
}
src/main/java/com/ruoyi/project/monitor/controller/SysOperlogController.java
@@ -1,70 +1,70 @@
package com.ruoyi.project.monitor.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.monitor.domain.SysOperLog;
import com.ruoyi.project.monitor.service.ISysOperLogService;
/**
 * æ“ä½œæ—¥å¿—记录
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/operlog")
@AllArgsConstructor
public class SysOperlogController extends BaseController
{
    private ISysOperLogService operLogService;
    @PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysOperLog operLog)
    {
        startPage();
        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
        return getDataTable(list);
    }
    @Log(title = "操作日志", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('monitor:operlog:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysOperLog operLog)
    {
        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
        ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
        util.exportExcel(response, list, "操作日志");
    }
    @Log(title = "操作日志", businessType = BusinessType.DELETE)
    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
    @DeleteMapping("/{operIds}")
    public AjaxResult remove(@PathVariable Long[] operIds)
    {
        return toAjax(operLogService.deleteOperLogByIds(operIds));
    }
    @Log(title = "操作日志", businessType = BusinessType.CLEAN)
    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
    @DeleteMapping("/clean")
    public AjaxResult clean()
    {
        operLogService.cleanOperLog();
        return success();
    }
}
package com.ruoyi.project.monitor.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.monitor.domain.SysOperLog;
import com.ruoyi.project.monitor.service.ISysOperLogService;
/**
 * æ“ä½œæ—¥å¿—记录
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/operlog")
@AllArgsConstructor
public class SysOperlogController extends BaseController
{
    private ISysOperLogService operLogService;
    @PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysOperLog operLog)
    {
        startPage();
        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
        return getDataTable(list);
    }
    @Log(title = "操作日志", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('monitor:operlog:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysOperLog operLog)
    {
        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
        ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
        util.exportExcel(response, list, "操作日志");
    }
    @Log(title = "操作日志", businessType = BusinessType.DELETE)
    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
    @DeleteMapping("/{operIds}")
    public AjaxResult remove(@PathVariable Long[] operIds)
    {
        return toAjax(operLogService.deleteOperLogByIds(operIds));
    }
    @Log(title = "操作日志", businessType = BusinessType.CLEAN)
    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
    @DeleteMapping("/clean")
    public AjaxResult clean()
    {
        operLogService.cleanOperLog();
        return success();
    }
}
src/main/java/com/ruoyi/project/monitor/controller/SysUserOnlineController.java
@@ -1,83 +1,83 @@
package com.ruoyi.project.monitor.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.redis.RedisCache;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.monitor.domain.SysUserOnline;
import com.ruoyi.project.system.service.ISysUserOnlineService;
/**
 * åœ¨çº¿ç”¨æˆ·ç›‘控
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/online")
@AllArgsConstructor
public class SysUserOnlineController extends BaseController
{
    private ISysUserOnlineService userOnlineService;
    private RedisCache redisCache;
    @PreAuthorize("@ss.hasPermi('monitor:online:list')")
    @GetMapping("/list")
    public TableDataInfo list(String ipaddr, String userName)
    {
        Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
        List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
        for (String key : keys)
        {
            LoginUser user = redisCache.getCacheObject(key);
            if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
            {
                userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
            }
            else if (StringUtils.isNotEmpty(ipaddr))
            {
                userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
            }
            else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser()))
            {
                userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
            }
            else
            {
                userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
            }
        }
        Collections.reverse(userOnlineList);
        userOnlineList.removeAll(Collections.singleton(null));
        return getDataTable(userOnlineList);
    }
    /**
     * å¼ºé€€ç”¨æˆ·
     */
    @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
    @Log(title = "在线用户", businessType = BusinessType.FORCE)
    @DeleteMapping("/{tokenId}")
    public AjaxResult forceLogout(@PathVariable String tokenId)
    {
        redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
        return success();
    }
}
package com.ruoyi.project.monitor.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.redis.RedisCache;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.monitor.domain.SysUserOnline;
import com.ruoyi.project.system.service.ISysUserOnlineService;
/**
 * åœ¨çº¿ç”¨æˆ·ç›‘控
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/monitor/online")
@AllArgsConstructor
public class SysUserOnlineController extends BaseController
{
    private ISysUserOnlineService userOnlineService;
    private RedisCache redisCache;
    @PreAuthorize("@ss.hasPermi('monitor:online:list')")
    @GetMapping("/list")
    public TableDataInfo list(String ipaddr, String userName)
    {
        Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
        List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
        for (String key : keys)
        {
            LoginUser user = redisCache.getCacheObject(key);
            if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName))
            {
                userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
            }
            else if (StringUtils.isNotEmpty(ipaddr))
            {
                userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
            }
            else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser()))
            {
                userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
            }
            else
            {
                userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
            }
        }
        Collections.reverse(userOnlineList);
        userOnlineList.removeAll(Collections.singleton(null));
        return getDataTable(userOnlineList);
    }
    /**
     * å¼ºé€€ç”¨æˆ·
     */
    @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
    @Log(title = "在线用户", businessType = BusinessType.FORCE)
    @DeleteMapping("/{tokenId}")
    public AjaxResult forceLogout(@PathVariable String tokenId)
    {
        redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
        return success();
    }
}
src/main/java/com/ruoyi/project/system/controller/SysConfigController.java
@@ -1,127 +1,127 @@
package com.ruoyi.project.system.controller;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysConfig;
import com.ruoyi.project.system.service.ISysConfigService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * å‚数配置 ä¿¡æ¯æ“ä½œå¤„理
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/config")
@AllArgsConstructor
public class SysConfigController extends BaseController
{
    private ISysConfigService configService;
    /**
     * èŽ·å–å‚æ•°é…ç½®åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:config:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysConfig config)
    {
        startPage();
        List<SysConfig> list = configService.selectConfigList(config);
        return getDataTable(list);
    }
    @Log(title = "参数管理", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:config:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysConfig config)
    {
        List<SysConfig> list = configService.selectConfigList(config);
        ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
        util.exportExcel(response, list, "参数数据");
    }
    /**
     * æ ¹æ®å‚数编号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:config:query')")
    @GetMapping(value = "/{configId}")
    public AjaxResult getInfo(@PathVariable Long configId)
    {
        return success(configService.selectConfigById(configId));
    }
    /**
     * æ ¹æ®å‚数键名查询参数值
     */
    @GetMapping(value = "/configKey/{configKey}")
    public AjaxResult getConfigKey(@PathVariable String configKey)
    {
        return success(configService.selectConfigByKey(configKey));
    }
    /**
     * æ–°å¢žå‚数配置
     */
    @PreAuthorize("@ss.hasPermi('system:config:add')")
    @Log(title = "参数管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysConfig config)
    {
        if (!configService.checkConfigKeyUnique(config))
        {
            return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
        }
        config.setCreateBy(getUsername());
        return toAjax(configService.insertConfig(config));
    }
    /**
     * ä¿®æ”¹å‚数配置
     */
    @PreAuthorize("@ss.hasPermi('system:config:edit')")
    @Log(title = "参数管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysConfig config)
    {
        if (!configService.checkConfigKeyUnique(config))
        {
            return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在");
        }
        config.setUpdateBy(getUsername());
        return toAjax(configService.updateConfig(config));
    }
    /**
     * åˆ é™¤å‚数配置
     */
    @PreAuthorize("@ss.hasPermi('system:config:remove')")
    @Log(title = "参数管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{configIds}")
    public AjaxResult remove(@PathVariable Long[] configIds)
    {
        configService.deleteConfigByIds(configIds);
        return success();
    }
    /**
     * åˆ·æ–°å‚数缓存
     */
    @PreAuthorize("@ss.hasPermi('system:config:remove')")
    @Log(title = "参数管理", businessType = BusinessType.CLEAN)
    @DeleteMapping("/refreshCache")
    public AjaxResult refreshCache()
    {
        configService.resetConfigCache();
        return success();
    }
}
package com.ruoyi.project.system.controller;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysConfig;
import com.ruoyi.project.system.service.ISysConfigService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * å‚数配置 ä¿¡æ¯æ“ä½œå¤„理
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/config")
@AllArgsConstructor
public class SysConfigController extends BaseController
{
    private ISysConfigService configService;
    /**
     * èŽ·å–å‚æ•°é…ç½®åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:config:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysConfig config)
    {
        startPage();
        List<SysConfig> list = configService.selectConfigList(config);
        return getDataTable(list);
    }
    @Log(title = "参数管理", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:config:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysConfig config)
    {
        List<SysConfig> list = configService.selectConfigList(config);
        ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
        util.exportExcel(response, list, "参数数据");
    }
    /**
     * æ ¹æ®å‚数编号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:config:query')")
    @GetMapping(value = "/{configId}")
    public AjaxResult getInfo(@PathVariable Long configId)
    {
        return success(configService.selectConfigById(configId));
    }
    /**
     * æ ¹æ®å‚数键名查询参数值
     */
    @GetMapping(value = "/configKey/{configKey}")
    public AjaxResult getConfigKey(@PathVariable String configKey)
    {
        return success(configService.selectConfigByKey(configKey));
    }
    /**
     * æ–°å¢žå‚数配置
     */
    @PreAuthorize("@ss.hasPermi('system:config:add')")
    @Log(title = "参数管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysConfig config)
    {
        if (!configService.checkConfigKeyUnique(config))
        {
            return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
        }
        config.setCreateBy(getUsername());
        return toAjax(configService.insertConfig(config));
    }
    /**
     * ä¿®æ”¹å‚数配置
     */
    @PreAuthorize("@ss.hasPermi('system:config:edit')")
    @Log(title = "参数管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysConfig config)
    {
        if (!configService.checkConfigKeyUnique(config))
        {
            return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在");
        }
        config.setUpdateBy(getUsername());
        return toAjax(configService.updateConfig(config));
    }
    /**
     * åˆ é™¤å‚数配置
     */
    @PreAuthorize("@ss.hasPermi('system:config:remove')")
    @Log(title = "参数管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{configIds}")
    public AjaxResult remove(@PathVariable Long[] configIds)
    {
        configService.deleteConfigByIds(configIds);
        return success();
    }
    /**
     * åˆ·æ–°å‚数缓存
     */
    @PreAuthorize("@ss.hasPermi('system:config:remove')")
    @Log(title = "参数管理", businessType = BusinessType.CLEAN)
    @DeleteMapping("/refreshCache")
    public AjaxResult refreshCache()
    {
        configService.resetConfigCache();
        return success();
    }
}
src/main/java/com/ruoyi/project/system/controller/SysDeptController.java
@@ -1,134 +1,134 @@
package com.ruoyi.project.system.controller;
import java.util.List;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.service.ISysDeptService;
/**
 * éƒ¨é—¨ä¿¡æ¯
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/dept")
@AllArgsConstructor
public class SysDeptController extends BaseController
{
    private ISysDeptService deptService;
    /**
     * èŽ·å–éƒ¨é—¨åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:dept:list')")
    @GetMapping("/list")
    public AjaxResult list(SysDept dept)
    {
        List<SysDept> depts = deptService.selectDeptList(dept);
        return success(depts);
    }
    /**
     * æŸ¥è¯¢éƒ¨é—¨åˆ—表(排除节点)
     */
    @PreAuthorize("@ss.hasPermi('system:dept:list')")
    @GetMapping("/list/exclude/{deptId}")
    public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId)
    {
        List<SysDept> depts = deptService.selectDeptList(new SysDept());
        depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
        return success(depts);
    }
    /**
     * æ ¹æ®éƒ¨é—¨ç¼–号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:dept:query')")
    @GetMapping(value = "/{deptId}")
    public AjaxResult getInfo(@PathVariable Long deptId)
    {
        deptService.checkDeptDataScope(deptId);
        return success(deptService.selectDeptById(deptId));
    }
    /**
     * æ–°å¢žéƒ¨é—¨
     */
    @PreAuthorize("@ss.hasPermi('system:dept:add')")
    @Log(title = "部门管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysDept dept)
    {
        if (!deptService.checkDeptNameUnique(dept))
        {
            return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
        }
        dept.setCreateBy(getUsername());
        return toAjax(deptService.insertDept(dept));
    }
    /**
     * ä¿®æ”¹éƒ¨é—¨
     */
    @PreAuthorize("@ss.hasPermi('system:dept:edit')")
    @Log(title = "部门管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysDept dept)
    {
        Long deptId = dept.getDeptId();
        deptService.checkDeptDataScope(deptId);
        if (!deptService.checkDeptNameUnique(dept))
        {
            return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
        }
        else if (dept.getParentId().equals(deptId))
        {
            return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
        }
        else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0)
        {
            return error("该部门包含未停用的子部门!");
        }
        dept.setUpdateBy(getUsername());
        return toAjax(deptService.updateDept(dept));
    }
    /**
     * åˆ é™¤éƒ¨é—¨
     */
    @PreAuthorize("@ss.hasPermi('system:dept:remove')")
    @Log(title = "部门管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{deptId}")
    public AjaxResult remove(@PathVariable Long deptId)
    {
        if (deptService.hasChildByDeptId(deptId))
        {
            return warn("存在下级部门,不允许删除");
        }
        if (deptService.checkDeptExistUser(deptId))
        {
            return warn("部门存在用户,不允许删除");
        }
        deptService.checkDeptDataScope(deptId);
        return toAjax(deptService.deleteDeptById(deptId));
    }
}
package com.ruoyi.project.system.controller;
import java.util.List;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.service.ISysDeptService;
/**
 * éƒ¨é—¨ä¿¡æ¯
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/dept")
@AllArgsConstructor
public class SysDeptController extends BaseController
{
    private ISysDeptService deptService;
    /**
     * èŽ·å–éƒ¨é—¨åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:dept:list')")
    @GetMapping("/list")
    public AjaxResult list(SysDept dept)
    {
        List<SysDept> depts = deptService.selectDeptList(dept);
        return success(depts);
    }
    /**
     * æŸ¥è¯¢éƒ¨é—¨åˆ—表(排除节点)
     */
    @PreAuthorize("@ss.hasPermi('system:dept:list')")
    @GetMapping("/list/exclude/{deptId}")
    public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId)
    {
        List<SysDept> depts = deptService.selectDeptList(new SysDept());
        depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
        return success(depts);
    }
    /**
     * æ ¹æ®éƒ¨é—¨ç¼–号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:dept:query')")
    @GetMapping(value = "/{deptId}")
    public AjaxResult getInfo(@PathVariable Long deptId)
    {
        deptService.checkDeptDataScope(deptId);
        return success(deptService.selectDeptById(deptId));
    }
    /**
     * æ–°å¢žéƒ¨é—¨
     */
    @PreAuthorize("@ss.hasPermi('system:dept:add')")
    @Log(title = "部门管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysDept dept)
    {
        if (!deptService.checkDeptNameUnique(dept))
        {
            return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
        }
        dept.setCreateBy(getUsername());
        return toAjax(deptService.insertDept(dept));
    }
    /**
     * ä¿®æ”¹éƒ¨é—¨
     */
    @PreAuthorize("@ss.hasPermi('system:dept:edit')")
    @Log(title = "部门管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysDept dept)
    {
        Long deptId = dept.getDeptId();
        deptService.checkDeptDataScope(deptId);
        if (!deptService.checkDeptNameUnique(dept))
        {
            return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
        }
        else if (dept.getParentId().equals(deptId))
        {
            return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
        }
        else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0)
        {
            return error("该部门包含未停用的子部门!");
        }
        dept.setUpdateBy(getUsername());
        return toAjax(deptService.updateDept(dept));
    }
    /**
     * åˆ é™¤éƒ¨é—¨
     */
    @PreAuthorize("@ss.hasPermi('system:dept:remove')")
    @Log(title = "部门管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{deptId}")
    public AjaxResult remove(@PathVariable Long deptId)
    {
        if (deptService.hasChildByDeptId(deptId))
        {
            return warn("存在下级部门,不允许删除");
        }
        if (deptService.checkDeptExistUser(deptId))
        {
            return warn("部门存在用户,不允许删除");
        }
        deptService.checkDeptDataScope(deptId);
        return toAjax(deptService.deleteDeptById(deptId));
    }
}
src/main/java/com/ruoyi/project/system/controller/SysDictDataController.java
@@ -1,120 +1,120 @@
package com.ruoyi.project.system.controller;
import java.util.ArrayList;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysDictData;
import com.ruoyi.project.system.service.ISysDictDataService;
import com.ruoyi.project.system.service.ISysDictTypeService;
/**
 * æ•°æ®å­—典信息
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/dict/data")
@AllArgsConstructor
public class SysDictDataController extends BaseController
{
    private ISysDictDataService dictDataService;
    private ISysDictTypeService dictTypeService;
    @PreAuthorize("@ss.hasPermi('system:dict:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysDictData dictData)
    {
        startPage();
        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
        return getDataTable(list);
    }
    @Log(title = "字典数据", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:dict:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysDictData dictData)
    {
        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
        ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class);
        util.exportExcel(response, list, "字典数据");
    }
    /**
     * æŸ¥è¯¢å­—典数据详细
     */
    @PreAuthorize("@ss.hasPermi('system:dict:query')")
    @GetMapping(value = "/{dictCode}")
    public AjaxResult getInfo(@PathVariable Long dictCode)
    {
        return success(dictDataService.selectDictDataById(dictCode));
    }
    /**
     * æ ¹æ®å­—典类型查询字典数据信息
     */
    @GetMapping(value = "/type/{dictType}")
    public AjaxResult dictType(@PathVariable String dictType)
    {
        List<SysDictData> data = dictTypeService.selectDictDataByType(dictType);
        if (StringUtils.isNull(data))
        {
            data = new ArrayList<SysDictData>();
        }
        return success(data);
    }
    /**
     * æ–°å¢žå­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:add')")
    @Log(title = "字典数据", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysDictData dict)
    {
        dict.setCreateBy(getUsername());
        return toAjax(dictDataService.insertDictData(dict));
    }
    /**
     * ä¿®æ”¹ä¿å­˜å­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
    @Log(title = "字典数据", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysDictData dict)
    {
        dict.setUpdateBy(getUsername());
        return toAjax(dictDataService.updateDictData(dict));
    }
    /**
     * åˆ é™¤å­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
    @Log(title = "字典类型", businessType = BusinessType.DELETE)
    @DeleteMapping("/{dictCodes}")
    public AjaxResult remove(@PathVariable Long[] dictCodes)
    {
        dictDataService.deleteDictDataByIds(dictCodes);
        return success();
    }
}
package com.ruoyi.project.system.controller;
import java.util.ArrayList;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysDictData;
import com.ruoyi.project.system.service.ISysDictDataService;
import com.ruoyi.project.system.service.ISysDictTypeService;
/**
 * æ•°æ®å­—典信息
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/dict/data")
@AllArgsConstructor
public class SysDictDataController extends BaseController
{
    private ISysDictDataService dictDataService;
    private ISysDictTypeService dictTypeService;
    @PreAuthorize("@ss.hasPermi('system:dict:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysDictData dictData)
    {
        startPage();
        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
        return getDataTable(list);
    }
    @Log(title = "字典数据", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:dict:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysDictData dictData)
    {
        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
        ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class);
        util.exportExcel(response, list, "字典数据");
    }
    /**
     * æŸ¥è¯¢å­—典数据详细
     */
    @PreAuthorize("@ss.hasPermi('system:dict:query')")
    @GetMapping(value = "/{dictCode}")
    public AjaxResult getInfo(@PathVariable Long dictCode)
    {
        return success(dictDataService.selectDictDataById(dictCode));
    }
    /**
     * æ ¹æ®å­—典类型查询字典数据信息
     */
    @GetMapping(value = "/type/{dictType}")
    public AjaxResult dictType(@PathVariable String dictType)
    {
        List<SysDictData> data = dictTypeService.selectDictDataByType(dictType);
        if (StringUtils.isNull(data))
        {
            data = new ArrayList<SysDictData>();
        }
        return success(data);
    }
    /**
     * æ–°å¢žå­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:add')")
    @Log(title = "字典数据", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysDictData dict)
    {
        dict.setCreateBy(getUsername());
        return toAjax(dictDataService.insertDictData(dict));
    }
    /**
     * ä¿®æ”¹ä¿å­˜å­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
    @Log(title = "字典数据", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysDictData dict)
    {
        dict.setUpdateBy(getUsername());
        return toAjax(dictDataService.updateDictData(dict));
    }
    /**
     * åˆ é™¤å­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
    @Log(title = "字典类型", businessType = BusinessType.DELETE)
    @DeleteMapping("/{dictCodes}")
    public AjaxResult remove(@PathVariable Long[] dictCodes)
    {
        dictDataService.deleteDictDataByIds(dictCodes);
        return success();
    }
}
src/main/java/com/ruoyi/project/system/controller/SysDictTypeController.java
@@ -1,132 +1,132 @@
package com.ruoyi.project.system.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysDictType;
import com.ruoyi.project.system.service.ISysDictTypeService;
/**
 * æ•°æ®å­—典信息
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/dict/type")
@AllArgsConstructor
public class SysDictTypeController extends BaseController
{
    private ISysDictTypeService dictTypeService;
    @PreAuthorize("@ss.hasPermi('system:dict:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysDictType dictType)
    {
        startPage();
        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
        return getDataTable(list);
    }
    @Log(title = "字典类型", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:dict:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysDictType dictType)
    {
        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
        ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class);
        util.exportExcel(response, list, "字典类型");
    }
    /**
     * æŸ¥è¯¢å­—典类型详细
     */
    @PreAuthorize("@ss.hasPermi('system:dict:query')")
    @GetMapping(value = "/{dictId}")
    public AjaxResult getInfo(@PathVariable Long dictId)
    {
        return success(dictTypeService.selectDictTypeById(dictId));
    }
    /**
     * æ–°å¢žå­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:add')")
    @Log(title = "字典类型", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysDictType dict)
    {
        if (!dictTypeService.checkDictTypeUnique(dict))
        {
            return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在");
        }
        dict.setCreateBy(getUsername());
        return toAjax(dictTypeService.insertDictType(dict));
    }
    /**
     * ä¿®æ”¹å­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
    @Log(title = "字典类型", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysDictType dict)
    {
        if (!dictTypeService.checkDictTypeUnique(dict))
        {
            return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在");
        }
        dict.setUpdateBy(getUsername());
        return toAjax(dictTypeService.updateDictType(dict));
    }
    /**
     * åˆ é™¤å­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
    @Log(title = "字典类型", businessType = BusinessType.DELETE)
    @DeleteMapping("/{dictIds}")
    public AjaxResult remove(@PathVariable Long[] dictIds)
    {
        dictTypeService.deleteDictTypeByIds(dictIds);
        return success();
    }
    /**
     * åˆ·æ–°å­—典缓存
     */
    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
    @Log(title = "字典类型", businessType = BusinessType.CLEAN)
    @DeleteMapping("/refreshCache")
    public AjaxResult refreshCache()
    {
        dictTypeService.resetDictCache();
        return success();
    }
    /**
     * èŽ·å–å­—å…¸é€‰æ‹©æ¡†åˆ—è¡¨
     */
    @GetMapping("/optionselect")
    public AjaxResult optionselect()
    {
        List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll();
        return success(dictTypes);
    }
}
package com.ruoyi.project.system.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysDictType;
import com.ruoyi.project.system.service.ISysDictTypeService;
/**
 * æ•°æ®å­—典信息
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/dict/type")
@AllArgsConstructor
public class SysDictTypeController extends BaseController
{
    private ISysDictTypeService dictTypeService;
    @PreAuthorize("@ss.hasPermi('system:dict:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysDictType dictType)
    {
        startPage();
        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
        return getDataTable(list);
    }
    @Log(title = "字典类型", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:dict:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysDictType dictType)
    {
        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
        ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class);
        util.exportExcel(response, list, "字典类型");
    }
    /**
     * æŸ¥è¯¢å­—典类型详细
     */
    @PreAuthorize("@ss.hasPermi('system:dict:query')")
    @GetMapping(value = "/{dictId}")
    public AjaxResult getInfo(@PathVariable Long dictId)
    {
        return success(dictTypeService.selectDictTypeById(dictId));
    }
    /**
     * æ–°å¢žå­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:add')")
    @Log(title = "字典类型", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysDictType dict)
    {
        if (!dictTypeService.checkDictTypeUnique(dict))
        {
            return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在");
        }
        dict.setCreateBy(getUsername());
        return toAjax(dictTypeService.insertDictType(dict));
    }
    /**
     * ä¿®æ”¹å­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
    @Log(title = "字典类型", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysDictType dict)
    {
        if (!dictTypeService.checkDictTypeUnique(dict))
        {
            return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在");
        }
        dict.setUpdateBy(getUsername());
        return toAjax(dictTypeService.updateDictType(dict));
    }
    /**
     * åˆ é™¤å­—典类型
     */
    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
    @Log(title = "字典类型", businessType = BusinessType.DELETE)
    @DeleteMapping("/{dictIds}")
    public AjaxResult remove(@PathVariable Long[] dictIds)
    {
        dictTypeService.deleteDictTypeByIds(dictIds);
        return success();
    }
    /**
     * åˆ·æ–°å­—典缓存
     */
    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
    @Log(title = "字典类型", businessType = BusinessType.CLEAN)
    @DeleteMapping("/refreshCache")
    public AjaxResult refreshCache()
    {
        dictTypeService.resetDictCache();
        return success();
    }
    /**
     * èŽ·å–å­—å…¸é€‰æ‹©æ¡†åˆ—è¡¨
     */
    @GetMapping("/optionselect")
    public AjaxResult optionselect()
    {
        List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll();
        return success(dictTypes);
    }
}
src/main/java/com/ruoyi/project/system/controller/SysLoginController.java
@@ -1,155 +1,155 @@
package com.ruoyi.project.system.controller;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.security.LoginBody;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.security.service.SysLoginService;
import com.ruoyi.framework.security.service.SysPermissionService;
import com.ruoyi.framework.security.service.TokenService;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysMenu;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.domain.vo.SysUserDeptVo;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.service.ISysMenuService;
import com.ruoyi.project.system.service.ISysUserDeptService;
import com.ruoyi.project.system.service.ISysUserService;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
 * ç™»å½•验证
 *
 * @author ruoyi
 */
@RestController
@AllArgsConstructor
public class SysLoginController
{
    private SysLoginService loginService;
    private ISysMenuService menuService;
    private SysPermissionService permissionService;
    private TokenService tokenService;
    private ISysUserDeptService userDeptService;
    private ISysUserService userService;
    private SysDeptMapper sysDeptMapper;
    /**
     * ç™»å½•方法
     *
     * @param loginBody ç™»å½•信息
     * @return ç»“æžœ
     */
    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // ç”Ÿæˆä»¤ç‰Œ
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }
    /**
     * èŽ·å–ç”¨æˆ·ä¿¡æ¯
     *
     * @return ç”¨æˆ·ä¿¡æ¯
     */
    @GetMapping("/getInfo")
    public AjaxResult getInfo()
    {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        SysUser user = loginUser.getUser();
        // èŽ·å–å½“å‰ç™»å½•å…¬å¸
        Long tenantId = loginUser.getTenantId();
        if(null != tenantId){
            user.setTenantId(tenantId);
            SysDept sysDept = sysDeptMapper.selectDeptById(tenantId.longValue());
            if(!ObjectUtils.isEmpty(sysDept)){
                user.setCurrentFactoryName(sysDept.getDeptName());
            }
        }
        // è§’色集合
        Set<String> roles = permissionService.getRolePermission(user);
        // æƒé™é›†åˆ
        Set<String> permissions = permissionService.getMenuPermission(user);
        if (!loginUser.getPermissions().equals(permissions))
        {
            loginUser.setPermissions(permissions);
            tokenService.refreshToken(loginUser);
        }
        AjaxResult ajax = AjaxResult.success();
        ajax.put("user", user);
        ajax.put("aiEnabled", loginUser.getAiEnabled());
        ajax.put("roles", roles);
        ajax.put("permissions", permissions);
        return ajax;
    }
    /**
     * èŽ·å–è·¯ç”±ä¿¡æ¯
     *
     * @return è·¯ç”±ä¿¡æ¯
     */
    @GetMapping("getRouters")
    public AjaxResult getRouters()
    {
        Long userId = SecurityUtils.getUserId();
        List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
        return AjaxResult.success(menuService.buildMenus(menus));
    }
    @PostMapping("/loginCheck")
    public AjaxResult loginCheck(@RequestBody LoginBody loginBody)
    {
        try {
            Long userId = loginService.loginCheck(loginBody.getUsername(), loginBody.getPassword());
            return AjaxResult.success(userId);
        }catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }
    @GetMapping("/userLoginFacotryList")
    public AjaxResult userLoginFacotryList(SysUserDeptVo sysUserDeptVo){
        List<SysUserDeptVo> sysUserDeptVoList = userDeptService.userLoginFacotryList(sysUserDeptVo);
        Map<Long, SysUserDeptVo> map = sysUserDeptVoList.stream()
            .collect(Collectors.toMap(
                    SysUserDeptVo::getDeptId,
                    item -> item,
                    (existing, replacement) -> existing // å¦‚果重复,保留第一个
            ));
        List<SysUserDeptVo> uniqueList = new ArrayList<>(map.values());
        return AjaxResult.success(uniqueList);
    }
    /**
     * é€‰æ‹©å…¬å¸ç™»å½•
     *
     * @param loginBody ç™»å½•信息
     * @return ç»“æžœ
     */
    @PostMapping("/loginCheckFactory")
    public AjaxResult loginCheckFactory(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // ç”Ÿæˆä»¤ç‰Œ
        String token = loginService.loginCheckFactory(loginBody.getUsername(), loginBody.getPassword(),loginBody.getFactoryId());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }
}
package com.ruoyi.project.system.controller;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.security.LoginBody;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.security.service.SysLoginService;
import com.ruoyi.framework.security.service.SysPermissionService;
import com.ruoyi.framework.security.service.TokenService;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysMenu;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.domain.vo.SysUserDeptVo;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.service.ISysMenuService;
import com.ruoyi.project.system.service.ISysUserDeptService;
import com.ruoyi.project.system.service.ISysUserService;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
 * ç™»å½•验证
 *
 * @author ruoyi
 */
@RestController
@AllArgsConstructor
public class SysLoginController
{
    private SysLoginService loginService;
    private ISysMenuService menuService;
    private SysPermissionService permissionService;
    private TokenService tokenService;
    private ISysUserDeptService userDeptService;
    private ISysUserService userService;
    private SysDeptMapper sysDeptMapper;
    /**
     * ç™»å½•方法
     *
     * @param loginBody ç™»å½•信息
     * @return ç»“æžœ
     */
    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // ç”Ÿæˆä»¤ç‰Œ
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }
    /**
     * èŽ·å–ç”¨æˆ·ä¿¡æ¯
     *
     * @return ç”¨æˆ·ä¿¡æ¯
     */
    @GetMapping("/getInfo")
    public AjaxResult getInfo()
    {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        SysUser user = loginUser.getUser();
        // èŽ·å–å½“å‰ç™»å½•å…¬å¸
        Long tenantId = loginUser.getTenantId();
        if(null != tenantId){
            user.setTenantId(tenantId);
            SysDept sysDept = sysDeptMapper.selectDeptById(tenantId.longValue());
            if(!ObjectUtils.isEmpty(sysDept)){
                user.setCurrentFactoryName(sysDept.getDeptName());
            }
        }
        // è§’色集合
        Set<String> roles = permissionService.getRolePermission(user);
        // æƒé™é›†åˆ
        Set<String> permissions = permissionService.getMenuPermission(user);
        if (!loginUser.getPermissions().equals(permissions))
        {
            loginUser.setPermissions(permissions);
            tokenService.refreshToken(loginUser);
        }
        AjaxResult ajax = AjaxResult.success();
        ajax.put("user", user);
        ajax.put("aiEnabled", loginUser.getAiEnabled());
        ajax.put("roles", roles);
        ajax.put("permissions", permissions);
        return ajax;
    }
    /**
     * èŽ·å–è·¯ç”±ä¿¡æ¯
     *
     * @return è·¯ç”±ä¿¡æ¯
     */
    @GetMapping("getRouters")
    public AjaxResult getRouters()
    {
        Long userId = SecurityUtils.getUserId();
        List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
        return AjaxResult.success(menuService.buildMenus(menus));
    }
    @PostMapping("/loginCheck")
    public AjaxResult loginCheck(@RequestBody LoginBody loginBody)
    {
        try {
            Long userId = loginService.loginCheck(loginBody.getUsername(), loginBody.getPassword());
            return AjaxResult.success(userId);
        }catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }
    @GetMapping("/userLoginFacotryList")
    public AjaxResult userLoginFacotryList(SysUserDeptVo sysUserDeptVo){
        List<SysUserDeptVo> sysUserDeptVoList = userDeptService.userLoginFacotryList(sysUserDeptVo);
        Map<Long, SysUserDeptVo> map = sysUserDeptVoList.stream()
            .collect(Collectors.toMap(
                    SysUserDeptVo::getDeptId,
                    item -> item,
                    (existing, replacement) -> existing // å¦‚果重复,保留第一个
            ));
        List<SysUserDeptVo> uniqueList = new ArrayList<>(map.values());
        return AjaxResult.success(uniqueList);
    }
    /**
     * é€‰æ‹©å…¬å¸ç™»å½•
     *
     * @param loginBody ç™»å½•信息
     * @return ç»“æžœ
     */
    @PostMapping("/loginCheckFactory")
    public AjaxResult loginCheckFactory(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // ç”Ÿæˆä»¤ç‰Œ
        String token = loginService.loginCheckFactory(loginBody.getUsername(), loginBody.getPassword(),loginBody.getFactoryId());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }
}
src/main/java/com/ruoyi/project/system/controller/SysMenuController.java
@@ -1,136 +1,136 @@
package com.ruoyi.project.system.controller;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.SysMenu;
import com.ruoyi.project.system.service.ISysMenuService;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * èœå•信息
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/menu")
@AllArgsConstructor
public class SysMenuController extends BaseController
{
    private ISysMenuService menuService;
    /**
     * èŽ·å–èœå•åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:menu:list')")
    @GetMapping("/list")
    public AjaxResult list(SysMenu menu)
    {
        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
        return success(menus);
    }
    /**
     * æ ¹æ®èœå•编号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:menu:query')")
    @GetMapping(value = "/{menuId}")
    public AjaxResult getInfo(@PathVariable Long menuId)
    {
        return success(menuService.selectMenuById(menuId));
    }
    /**
     * èŽ·å–èœå•ä¸‹æ‹‰æ ‘åˆ—è¡¨
     */
    @GetMapping("/treeselect")
    public AjaxResult treeselect(SysMenu menu)
    {
        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
        return success(menuService.buildMenuTreeSelect(menus));
    }
    /**
     * åŠ è½½å¯¹åº”è§’è‰²èœå•åˆ—è¡¨æ ‘
     */
    @GetMapping(value = "/roleMenuTreeselect/{roleId}")
    public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId)
    {
        List<SysMenu> menus = menuService.selectMenuList(getUserId());
        AjaxResult ajax = AjaxResult.success();
        ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId));
        ajax.put("menus", menuService.buildMenuTreeSelect(menus));
        return ajax;
    }
    /**
     * æ–°å¢žèœå•
     */
    @PreAuthorize("@ss.hasPermi('system:menu:add')")
    @Log(title = "菜单管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysMenu menu)
    {
        if (!menuService.checkMenuNameUnique(menu))
        {
            return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
        }
        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
        {
            return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
        }
        menu.setCreateBy(getUsername());
        return toAjax(menuService.insertMenu(menu));
    }
    /**
     * ä¿®æ”¹èœå•
     */
    @PreAuthorize("@ss.hasPermi('system:menu:edit')")
    @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysMenu menu)
    {
        if (!menuService.checkMenuNameUnique(menu))
        {
            return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
        }
        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
        {
            return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
        }
        else if (menu.getMenuId().equals(menu.getParentId()))
        {
            return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
        }
        menu.setUpdateBy(getUsername());
        return toAjax(menuService.updateMenu(menu));
    }
    /**
     * åˆ é™¤èœå•
     */
    @PreAuthorize("@ss.hasPermi('system:menu:remove')")
    @Log(title = "菜单管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{menuId}")
    public AjaxResult remove(@PathVariable("menuId") Long menuId)
    {
        if (menuService.hasChildByMenuId(menuId))
        {
            return warn("存在子菜单,不允许删除");
        }
        if (menuService.checkMenuExistRole(menuId))
        {
            return warn("菜单已分配,不允许删除");
        }
        return toAjax(menuService.deleteMenuById(menuId));
    }
package com.ruoyi.project.system.controller;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.SysMenu;
import com.ruoyi.project.system.service.ISysMenuService;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * èœå•信息
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/menu")
@AllArgsConstructor
public class SysMenuController extends BaseController
{
    private ISysMenuService menuService;
    /**
     * èŽ·å–èœå•åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:menu:list')")
    @GetMapping("/list")
    public AjaxResult list(SysMenu menu)
    {
        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
        return success(menus);
    }
    /**
     * æ ¹æ®èœå•编号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:menu:query')")
    @GetMapping(value = "/{menuId}")
    public AjaxResult getInfo(@PathVariable Long menuId)
    {
        return success(menuService.selectMenuById(menuId));
    }
    /**
     * èŽ·å–èœå•ä¸‹æ‹‰æ ‘åˆ—è¡¨
     */
    @GetMapping("/treeselect")
    public AjaxResult treeselect(SysMenu menu)
    {
        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
        return success(menuService.buildMenuTreeSelect(menus));
    }
    /**
     * åŠ è½½å¯¹åº”è§’è‰²èœå•åˆ—è¡¨æ ‘
     */
    @GetMapping(value = "/roleMenuTreeselect/{roleId}")
    public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId)
    {
        List<SysMenu> menus = menuService.selectMenuList(getUserId());
        AjaxResult ajax = AjaxResult.success();
        ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId));
        ajax.put("menus", menuService.buildMenuTreeSelect(menus));
        return ajax;
    }
    /**
     * æ–°å¢žèœå•
     */
    @PreAuthorize("@ss.hasPermi('system:menu:add')")
    @Log(title = "菜单管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysMenu menu)
    {
        if (!menuService.checkMenuNameUnique(menu))
        {
            return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
        }
        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
        {
            return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
        }
        menu.setCreateBy(getUsername());
        return toAjax(menuService.insertMenu(menu));
    }
    /**
     * ä¿®æ”¹èœå•
     */
    @PreAuthorize("@ss.hasPermi('system:menu:edit')")
    @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysMenu menu)
    {
        if (!menuService.checkMenuNameUnique(menu))
        {
            return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
        }
        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
        {
            return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
        }
        else if (menu.getMenuId().equals(menu.getParentId()))
        {
            return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
        }
        menu.setUpdateBy(getUsername());
        return toAjax(menuService.updateMenu(menu));
    }
    /**
     * åˆ é™¤èœå•
     */
    @PreAuthorize("@ss.hasPermi('system:menu:remove')")
    @Log(title = "菜单管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{menuId}")
    public AjaxResult remove(@PathVariable("menuId") Long menuId)
    {
        if (menuService.hasChildByMenuId(menuId))
        {
            return warn("存在子菜单,不允许删除");
        }
        if (menuService.checkMenuExistRole(menuId))
        {
            return warn("菜单已分配,不允许删除");
        }
        return toAjax(menuService.deleteMenuById(menuId));
    }
}
src/main/java/com/ruoyi/project/system/controller/SysNoticeController.java
@@ -1,96 +1,96 @@
package com.ruoyi.project.system.controller;
import java.util.List;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysNotice;
import com.ruoyi.project.system.service.ISysNoticeService;
/**
 * å…¬å‘Š ä¿¡æ¯æ“ä½œå¤„ç†
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/notice")
@AllArgsConstructor
public class SysNoticeController extends BaseController {
    private ISysNoticeService noticeService;
    /**
     * èŽ·å–é€šçŸ¥å…¬å‘Šåˆ—è¡¨
     */
    @GetMapping("/list")
    public R<IPage<SysNotice>> list(SysNotice notice, Page page) {
        IPage<SysNotice> list = noticeService.selectNoticeList(notice, page);
        return R.ok(list);
    }
    /**
     * èŽ·å–æœªè¯»æ•°é‡
     */
    @GetMapping("/getCount")
    public R getCount(Long consigneeId) {
        return R.ok(noticeService.getCount(consigneeId));
    }
    /**
     * æ ¹æ®é€šçŸ¥å…¬å‘Šç¼–号获取详细信息
     */
    @GetMapping(value = "/{noticeId}")
    public AjaxResult getInfo(@PathVariable Long noticeId) {
        return success(noticeService.selectNoticeById(noticeId));
    }
    /**
     * æ–°å¢žé€šçŸ¥å…¬å‘Š
     */
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysNotice notice) {
        return toAjax(noticeService.insertNotice(notice));
    }
    /**
     * ä¿®æ”¹é€šçŸ¥å…¬å‘Š
     */
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysNotice notice) {
        return toAjax(noticeService.updateNotice(notice));
    }
    /**
     * åˆ é™¤é€šçŸ¥å…¬å‘Š
     */
    @DeleteMapping("/{noticeIds}")
    public AjaxResult remove(@PathVariable Long[] noticeIds) {
        return toAjax(noticeService.deleteNoticeByIds(noticeIds));
    }
    /**
     * ä¸€é”®å·²è¯»
     */
    @PostMapping("/readAll")
    public AjaxResult readAll() {
        return toAjax(noticeService.readAll());
    }
    @PostMapping("appReadNotice")
    @Operation(summary = "移动端根据消息ID进行已读")
    public AjaxResult appReadNotice(@RequestParam("noticeId") Long noticeId) {
        boolean result = noticeService.appReadNotice(noticeId);
        return toAjax(result);
    }
}
package com.ruoyi.project.system.controller;
import java.util.List;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysNotice;
import com.ruoyi.project.system.service.ISysNoticeService;
/**
 * å…¬å‘Š ä¿¡æ¯æ“ä½œå¤„ç†
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/notice")
@AllArgsConstructor
public class SysNoticeController extends BaseController {
    private ISysNoticeService noticeService;
    /**
     * èŽ·å–é€šçŸ¥å…¬å‘Šåˆ—è¡¨
     */
    @GetMapping("/list")
    public R<IPage<SysNotice>> list(SysNotice notice, Page page) {
        IPage<SysNotice> list = noticeService.selectNoticeList(notice, page);
        return R.ok(list);
    }
    /**
     * èŽ·å–æœªè¯»æ•°é‡
     */
    @GetMapping("/getCount")
    public R getCount(Long consigneeId) {
        return R.ok(noticeService.getCount(consigneeId));
    }
    /**
     * æ ¹æ®é€šçŸ¥å…¬å‘Šç¼–号获取详细信息
     */
    @GetMapping(value = "/{noticeId}")
    public AjaxResult getInfo(@PathVariable Long noticeId) {
        return success(noticeService.selectNoticeById(noticeId));
    }
    /**
     * æ–°å¢žé€šçŸ¥å…¬å‘Š
     */
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysNotice notice) {
        return toAjax(noticeService.insertNotice(notice));
    }
    /**
     * ä¿®æ”¹é€šçŸ¥å…¬å‘Š
     */
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysNotice notice) {
        return toAjax(noticeService.updateNotice(notice));
    }
    /**
     * åˆ é™¤é€šçŸ¥å…¬å‘Š
     */
    @DeleteMapping("/{noticeIds}")
    public AjaxResult remove(@PathVariable Long[] noticeIds) {
        return toAjax(noticeService.deleteNoticeByIds(noticeIds));
    }
    /**
     * ä¸€é”®å·²è¯»
     */
    @PostMapping("/readAll")
    public AjaxResult readAll() {
        return toAjax(noticeService.readAll());
    }
    @PostMapping("appReadNotice")
    @Operation(summary = "移动端根据消息ID进行已读")
    public AjaxResult appReadNotice(@RequestParam("noticeId") Long noticeId) {
        boolean result = noticeService.appReadNotice(noticeId);
        return toAjax(result);
    }
}
src/main/java/com/ruoyi/project/system/controller/SysPostController.java
@@ -1,133 +1,133 @@
package com.ruoyi.project.system.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysPost;
import com.ruoyi.project.system.service.ISysPostService;
/**
 * å²—位信息操作处理
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/post")
@AllArgsConstructor
public class SysPostController extends BaseController
{
    private ISysPostService postService;
    /**
     * èŽ·å–å²—ä½åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:post:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysPost post)
    {
        startPage();
        List<SysPost> list = postService.selectPostList(post);
        return getDataTable(list);
    }
    /**
     * å¯¼å‡ºå²—位列表
     */
    @Log(title = "岗位管理", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:post:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysPost post)
    {
        List<SysPost> list = postService.selectPostList(post);
        ExcelUtil<SysPost> util = new ExcelUtil<SysPost>(SysPost.class);
        util.exportExcel(response, list, "岗位数据");
    }
    /**
     * æ ¹æ®å²—位编号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:post:query')")
    @GetMapping(value = "/{postId}")
    public AjaxResult getInfo(@PathVariable Long postId)
    {
        return success(postService.selectPostById(postId));
    }
    /**
     * æ–°å¢žå²—位
     */
    @PreAuthorize("@ss.hasPermi('system:post:add')")
    @Log(title = "岗位管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysPost post)
    {
        if (!postService.checkPostNameUnique(post))
        {
            return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在");
        }
        else if (!postService.checkPostCodeUnique(post))
        {
            return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在");
        }
        post.setCreateBy(getUsername());
        return toAjax(postService.insertPost(post));
    }
    /**
     * ä¿®æ”¹å²—位
     */
    @PreAuthorize("@ss.hasPermi('system:post:edit')")
    @Log(title = "岗位管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysPost post)
    {
        if (!postService.checkPostNameUnique(post))
        {
            return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在");
        }
        else if (!postService.checkPostCodeUnique(post))
        {
            return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在");
        }
        post.setUpdateBy(getUsername());
        return toAjax(postService.updatePost(post));
    }
    /**
     * åˆ é™¤å²—位
     */
    @PreAuthorize("@ss.hasPermi('system:post:remove')")
    @Log(title = "岗位管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{postIds}")
    public AjaxResult remove(@PathVariable Long[] postIds)
    {
        return toAjax(postService.deletePostByIds(postIds));
    }
    /**
     * èŽ·å–å²—ä½é€‰æ‹©æ¡†åˆ—è¡¨
     */
    @GetMapping("/optionselect")
    public AjaxResult optionselect()
    {
        List<SysPost> posts = postService.selectPostAll();
        return success(posts);
    }
}
package com.ruoyi.project.system.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysPost;
import com.ruoyi.project.system.service.ISysPostService;
/**
 * å²—位信息操作处理
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/post")
@AllArgsConstructor
public class SysPostController extends BaseController
{
    private ISysPostService postService;
    /**
     * èŽ·å–å²—ä½åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:post:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysPost post)
    {
        startPage();
        List<SysPost> list = postService.selectPostList(post);
        return getDataTable(list);
    }
    /**
     * å¯¼å‡ºå²—位列表
     */
    @Log(title = "岗位管理", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:post:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysPost post)
    {
        List<SysPost> list = postService.selectPostList(post);
        ExcelUtil<SysPost> util = new ExcelUtil<SysPost>(SysPost.class);
        util.exportExcel(response, list, "岗位数据");
    }
    /**
     * æ ¹æ®å²—位编号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:post:query')")
    @GetMapping(value = "/{postId}")
    public AjaxResult getInfo(@PathVariable Long postId)
    {
        return success(postService.selectPostById(postId));
    }
    /**
     * æ–°å¢žå²—位
     */
    @PreAuthorize("@ss.hasPermi('system:post:add')")
    @Log(title = "岗位管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysPost post)
    {
        if (!postService.checkPostNameUnique(post))
        {
            return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在");
        }
        else if (!postService.checkPostCodeUnique(post))
        {
            return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在");
        }
        post.setCreateBy(getUsername());
        return toAjax(postService.insertPost(post));
    }
    /**
     * ä¿®æ”¹å²—位
     */
    @PreAuthorize("@ss.hasPermi('system:post:edit')")
    @Log(title = "岗位管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysPost post)
    {
        if (!postService.checkPostNameUnique(post))
        {
            return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在");
        }
        else if (!postService.checkPostCodeUnique(post))
        {
            return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在");
        }
        post.setUpdateBy(getUsername());
        return toAjax(postService.updatePost(post));
    }
    /**
     * åˆ é™¤å²—位
     */
    @PreAuthorize("@ss.hasPermi('system:post:remove')")
    @Log(title = "岗位管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{postIds}")
    public AjaxResult remove(@PathVariable Long[] postIds)
    {
        return toAjax(postService.deletePostByIds(postIds));
    }
    /**
     * èŽ·å–å²—ä½é€‰æ‹©æ¡†åˆ—è¡¨
     */
    @GetMapping("/optionselect")
    public AjaxResult optionselect()
    {
        List<SysPost> posts = postService.selectPostAll();
        return success(posts);
    }
}
src/main/java/com/ruoyi/project/system/controller/SysProfileController.java
@@ -1,133 +1,133 @@
package com.ruoyi.project.system.controller;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.MimeTypeUtils;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.config.RuoYiConfig;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.security.service.TokenService;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.service.ISysUserService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
 * ä¸ªäººä¿¡æ¯ ä¸šåŠ¡å¤„ç†
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/user/profile")
@AllArgsConstructor
public class SysProfileController extends BaseController
{
    private ISysUserService userService;
    private TokenService tokenService;
    /**
     * ä¸ªäººä¿¡æ¯
     */
    @GetMapping
    public AjaxResult profile()
    {
        LoginUser loginUser = getLoginUser();
        SysUser user = loginUser.getUser();
        AjaxResult ajax = AjaxResult.success(user);
        ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
        ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
        return ajax;
    }
    /**
     * ä¿®æ”¹ç”¨æˆ·
     */
    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult updateProfile(@RequestBody SysUser user)
    {
        LoginUser loginUser = getLoginUser();
        SysUser currentUser = loginUser.getUser();
        currentUser.setNickName(user.getNickName());
        currentUser.setEmail(user.getEmail());
        currentUser.setPhonenumber(user.getPhonenumber());
        currentUser.setSex(user.getSex());
        if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser))
        {
            return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在");
        }
        if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser))
        {
            return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在");
        }
        if (userService.updateUserProfile(currentUser) > 0)
        {
            // æ›´æ–°ç¼“存用户信息
            tokenService.setLoginUser(loginUser);
            return success();
        }
        return error("修改个人信息异常,请联系管理员");
    }
    /**
     * é‡ç½®å¯†ç 
     */
    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
    @PutMapping("/updatePwd")
    public AjaxResult updatePwd(@RequestBody Map<String, String> params)
    {
        String oldPassword = params.get("oldPassword");
        String newPassword = params.get("newPassword");
        LoginUser loginUser = getLoginUser();
        String userName = loginUser.getUsername();
        String password = loginUser.getPassword();
        if (!SecurityUtils.matchesPassword(oldPassword, password))
        {
            return error("修改密码失败,旧密码错误");
        }
        if (SecurityUtils.matchesPassword(newPassword, password))
        {
            return error("新密码不能与旧密码相同");
        }
        newPassword = SecurityUtils.encryptPassword(newPassword);
        if (userService.resetUserPwd(userName, newPassword) > 0)
        {
            // æ›´æ–°ç¼“存用户密码
            loginUser.getUser().setPassword(newPassword);
            tokenService.setLoginUser(loginUser);
            return success();
        }
        return error("修改密码异常,请联系管理员");
    }
    /**
     * å¤´åƒä¸Šä¼ 
     */
    @Log(title = "用户头像", businessType = BusinessType.UPDATE)
    @PostMapping("/avatar")
    public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception
    {
        if (!file.isEmpty())
        {
            LoginUser loginUser = getLoginUser();
            String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION);
            if (userService.updateUserAvatar(loginUser.getUsername(), avatar))
            {
                AjaxResult ajax = AjaxResult.success();
                ajax.put("imgUrl", avatar);
                // æ›´æ–°ç¼“存用户头像
                loginUser.getUser().setAvatar(avatar);
                tokenService.setLoginUser(loginUser);
                return ajax;
            }
        }
        return error("上传图片异常,请联系管理员");
    }
}
package com.ruoyi.project.system.controller;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.MimeTypeUtils;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.config.RuoYiConfig;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.security.service.TokenService;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.service.ISysUserService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Map;
/**
 * ä¸ªäººä¿¡æ¯ ä¸šåŠ¡å¤„ç†
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/user/profile")
@AllArgsConstructor
public class SysProfileController extends BaseController
{
    private ISysUserService userService;
    private TokenService tokenService;
    /**
     * ä¸ªäººä¿¡æ¯
     */
    @GetMapping
    public AjaxResult profile()
    {
        LoginUser loginUser = getLoginUser();
        SysUser user = loginUser.getUser();
        AjaxResult ajax = AjaxResult.success(user);
        ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
        ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
        return ajax;
    }
    /**
     * ä¿®æ”¹ç”¨æˆ·
     */
    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult updateProfile(@RequestBody SysUser user)
    {
        LoginUser loginUser = getLoginUser();
        SysUser currentUser = loginUser.getUser();
        currentUser.setNickName(user.getNickName());
        currentUser.setEmail(user.getEmail());
        currentUser.setPhonenumber(user.getPhonenumber());
        currentUser.setSex(user.getSex());
        if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser))
        {
            return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在");
        }
        if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser))
        {
            return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在");
        }
        if (userService.updateUserProfile(currentUser) > 0)
        {
            // æ›´æ–°ç¼“存用户信息
            tokenService.setLoginUser(loginUser);
            return success();
        }
        return error("修改个人信息异常,请联系管理员");
    }
    /**
     * é‡ç½®å¯†ç 
     */
    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
    @PutMapping("/updatePwd")
    public AjaxResult updatePwd(@RequestBody Map<String, String> params)
    {
        String oldPassword = params.get("oldPassword");
        String newPassword = params.get("newPassword");
        LoginUser loginUser = getLoginUser();
        String userName = loginUser.getUsername();
        String password = loginUser.getPassword();
        if (!SecurityUtils.matchesPassword(oldPassword, password))
        {
            return error("修改密码失败,旧密码错误");
        }
        if (SecurityUtils.matchesPassword(newPassword, password))
        {
            return error("新密码不能与旧密码相同");
        }
        newPassword = SecurityUtils.encryptPassword(newPassword);
        if (userService.resetUserPwd(userName, newPassword) > 0)
        {
            // æ›´æ–°ç¼“存用户密码
            loginUser.getUser().setPassword(newPassword);
            tokenService.setLoginUser(loginUser);
            return success();
        }
        return error("修改密码异常,请联系管理员");
    }
    /**
     * å¤´åƒä¸Šä¼ 
     */
    @Log(title = "用户头像", businessType = BusinessType.UPDATE)
    @PostMapping("/avatar")
    public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception
    {
        if (!file.isEmpty())
        {
            LoginUser loginUser = getLoginUser();
            String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION);
            if (userService.updateUserAvatar(loginUser.getUsername(), avatar))
            {
                AjaxResult ajax = AjaxResult.success();
                ajax.put("imgUrl", avatar);
                // æ›´æ–°ç¼“存用户头像
                loginUser.getUser().setAvatar(avatar);
                tokenService.setLoginUser(loginUser);
                return ajax;
            }
        }
        return error("上传图片异常,请联系管理员");
    }
}
src/main/java/com/ruoyi/project/system/controller/SysRegisterController.java
@@ -1,33 +1,33 @@
package com.ruoyi.project.system.controller;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.RegisterBody;
import com.ruoyi.framework.security.service.SysRegisterService;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.service.ISysConfigService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
 * æ³¨å†ŒéªŒè¯
 *
 * @author ruoyi
 */
@RestController
@AllArgsConstructor
public class SysRegisterController extends BaseController {
    private SysRegisterService registerService;
    private ISysConfigService configService;
    @PostMapping("/register")
    public AjaxResult register(@RequestBody RegisterBody user) {
        if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) {
            return error("当前系统没有开启注册功能!");
        }
        String msg = registerService.register(user);
        return StringUtils.isEmpty(msg) ? success() : error(msg);
    }
}
package com.ruoyi.project.system.controller;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.RegisterBody;
import com.ruoyi.framework.security.service.SysRegisterService;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.service.ISysConfigService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
 * æ³¨å†ŒéªŒè¯
 *
 * @author ruoyi
 */
@RestController
@AllArgsConstructor
public class SysRegisterController extends BaseController {
    private SysRegisterService registerService;
    private ISysConfigService configService;
    @PostMapping("/register")
    public AjaxResult register(@RequestBody RegisterBody user) {
        if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser")))) {
            return error("当前系统没有开启注册功能!");
        }
        String msg = registerService.register(user);
        return StringUtils.isEmpty(msg) ? success() : error(msg);
    }
}
src/main/java/com/ruoyi/project/system/controller/SysRoleController.java
@@ -1,255 +1,255 @@
package com.ruoyi.project.system.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.security.service.SysPermissionService;
import com.ruoyi.framework.security.service.TokenService;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysRole;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.domain.SysUserRole;
import com.ruoyi.project.system.service.ISysDeptService;
import com.ruoyi.project.system.service.ISysRoleService;
import com.ruoyi.project.system.service.ISysUserService;
/**
 * è§’色信息
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/role")
@AllArgsConstructor
public class SysRoleController extends BaseController
{
    private ISysRoleService roleService;
    private TokenService tokenService;
    private SysPermissionService permissionService;
    private ISysUserService userService;
    private ISysDeptService deptService;
    @PreAuthorize("@ss.hasPermi('system:role:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysRole role)
    {
        startPage();
        List<SysRole> list = roleService.selectRoleList(role);
        return getDataTable(list);
    }
    @Log(title = "角色管理", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:role:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysRole role)
    {
        List<SysRole> list = roleService.selectRoleList(role);
        ExcelUtil<SysRole> util = new ExcelUtil<SysRole>(SysRole.class);
        util.exportExcel(response, list, "角色数据");
    }
    /**
     * æ ¹æ®è§’色编号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:role:query')")
    @GetMapping(value = "/{roleId}")
    public AjaxResult getInfo(@PathVariable Long roleId)
    {
        roleService.checkRoleDataScope(roleId);
        return success(roleService.selectRoleById(roleId));
    }
    /**
     * æ–°å¢žè§’色
     */
    @PreAuthorize("@ss.hasPermi('system:role:add')")
    @Log(title = "角色管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysRole role)
    {
        if (!roleService.checkRoleNameUnique(role))
        {
            return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
        }
        else if (!roleService.checkRoleKeyUnique(role))
        {
            return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在");
        }
        role.setCreateBy(getUsername());
        return toAjax(roleService.insertRole(role));
    }
    /**
     * ä¿®æ”¹ä¿å­˜è§’色
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysRole role)
    {
        roleService.checkRoleAllowed(role);
        roleService.checkRoleDataScope(role.getRoleId());
        if (!roleService.checkRoleNameUnique(role))
        {
            return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在");
        }
        else if (!roleService.checkRoleKeyUnique(role))
        {
            return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在");
        }
        role.setUpdateBy(getUsername());
        if (roleService.updateRole(role) > 0)
        {
            // æ›´æ–°ç¼“存用户权限
            LoginUser loginUser = getLoginUser();
            if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin())
            {
                loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName()));
                loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser()));
                tokenService.setLoginUser(loginUser);
            }
            return success();
        }
        return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员");
    }
    /**
     * ä¿®æ”¹ä¿å­˜æ•°æ®æƒé™
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
    @PutMapping("/dataScope")
    public AjaxResult dataScope(@RequestBody SysRole role)
    {
        roleService.checkRoleAllowed(role);
        roleService.checkRoleDataScope(role.getRoleId());
        return toAjax(roleService.authDataScope(role));
    }
    /**
     * çŠ¶æ€ä¿®æ”¹
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
    @PutMapping("/changeStatus")
    public AjaxResult changeStatus(@RequestBody SysRole role)
    {
        roleService.checkRoleAllowed(role);
        roleService.checkRoleDataScope(role.getRoleId());
        role.setUpdateBy(getUsername());
        return toAjax(roleService.updateRoleStatus(role));
    }
    /**
     * åˆ é™¤è§’色
     */
    @PreAuthorize("@ss.hasPermi('system:role:remove')")
    @Log(title = "角色管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{roleIds}")
    public AjaxResult remove(@PathVariable Long[] roleIds)
    {
        return toAjax(roleService.deleteRoleByIds(roleIds));
    }
    /**
     * èŽ·å–è§’è‰²é€‰æ‹©æ¡†åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:role:query')")
    @GetMapping("/optionselect")
    public AjaxResult optionselect()
    {
        return success(roleService.selectRoleAll());
    }
    /**
     * æŸ¥è¯¢å·²åˆ†é…ç”¨æˆ·è§’色列表
     */
    @PreAuthorize("@ss.hasPermi('system:role:list')")
    @GetMapping("/authUser/allocatedList")
    public TableDataInfo allocatedList(SysUser user)
    {
        startPage();
        List<SysUser> list = userService.selectAllocatedList(user);
        return getDataTable(list);
    }
    /**
     * æŸ¥è¯¢æœªåˆ†é…ç”¨æˆ·è§’色列表
     */
    @PreAuthorize("@ss.hasPermi('system:role:list')")
    @GetMapping("/authUser/unallocatedList")
    public TableDataInfo unallocatedList(SysUser user)
    {
        startPage();
        List<SysUser> list = userService.selectUnallocatedList(user);
        return getDataTable(list);
    }
    /**
     * å–消授权用户
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.GRANT)
    @PutMapping("/authUser/cancel")
    public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole)
    {
        return toAjax(roleService.deleteAuthUser(userRole));
    }
    /**
     * æ‰¹é‡å–消授权用户
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.GRANT)
    @PutMapping("/authUser/cancelAll")
    public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds)
    {
        return toAjax(roleService.deleteAuthUsers(roleId, userIds));
    }
    /**
     * æ‰¹é‡é€‰æ‹©ç”¨æˆ·æŽˆæƒ
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.GRANT)
    @PutMapping("/authUser/selectAll")
    public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds)
    {
        roleService.checkRoleDataScope(roleId);
        return toAjax(roleService.insertAuthUsers(roleId, userIds));
    }
    /**
     * èŽ·å–å¯¹åº”è§’è‰²éƒ¨é—¨æ ‘åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:role:query')")
    @GetMapping(value = "/deptTree/{roleId}")
    public AjaxResult deptTree(@PathVariable("roleId") Long roleId)
    {
        AjaxResult ajax = AjaxResult.success();
        ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId));
        ajax.put("depts", deptService.selectDeptTreeList(new SysDept()));
        return ajax;
    }
}
package com.ruoyi.project.system.controller;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.security.service.SysPermissionService;
import com.ruoyi.framework.security.service.TokenService;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysRole;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.domain.SysUserRole;
import com.ruoyi.project.system.service.ISysDeptService;
import com.ruoyi.project.system.service.ISysRoleService;
import com.ruoyi.project.system.service.ISysUserService;
/**
 * è§’色信息
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/role")
@AllArgsConstructor
public class SysRoleController extends BaseController
{
    private ISysRoleService roleService;
    private TokenService tokenService;
    private SysPermissionService permissionService;
    private ISysUserService userService;
    private ISysDeptService deptService;
    @PreAuthorize("@ss.hasPermi('system:role:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysRole role)
    {
        startPage();
        List<SysRole> list = roleService.selectRoleList(role);
        return getDataTable(list);
    }
    @Log(title = "角色管理", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:role:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysRole role)
    {
        List<SysRole> list = roleService.selectRoleList(role);
        ExcelUtil<SysRole> util = new ExcelUtil<SysRole>(SysRole.class);
        util.exportExcel(response, list, "角色数据");
    }
    /**
     * æ ¹æ®è§’色编号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:role:query')")
    @GetMapping(value = "/{roleId}")
    public AjaxResult getInfo(@PathVariable Long roleId)
    {
        roleService.checkRoleDataScope(roleId);
        return success(roleService.selectRoleById(roleId));
    }
    /**
     * æ–°å¢žè§’色
     */
    @PreAuthorize("@ss.hasPermi('system:role:add')")
    @Log(title = "角色管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysRole role)
    {
        if (!roleService.checkRoleNameUnique(role))
        {
            return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
        }
        else if (!roleService.checkRoleKeyUnique(role))
        {
            return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在");
        }
        role.setCreateBy(getUsername());
        return toAjax(roleService.insertRole(role));
    }
    /**
     * ä¿®æ”¹ä¿å­˜è§’色
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysRole role)
    {
        roleService.checkRoleAllowed(role);
        roleService.checkRoleDataScope(role.getRoleId());
        if (!roleService.checkRoleNameUnique(role))
        {
            return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在");
        }
        else if (!roleService.checkRoleKeyUnique(role))
        {
            return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在");
        }
        role.setUpdateBy(getUsername());
        if (roleService.updateRole(role) > 0)
        {
            // æ›´æ–°ç¼“存用户权限
            LoginUser loginUser = getLoginUser();
            if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin())
            {
                loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName()));
                loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser()));
                tokenService.setLoginUser(loginUser);
            }
            return success();
        }
        return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员");
    }
    /**
     * ä¿®æ”¹ä¿å­˜æ•°æ®æƒé™
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
    @PutMapping("/dataScope")
    public AjaxResult dataScope(@RequestBody SysRole role)
    {
        roleService.checkRoleAllowed(role);
        roleService.checkRoleDataScope(role.getRoleId());
        return toAjax(roleService.authDataScope(role));
    }
    /**
     * çŠ¶æ€ä¿®æ”¹
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
    @PutMapping("/changeStatus")
    public AjaxResult changeStatus(@RequestBody SysRole role)
    {
        roleService.checkRoleAllowed(role);
        roleService.checkRoleDataScope(role.getRoleId());
        role.setUpdateBy(getUsername());
        return toAjax(roleService.updateRoleStatus(role));
    }
    /**
     * åˆ é™¤è§’色
     */
    @PreAuthorize("@ss.hasPermi('system:role:remove')")
    @Log(title = "角色管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{roleIds}")
    public AjaxResult remove(@PathVariable Long[] roleIds)
    {
        return toAjax(roleService.deleteRoleByIds(roleIds));
    }
    /**
     * èŽ·å–è§’è‰²é€‰æ‹©æ¡†åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:role:query')")
    @GetMapping("/optionselect")
    public AjaxResult optionselect()
    {
        return success(roleService.selectRoleAll());
    }
    /**
     * æŸ¥è¯¢å·²åˆ†é…ç”¨æˆ·è§’色列表
     */
    @PreAuthorize("@ss.hasPermi('system:role:list')")
    @GetMapping("/authUser/allocatedList")
    public TableDataInfo allocatedList(SysUser user)
    {
        startPage();
        List<SysUser> list = userService.selectAllocatedList(user);
        return getDataTable(list);
    }
    /**
     * æŸ¥è¯¢æœªåˆ†é…ç”¨æˆ·è§’色列表
     */
    @PreAuthorize("@ss.hasPermi('system:role:list')")
    @GetMapping("/authUser/unallocatedList")
    public TableDataInfo unallocatedList(SysUser user)
    {
        startPage();
        List<SysUser> list = userService.selectUnallocatedList(user);
        return getDataTable(list);
    }
    /**
     * å–消授权用户
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.GRANT)
    @PutMapping("/authUser/cancel")
    public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole)
    {
        return toAjax(roleService.deleteAuthUser(userRole));
    }
    /**
     * æ‰¹é‡å–消授权用户
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.GRANT)
    @PutMapping("/authUser/cancelAll")
    public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds)
    {
        return toAjax(roleService.deleteAuthUsers(roleId, userIds));
    }
    /**
     * æ‰¹é‡é€‰æ‹©ç”¨æˆ·æŽˆæƒ
     */
    @PreAuthorize("@ss.hasPermi('system:role:edit')")
    @Log(title = "角色管理", businessType = BusinessType.GRANT)
    @PutMapping("/authUser/selectAll")
    public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds)
    {
        roleService.checkRoleDataScope(roleId);
        return toAjax(roleService.insertAuthUsers(roleId, userIds));
    }
    /**
     * èŽ·å–å¯¹åº”è§’è‰²éƒ¨é—¨æ ‘åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:role:query')")
    @GetMapping(value = "/deptTree/{roleId}")
    public AjaxResult deptTree(@PathVariable("roleId") Long roleId)
    {
        AjaxResult ajax = AjaxResult.success();
        ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId));
        ajax.put("depts", deptService.selectDeptTreeList(new SysDept()));
        return ajax;
    }
}
src/main/java/com/ruoyi/project/system/controller/SysUserController.java
@@ -1,292 +1,292 @@
package com.ruoyi.project.system.controller;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletResponse;
import com.ruoyi.project.system.domain.vo.SysUserDeptVo;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.project.system.service.*;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysRole;
import com.ruoyi.project.system.domain.SysUser;
/**
 * ç”¨æˆ·ä¿¡æ¯
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/user")
@AllArgsConstructor
public class SysUserController extends BaseController
{
    private ISysUserService userService;
    private ISysRoleService roleService;
    private ISysDeptService deptService;
    private ISysPostService postService;
    private ISysUserDeptService userDeptService;
    /**
     * èŽ·å–ç”¨æˆ·åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysUser user)
    {
        startPage();
        List<SysUser> list = userService.selectUserList(user);
        return getDataTable(list);
    }
    /**
     * èŽ·å–ç”¨æˆ·åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/listAll")
    public AjaxResult listAll(SysUser user)
    {
        List<SysUser> list = userService.selectUserList(user);
        return AjaxResult.success(list);
    }
    @Log(title = "用户管理", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:user:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysUser user)
    {
        List<SysUser> list = userService.selectUserList(user);
        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
        util.exportExcel(response, list, "用户数据");
    }
    @Log(title = "用户管理", businessType = BusinessType.IMPORT)
    @PreAuthorize("@ss.hasPermi('system:user:import')")
    @PostMapping("/importData")
    public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
    {
        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
        List<SysUser> userList = util.importExcel(file.getInputStream());
        String operName = getUsername();
        String message = userService.importUser(userList, updateSupport, operName);
        return success(message);
    }
    @PostMapping("/importTemplate")
    public void importTemplate(HttpServletResponse response)
    {
        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
        util.importTemplateExcel(response, "用户数据");
    }
    /**
     * æ ¹æ®ç”¨æˆ·ç¼–号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:user:query')")
    @GetMapping(value = { "/", "/{userId}" })
    public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId)
    {
        AjaxResult ajax = AjaxResult.success();
        if (StringUtils.isNotNull(userId))
        {
            userService.checkUserDataScope(userId);
            SysUser sysUser = userService.selectUserById(userId);
            ajax.put(AjaxResult.DATA_TAG, sysUser);
            ajax.put("postIds", postService.selectPostListByUserId(userId));
            ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList()));
        }
        List<SysRole> roles = roleService.selectRoleAll();
        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
        ajax.put("posts", postService.selectPostAll());
        SysUserDeptVo sysUserDeptVo = new SysUserDeptVo();
        sysUserDeptVo.setUserId(userId);
        List<SysUserDeptVo> sysUserDeptVos = userDeptService.userLoginFacotryList(sysUserDeptVo);
        ajax.put("deptIds",sysUserDeptVos.stream().map(SysUserDeptVo::getDeptId).collect(Collectors.toList()));
        return ajax;
    }
    /**
     * æ–°å¢žç”¨æˆ·
     */
    @PreAuthorize("@ss.hasPermi('system:user:add')")
    @Log(title = "用户管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysUser user)
    {
        roleService.checkRoleDataScope(user.getRoleIds());
        if (!userService.checkUserNameUnique(user))
        {
            return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
        }
        else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
        {
            return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
        }
        else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
        {
            return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
        }
        user.setCreateBy(getUsername());
        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
        user.setTenantId(user.getDeptId());
        return toAjax(userService.insertUser(user));
    }
    /**
     * ä¿®æ”¹ç”¨æˆ·
     */
    @PreAuthorize("@ss.hasPermi('system:user:edit')")
    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysUser user)
    {
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(user.getUserId());
        roleService.checkRoleDataScope(user.getRoleIds());
        if (!userService.checkUserNameUnique(user))
        {
            return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在");
        }
        else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
        {
            return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
        }
        else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
        {
            return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
        }
        user.setUpdateBy(getUsername());
        userService.bindUserDept(user);
        return toAjax(userService.updateUser(user));
    }
    /**
     * åˆ é™¤ç”¨æˆ·
     */
    @PreAuthorize("@ss.hasPermi('system:user:remove')")
    @Log(title = "用户管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{userIds}")
    public AjaxResult remove(@PathVariable Long[] userIds)
    {
        if (ArrayUtils.contains(userIds, getUserId()))
        {
            return error("当前用户不能删除");
        }
        return toAjax(userService.deleteUserByIds(userIds));
    }
    /**
     * é‡ç½®å¯†ç 
     */
    @PreAuthorize("@ss.hasPermi('system:user:resetPwd')")
    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
    @PutMapping("/resetPwd")
    public AjaxResult resetPwd(@RequestBody SysUser user)
    {
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(user.getUserId());
        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
        user.setUpdateBy(getUsername());
        return toAjax(userService.resetPwd(user));
    }
    /**
     * çŠ¶æ€ä¿®æ”¹
     */
    @PreAuthorize("@ss.hasPermi('system:user:edit')")
    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
    @PutMapping("/changeStatus")
    public AjaxResult changeStatus(@RequestBody SysUser user)
    {
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(user.getUserId());
        user.setUpdateBy(getUsername());
        return toAjax(userService.updateUserStatus(user));
    }
    /**
     * æ ¹æ®ç”¨æˆ·ç¼–号获取授权角色
     */
    @PreAuthorize("@ss.hasPermi('system:user:query')")
    @GetMapping("/authRole/{userId}")
    public AjaxResult authRole(@PathVariable("userId") Long userId)
    {
        AjaxResult ajax = AjaxResult.success();
        SysUser user = userService.selectUserById(userId);
        List<SysRole> roles = roleService.selectRolesByUserId(userId);
        ajax.put("user", user);
        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
        return ajax;
    }
    /**
     * ç”¨æˆ·æŽˆæƒè§’色
     */
    @PreAuthorize("@ss.hasPermi('system:user:edit')")
    @Log(title = "用户管理", businessType = BusinessType.GRANT)
    @PutMapping("/authRole")
    public AjaxResult insertAuthRole(Long userId, Long[] roleIds)
    {
        userService.checkUserDataScope(userId);
        roleService.checkRoleDataScope(roleIds);
        userService.insertUserAuth(userId, roleIds);
        return success();
    }
    /**
     * èŽ·å–éƒ¨é—¨æ ‘åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/deptTree")
    public AjaxResult deptTree(SysDept dept)
    {
        return success(deptService.selectDeptTreeList(dept));
    }
    /**
     * ä¸åˆ†é¡µç”¨æˆ·æŸ¥è¯¢
     * @param user
     * @return
     */
    @GetMapping("/userListNoPage")
    public AjaxResult userListNoPage(SysUser user){
        List<SysUser> sysUserList = userService.userListNoPage(user);
        return AjaxResult.success(sysUserList);
    }
    /**
     * æŸ¥è¯¢å½“前用户公司下所有用户
     * @param user
     * @return
     */
    @GetMapping("/userListNoPageByTenantId")
    public AjaxResult userListNoPageByTenantId(SysUser user){
        //获取登录用户信息
        SysUser loginUser = SecurityUtils.getLoginUser().getUser();
        user.setTenantId(loginUser.getTenantId());
        List<SysUser> sysUserList = userService.userListNoPage(user);
        return AjaxResult.success(sysUserList);
    }
}
package com.ruoyi.project.system.controller;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletResponse;
import com.ruoyi.project.system.domain.vo.SysUserDeptVo;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.project.system.service.*;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysRole;
import com.ruoyi.project.system.domain.SysUser;
/**
 * ç”¨æˆ·ä¿¡æ¯
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/system/user")
@AllArgsConstructor
public class SysUserController extends BaseController
{
    private ISysUserService userService;
    private ISysRoleService roleService;
    private ISysDeptService deptService;
    private ISysPostService postService;
    private ISysUserDeptService userDeptService;
    /**
     * èŽ·å–ç”¨æˆ·åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/list")
    public TableDataInfo list(SysUser user)
    {
        startPage();
        List<SysUser> list = userService.selectUserList(user);
        return getDataTable(list);
    }
    /**
     * èŽ·å–ç”¨æˆ·åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/listAll")
    public AjaxResult listAll(SysUser user)
    {
        List<SysUser> list = userService.selectUserList(user);
        return AjaxResult.success(list);
    }
    @Log(title = "用户管理", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('system:user:export')")
    @PostMapping("/export")
    public void export(HttpServletResponse response, SysUser user)
    {
        List<SysUser> list = userService.selectUserList(user);
        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
        util.exportExcel(response, list, "用户数据");
    }
    @Log(title = "用户管理", businessType = BusinessType.IMPORT)
    @PreAuthorize("@ss.hasPermi('system:user:import')")
    @PostMapping("/importData")
    public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
    {
        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
        List<SysUser> userList = util.importExcel(file.getInputStream());
        String operName = getUsername();
        String message = userService.importUser(userList, updateSupport, operName);
        return success(message);
    }
    @PostMapping("/importTemplate")
    public void importTemplate(HttpServletResponse response)
    {
        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
        util.importTemplateExcel(response, "用户数据");
    }
    /**
     * æ ¹æ®ç”¨æˆ·ç¼–号获取详细信息
     */
    @PreAuthorize("@ss.hasPermi('system:user:query')")
    @GetMapping(value = { "/", "/{userId}" })
    public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId)
    {
        AjaxResult ajax = AjaxResult.success();
        if (StringUtils.isNotNull(userId))
        {
            userService.checkUserDataScope(userId);
            SysUser sysUser = userService.selectUserById(userId);
            ajax.put(AjaxResult.DATA_TAG, sysUser);
            ajax.put("postIds", postService.selectPostListByUserId(userId));
            ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList()));
        }
        List<SysRole> roles = roleService.selectRoleAll();
        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
        ajax.put("posts", postService.selectPostAll());
        SysUserDeptVo sysUserDeptVo = new SysUserDeptVo();
        sysUserDeptVo.setUserId(userId);
        List<SysUserDeptVo> sysUserDeptVos = userDeptService.userLoginFacotryList(sysUserDeptVo);
        ajax.put("deptIds",sysUserDeptVos.stream().map(SysUserDeptVo::getDeptId).collect(Collectors.toList()));
        return ajax;
    }
    /**
     * æ–°å¢žç”¨æˆ·
     */
    @PreAuthorize("@ss.hasPermi('system:user:add')")
    @Log(title = "用户管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysUser user)
    {
        roleService.checkRoleDataScope(user.getRoleIds());
        if (!userService.checkUserNameUnique(user))
        {
            return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
        }
        else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
        {
            return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
        }
        else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
        {
            return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
        }
        user.setCreateBy(getUsername());
        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
        user.setTenantId(user.getDeptId());
        return toAjax(userService.insertUser(user));
    }
    /**
     * ä¿®æ”¹ç”¨æˆ·
     */
    @PreAuthorize("@ss.hasPermi('system:user:edit')")
    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@Validated @RequestBody SysUser user)
    {
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(user.getUserId());
        roleService.checkRoleDataScope(user.getRoleIds());
        if (!userService.checkUserNameUnique(user))
        {
            return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在");
        }
        else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
        {
            return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
        }
        else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
        {
            return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
        }
        user.setUpdateBy(getUsername());
        userService.bindUserDept(user);
        return toAjax(userService.updateUser(user));
    }
    /**
     * åˆ é™¤ç”¨æˆ·
     */
    @PreAuthorize("@ss.hasPermi('system:user:remove')")
    @Log(title = "用户管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{userIds}")
    public AjaxResult remove(@PathVariable Long[] userIds)
    {
        if (ArrayUtils.contains(userIds, getUserId()))
        {
            return error("当前用户不能删除");
        }
        return toAjax(userService.deleteUserByIds(userIds));
    }
    /**
     * é‡ç½®å¯†ç 
     */
    @PreAuthorize("@ss.hasPermi('system:user:resetPwd')")
    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
    @PutMapping("/resetPwd")
    public AjaxResult resetPwd(@RequestBody SysUser user)
    {
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(user.getUserId());
        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
        user.setUpdateBy(getUsername());
        return toAjax(userService.resetPwd(user));
    }
    /**
     * çŠ¶æ€ä¿®æ”¹
     */
    @PreAuthorize("@ss.hasPermi('system:user:edit')")
    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
    @PutMapping("/changeStatus")
    public AjaxResult changeStatus(@RequestBody SysUser user)
    {
        userService.checkUserAllowed(user);
        userService.checkUserDataScope(user.getUserId());
        user.setUpdateBy(getUsername());
        return toAjax(userService.updateUserStatus(user));
    }
    /**
     * æ ¹æ®ç”¨æˆ·ç¼–号获取授权角色
     */
    @PreAuthorize("@ss.hasPermi('system:user:query')")
    @GetMapping("/authRole/{userId}")
    public AjaxResult authRole(@PathVariable("userId") Long userId)
    {
        AjaxResult ajax = AjaxResult.success();
        SysUser user = userService.selectUserById(userId);
        List<SysRole> roles = roleService.selectRolesByUserId(userId);
        ajax.put("user", user);
        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
        return ajax;
    }
    /**
     * ç”¨æˆ·æŽˆæƒè§’色
     */
    @PreAuthorize("@ss.hasPermi('system:user:edit')")
    @Log(title = "用户管理", businessType = BusinessType.GRANT)
    @PutMapping("/authRole")
    public AjaxResult insertAuthRole(Long userId, Long[] roleIds)
    {
        userService.checkUserDataScope(userId);
        roleService.checkRoleDataScope(roleIds);
        userService.insertUserAuth(userId, roleIds);
        return success();
    }
    /**
     * èŽ·å–éƒ¨é—¨æ ‘åˆ—è¡¨
     */
    @PreAuthorize("@ss.hasPermi('system:user:list')")
    @GetMapping("/deptTree")
    public AjaxResult deptTree(SysDept dept)
    {
        return success(deptService.selectDeptTreeList(dept));
    }
    /**
     * ä¸åˆ†é¡µç”¨æˆ·æŸ¥è¯¢
     * @param user
     * @return
     */
    @GetMapping("/userListNoPage")
    public AjaxResult userListNoPage(SysUser user){
        List<SysUser> sysUserList = userService.userListNoPage(user);
        return AjaxResult.success(sysUserList);
    }
    /**
     * æŸ¥è¯¢å½“前用户公司下所有用户
     * @param user
     * @return
     */
    @GetMapping("/userListNoPageByTenantId")
    public AjaxResult userListNoPageByTenantId(SysUser user){
        //获取登录用户信息
        SysUser loginUser = SecurityUtils.getLoginUser().getUser();
        user.setTenantId(loginUser.getTenantId());
        List<SysUser> sysUserList = userService.userListNoPage(user);
        return AjaxResult.success(sysUserList);
    }
}
src/main/java/com/ruoyi/project/tool/gen/controller/GenController.java
@@ -1,255 +1,255 @@
package com.ruoyi.project.tool.gen.controller;
import com.alibaba.druid.DbType;
import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlCreateTableStatement;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.sql.SqlUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.config.GenConfig;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.tool.gen.domain.GenTable;
import com.ruoyi.project.tool.gen.domain.GenTableColumn;
import com.ruoyi.project.tool.gen.service.IGenTableColumnService;
import com.ruoyi.project.tool.gen.service.IGenTableService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * ä»£ç ç”Ÿæˆ æ“ä½œå¤„理
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/tool/gen")
@AllArgsConstructor
public class GenController extends BaseController
{
    private IGenTableService genTableService;
    private IGenTableColumnService genTableColumnService;
    /**
     * æŸ¥è¯¢ä»£ç ç”Ÿæˆåˆ—表
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:list')")
    @GetMapping("/list")
    public TableDataInfo genList(GenTable genTable)
    {
        startPage();
        List<GenTable> list = genTableService.selectGenTableList(genTable);
        return getDataTable(list);
    }
    /**
     * èŽ·å–ä»£ç ç”Ÿæˆä¿¡æ¯
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:query')")
    @GetMapping(value = "/{talbleId}")
    public AjaxResult getInfo(@PathVariable Long talbleId)
    {
        GenTable table = genTableService.selectGenTableById(talbleId);
        List<GenTable> tables = genTableService.selectGenTableAll();
        List<GenTableColumn> list = genTableColumnService.selectGenTableColumnListByTableId(talbleId);
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("info", table);
        map.put("rows", list);
        map.put("tables", tables);
        return success(map);
    }
    /**
     * æŸ¥è¯¢æ•°æ®åº“列表
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:list')")
    @GetMapping("/db/list")
    public TableDataInfo dataList(GenTable genTable)
    {
        startPage();
        List<GenTable> list = genTableService.selectDbTableList(genTable);
        return getDataTable(list);
    }
    /**
     * æŸ¥è¯¢æ•°æ®è¡¨å­—段列表
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:list')")
    @GetMapping(value = "/column/{tableId}")
    public TableDataInfo columnList(Long tableId)
    {
        TableDataInfo dataInfo = new TableDataInfo();
        List<GenTableColumn> list = genTableColumnService.selectGenTableColumnListByTableId(tableId);
        dataInfo.setRows(list);
        dataInfo.setTotal(list.size());
        return dataInfo;
    }
    /**
     * å¯¼å…¥è¡¨ç»“构(保存)
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:import')")
    @Log(title = "代码生成", businessType = BusinessType.IMPORT)
    @PostMapping("/importTable")
    public AjaxResult importTableSave(String tables)
    {
        String[] tableNames = Convert.toStrArray(tables);
        // æŸ¥è¯¢è¡¨ä¿¡æ¯
        List<GenTable> tableList = genTableService.selectDbTableListByNames(tableNames);
        genTableService.importGenTable(tableList, SecurityUtils.getUsername());
        return success();
    }
    /**
     * åˆ›å»ºè¡¨ç»“构(保存)
     */
    @PreAuthorize("@ss.hasRole('admin')")
    @Log(title = "创建表", businessType = BusinessType.OTHER)
    @PostMapping("/createTable")
    public AjaxResult createTableSave(String sql)
    {
        try
        {
            SqlUtil.filterKeyword(sql);
            List<SQLStatement> sqlStatements = SQLUtils.parseStatements(sql, DbType.mysql);
            List<String> tableNames = new ArrayList<>();
            for (SQLStatement sqlStatement : sqlStatements)
            {
                if (sqlStatement instanceof MySqlCreateTableStatement)
                {
                    MySqlCreateTableStatement createTableStatement = (MySqlCreateTableStatement) sqlStatement;
                    if (genTableService.createTable(createTableStatement.toString()))
                    {
                        String tableName = createTableStatement.getTableName().replaceAll("`", "");
                        tableNames.add(tableName);
                    }
                }
            }
            List<GenTable> tableList = genTableService.selectDbTableListByNames(tableNames.toArray(new String[tableNames.size()]));
            String operName = SecurityUtils.getUsername();
            genTableService.importGenTable(tableList, operName);
            return AjaxResult.success();
        }
        catch (Exception e)
        {
            logger.error(e.getMessage(), e);
            return AjaxResult.error("创建表结构异常");
        }
    }
    /**
     * ä¿®æ”¹ä¿å­˜ä»£ç ç”Ÿæˆä¸šåŠ¡
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:edit')")
    @Log(title = "代码生成", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult editSave(@Validated @RequestBody GenTable genTable)
    {
        genTableService.validateEdit(genTable);
        genTableService.updateGenTable(genTable);
        return success();
    }
    /**
     * åˆ é™¤ä»£ç ç”Ÿæˆ
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:remove')")
    @Log(title = "代码生成", businessType = BusinessType.DELETE)
    @DeleteMapping("/{tableIds}")
    public AjaxResult remove(@PathVariable Long[] tableIds)
    {
        genTableService.deleteGenTableByIds(tableIds);
        return success();
    }
    /**
     * é¢„览代码
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:preview')")
    @GetMapping("/preview/{tableId}")
    public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException
    {
        Map<String, String> dataMap = genTableService.previewCode(tableId);
        return success(dataMap);
    }
    /**
     * ç”Ÿæˆä»£ç ï¼ˆä¸‹è½½æ–¹å¼ï¼‰
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:code')")
    @Log(title = "代码生成", businessType = BusinessType.GENCODE)
    @GetMapping("/download/{tableName}")
    public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException
    {
        byte[] data = genTableService.downloadCode(tableName);
        genCode(response, data);
    }
    /**
     * ç”Ÿæˆä»£ç ï¼ˆè‡ªå®šä¹‰è·¯å¾„)
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:code')")
    @Log(title = "代码生成", businessType = BusinessType.GENCODE)
    @GetMapping("/genCode/{tableName}")
    public AjaxResult genCode(@PathVariable("tableName") String tableName)
    {
        if (!GenConfig.isAllowOverwrite())
        {
            return AjaxResult.error("【系统预设】不允许生成文件覆盖到本地");
        }
        genTableService.generatorCode(tableName);
        return success();
    }
    /**
     * åŒæ­¥æ•°æ®åº“
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:edit')")
    @Log(title = "代码生成", businessType = BusinessType.UPDATE)
    @GetMapping("/synchDb/{tableName}")
    public AjaxResult synchDb(@PathVariable("tableName") String tableName)
    {
        genTableService.synchDb(tableName);
        return success();
    }
    /**
     * æ‰¹é‡ç”Ÿæˆä»£ç 
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:code')")
    @Log(title = "代码生成", businessType = BusinessType.GENCODE)
    @GetMapping("/batchGenCode")
    public void batchGenCode(HttpServletResponse response, String tables) throws IOException
    {
        String[] tableNames = Convert.toStrArray(tables);
        byte[] data = genTableService.downloadCode(tableNames);
        genCode(response, data);
    }
    /**
     * ç”Ÿæˆzip文件
     */
    private void genCode(HttpServletResponse response, byte[] data) throws IOException
    {
        response.reset();
        response.addHeader("Access-Control-Allow-Origin", "*");
        response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
        response.setHeader("Content-Disposition", "attachment; filename=\"ruoyi.zip\"");
        response.addHeader("Content-Length", "" + data.length);
        response.setContentType("application/octet-stream; charset=UTF-8");
        IOUtils.write(data, response.getOutputStream());
    }
package com.ruoyi.project.tool.gen.controller;
import com.alibaba.druid.DbType;
import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlCreateTableStatement;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.sql.SqlUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.config.GenConfig;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.project.tool.gen.domain.GenTable;
import com.ruoyi.project.tool.gen.domain.GenTableColumn;
import com.ruoyi.project.tool.gen.service.IGenTableColumnService;
import com.ruoyi.project.tool.gen.service.IGenTableService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * ä»£ç ç”Ÿæˆ æ“ä½œå¤„理
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/tool/gen")
@AllArgsConstructor
public class GenController extends BaseController
{
    private IGenTableService genTableService;
    private IGenTableColumnService genTableColumnService;
    /**
     * æŸ¥è¯¢ä»£ç ç”Ÿæˆåˆ—表
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:list')")
    @GetMapping("/list")
    public TableDataInfo genList(GenTable genTable)
    {
        startPage();
        List<GenTable> list = genTableService.selectGenTableList(genTable);
        return getDataTable(list);
    }
    /**
     * èŽ·å–ä»£ç ç”Ÿæˆä¿¡æ¯
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:query')")
    @GetMapping(value = "/{talbleId}")
    public AjaxResult getInfo(@PathVariable Long talbleId)
    {
        GenTable table = genTableService.selectGenTableById(talbleId);
        List<GenTable> tables = genTableService.selectGenTableAll();
        List<GenTableColumn> list = genTableColumnService.selectGenTableColumnListByTableId(talbleId);
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("info", table);
        map.put("rows", list);
        map.put("tables", tables);
        return success(map);
    }
    /**
     * æŸ¥è¯¢æ•°æ®åº“列表
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:list')")
    @GetMapping("/db/list")
    public TableDataInfo dataList(GenTable genTable)
    {
        startPage();
        List<GenTable> list = genTableService.selectDbTableList(genTable);
        return getDataTable(list);
    }
    /**
     * æŸ¥è¯¢æ•°æ®è¡¨å­—段列表
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:list')")
    @GetMapping(value = "/column/{tableId}")
    public TableDataInfo columnList(Long tableId)
    {
        TableDataInfo dataInfo = new TableDataInfo();
        List<GenTableColumn> list = genTableColumnService.selectGenTableColumnListByTableId(tableId);
        dataInfo.setRows(list);
        dataInfo.setTotal(list.size());
        return dataInfo;
    }
    /**
     * å¯¼å…¥è¡¨ç»“构(保存)
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:import')")
    @Log(title = "代码生成", businessType = BusinessType.IMPORT)
    @PostMapping("/importTable")
    public AjaxResult importTableSave(String tables)
    {
        String[] tableNames = Convert.toStrArray(tables);
        // æŸ¥è¯¢è¡¨ä¿¡æ¯
        List<GenTable> tableList = genTableService.selectDbTableListByNames(tableNames);
        genTableService.importGenTable(tableList, SecurityUtils.getUsername());
        return success();
    }
    /**
     * åˆ›å»ºè¡¨ç»“构(保存)
     */
    @PreAuthorize("@ss.hasRole('admin')")
    @Log(title = "创建表", businessType = BusinessType.OTHER)
    @PostMapping("/createTable")
    public AjaxResult createTableSave(String sql)
    {
        try
        {
            SqlUtil.filterKeyword(sql);
            List<SQLStatement> sqlStatements = SQLUtils.parseStatements(sql, DbType.mysql);
            List<String> tableNames = new ArrayList<>();
            for (SQLStatement sqlStatement : sqlStatements)
            {
                if (sqlStatement instanceof MySqlCreateTableStatement)
                {
                    MySqlCreateTableStatement createTableStatement = (MySqlCreateTableStatement) sqlStatement;
                    if (genTableService.createTable(createTableStatement.toString()))
                    {
                        String tableName = createTableStatement.getTableName().replaceAll("`", "");
                        tableNames.add(tableName);
                    }
                }
            }
            List<GenTable> tableList = genTableService.selectDbTableListByNames(tableNames.toArray(new String[tableNames.size()]));
            String operName = SecurityUtils.getUsername();
            genTableService.importGenTable(tableList, operName);
            return AjaxResult.success();
        }
        catch (Exception e)
        {
            logger.error(e.getMessage(), e);
            return AjaxResult.error("创建表结构异常");
        }
    }
    /**
     * ä¿®æ”¹ä¿å­˜ä»£ç ç”Ÿæˆä¸šåŠ¡
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:edit')")
    @Log(title = "代码生成", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult editSave(@Validated @RequestBody GenTable genTable)
    {
        genTableService.validateEdit(genTable);
        genTableService.updateGenTable(genTable);
        return success();
    }
    /**
     * åˆ é™¤ä»£ç ç”Ÿæˆ
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:remove')")
    @Log(title = "代码生成", businessType = BusinessType.DELETE)
    @DeleteMapping("/{tableIds}")
    public AjaxResult remove(@PathVariable Long[] tableIds)
    {
        genTableService.deleteGenTableByIds(tableIds);
        return success();
    }
    /**
     * é¢„览代码
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:preview')")
    @GetMapping("/preview/{tableId}")
    public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException
    {
        Map<String, String> dataMap = genTableService.previewCode(tableId);
        return success(dataMap);
    }
    /**
     * ç”Ÿæˆä»£ç ï¼ˆä¸‹è½½æ–¹å¼ï¼‰
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:code')")
    @Log(title = "代码生成", businessType = BusinessType.GENCODE)
    @GetMapping("/download/{tableName}")
    public void download(HttpServletResponse response, @PathVariable("tableName") String tableName) throws IOException
    {
        byte[] data = genTableService.downloadCode(tableName);
        genCode(response, data);
    }
    /**
     * ç”Ÿæˆä»£ç ï¼ˆè‡ªå®šä¹‰è·¯å¾„)
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:code')")
    @Log(title = "代码生成", businessType = BusinessType.GENCODE)
    @GetMapping("/genCode/{tableName}")
    public AjaxResult genCode(@PathVariable("tableName") String tableName)
    {
        if (!GenConfig.isAllowOverwrite())
        {
            return AjaxResult.error("【系统预设】不允许生成文件覆盖到本地");
        }
        genTableService.generatorCode(tableName);
        return success();
    }
    /**
     * åŒæ­¥æ•°æ®åº“
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:edit')")
    @Log(title = "代码生成", businessType = BusinessType.UPDATE)
    @GetMapping("/synchDb/{tableName}")
    public AjaxResult synchDb(@PathVariable("tableName") String tableName)
    {
        genTableService.synchDb(tableName);
        return success();
    }
    /**
     * æ‰¹é‡ç”Ÿæˆä»£ç 
     */
    @PreAuthorize("@ss.hasPermi('tool:gen:code')")
    @Log(title = "代码生成", businessType = BusinessType.GENCODE)
    @GetMapping("/batchGenCode")
    public void batchGenCode(HttpServletResponse response, String tables) throws IOException
    {
        String[] tableNames = Convert.toStrArray(tables);
        byte[] data = genTableService.downloadCode(tableNames);
        genCode(response, data);
    }
    /**
     * ç”Ÿæˆzip文件
     */
    private void genCode(HttpServletResponse response, byte[] data) throws IOException
    {
        response.reset();
        response.addHeader("Access-Control-Allow-Origin", "*");
        response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
        response.setHeader("Content-Disposition", "attachment; filename=\"ruoyi.zip\"");
        response.addHeader("Content-Length", "" + data.length);
        response.setContentType("application/octet-stream; charset=UTF-8");
        IOUtils.write(data, response.getOutputStream());
    }
}
src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java
@@ -1,27 +1,19 @@
package com.ruoyi.purchase.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.basic.service.ISupplierService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.purchase.dto.InvoicePurchaseReportDto;
import com.ruoyi.purchase.dto.VatDto;
import com.ruoyi.purchase.pojo.InvoicePurchase;
import com.ruoyi.purchase.service.IInvoicePurchaseService;
import com.ruoyi.waterrecord.pojo.WaterRecord;
import io.swagger.v3.oas.annotations.tags.Tag;
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.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
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;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
@RestController
@Tag(name = "采购报表")
@@ -29,40 +21,45 @@
@AllArgsConstructor
public class AccountingReportController {
    private IInvoicePurchaseService invoicePurchaseService;
    private final ISupplierService supplierService;
    @GetMapping("/list")
    @Log(title = "采购报表-项目利润", businessType = BusinessType.OTHER)
    public AjaxResult list(Page page, InvoicePurchaseReportDto invoicePurchaseReportDto) {
        IPage<InvoicePurchaseReportDto> result =invoicePurchaseService.listPurchaseReport(page, invoicePurchaseReportDto);
        return AjaxResult.success(result);
    public AjaxResult list(Page page) {
        return AjaxResult.success();
    }
    @Log(title = "采购报表-项目利润导出", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    @Operation(summary = "采购报表-项目利润导出")
    public void export(HttpServletResponse response) {
        Page page = new Page(-1,-1);
        InvoicePurchaseReportDto waterRecord = new InvoicePurchaseReportDto();
        IPage<InvoicePurchaseReportDto> listPage = invoicePurchaseService.listPurchaseReport(page, waterRecord);
        ExcelUtil<InvoicePurchaseReportDto> util = new ExcelUtil<InvoicePurchaseReportDto>(InvoicePurchaseReportDto.class);
        util.exportExcel(response, listPage.getRecords() , "项目利润导出");
    }
    @Log(title = "采购报表-增值税比对", businessType = BusinessType.OTHER)
    @GetMapping("/listVat")
    public AjaxResult listVat(Page page,String month) {
        IPage<VatDto> result = invoicePurchaseService.listVat(page, month);
        return AjaxResult.success(result);
        return AjaxResult.success();
    }
    @Log(title = "采购报表-增值税比对", businessType = BusinessType.EXPORT)
    @PostMapping("/exportTwo")
    @Operation(summary = "采购报表-增值税比对")
    public void exportTwo(HttpServletResponse response) {
        Page page = new Page(-1,-1);
        IPage<VatDto> result = invoicePurchaseService.listVat(page, null);
        ExcelUtil<VatDto> util = new ExcelUtil<VatDto>(VatDto.class);
        util.exportExcel(response, result.getRecords() , "增值税比对");
    }
    @GetMapping("/supplierTransactions")
    @Log(title = "供应商往来", businessType = BusinessType.OTHER)
    @Operation(summary = "供应商往来")
    public R supplierTransactions(Page page, String supplierName) {
        return R.ok(supplierService.supplierTransactions(page,supplierName));
    }
    @GetMapping("/supplierTransactionsDetails")
    @Log(title = "供应商往来明细", businessType = BusinessType.OTHER)
    @Operation(summary = "供应商往来明细")
    public R supplierTransactionsDetails(Page page, Long supplierId) {
        return R.ok(supplierService.supplierTransactionsDetails(page,supplierId));
    }
}
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
@@ -1,8 +1,6 @@
package com.ruoyi.purchase.controller;
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.extension.plugins.pagination.Page;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
@@ -17,27 +15,23 @@
import com.ruoyi.purchase.pojo.PurchaseLedgerTemplate;
import com.ruoyi.purchase.pojo.SalesLedgerProductTemplate;
import com.ruoyi.purchase.service.IPurchaseLedgerService;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.service.ISalesLedgerProductService;
import com.ruoyi.sales.service.ISalesLedgerService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.annotations.ApiParam;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
 * é‡‡è´­å°è´¦Controller
@@ -132,19 +126,6 @@
        util.exportExcel(response, list, "【请填写功能名称】数据");
    }
    /**
     * å¯¼å‡ºæ¥ç¥¨ç™»è®°åˆ—表
     */
    @Log(title = "导出来票登记列表", businessType = BusinessType.EXPORT)
    @PostMapping("/exportOne")
    public void exportOne(HttpServletResponse response, PurchaseLedger purchaseLedger) {
        Page page = new Page();
        page.setCurrent(-1);
        page.setSize(-1);
        IPage<PurchaseLedgerDto> purchaseLedgerDtoIPage = purchaseLedgerService.selectPurchaseLedgerListPage(page, new PurchaseLedgerDto());
        ExcelUtil<PurchaseLedgerDto> util = new ExcelUtil<PurchaseLedgerDto>(PurchaseLedgerDto.class);
        util.exportExcel(response, purchaseLedgerDtoIPage.getRecords(), "导出来票登记列表");
    }
    /**
     * æ–°å¢žä¿®æ”¹é‡‡è´­å°è´¦
@@ -219,7 +200,7 @@
     */
    @GetMapping("/getProductBySalesNo")
    public AjaxResult getProductBySalesNo(Long id) {
        return AjaxResult.success(purchaseLedgerService.getProductBySalesNo(id));
        return AjaxResult.success();
    }
    /**
src/main/java/com/ruoyi/purchase/controller/PurchaseReturnOrdersController.java
@@ -1,12 +1,18 @@
package com.ruoyi.purchase.controller;
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.pojo.AccountStatementDetails;
import com.ruoyi.account.service.AccountStatementDetailsService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
import com.ruoyi.purchase.service.PurchaseReturnOrdersService;
import com.ruoyi.purchase.vo.PurchaseStockInProductVo;
import io.swagger.v3.oas.annotations.Operation;
@@ -31,6 +37,7 @@
public class PurchaseReturnOrdersController {
    private PurchaseReturnOrdersService purchaseReturnOrdersService;
    private PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
    private AccountStatementDetailsService accountStatementDetailsService;
    @GetMapping("/listPage")
@@ -56,6 +63,13 @@
    @PostMapping("/deleteById/{id}")
    public AjaxResult deleteById(@PathVariable Long id) {
        //如果该采购退货已经生成对账单则无法删除
        PurchaseReturnOrders purchaseReturnOrders = purchaseReturnOrdersService.getById(id);
        List<AccountStatementDetails> accountStatementDetails = accountStatementDetailsService.list(Wrappers.<AccountStatementDetails>lambdaQuery()
                .eq(AccountStatementDetails::getReceiptNumber, purchaseReturnOrders.getNo()));
        if (CollectionUtils.isNotEmpty(accountStatementDetails)){
            throw new ServiceException("该采购退货单已经生成对账单,无法删除");
        }
        purchaseReturnOrdersService.deleteById(id);
        return AjaxResult.success();
    }
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
@@ -165,26 +165,11 @@
    private BigDecimal invoiceAmount;
    /**
     * æ¥ç¥¨ç™»è®°id
     */
    private Long ticketRegistrationId;
    /**
     * åˆåŒé‡‘额(产品含税总价)
     */
    @Excel(name = "合同金额")
    private BigDecimal contractAmount = BigDecimal.ZERO;
    @TableField(exist = false)
    @Schema(description = "来票金额")
    @Excel(name = "已来票金额(元)")
    private BigDecimal receiptPaymentAmount =  BigDecimal.ZERO;
    @Schema(description = "未来票金额")
    @TableField(exist = false)
    @Excel(name = "未来票金额(元)")
    private BigDecimal unReceiptPaymentAmount =BigDecimal.ZERO;
    @Schema(description = "文件类型  å– 4")
    @TableField(exist = false)
src/main/java/com/ruoyi/purchase/dto/TicketRegistrationDto.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
@@ -3,7 +3,6 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.purchase.dto.PaymentRegistrationDto;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import org.apache.ibatis.annotations.Param;
@@ -23,8 +22,6 @@
    int updateContractAmountById(@Param("id") Long id, @Param("totalTaxInclusiveAmount") BigDecimal totalTaxInclusiveAmount);
    IPage<PurchaseLedgerDto> selectPurchaseLedgerListPage(IPage ipage, @Param("c") PurchaseLedgerDto purchaseLedger);
    List<PaymentRegistrationDto> getPaymentRegistrationDtoById(Long id);
    List<IncomeExpenseAnalysisDto> selectPurchaseStats(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("dateFormat") String dateFormat);
src/main/java/com/ruoyi/purchase/mapper/PurchaseReturnOrdersMapper.java
@@ -3,8 +3,8 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.PurchaseReturnVo;
import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto;
import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo;
import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
import com.ruoyi.purchase.dto.PurchaseReturnOrderHasAllInfoDto;
import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
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
@@ -136,14 +136,6 @@
     */
    private String phoneNumber;
    @TableField(exist = false)
    @Schema(description = "来票金额")
    private String receiptPaymentAmount;
    @Schema(description = "未来票金额")
    @TableField(exist = false)
    private String unReceiptPaymentAmount;
    @Schema(description = "文件类型  å– 4")
    @TableField(exist = false)
    private Integer type;
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
@@ -6,7 +6,6 @@
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.sales.pojo.InvoiceRegistrationProduct;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import org.springframework.web.multipart.MultipartFile;
@@ -39,8 +38,6 @@
    PurchaseLedgerDto getPurchaseNoById(Long id);
    IPage<PurchaseLedgerDto> selectPurchaseLedgerListPage(IPage ipage, PurchaseLedgerDto purchaseLedger);
    List<InvoiceRegistrationProduct> getProductBySalesNo(Long id);
    String getPurchaseNo();
src/main/java/com/ruoyi/purchase/service/ITicketRegistrationService.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
@@ -37,14 +37,8 @@
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
import com.ruoyi.purchase.dto.PurchaseLedgerImportDto;
import com.ruoyi.purchase.dto.PurchaseLedgerProductImportDto;
import com.ruoyi.purchase.mapper.PaymentRegistrationMapper;
import com.ruoyi.purchase.mapper.ProductRecordMapper;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.mapper.TicketRegistrationMapper;
import com.ruoyi.purchase.pojo.PaymentRegistration;
import com.ruoyi.purchase.pojo.ProductRecord;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.purchase.pojo.TicketRegistration;
import com.ruoyi.purchase.service.IPurchaseLedgerService;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityInspectParamMapper;
@@ -55,11 +49,9 @@
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
import com.ruoyi.sales.mapper.CommonFileMapper;
import com.ruoyi.sales.mapper.InvoiceRegistrationProductMapper;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.pojo.InvoiceRegistrationProduct;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.service.impl.CommonFileServiceImpl;
@@ -103,10 +95,6 @@
    private final ProductMapper productMapper;
    private final ProductModelMapper productModelMapper;
    private final SysUserMapper sysUserMapper;
    private final TicketRegistrationMapper ticketRegistrationMapper;
    private final ProductRecordMapper productRecordMapper;
    private final PaymentRegistrationMapper paymentRegistrationMapper;
    private final InvoiceRegistrationProductMapper invoiceRegistrationProductMapper;
    private final StringRedisTemplate redisTemplate;
    private final QualityInspectMapper qualityInspectMapper;
    private final CommonFileServiceImpl commonFileService;
@@ -303,9 +291,6 @@
                LocalDateTime localDateTime = entryDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
                salesLedgerProduct.setRegisterDate(localDateTime);
                salesLedgerProduct.setFutureTickets(salesLedgerProduct.getQuantity());
                salesLedgerProduct.setFutureTicketsAmount(salesLedgerProduct.getTaxInclusiveTotalPrice());
                salesLedgerProduct.setPendingTicketsTotal(salesLedgerProduct.getTaxInclusiveTotalPrice());
                salesLedgerProductMapper.insert(salesLedgerProduct);
            }
        }
@@ -353,18 +338,6 @@
        queryWrapper.in(SalesLedgerProduct::getSalesLedgerId, ids)
                .eq(SalesLedgerProduct::getType, 2);
        salesLedgerProductMapper.delete(queryWrapper);
        // æ‰¹é‡åˆ é™¤å…³è”的采购台账的来票登记
        LambdaQueryWrapper<TicketRegistration> ticketRegistrationLambdaQueryWrapper = new LambdaQueryWrapper<>();
        ticketRegistrationLambdaQueryWrapper.in(TicketRegistration::getPurchaseLedgerId,ids);
        ticketRegistrationMapper.delete(ticketRegistrationLambdaQueryWrapper);
        // æ‰¹é‡åˆ é™¤å…³è”的采购台账的来票登记记录
        LambdaQueryWrapper<ProductRecord> productRecordLambdaQueryWrapper = new LambdaQueryWrapper<>();
        productRecordLambdaQueryWrapper.in(ProductRecord::getPurchaseLedgerId,ids);
        productRecordMapper.delete(productRecordLambdaQueryWrapper);
        // æ‰¹é‡åˆ é™¤ä»˜æ¬¾ç™»è®°
        LambdaQueryWrapper<PaymentRegistration> paymentRegistrationLambdaQueryWrapper = new LambdaQueryWrapper<>();
        paymentRegistrationLambdaQueryWrapper.in(PaymentRegistration::getPurchaseLedgerId, ids);
        paymentRegistrationMapper.delete(paymentRegistrationLambdaQueryWrapper);
        //批量删除检验标准
        LambdaQueryWrapper<QualityInspect> materialInspectLambdaQueryWrapper = new LambdaQueryWrapper<>();
        materialInspectLambdaQueryWrapper.in(QualityInspect::getPurchaseLedgerId, ids);
@@ -467,14 +440,6 @@
        queryWrapper.eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedger.getId())
                .eq(SalesLedgerProduct::getType, 2);
        List<SalesLedgerProduct> productList = salesLedgerProductMapper.selectList(queryWrapper);
        productList.forEach(product -> {
            product.setFutureTickets(product.getFutureTickets() != null ? product.getFutureTickets() : product.getQuantity());
            product.setFutureTicketsAmount(product.getFutureTicketsAmount() != null ? product.getFutureTicketsAmount() : product.getTaxInclusiveTotalPrice());
            product.setTicketsNum(null);
            product.setTicketsAmount(null);
            product.setTempFutureTickets(product.getFutureTickets());
            product.setTempFutureTicketsAmount(product.getFutureTicketsAmount());
        });
        resultDto.setProductData(productList);
        return resultDto;
    }
@@ -501,12 +466,6 @@
        PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectById(id);
        BeanUtils.copyProperties(purchaseLedger, purchaseLedgerDto);
//        TicketRegistration ticketRegistration = ticketRegistrationMapper.selectOne(new LambdaQueryWrapper<TicketRegistration>().eq(TicketRegistration::getPurchaseLedgerId, id));
//        if (ticketRegistration != null) {
//            purchaseLedgerDto.setInvoiceNumber(ticketRegistration.getInvoiceNumber());
//            purchaseLedgerDto.setInvoiceAmount(ticketRegistration.getInvoiceAmount());
//            purchaseLedgerDto.setTicketRegistrationId(ticketRegistration.getId());
//        }
        return purchaseLedgerDto;
    }
@@ -537,18 +496,6 @@
            records.forEach(dto -> dto.setSalesLedgerFiles(fileMap.getOrDefault(dto.getId(), new ArrayList<>())));
        }
        return purchaseLedgerDtoIPage;
    }
    @Override
    public List<InvoiceRegistrationProduct> getProductBySalesNo(Long id) {
        List<InvoiceRegistrationProduct> invoiceRegistrationProducts = invoiceRegistrationProductMapper.selectList(new LambdaQueryWrapper<InvoiceRegistrationProduct>()
                .select(InvoiceRegistrationProduct::getId, InvoiceRegistrationProduct::getProductCategory, InvoiceRegistrationProduct::getSpecificationModel,
                        InvoiceRegistrationProduct::getUnit, InvoiceRegistrationProduct::getQuantity)
                .eq(InvoiceRegistrationProduct::getSalesLedgerId, id));
        if (invoiceRegistrationProducts.isEmpty()) {
            return new ArrayList<>();
        }
        return invoiceRegistrationProducts;
    }
    @Override
@@ -655,8 +602,6 @@
                    salesLedgerProduct.setType(2);
                    // è®¡ç®—不含税总价
                    salesLedgerProduct.setTaxExclusiveTotalPrice(salesLedgerProduct.getTaxInclusiveTotalPrice().divide(new BigDecimal(1).add(salesLedgerProduct.getTaxRate().divide(new BigDecimal(100))), 2, RoundingMode.HALF_UP));
                    salesLedgerProduct.setFutureTickets(salesLedgerProduct.getQuantity());
                    salesLedgerProduct.setFutureTicketsAmount(salesLedgerProduct.getTaxExclusiveTotalPrice());
                    list.stream()
                            .filter(map -> map.get("productName").equals(salesLedgerProduct.getProductCategory()) && map.get("model").equals(salesLedgerProduct.getSpecificationModel()))
                            .findFirst()
@@ -667,7 +612,6 @@
                    salesLedgerProduct.setRegister(loginUser.getNickName());
                    salesLedgerProduct.setRegisterDate(LocalDateTime.now());
                    salesLedgerProduct.setApproveStatus(0);
                    salesLedgerProduct.setPendingTicketsTotal(salesLedgerProductImportDto.getTaxInclusiveTotalPrice());
                    // æ˜¯å¦è´¨æ£€åˆ¤æ–­
                    salesLedgerProduct.setIsChecked(salesLedgerProductImportDto.getIsChecked() == 1);
                    if(salesLedgerProductImportDto.getIsChecked() == 1){
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java
@@ -6,12 +6,7 @@
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.pojo.AccountIncome;
import com.ruoyi.account.service.AccountIncomeService;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
import com.ruoyi.purchase.dto.PurchaseReturnOrderHasAllInfoDto;
@@ -29,13 +24,11 @@
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.service.ISalesLedgerService;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import com.ruoyi.stock.pojo.StockOutRecord;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
 * <p>
@@ -52,7 +45,6 @@
    private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
    private final PurchaseReturnOrderProductsMapper purchaseReturnOrderProductsMapper;
    private final ISalesLedgerService salesLedgerService;
    private final AccountIncomeService accountIncomeService;
    private final StockUtils stockUtils;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
@@ -84,21 +76,6 @@
        }else {
            throw new RuntimeException("请选择退货商品");
        }
        // å¯¹è´¢åŠ¡ç®¡ç†å°è´¦æ·»åŠ ä¸€ç¬”è¿›è´¦æ”¶å…¥.
        AccountIncome accountIncome = new AccountIncome();
        LoginUser loginUser = SecurityUtils.getLoginUser();
        accountIncome.setInputUser(loginUser.getUsername());
        accountIncome.setIncomeMoney(purchaseReturnOrderDto.getTotalAmount());
        accountIncome.setIncomeDate(DateUtils.toDate(purchaseReturnOrderDto.getPreparedAt()));
        accountIncome.setInputTime(DateUtils.toDate(purchaseReturnOrderDto.getPreparedAt()));
        accountIncome.setBusinessId(purchaseReturnOrderDto.getId());
        accountIncome.setBusinessType(1);
        accountIncome.setIncomeType("4");
        accountIncome.setCustomerName(purchaseReturnOrderDto.getSupplierName());
        accountIncome.setIncomeDescribed(purchaseReturnOrderDto.getRemark());
        accountIncome.setIncomeMethod(String.valueOf(purchaseReturnOrderDto.getIncomeType()));
        accountIncomeService.save(accountIncome);
        return true;
    }
@@ -122,15 +99,9 @@
        updateWrapper.eq(PurchaseReturnOrderProducts::getPurchaseReturnOrderId, id);
        purchaseReturnOrderProductsMapper.delete(updateWrapper);
        //(采购退货的数据需要删掉)
        stockOutRecordMapper.delete(Wrappers.<StockOutRecord>lambdaQuery()
                .eq(StockOutRecord::getRecordType,StockOutQualifiedRecordTypeEnum.PURCHASE_RETURN_STOCK_OUT.getCode())
                .in(StockOutRecord::getRecordId, purchaseReturnOrderProducts.stream().map(PurchaseReturnOrderProducts::getId).collect(Collectors.toList())));
        // è´¢åŠ¡
        LambdaUpdateWrapper<AccountIncome> updateWrapperAccountIncome = new LambdaUpdateWrapper<>();
        updateWrapperAccountIncome.eq(AccountIncome::getBusinessId, id);
        updateWrapperAccountIncome.eq(AccountIncome::getBusinessType, 1);
        updateWrapperAccountIncome.eq(AccountIncome::getIncomeType, 4);
        accountIncomeService.remove(updateWrapperAccountIncome);
        purchaseReturnOrderProducts.stream().forEach(purchaseReturnOrderProducts1 -> {
            stockUtils.deleteStockOutRecord(purchaseReturnOrderProducts1.getId(),StockOutQualifiedRecordTypeEnum.PURCHASE_RETURN_STOCK_OUT.getCode());
        });
    }
    @Override
src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.purchase.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Schema(name = "SupplierTransactionsDetailsVo", description = "采购管理--供应商往来明细(返回)")
public class SupplierTransactionsDetailsVo {
    @Schema(description = "采购单ID")
    private Long purchaseLedgerId;
    @Schema(description = "采购合同号")
    private String purchaseContractNumber;
    @Schema(description = "采购合同签订日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date executionDate;
    @Schema(description = "合同金额")
    private BigDecimal contractAmount;
    @Schema(description = "付款金额")
    private BigDecimal paymentAmount;
    @Schema(description = "应付金额")
    private BigDecimal payableAmount;
}
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package com.ruoyi.purchase.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Data
@Schema(name = "SupplierTransactionsVo", description = "采购管理--供应商往来(返回)")
public class SupplierTransactionsVo {
    @Schema(description = "供应商ID")
    private Long supplierId;
    @Schema(description = "供应商名称")
    private String supplierName;
    @Schema(description = "合同总金额")
    //该供应商采购合同累计金额
    private BigDecimal contractAmounts;
    @Schema(description = "付款金额")
    //该供应商采购付款累计金额
    private BigDecimal paymentAmount;
    @Schema(description = "应付金额")
    //该供应商采购应付累计金额=财务(入库-退货)
    private BigDecimal payableAmount;
}
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java
@@ -2,7 +2,9 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.dto.QualityInspectDto;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectFile;
@@ -10,7 +12,9 @@
import com.ruoyi.quality.service.IQualityInspectFileService;
import com.ruoyi.quality.service.IQualityInspectParamService;
import com.ruoyi.quality.service.IQualityInspectService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
@@ -37,8 +41,10 @@
     * @return
     */
    @PostMapping("/add")
    public AjaxResult add(@RequestBody QualityInspectDto qualityInspectDto) {
        return AjaxResult.success(qualityInspectService.add(qualityInspectDto));
    @Operation(summary = "新增检验")
    @Log(title = "新增检验", businessType = BusinessType.INSERT)
    public R<?> add(@RequestBody QualityInspectDto qualityInspectDto) {
        return R.ok(qualityInspectService.add(qualityInspectDto));
    }
    /**
@@ -48,9 +54,11 @@
     * @return
     */
    @DeleteMapping("/del")
    public AjaxResult delQualityInspect(@RequestBody List<Integer> ids) {
    @Operation(summary = "删除检验")
    @Log(title = "删除检验", businessType = BusinessType.DELETE)
    public R<?> delQualityInspect(@RequestBody List<Integer> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return AjaxResult.error("请选择至少一条数据");
            return R.fail("请选择至少一条数据");
        }
        //如果已经提交就不允许删除
        List<QualityInspect> qualityInspects = qualityInspectService.listByIds(ids);
@@ -66,7 +74,7 @@
        qualityInspectFileService.remove(Wrappers.<QualityInspectFile>lambdaQuery()
                .in(QualityInspectFile::getInspectId, ids));
        //删除检验单
        return AjaxResult.success(qualityInspectService.removeBatchByIds(ids));
        return R.ok(qualityInspectService.removeBatchByIds(ids));
    }
    /**
@@ -76,8 +84,10 @@
     * @return
     */
    @GetMapping("/{id}")
    public AjaxResult QualityInspectDetail(@PathVariable("id") Integer id) {
        return AjaxResult.success(qualityInspectService.getDetailById(id));
    @Operation(summary = "检验详情")
    @Log(title = "检验详情", businessType = BusinessType.OTHER)
    public R<?> QualityInspectDetail(@PathVariable("id") Integer id) {
        return R.ok(qualityInspectService.getDetailById(id));
    }
    /**
@@ -87,8 +97,10 @@
     * @return
     */
    @PostMapping("/update")
    public AjaxResult update(@RequestBody QualityInspectDto qualityInspectDto) {
        return AjaxResult.success(qualityInspectService.updateQualityInspect(qualityInspectDto));
    @Operation(summary = "修改检验")
    @Log(title = "修改检验", businessType = BusinessType.UPDATE)
    public R<?> update(@RequestBody QualityInspectDto qualityInspectDto) {
        return R.ok(qualityInspectService.updateQualityInspect(qualityInspectDto));
    }
    /**
@@ -99,8 +111,10 @@
     * @return
     */
    @GetMapping("/listPage")
    public AjaxResult qualityInspectListPage(Page page, QualityInspectDto qualityInspect) {
        return AjaxResult.success(qualityInspectService.qualityInspectListPage(page, qualityInspect));
    @Operation(summary = "分页查询检验")
    @Log(title = "分页查询检验", businessType = BusinessType.OTHER)
    public R<?> qualityInspectListPage(Page page, QualityInspectDto qualityInspect) {
        return R.ok(qualityInspectService.qualityInspectListPage(page, qualityInspect));
    }
    /**
@@ -110,6 +124,8 @@
     * @param qualityInspect
     */
    @PostMapping("/export")
    @Operation(summary = "导出检验")
    @Log(title = "导出检验", businessType = BusinessType.EXPORT)
    public void qualityInspectExport(HttpServletResponse response, QualityInspect qualityInspect) {
        qualityInspectService.qualityInspectExport(response, qualityInspect);
    }
@@ -121,8 +137,10 @@
     * @return
     */
    @PostMapping("/submit")
    public AjaxResult submit(@RequestBody QualityInspect qualityInspect) {
        return AjaxResult.success(qualityInspectService.submit(qualityInspect));
    @Operation(summary = "提交检验")
    @Log(title = "提交检验", businessType = BusinessType.OTHER)
    public R<?> submit(@Valid @RequestBody QualityInspect qualityInspect) {
        return R.ok(qualityInspectService.submit(qualityInspect));
    }
    /**
@@ -132,6 +150,8 @@
     * @param qualityInspect
     */
    @PostMapping("/down")
    @Operation(summary = "下载检验")
    @Log(title = "下载检验", businessType = BusinessType.OTHER)
    public void down(HttpServletResponse response, @RequestBody QualityInspect qualityInspect) {
        qualityInspectService.down(response, qualityInspect);
    }
src/main/java/com/ruoyi/quality/controller/QualityInspectParamController.java
@@ -2,13 +2,16 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectFile;
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.service.IQualityInspectFileService;
import com.ruoyi.quality.service.IQualityInspectParamService;
import com.ruoyi.quality.service.IQualityInspectService;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
@@ -32,8 +35,10 @@
     * @return
     */
    @GetMapping("/{inspectId}")
    public AjaxResult QualityInspectParamDetail(@PathVariable("inspectId") Integer inspectId) {
        return AjaxResult.success(qualityInspectParamService.qualityInspectParamDetail(inspectId));
    @Operation(summary = "检验参数项详情")
    @Log(title = "检验参数项详情", businessType = BusinessType.OTHER)
    public R<?> QualityInspectParamDetail(@PathVariable("inspectId") Integer inspectId) {
        return R.ok(qualityInspectParamService.qualityInspectParamDetail(inspectId));
    }
@@ -43,8 +48,10 @@
     * @return
     */
    @PostMapping("/update")
    public AjaxResult update(@RequestBody List<QualityInspectParam> qualityInspectParams) {
        return AjaxResult.success(qualityInspectParamService.updateBatchById(qualityInspectParams));
    @Operation(summary = "修改检验参数项")
    @Log(title = "修改检验参数项", businessType = BusinessType.UPDATE)
    public R<?> update(@RequestBody List<QualityInspectParam> qualityInspectParams) {
        return R.ok(qualityInspectParamService.updateBatchById(qualityInspectParams));
    }
    /**
@@ -53,11 +60,13 @@
     * @return
     */
    @DeleteMapping("/del")
    public AjaxResult delQualityUnqualified(@RequestBody List<Integer> ids) {
    @Operation(summary = "删除检验参数项")
    @Log(title = "删除检验参数项", businessType = BusinessType.DELETE)
    public R<?> delQualityUnqualified(@RequestBody List<Integer> ids) {
        if(CollectionUtils.isEmpty(ids)){
            return AjaxResult.error("请选择至少一条数据");
            return R.fail("请选择至少一条数据");
        }
        return AjaxResult.success(qualityInspectParamService.removeBatchByIds(ids));
        return R.ok(qualityInspectParamService.removeBatchByIds(ids));
    }
src/main/java/com/ruoyi/quality/controller/QualityReportController.java
@@ -1,6 +1,8 @@
package com.ruoyi.quality.controller;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.service.QualityReportService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -30,8 +32,9 @@
     */
    @Operation(summary = "获取检验统计数据")
    @GetMapping("/getInspectStatistics")
    public AjaxResult getInspectStatistics() {
        return AjaxResult.success(qualityReportService.getInspectStatistics());
    @Log(title = "获取检验统计数据", businessType = BusinessType.OTHER)
    public R<?> getInspectStatistics() {
        return R.ok(qualityReportService.getInspectStatistics());
    }
    /**
@@ -39,8 +42,9 @@
     */
    @Operation(summary = "获取合格率统计数据")
    @GetMapping("/getPassRateStatistics")
    public AjaxResult getPassRateStatistics() {
        return AjaxResult.success(qualityReportService.getPassRateStatistics());
    @Log(title = "获取合格率统计数据", businessType = BusinessType.OTHER)
    public R<?> getPassRateStatistics() {
        return R.ok(qualityReportService.getPassRateStatistics());
    }
    /**
@@ -48,8 +52,9 @@
     */
    @Operation(summary = "获取月度合格率统计数据")
    @GetMapping("/getMonthlyPassRateStatistics")
    public AjaxResult getMonthlyPassRateStatistics(@RequestParam("year") String year) {
        return AjaxResult.success(qualityReportService.getMonthlyPassRateStatistics(year));
    @Log(title = "获取月度合格率统计数据", businessType = BusinessType.OTHER)
    public R<?> getMonthlyPassRateStatistics(@RequestParam("year") String year) {
        return R.ok(qualityReportService.getMonthlyPassRateStatistics(year));
    }
    /**
@@ -57,8 +62,9 @@
     */
    @Operation(summary = "获取年度总合格率统计数据")
    @GetMapping("/getYearlyPassRateStatistics")
    public AjaxResult getYearlyPassRateStatistics(@RequestParam("year") String year) {
        return AjaxResult.success(qualityReportService.getYearlyPassRateStatistics(year));
    @Log(title = "获取年度总合格率统计数据", businessType = BusinessType.OTHER)
    public R<?> getYearlyPassRateStatistics(@RequestParam("year") String year) {
        return R.ok(qualityReportService.getYearlyPassRateStatistics(year));
    }
    /**
@@ -66,8 +72,9 @@
     */
    @Operation(summary = "获取月度完成明细数据")
    @GetMapping("/getMonthlyCompletionDetails")
    public AjaxResult getMonthlyCompletionDetails(@RequestParam("year") String year) {
        return AjaxResult.success(qualityReportService.getMonthlyCompletionDetails(year));
    @Log(title = "获取月度完成明细数据", businessType = BusinessType.OTHER)
    public R<?> getMonthlyCompletionDetails(@RequestParam("year") String year) {
        return R.ok(qualityReportService.getMonthlyCompletionDetails(year));
    }
    /**
@@ -75,8 +82,9 @@
     */
    @Operation(summary = "获取热点检测指标统计")
    @GetMapping("/getTopParameters")
    public AjaxResult getTopParameters(@RequestParam("modelType") Integer modelType) {
        return AjaxResult.success(qualityReportService.getTopParameters(modelType));
    @Log(title = "获取热点检测指标统计", businessType = BusinessType.OTHER)
    public R<?> getTopParameters(@RequestParam("modelType") Integer modelType) {
        return R.ok(qualityReportService.getTopParameters(modelType));
    }
}
src/main/java/com/ruoyi/quality/controller/QualityTestStandardBindingController.java
@@ -1,8 +1,12 @@
package com.ruoyi.quality.controller;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.pojo.QualityTestStandardBinding;
import com.ruoyi.quality.service.QualityTestStandardBindingService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
@@ -20,6 +24,7 @@
@RestController
@RequestMapping("/qualityTestStandardBinding")
@AllArgsConstructor
@Tag(name = "检测标准主表与产品关联表")
public class QualityTestStandardBindingController {
    private QualityTestStandardBindingService qualityTestStandardBindingService;
@@ -31,8 +36,10 @@
     * @return
     */
    @PostMapping("/add")
    public AjaxResult add(@RequestBody List<QualityTestStandardBinding> qualityTestStandardBindings) {
        return AjaxResult.success(qualityTestStandardBindingService.add(qualityTestStandardBindings));
    @Operation(summary = "新增检测标准主表与产品关联表")
    @Log(title = "新增检测标准主表与产品关联表", businessType = BusinessType.INSERT)
    public R<?> add(@RequestBody List<QualityTestStandardBinding> qualityTestStandardBindings) {
        return R.ok(qualityTestStandardBindingService.add(qualityTestStandardBindings));
    }
    /**
@@ -42,11 +49,13 @@
     * @return
     */
    @DeleteMapping("/del")
    public AjaxResult delQualityTestStandard(@RequestBody List<Integer> ids) {
    @Operation(summary = "删除检测标准主表与产品关联表")
    @Log(title = "删除检测标准主表与产品关联表", businessType = BusinessType.DELETE)
    public R<?> delQualityTestStandard(@RequestBody List<Integer> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return AjaxResult.error("请选择至少一条数据");
            return R.fail("请选择至少一条数据");
        }
        return AjaxResult.success(qualityTestStandardBindingService.removeBatchByIds(ids));
        return R.ok(qualityTestStandardBindingService.removeBatchByIds(ids));
    }
    /**
@@ -55,8 +64,10 @@
     * @return
     */
    @GetMapping("/list")
    public AjaxResult listBinding(Long testStandardId) {
        return AjaxResult.success(qualityTestStandardBindingService.listBinding(testStandardId));
    @Operation(summary = "检测指标维护查询")
    @Log(title = "检测指标维护查询", businessType = BusinessType.OTHER)
    public R<?> listBinding(Long testStandardId) {
        return R.ok(qualityTestStandardBindingService.listBinding(testStandardId));
    }
}
src/main/java/com/ruoyi/quality/controller/QualityTestStandardController.java
@@ -2,11 +2,15 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
import com.ruoyi.quality.service.IQualityTestStandardService;
import com.ruoyi.quality.service.QualityTestStandardParamService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
@@ -24,6 +28,7 @@
 */
@RestController
@RequestMapping("/qualityTestStandard")
@Tag(name = "检测标准主表")
public class QualityTestStandardController {
    @Resource
@@ -39,8 +44,10 @@
     * @return
     */
    @PostMapping("/add")
    public AjaxResult add(@RequestBody QualityTestStandard qualityTestStandard) {
        return AjaxResult.success(qualityTestStandardService.save(qualityTestStandard));
    @Operation(summary = "新增检测标准主表")
    @Log(title = "新增检测标准主表", businessType = BusinessType.INSERT)
    public R<?> add(@RequestBody QualityTestStandard qualityTestStandard) {
        return R.ok(qualityTestStandardService.save(qualityTestStandard));
    }
    /**
@@ -49,11 +56,13 @@
     * @return
     */
    @DeleteMapping("/del")
    public AjaxResult delQualityTestStandard(@RequestBody List<Integer> ids) {
    @Operation(summary = "删除检测标准主表")
    @Log(title = "删除检测标准主表", businessType = BusinessType.DELETE)
    public R<?> delQualityTestStandard(@RequestBody List<Integer> ids) {
        if(CollectionUtils.isEmpty(ids)){
            return AjaxResult.error("请选择至少一条数据");
            return R.fail("请选择至少一条数据");
        }
        return AjaxResult.success(qualityTestStandardService.delQualityTestStandard(ids));
        return R.ok(qualityTestStandardService.delQualityTestStandard(ids));
    }
    /**
@@ -62,8 +71,10 @@
     * @return
     */
    @PostMapping("/update")
    public AjaxResult update(@RequestBody QualityTestStandard qualityTestStandard) {
        return AjaxResult.success(qualityTestStandardService.updateById(qualityTestStandard));
    @Operation(summary = "检测标准主表修改")
    @Log(title = "检测标准主表修改", businessType = BusinessType.UPDATE)
    public R<?> update(@RequestBody QualityTestStandard qualityTestStandard) {
        return R.ok(qualityTestStandardService.updateById(qualityTestStandard));
    }
    /**
@@ -73,8 +84,10 @@
     * @return
     */
    @GetMapping("/listPage")
    public AjaxResult qualityTestStandardListPage(Page page, QualityTestStandard qualityTestStandard) {
        return AjaxResult.success(qualityTestStandardService.qualityTestStandardListPage(page, qualityTestStandard));
    @Operation(summary = "检测标准主表分页查询")
    @Log(title = "检测标准主表分页查询", businessType = BusinessType.OTHER)
    public R<?> qualityTestStandardListPage(Page page, QualityTestStandard qualityTestStandard) {
        return R.ok(qualityTestStandardService.qualityTestStandardListPage(page, qualityTestStandard));
    }
    /**
@@ -83,8 +96,10 @@
     * @return
     */
    @PostMapping("/copyParam")
    public AjaxResult copyParam(@RequestBody QualityTestStandard qualityTestStandard) {
        return AjaxResult.success(qualityTestStandardService.copyParam(qualityTestStandard));
    @Operation(summary = "检测标准复制参数")
    @Log(title = "检测标准复制参数", businessType = BusinessType.OTHER)
    public R<?> copyParam(@RequestBody QualityTestStandard qualityTestStandard) {
        return R.ok(qualityTestStandardService.copyParam(qualityTestStandard));
    }
    /**
@@ -93,8 +108,10 @@
     * @return
     */
    @PostMapping("/qualityTestStandardAudit")
    public AjaxResult qualityTestStandardAudit(@RequestBody List<QualityTestStandard> qualityTestStandards) {
        return AjaxResult.success(qualityTestStandardService.updateBatchById(qualityTestStandards));
    @Operation(summary = "检测标准批量审核")
    @Log(title = "检测标准批量审核", businessType = BusinessType.OTHER)
    public R<?> qualityTestStandardAudit(@RequestBody List<QualityTestStandard> qualityTestStandards) {
        return R.ok(qualityTestStandardService.updateBatchById(qualityTestStandards));
    }
    /**
@@ -102,8 +119,10 @@
     * @return
     */
    @GetMapping("/getQualityTestStandardByProductId")
    public AjaxResult getQualityTestStandardByProductId(@Nonnull Long productId, @Nonnull Integer inspectType, String process) {
        return AjaxResult.success(qualityTestStandardService.getQualityTestStandardByProductId(productId,inspectType,process));
    @Operation(summary = "根据产品id查询相关的检验标准")
    @Log(title = "根据产品id查询相关的检验标准", businessType = BusinessType.OTHER)
    public R<?> getQualityTestStandardByProductId(@Nonnull Long productId, @Nonnull Integer inspectType, String process) {
        return R.ok(qualityTestStandardService.getQualityTestStandardByProductId(productId,inspectType,process));
    }
    /**
@@ -111,8 +130,10 @@
     * @return
     */
    @GetMapping("/getQualityTestStandardParamByTestStandardId")
    public AjaxResult getQualityTestStandardParamByTestStandardId(Long testStandardId) {
        return AjaxResult.success(qualityTestStandardParamService.list(Wrappers.<QualityTestStandardParam>lambdaQuery().eq(QualityTestStandardParam::getTestStandardId, testStandardId)));
    @Operation(summary = "根据检测标准id查询相关的检验标准参数")
    @Log(title = "根据检测标准id查询相关的检验标准参数", businessType = BusinessType.OTHER)
    public R<?> getQualityTestStandardParamByTestStandardId(Long testStandardId) {
        return R.ok(qualityTestStandardParamService.list(Wrappers.<QualityTestStandardParam>lambdaQuery().eq(QualityTestStandardParam::getTestStandardId, testStandardId)));
    }
}
src/main/java/com/ruoyi/quality/controller/QualityTestStandardParamController.java
@@ -2,10 +2,14 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
import com.ruoyi.quality.service.QualityTestStandardParamService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
@@ -24,6 +28,7 @@
@RestController
@RequestMapping("/qualityTestStandardParam")
@AllArgsConstructor
@Tag(name = "检测标准参数")
public class QualityTestStandardParamController {
    private QualityTestStandardParamService qualityTestStandardParamService;
@@ -34,8 +39,10 @@
     * @return
     */
    @PostMapping("/add")
    public AjaxResult add(@RequestBody QualityTestStandardParam qualityTestStandardParam) {
        return AjaxResult.success(qualityTestStandardParamService.save(qualityTestStandardParam));
    @Operation(summary = "新增检测标准参数")
    @Log(title = "新增检测标准参数", businessType = BusinessType.INSERT)
    public R<?> add(@RequestBody QualityTestStandardParam qualityTestStandardParam) {
        return R.ok(qualityTestStandardParamService.save(qualityTestStandardParam));
    }
    /**
@@ -44,11 +51,13 @@
     * @return
     */
    @DeleteMapping("/del")
    public AjaxResult delQualityTestStandard(@RequestBody List<Integer> ids) {
    @Operation(summary = "删除检测指标维护")
    @Log(title = "删除检测指标维护", businessType = BusinessType.DELETE)
    public R<?> delQualityTestStandard(@RequestBody List<Integer> ids) {
        if(CollectionUtils.isEmpty(ids)){
            return AjaxResult.error("请选择至少一条数据");
            return R.fail("请选择至少一条数据");
        }
        return AjaxResult.success(qualityTestStandardParamService.removeBatchByIds(ids));
        return R.ok(qualityTestStandardParamService.removeBatchByIds(ids));
    }
    /**
@@ -57,8 +66,10 @@
     * @return
     */
    @PostMapping("/update")
    public AjaxResult update(@RequestBody QualityTestStandardParam qualityTestStandardParam) {
        return AjaxResult.success(qualityTestStandardParamService.updateById(qualityTestStandardParam));
    @Operation(summary = "检测指标维护修改")
    @Log(title = "检测指标维护修改", businessType = BusinessType.UPDATE)
    public R<?> update(@RequestBody QualityTestStandardParam qualityTestStandardParam) {
        return R.ok(qualityTestStandardParamService.updateById(qualityTestStandardParam));
    }
    /**
@@ -66,8 +77,10 @@
     * @return
     */
    @GetMapping("/list")
    public AjaxResult list(Long testStandardId) {
        return AjaxResult.success(qualityTestStandardParamService.list(Wrappers.<QualityTestStandardParam>lambdaQuery().eq(QualityTestStandardParam::getTestStandardId,testStandardId)));
    @Operation(summary = "检测指标维护查询")
    @Log(title = "检测指标维护查询", businessType = BusinessType.OTHER)
    public R<?> list(Long testStandardId) {
        return R.ok(qualityTestStandardParamService.list(Wrappers.<QualityTestStandardParam>lambdaQuery().eq(QualityTestStandardParam::getTestStandardId,testStandardId)));
    }
}
src/main/java/com/ruoyi/quality/controller/QualityUnqualifiedController.java
@@ -1,9 +1,13 @@
package com.ruoyi.quality.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.pojo.QualityUnqualified;
import com.ruoyi.quality.service.IQualityUnqualifiedService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
@@ -16,6 +20,7 @@
 */
@RestController
@RequestMapping("/quality/qualityUnqualified")
@Tag(name = "不合格管理")
public class QualityUnqualifiedController {
    @Resource
@@ -28,9 +33,11 @@
     * @return
     */
    @PostMapping("/add")
    public AjaxResult add(@RequestBody QualityUnqualified qualityUnqualified) {
    @Operation(summary = "新增不合格管理")
    @Log(title = "新增不合格管理", businessType = BusinessType.INSERT)
    public R<?> add(@RequestBody QualityUnqualified qualityUnqualified) {
        qualityUnqualified.setInspectState(0);
        return AjaxResult.success(qualityUnqualifiedService.save(qualityUnqualified));
        return R.ok(qualityUnqualifiedService.save(qualityUnqualified));
    }
    /**
@@ -39,13 +46,15 @@
     * @return
     */
    @DeleteMapping("/del")
    public AjaxResult delQualityUnqualified(@RequestBody List<Integer> ids) {
    @Operation(summary = "删除不合格管理")
    @Log(title = "删除不合格管理", businessType = BusinessType.DELETE)
    public R<?> delQualityUnqualified(@RequestBody List<Integer> ids) {
       qualityUnqualifiedService.listByIds(ids).stream().forEach(qualityUnqualified -> {
           if (qualityUnqualified.getInspectState()==1){
               throw new RuntimeException("该不合格数据已经处理无法删除!");
           }
       });
        return AjaxResult.success(qualityUnqualifiedService.removeBatchByIds(ids));
        return R.ok(qualityUnqualifiedService.removeBatchByIds(ids));
    }
    /**
@@ -54,8 +63,10 @@
     * @return
     */
    @GetMapping("/{id}")
    public AjaxResult QualityUnqualifiedDetail(@PathVariable("id") Integer id) {
        return AjaxResult.success(qualityUnqualifiedService.getUnqualified(id));
    @Operation(summary = "不合格管理详情")
    @Log(title = "不合格管理详情", businessType = BusinessType.OTHER)
    public R<?> QualityUnqualifiedDetail(@PathVariable("id") Integer id) {
        return R.ok(qualityUnqualifiedService.getUnqualified(id));
    }
    /**
@@ -64,8 +75,10 @@
     * @return
     */
    @PostMapping("/update")
    public AjaxResult update(@RequestBody QualityUnqualified qualityUnqualified) {
        return AjaxResult.success(qualityUnqualifiedService.updateById(qualityUnqualified));
    @Operation(summary = "不合格管理修改")
    @Log(title = "不合格管理修改", businessType = BusinessType.UPDATE)
    public R<?> update(@RequestBody QualityUnqualified qualityUnqualified) {
        return R.ok(qualityUnqualifiedService.updateById(qualityUnqualified));
    }
    /**
@@ -75,8 +88,10 @@
     * @return
     */
    @GetMapping("/listPage")
    public AjaxResult qualityUnqualifiedListPage(Page page, QualityUnqualified qualityUnqualified) {
        return AjaxResult.success(qualityUnqualifiedService.qualityUnqualifiedListPage(page, qualityUnqualified));
    @Operation(summary = "不合格管理分页查询")
    @Log(title = "不合格管理分页查询", businessType = BusinessType.OTHER)
    public R<?> qualityUnqualifiedListPage(Page page, QualityUnqualified qualityUnqualified) {
        return R.ok(qualityUnqualifiedService.qualityUnqualifiedListPage(page, qualityUnqualified));
    }
    /**
@@ -85,6 +100,8 @@
     * @param qualityUnqualified
     */
    @PostMapping("/export")
    @Operation(summary = "不合格管理导出")
    @Log(title = "不合格管理导出", businessType = BusinessType.EXPORT)
    public void qualityUnqualifiedExport(HttpServletResponse response,QualityUnqualified qualityUnqualified) {
        qualityUnqualifiedService.qualityUnqualifiedExport(response, qualityUnqualified);
    }
@@ -95,8 +112,10 @@
     * @return
     */
    @PostMapping("/deal")
    public AjaxResult deal(@RequestBody QualityUnqualified qualityUnqualified) {
        return AjaxResult.success(qualityUnqualifiedService.deal(qualityUnqualified));
    @Operation(summary = "不合格管理处理")
    @Log(title = "不合格管理处理", businessType = BusinessType.OTHER)
    public R<?> deal(@RequestBody QualityUnqualified qualityUnqualified) {
        return R.ok(qualityUnqualifiedService.deal(qualityUnqualified));
    }
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -5,9 +5,10 @@
import com.ruoyi.dto.DateQueryDto;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -20,6 +21,7 @@
@TableName(value = "quality_inspect")
@Data
public class QualityInspect extends DateQueryDto implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    /**
@@ -32,7 +34,7 @@
     * ç±»åˆ«(0:原材料检验;1:过程检验;2:出厂检验)
     */
    @Excel(name = "类别",readConverterExp = "0=原材料检验,1=过程检验,2=出厂检验")
    @NotBlank(message = "类别不能为空!!")
    @NotNull(message = "类别不能为空")
    private Integer inspectType;
    /**
@@ -72,7 +74,7 @@
    /**
     * å…³è”产品id
     */
    @NotBlank(message = "产品id不能为空")
    @NotNull(message = "产品id不能为空")
    private Long productId;
    /**
@@ -101,10 +103,12 @@
    @Excel(name = "合格数量")
    @TableField("qualified_quantity")
    @NotNull(message = "合格数量不能为空")
    private BigDecimal qualifiedQuantity;
    @Excel(name = "不合格数量")
    @TableField("unqualified_quantity")
    @NotNull(message = "不合格数量不能为空")
    private BigDecimal unqualifiedQuantity;
    /**
src/main/java/com/ruoyi/quality/pojo/QualityUnqualified.java
@@ -143,4 +143,7 @@
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @Schema(description = "关联产品型号id")
    private Long productModelId;
}
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -95,6 +95,14 @@
            throw new RuntimeException("请先判断是否合格");
        }
        if (ObjectUtils.isNull(qualityInspect.getQualifiedQuantity())) {
            throw new RuntimeException("合格数量不能为空");
        }
        if (ObjectUtils.isNull(qualityInspect.getUnqualifiedQuantity())) {
            throw new RuntimeException("不合格数量不能为空");
        }
        // åŒºåˆ†åˆæ ¼æ•°é‡ä»¥åŠä¸åˆæ ¼å¤„理进行对应的处理
        Assert.isTrue(qualityInspect.getQuantity().compareTo(qualityInspect.getQualifiedQuantity().add(qualityInspect.getUnqualifiedQuantity())) == 0,"请检查合格数量和不合格数量,需要合格数量+不合格数量与总数保持一致");
        if(qualityInspect.getQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0){
@@ -121,6 +129,7 @@
            BeanUtils.copyProperties(qualityInspect, qualityUnqualified);
            qualityUnqualified.setInspectState(0);//待处理
            qualityUnqualified.setQuantity(qualityInspect.getUnqualifiedQuantity());
            qualityUnqualified.setProductModelId(qualityInspect.getProductModelId());
            List<QualityInspectParam> inspectParams = qualityInspectParamService.list(Wrappers.<QualityInspectParam>lambdaQuery().eq(QualityInspectParam::getInspectId, inspect.getId()));
            String text = inspectParams.stream().map(QualityInspectParam::getParameterItem).collect(Collectors.joining(","));
            qualityUnqualified.setDefectivePhenomena(text + "这些指标中存在不合格");//不合格现象
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
@@ -1,11 +1,16 @@
package com.ruoyi.sales.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.service.ICustomerService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.sales.dto.StatisticsTableDto;
import com.ruoyi.sales.service.impl.MetricStatisticsServiceImpl;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -21,7 +26,8 @@
@AllArgsConstructor
public class MetricStatisticsController extends BaseController {
    private MetricStatisticsServiceImpl metricStatisticsService;
    private final MetricStatisticsServiceImpl metricStatisticsService;
    private final ICustomerService customerService;
    @Operation(summary = "头部总计")
    @GetMapping("/total")
@@ -35,4 +41,18 @@
        return metricStatisticsService.statisticsTable(statisticsTableDto);
    }
    @GetMapping("/customewTransactions")
    @Log(title = "客户往来", businessType = BusinessType.OTHER)
    @Operation(summary = "客户往来")
    public R customewTransactions(Page page, String customerName) {
        return R.ok(customerService.customewTransactions(page,customerName));
    }
    @GetMapping("/customewTransactionsDetails")
    @Log(title = "客户往来明细", businessType = BusinessType.OTHER)
    @Operation(summary = "客户往来明细")
    public R customewTransactionsDetails(Page page, Long customerId) {
        return R.ok(customerService.customewTransactionsDetails(page,customerId));
    }
}
src/main/java/com/ruoyi/sales/controller/ReceiptPaymentController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java
@@ -1,11 +1,11 @@
package com.ruoyi.sales.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
@@ -13,25 +13,20 @@
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.framework.web.page.TableDataInfo;
import com.ruoyi.sales.dto.InvoiceLedgerDto;
import com.ruoyi.sales.dto.SalesLedgerDto;
import com.ruoyi.sales.mapper.InvoiceLedgerMapper;
import com.ruoyi.sales.mapper.ReceiptPaymentMapper;
import com.ruoyi.sales.pojo.ReceiptPayment;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.service.ICommonFileService;
import com.ruoyi.sales.service.ISalesLedgerService;
import com.ruoyi.sales.vo.SalesLedgerVo;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.annotations.ApiParam;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.basic.utils.FileUtil;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -57,8 +52,6 @@
    private ISalesLedgerService salesLedgerService;
    private ICommonFileService commonFileService;
    private InvoiceLedgerMapper invoiceLedgerMapper;
    private ReceiptPaymentMapper receiptPaymentMapper;
    private final FileUtil fileUtil;
    /**
@@ -122,20 +115,6 @@
        if(CollectionUtils.isEmpty(list)){
            return getDataTable(list);
        }
        List<Long> salesLedgerIds = list.stream().map(SalesLedger::getId).collect(Collectors.toList());
        List<InvoiceLedgerDto> invoiceLedgerDtoList = invoiceLedgerMapper.invoicedTotal(salesLedgerIds);
        if(CollectionUtils.isEmpty(invoiceLedgerDtoList)){
            return getDataTable(list);
        }
        for (SalesLedger salesLedger : list) {
            for (InvoiceLedgerDto invoiceLedgerDto : invoiceLedgerDtoList) {
                if (salesLedger.getId().intValue() == invoiceLedgerDto.getSalesLedgerId()) {
                    BigDecimal noInvoiceAmountTotal = salesLedger.getContractAmount().subtract(invoiceLedgerDto.getInvoiceTotal());
                    salesLedger.setNoInvoiceAmountTotal(noInvoiceAmountTotal);
                }
            }
        }
        return getDataTable(list);
    }
@@ -267,77 +246,29 @@
        //  èŽ·å–å½“å‰é¡µæ‰€æœ‰å°è´¦è®°å½•çš„ ID é›†åˆ
        List<Long> salesLedgerIds = iPage.getRecords().stream().map(SalesLedger::getId).collect(Collectors.toList());
        //  æŸ¥è¯¢å‘票信息的已开票金额
        List<InvoiceLedgerDto> invoiceLedgerDtoList = invoiceLedgerMapper.invoicedTotal(salesLedgerIds);
        if (CollectionUtils.isEmpty(invoiceLedgerDtoList)) {
            invoiceLedgerDtoList = Collections.emptyList();
        }
        //  è½¬æ¢å‘票数据, key ä¸ºå°è´¦ID, value ä¸ºè¯¥å°è´¦çš„æ€»å¼€ç¥¨é‡‘额
        Map<Long, BigDecimal> invoiceTotals = invoiceLedgerDtoList.stream()
                .filter(dto -> dto.getSalesLedgerId() != null && dto.getInvoiceTotal() != null)
                .collect(Collectors.toMap(
                        dto -> dto.getSalesLedgerId().longValue(),
                        InvoiceLedgerDto::getInvoiceTotal,
                        BigDecimal::add // å­˜åœ¨é‡å¤ID执行累加
                ));
        //  æŸ¥è¯¢å›žæ¬¾/付款记录
        List<ReceiptPayment> receiptPayments = Collections.emptyList();
        if (!CollectionUtils.isEmpty(salesLedgerIds)) {
            receiptPayments = receiptPaymentMapper.selectList(new LambdaQueryWrapper<ReceiptPayment>()
                    .in(ReceiptPayment::getSalesLedgerId, salesLedgerIds));
        }
        //  è½¬æ¢å›žæ¬¾æ•°æ®, key ä¸ºå°è´¦ID, value ä¸ºè¯¥å°è´¦çš„æ€»å›žæ¬¾é‡‘额
        Map<Long, BigDecimal> receiptTotals = new HashMap<>();
        if (!CollectionUtils.isEmpty(receiptPayments)) {
            for (ReceiptPayment receiptPayment : receiptPayments) {
                if (receiptPayment.getSalesLedgerId() != null && receiptPayment.getReceiptPaymentAmount() != null) {
                    //  å¦‚æžœ key å­˜åœ¨åˆ™ç›¸åŠ ,不存在则放入
                    receiptTotals.merge(receiptPayment.getSalesLedgerId(), receiptPayment.getReceiptPaymentAmount(), BigDecimal::add);
                }
            }
        }
        for (SalesLedgerVo salesLedgerVo : iPage.getRecords()) {
            Long ledgerId = salesLedgerVo.getId();
            // åˆåŒæ€»é‡‘额
            BigDecimal contractAmount = salesLedgerVo.getContractAmount() == null ? BigDecimal.ZERO : salesLedgerVo.getContractAmount();
            // å¼€ç¥¨æ€»é¢å’Œå›žæ¬¾æ€»é¢
            BigDecimal invoiceTotal = invoiceTotals.getOrDefault(ledgerId, BigDecimal.ZERO);
            BigDecimal receiptPaymentAmountTotal = receiptTotals.getOrDefault(ledgerId, BigDecimal.ZERO);
            //  æœªå¼€ç¥¨é‡‘额 = åˆåŒé‡‘额 - å·²å¼€ç¥¨é‡‘额
            BigDecimal noInvoiceAmountTotal = contractAmount.subtract(invoiceTotal);
            if (noInvoiceAmountTotal.compareTo(BigDecimal.ZERO) < 0) {
                noInvoiceAmountTotal = BigDecimal.ZERO;
            }
            //  å¾…回款金额 = å·²å¼€ç¥¨é‡‘额 - å·²å›žæ¬¾é‡‘额
            BigDecimal noReceiptPaymentAmountTotal = invoiceTotal.subtract(receiptPaymentAmountTotal);
            if (noReceiptPaymentAmountTotal.compareTo(BigDecimal.ZERO) < 0) {
                noReceiptPaymentAmountTotal = BigDecimal.ZERO;
            }
            salesLedgerVo.setNoInvoiceAmountTotal(noInvoiceAmountTotal);
            salesLedgerVo.setInvoiceTotal(invoiceTotal);
            salesLedgerVo.setReceiptPaymentAmountTotal(receiptPaymentAmountTotal);
            salesLedgerVo.setNoReceiptAmount(noReceiptPaymentAmountTotal);
            //  å¦‚果已经有过开票或回款操作,则不允许编辑
            boolean hasInvoiceOperation = invoiceTotal.compareTo(BigDecimal.ZERO) > 0;
            boolean hasReceiptOperation = receiptPaymentAmountTotal.compareTo(BigDecimal.ZERO) > 0;
            salesLedgerVo.setIsEdit(!(hasInvoiceOperation || hasReceiptOperation));
            salesLedgerVo.setIsEdit(hasReceiptOperation);
            salesLedgerVo.setStorageBlobVOs(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.FILE, RecordTypeEnum.SALES_LEDGER, ledgerId));
        }
        if (ObjectUtils.isNotEmpty(salesLedgerDto.getStatus())) {
            if (salesLedgerDto.getStatus()) {
                // æ¸…除所有“未开票金额”为 0 çš„记录
                iPage.getRecords().removeIf(salesLedger ->
                        Objects.equals(salesLedger.getNoInvoiceAmountTotal(), new BigDecimal("0.00")));
                iPage.setTotal(iPage.getRecords().size());
            }
        }
src/main/java/com/ruoyi/sales/controller/SalesLedgerProductController.java
@@ -1,8 +1,6 @@
package com.ruoyi.sales.controller;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
@@ -13,7 +11,6 @@
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.purchase.dto.SimpleReturnOrderGroupDto;
import com.ruoyi.purchase.mapper.PurchaseReturnOrderProductsMapper;
import com.ruoyi.sales.dto.SalesLedgerProductDto;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.service.ISalesLedgerProductService;
import jakarta.servlet.http.HttpServletResponse;
@@ -43,26 +40,6 @@
    /**
     * å›žæ¬¾ç™»è®°åˆ†é¡µæŸ¥è¯¢
     */
    @GetMapping("/listPageSalesLedger")
    public AjaxResult listPage(Page page, SalesLedgerProductDto salesLedgerProduct) {
        IPage<SalesLedgerProductDto> list = salesLedgerProductService.listPage(page,salesLedgerProduct);
        return AjaxResult.success(list);
    }
    /**
     * ä»˜æ¬¾ç™»è®°åˆ†é¡µæŸ¥è¯¢
     */
    @GetMapping("/listPagePurchaseLedger")
    public AjaxResult listPagePurchaseLedger(Page page, SalesLedgerProductDto salesLedgerProduct) {
        IPage<SalesLedgerProductDto> list = salesLedgerProductService.listPagePurchaseLedger(page,salesLedgerProduct);
        return AjaxResult.success(list);
    }
    /**
     * æŸ¥è¯¢äº§å“ä¿¡æ¯åˆ—表
     */
    @GetMapping("/list")
@@ -76,12 +53,6 @@
        Map<Long, BigDecimal> returnOrderGroupDtoMap = groupListByProductIds.stream().collect(Collectors.toMap(SimpleReturnOrderGroupDto::getProductModelId, item -> item.getSumReturnQuantity()));
        list.forEach(item -> {
            if (item.getFutureTickets().compareTo(BigDecimal.ZERO) == 0) {
                item.setFutureTickets(BigDecimal.ZERO);
            }
            if (item.getFutureTicketsAmount().compareTo(BigDecimal.ZERO) == 0) {
                item.setFutureTicketsAmount(BigDecimal.ZERO);
            }
            if (item.getApproveStatus() != 2) {
                if (item.getHasSufficientStock() == 0) {
                    item.setApproveStatus(0);
在上述文件截断后对比
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/SalesLedgerImportDto.java src/main/java/com/ruoyi/sales/dto/SalesLedgerProductImportDto.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/SalesLedgerProductMapper.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/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/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/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/vo/CustomerTransactionsDetailsVo.java src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.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/mapper/StockInRecordMapper.java src/main/java/com/ruoyi/stock/mapper/StockOutRecordMapper.java src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.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/service/DocumentationFileService.java src/main/resources/application-dev.yml src/main/resources/approve-todo-agent-prompt.txt src/main/resources/financial-agent-prompt.txt 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/financial/AccountSubjectMapper.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/basic/CustomerMapper.xml src/main/resources/mapper/basic/ProductModelMapper.xml src/main/resources/mapper/basic/SupplierManageMapper.xml src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml src/main/resources/mapper/production/ProductionOperationTaskMapper.xml src/main/resources/mapper/production/ProductionOrderMapper.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/PurchaseReturnOrdersMapper.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/SalesLedgerProductMapper.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/purchase-agent-prompt.txt src/main/resources/sales-agent-prompt.txt src/main/resources/static/销售台账导入模板.xlsx