d8cf4b3db03a4a4c2d12ef21eaec78cb7d3b10d1..10b88a7ff17caf92f3d4e8a550c1085a70c2517a
3 天以前 gongchunyi
Merge dev_New_pro into dev_山西_晋和园_pro
10b88a 对比 | 目录
3 天以前 gongchunyi
fix: 去除AI与个推
78be20 对比 | 目录
3 天以前 gongchunyi
chore: 移除多余配置文件
de0e56 对比 | 目录
3 天以前 gongchunyi
Merge dev_pro_河南鹤壁 into dev_山西_晋和园_pro
d709e8 对比 | 目录
3 天以前 gongchunyi
refactor: 去除个推和mongo
5d3305 对比 | 目录
3 天以前 gongchunyi
chore: 智芯智能配置文件
4b1f57 对比 | 目录
3 天以前 liding
feat:采购批量审核-质检-入库
8beb17 对比 | 目录
3 天以前 huminmin
录入日期排序
5d1444 对比 | 目录
3 天以前
refactor(ai): 移除AI聊天相关实体类和服务实现
756f56 对比 | 目录
3 天以前 gongchunyi
chore: 智芯智能配置文件
6cb59f 对比 | 目录
4 天以前 zss
供应商往来查询入库金额sql调整优化
b071f1 对比 | 目录
4 天以前 zss
编辑采购台账报错异常
bc9d5f 对比 | 目录
4 天以前 liding
Merge remote-tracking branch 'origin/dev_New_pro' into dev_New_pro
820e9e 对比 | 目录
4 天以前 liding
fix:countTodayByCreateTime方法加上null值判断
eaa7d1 对比 | 目录
4 天以前 huminmin
修改排序
6178d3 对比 | 目录
4 天以前
Merge remote-tracking branch 'origin/dev_New_pro' into dev_New_pro
3d556b 对比 | 目录
4 天以前
feat(ai): 添加AI会话清理任务并优化财务分析功能
a0f6e4 对比 | 目录
4 天以前 huminmin
Merge branch 'dev_New_pro' of http://114.132.189.42:9002/r/product-inventor...
2b6625 对比 | 目录
4 天以前 huminmin
修改排序
2db7fd 对比 | 目录
4 天以前
fix(ai): 优化财务AI助手的工作规则和数据分析功能
261a7d 对比 | 目录
4 天以前 huminmin
修改排序
c7e1f7 对比 | 目录
4 天以前 张诺
提交 山西宸康工贸有限公司 提交配置文件
dede81 对比 | 目录
5 天以前 yuan
Merge remote-tracking branch 'origin/dev_pro_河南鹤壁' into dev_pro_河南鹤壁
ee5225 对比 | 目录
5 天以前 huminmin
领料台账需求数量按照父子层级计算
5eb213 对比 | 目录
5 天以前 gongchunyi
chore: 泽淇配置文件
609a29 对比 | 目录
5 天以前 huminmin
修改排序
7d606f 对比 | 目录
5 天以前 huminmin
修改排序
db9922 对比 | 目录
5 天以前 huminmin
修改生产工单排序
3a6b5c 对比 | 目录
5 天以前 huminmin
Merge branch 'dev_New_pro' of http://114.132.189.42:9002/r/product-inventor...
49133a 对比 | 目录
5 天以前 huminmin
销售台账是否编辑逻辑错误
bcddfd 对比 | 目录
5 天以前 liyong
发货审批
7d7c9a 对比 | 目录
5 天以前 liyong
Merge remote-tracking branch 'origin/dev_New_pro' into dev_New_pro
33f9ea 对比 | 目录
5 天以前 liyong
审批更新 添加采购,报价,发货的审批提交,审批审核通过之后的处理
e44ef6 对比 | 目录
5 天以前 huminmin
客户档案下有销售报价不能被删除
921a0c 对比 | 目录
5 天以前 liding
feat:修改自动编号的生成,根据传入的CreateTime生成对应的编号
acc94d 对比 | 目录
5 天以前 huminmin
列表查询按照id倒序
2c893b 对比 | 目录
5 天以前 huminmin
列表查询按照id倒序
31cd3d 对比 | 目录
5 天以前 huminmin
列表查询按照id倒序
e0a6b0 对比 | 目录
5 天以前 huminmin
列表查询按照id倒序
0291ae 对比 | 目录
5 天以前 gongchunyi
test: 移除测试类
10d139 对比 | 目录
5 天以前 gongchunyi
feat(stock):库存预警数量
ba5cd5 对比 | 目录
5 天以前 gongchunyi
feat: 审批数据倒叙返回
585d16 对比 | 目录
5 天以前 gongchunyi
11
f8b0f6 对比 | 目录
5 天以前 huminmin
关联销售台账或者销售退货的客户不能被删除
e79ebf 对比 | 目录
5 天以前 gongchunyi
111
2ff409 对比 | 目录
5 天以前 gongchunyi
Merge remote-tracking branch 'origin/dev_pro_娌冲崡楣ゅ' into dev_pro_娌冲崡楣ゅ
e7a970 对比 | 目录
5 天以前 huminmin
Merge branch 'dev_New_pro' of http://114.132.189.42:9002/r/product-inventor...
27a32b 对比 | 目录
5 天以前 huminmin
领料台账需求数量按照父子层级计算
64e7b8 对比 | 目录
5 天以前 liding
feat:1.销售采购合同按照录入日期生成 2.采购合同号按照录入日期生成
28d6f8 对比 | 目录
6 天以前 张诺
提交华强建材提交配置文件
b09868 对比 | 目录
已添加189个文件
已重命名2个文件
已修改281个文件
已删除138个文件
44578 ■■■■ 文件已修改
.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/1767778273_add_product_order_id_to_product_work_order.sql 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260113-product_model.sql 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260114_add_dimission_reason_and_remark_to_staff_join_leave_record.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260115_add_sys_post_id_to_staff_join_leave_record.sql 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260116_create_table_staff_contract.sql 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260116_create_table_staff_leave.sql 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260119-procurement_record_storage.sql 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260128_add_unique_to_staff_on_job.sql 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260209_create_personal_attendance_records.sql 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260306_create_purchase_return_orders.sql 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260520_首页生产看板前端联调文档.md 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260520_首页生产看板性能优化前端变更文档.md 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260521_采购智能体优化前端变更文档.md 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260521_首页HomeController接口升级前端变更文档.md 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_StockInRecord列表源单号前端联调文档.md 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_StockInRecord列表源单号前端联调文档_276补充.md 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_财务助手提问优化前端变更文档.md 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_财务升级AI模块前端变更联调文档.md 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_采购台账入库状态_销售产品入库审核状态前端联调文档.md 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_首页财务接口升级前端变更文档.md 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260523_ 协同审批新增出差时间和结束时间.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260523_新增入库记录表的预警数量.sql 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/add_rating_evaluation_fields.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/alter_table_measuring_instrument_ledger.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/financial-ai-front-integration.md 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/update_after_sales_service.sql 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/CodeGenerator.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/AccountDto.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/AccountDto2.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/AccountDto3.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/AccountReportDto.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/StatementAccountDto.java 35 ●●●●● 补丁 | 查看 | 原始文档 | 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 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseReturnDto.java 8 ●●●●● 补丁 | 查看 | 原始文档 | 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 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/dto/sales/SalesReturnDto.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/AccountReportVo.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/StatementAccountVo.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/bean/vo/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 4 ●●●● 补丁 | 查看 | 原始文档 | 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 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountExpenseController.java 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountFileController.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountIncomeController.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountStatementController.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/AccountingController.java 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/BorrowInfoController.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/controller/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/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 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/BorrowInfoMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | 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 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/purchase/AccountPaymentApplication.java 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/purchase/AccountPurchaseInvoice.java 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/purchase/AccountPurchasePayment.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/sales/AccountInvoiceApplication.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/sales/AccountSalesCollection.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/pojo/sales/AccountSalesInvoice.java 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountExpenseService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountFileService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountIncomeService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountStatementDetailsService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountStatementService.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/AccountingService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/BorrowInfoService.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/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 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/BorrowInfoServiceImpl.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | 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/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/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/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/aftersalesservice/dto/AfterSalesServiceExeclDto.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/aftersalesservice/pojo/AfterSalesService.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/aftersalesservice/service/impl/AfterSalesServiceServiceImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ApproveTodoAgent.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java 451 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/Assistant.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/FileAnalyzeAgent.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/FinancialIntentExecutor.java 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/PurchaseAgent.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java 161 ●●●●● 补丁 | 查看 | 原始文档 | 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/assistant/SeparateChatAssistant.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/bean/ChatForm.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/bean/PurchaseAiConfirmRequest.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/ApproveTodoAgentConfig.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/SalesAgentConfig.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/SeparateChatAssistantConfig.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/FinancialAiController.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/XiaozhiController.java 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/dto/AiChatSessionDto.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/mapper/AiChatSessionMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/pojo/AiChatSession.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/schedule/AiSessionCleanupTask.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/service/AiChatSessionService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/service/PurchaseAiService.java 1031 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java 2311 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java 1035 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java 643 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java 1475 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalInstanceDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeApproverDto.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/dto/FinReimbursementDto.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalInstanceVo.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeApproverVo.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeVo.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateVo.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApproveGetAndUpdateVo.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/ApproveProcessVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/bean/vo/FinReimbursementVo.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceController.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceNodeController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalRecordController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTaskController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateController.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeApproverController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApproveNodeController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApproveProcessConfigNodeController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApproveProcessController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementController.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementDetailController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/FinReimbursementTravelController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/HolidaySettingsController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/NotificationManagementController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/RpaProcessAutomationController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceNodeMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalRecordMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTaskMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeApproverMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementDetailMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/mapper/FinReimbursementTravelMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalInstance.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalInstanceNode.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalRecord.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTask.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplate.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNode.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNodeApprover.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApproveProcess.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursement.java 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursementDetail.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/FinReimbursementTravel.java 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalInstanceNodeService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalInstanceService.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalRecordService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTaskService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeApproverService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeService.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/ApprovalTemplateService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementDetailService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementService.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/FinReimbursementTravelService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceNodeServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java 846 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalRecordServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTaskServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeApproverServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeServiceImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateServiceImpl.java 250 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveBusinessStatusService.java 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java 129 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementDetailServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java 544 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementTravelServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/utils/ApproveProcessConfigNodeUtils.java 363 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/SupplierManageController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/SupplierManageFileController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java 16 ●●●●● 补丁 | 查看 | 原始文档 | 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 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/SupplierServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/task/ReturnVisitReminderTask.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/DutyPlanController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsController.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeDeptController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeUserController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/MeetingController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/NoticeTypeController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/RulesRegulationsManagementController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/RulesRegulationsManagementFileController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/SealApplicationManagementController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/StaffContactsPersonalController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/dto/EnterpriseNewsDto.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeDeptMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeUserMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNews.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeDept.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeUser.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeDeptService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeUserService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsService.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeDeptServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeUserServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsServiceImpl.java 409 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/vo/EnterpriseNewsVo.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/ApprovalStatusEnum.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/EnterpriseNewsStatusEnum.java 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/SalesQuotationStatusEnum.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/ShippingStatusEnum.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/TypeEnums.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/utils/OrderUtils.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/customervisits/service/impl/CustomerVisitsServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/controller/DeviceDefectRecordController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/controller/DeviceLedgerController.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/controller/DeviceMaintenanceController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/controller/DeviceMaintenanceFileController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/controller/DeviceRepairController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/MaintenanceTask.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/IDeviceLedgerService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceLedgerServiceImpl.java 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskServiceImpl.java 47 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/controller/HomeController.java 586 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/dto/StatisticsReceivablePayableDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java 588 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/controller/InspectionTaskController.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/controller/QrCodeController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/controller/QrCodeScanRecordController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/controller/TimingTaskController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/controller/SparePartsRequisitionRecordController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/officesupplies/controller/OfficeSuppliesController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/other/controller/PdaVersionController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/GasTankWarningController.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/InboundManagementController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementExceptionRecordController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementPlanController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementPriceManagementController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/ReturnManagementController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/controller/ReturnSaleProductController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/pojo/CustomStorage.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/pojo/ReturnManagement.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnManagementServiceImpl.java 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionBomStructureController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderPickController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingOperationParamController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionProductMainController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrder.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | 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/common/CommonController.java 4 ●●●● 补丁 | 查看 | 原始文档 | 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/SysUserClientController.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/controller/SysUserController.java 581 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/domain/GetuiConfig.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/domain/SysUserClient.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/mapper/SysUserClientMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/mapper/SysUserDeptMapper.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/ISysUserService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/SysUserClientService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/SysNoticeServiceImpl.java 109 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/SysUserClientServiceImpl.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/SysUserServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/UnipushService.java 218 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/tool/gen/controller/GenController.java 508 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/tool/swagger/TestController.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/controller/InfoController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/controller/PlanController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/controller/RolesController.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/projectManagement/pojo/Roles.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/InvoicePurchaseController.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/PaymentRegistrationController.java 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/PurchaseLedgerController.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/PurchaseReturnOrdersController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | 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 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/TicketRegistrationDto.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/VatDto.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/InvoicePurchaseMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/PaymentRegistrationMapper.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/ProductRecordMapper.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/PurchaseLedgerMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/mapper/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 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseReturnOrders.java 1 ●●●● 补丁 | 查看 | 原始文档 | 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 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/ITicketRegistrationService.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/PurchaseReportService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/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 518 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReportServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java 464 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/PurchaseReportVo.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java 47 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityInspectFileController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityInspectParamController.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityReportController.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityTestStandardBindingController.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityTestStandardController.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityTestStandardParamController.java 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityUnqualifiedController.java 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/IQualityInspectService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 63 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/utils/QualityInspectHelper.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeAccidentController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeCertificationController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeCertificationFileController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeContingencyPlanController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeHazardController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeHazardRecordController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeHiddenController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeHiddenFileController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeTrainingController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeTrainingDetailsController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeTrainingFileController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/InvoiceLedgerController.java 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/InvoiceRegistrationController.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/MetricStatisticsController.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/PaymentShippingController.java 8 ●●●● 补丁 | 查看 | 原始文档 | 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/controller/SalesQuotationController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/SalespersonManagementController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java 23 ●●●●● 补丁 | 查看 | 原始文档 | 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/SalesQuotationDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/excel/InvoiceLedgerExcelDto.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/excel/InvoiceRegisAndProductExcelDto.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/InvoiceLedgerFileMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/InvoiceLedgerMapper.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/InvoiceRegistrationMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/InvoiceRegistrationProductMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/ReceiptPaymentMapper.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/SalesLedgerProductMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/InvoiceLedger.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/InvoiceLedgerFile.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/InvoiceRegistration.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/InvoiceRegistrationProduct.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/PaymentShipping.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/ReceiptPayment.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesQuotation.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/ShippingInfo.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/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/ShippingInfoService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | 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/MetricStatisticsServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ReceiptPaymentServiceImpl.java 349 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java 96 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java 92 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/HolidayApplicationController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/PersonalAttendanceLocationConfigController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/PersonalAttendanceRecordsController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/PersonalShiftController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/StaffLeaveController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/StaffOnJobController.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/controller/StaffSchedulingController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/dto/StaffOnJobDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/mapper/StaffOnJobMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/IStaffOnJobService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/StaffLeaveServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java 214 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInRecordController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockUninventoryController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/pojo/StockInRecord.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/pojo/StockOutRecord.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/StockInRecordService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockUninventoryServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyBomStructureController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyOperationController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyOperationParamController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyRoutingController.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyRoutingOperationController.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyRoutingOperationParamController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/pojo/TechnologyParam.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/pojo/TechnologyRouting.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/warehouse/controller/DocumentationFileController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | 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-ckgm.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-hqjc.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-jhy.yml 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | 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 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/BorrowInfoMapper.xml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPaymentApplicationMapper.xml 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPurchaseInvoiceMapper.xml 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPurchasePaymentMapper.xml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountInvoiceApplicationMapper.xml 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountSalesCollectionMapper.xml 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountSalesInvoiceMapper.xml 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/aftersalesservice/AfterSalesNearExpiryMapper.xml 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/aftersalesservice/AfterSalesServiceMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalInstanceMapper.xml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalInstanceNodeMapper.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalRecordMapper.xml 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTaskMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTemplateMapper.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTemplateNodeApproverMapper.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApprovalTemplateNodeMapper.xml 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/ApproveProcessMapper.xml 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementDetailMapper.xml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementMapper.xml 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/FinReimbursementTravelMapper.xml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/approve/KnowledgeBaseMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/ProductModelMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/SupplierManageMapper.xml 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsMapper.xml 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeDeptMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeUserMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/NoticeMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/RulesRegulationsManagementMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/collaborativeApproval/SealApplicationManagementMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceMaintenanceMapper.xml 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceRepairMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerRecordMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/SparePartsMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/SparePartsRequisitionRecordMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderMapper.xml 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionPlanMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | 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 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityInspectMapper.xml 317 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityTestStandardMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml 35 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/InvoiceLedgerMapper.xml 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/InvoiceRegistrationMapper.xml 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/InvoiceRegistrationProductMapper.xml 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ReceiptPaymentMapper.xml 454 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerMapper.xml 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesQuotationMapper.xml 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/PersonalAttendanceRecordsMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/PersonalShiftMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/StaffLeaveMapper.xml 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/staff/StaffOnJobMapper.xml 108 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInRecordMapper.xml 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockOutRecordMapper.xml 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysDeptMapper.xml 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysDictTypeMapper.xml 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysNoticeMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysPostMapper.xml 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysRoleMapper.xml 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysUserMapper.xml 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/technology/TechnologyOperationMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/technology/TechnologyOperationParamMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/DocumentationBorrowManagementMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/DocumentationMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/DocumentationReturnManagementMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/warehouse/WarehouseGoodsShelvesRowcolMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/purchase-agent-prompt.txt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sales-agent-prompt.txt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/ruoyi/stock/service/impl/StockOutRecordBatchUpdateTest.java 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -6,6 +6,7 @@
!gradle/wrapper/gradle-wrapper.jar
claude.md
target/
test/
!.mvn/wrapper/maven-wrapper.jar
######################################################################
doc/1767778273_add_product_order_id_to_product_work_order.sql
ÎļþÒÑɾ³ý
doc/20260113-product_model.sql
ÎļþÒÑɾ³ý
doc/20260114_add_dimission_reason_and_remark_to_staff_join_leave_record.sql
ÎļþÒÑɾ³ý
doc/20260115_add_sys_post_id_to_staff_join_leave_record.sql
ÎļþÒÑɾ³ý
doc/20260116_create_table_staff_contract.sql
ÎļþÒÑɾ³ý
doc/20260116_create_table_staff_leave.sql
ÎļþÒÑɾ³ý
doc/20260119-procurement_record_storage.sql
ÎļþÒÑɾ³ý
doc/20260128_add_unique_to_staff_on_job.sql
ÎļþÒÑɾ³ý
doc/20260209_create_personal_attendance_records.sql
ÎļþÒÑɾ³ý
doc/20260306_create_purchase_return_orders.sql
ÎļþÒÑɾ³ý
doc/20260520_Ê×Ò³Éú²ú¿´°åǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,160 @@
# é¦–页生产看板前端联调文档
更新时间:2026-05-20
模块:`/home`(首页)
## 1. æŽ¥å£æ¸…单
1. `GET /home/productionOverview`:生产总览
2. `GET /home/productionRealtimeBoard`:生产实时看板
3. `GET /home/productionOrderProgress`:生产订单进度
4. `GET /home/todayProductionPlan`:今日生产计划
所有接口统一返回 `AjaxResult`:
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {}
}
```
## 2. ç”Ÿäº§æ€»è§ˆ
### 2.1 è¯·æ±‚
```http
GET /home/productionOverview
```
### 2.2 è¿”回 `data`
```json
{
  "totalOutput": 1280.00,
  "totalScrap": 25.00,
  "yieldRate": 98.08
}
```
字段说明:
- `totalOutput`:累计产出(件,合格数)
- `totalScrap`:累计报废(件)
- `yieldRate`:良率(0-100,前端展示时可拼接 `%`)
## 3. ç”Ÿäº§å®žæ—¶çœ‹æ¿
### 3.1 è¯·æ±‚
```http
GET /home/productionRealtimeBoard
```
### 3.2 è¿”回 `data`
```json
{
  "deviceOee": {
    "value": 74.00,
    "compareYesterday": 2.50
  },
  "orderAchievementRate": {
    "value": 81.30,
    "compareYesterday": -1.20
  },
  "defectRate": {
    "value": 1.40,
    "compareYesterday": 0.30
  }
}
```
字段说明:
- `value`:当日指标值(0-100)
- `compareYesterday`:较昨日变化值(可正可负;前端按正负决定箭头方向和颜色)
## 4. ç”Ÿäº§è®¢å•进度
### 4.1 è¯·æ±‚
```http
GET /home/productionOrderProgress?tab=all&pageNum=1&pageSize=10
```
参数:
- `tab`:`all` / `inProgress` / `completed` / `paused`
- `pageNum`:页码(默认 `1`)
- `pageSize`:每页条数(默认 `10`,最大 `50`)
### 4.2 è¿”回 `data`
```json
{
  "tab": "all",
  "total": 24,
  "pageNum": 1,
  "pageSize": 10,
  "inProgressCount": 6,
  "completedCount": 12,
  "pausedCount": 2,
  "records": [
    {
      "orderNo": "MO-20260518-001",
      "productName": "智能控制器",
      "plannedQuantity": 1000.00,
      "completedQuantity": 860.00,
      "completionRate": 86.00,
      "dueDate": "2026-05-20",
      "status": 2,
      "statusLabel": "进行中"
    }
  ]
}
```
字段说明:
- `completionRate`:完成率(0-100)
- `status`:后端状态码(`1`待开始,`2`进行中,`3`已完成,`4`已暂停)
- `statusLabel`:状态中文展示值
## 5. ä»Šæ—¥ç”Ÿäº§è®¡åˆ’
### 5.1 è¯·æ±‚
```http
GET /home/todayProductionPlan?limit=4
```
参数:
- `limit`:返回条数(默认 `4`,最大 `20`)
### 5.2 è¿”回 `data`
```json
{
  "total": 9,
  "records": [
    {
      "orderNo": "MO-20260518-004",
      "productName": "结构件A",
      "plannedQuantity": 1200.00,
      "dueDate": "2026-05-15",
      "status": 2,
      "statusLabel": "进行中"
    }
  ]
}
```
## 6. å‰ç«¯å±•示约定
- ç™¾åˆ†æ¯”字段统一是数值(如 `74.00`),前端自行拼接 `%`。
- æ‰€æœ‰æ•°å€¼ä¿ç•™ä¸¤ä½å°æ•°ã€‚
- `dueDate` å¯èƒ½ä¸º `null`,前端需兜底展示(如 `--`)。
- `compareYesterday` æ­£è´Ÿéƒ½å¯èƒ½å‡ºçŽ°ï¼Œå»ºè®®æŒ‰ `>0` ä¸Šå‡ã€`<0` ä¸‹é™ã€`=0` æŒå¹³å¤„理。
doc/20260520_Ê×Ò³Éú²ú¿´°åÐÔÄÜÓÅ»¯Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
# é¦–页生产看板性能优化前端变更文档
更新时间:2026-05-20
适用页面:首页
涉及区块:
1. ç”Ÿäº§è®¢å•进度
2. ä»Šæ—¥ç”Ÿäº§è®¡åˆ’
## 1. æœ¬æ¬¡ä¼˜åŒ–目标
针对大数据量场景(订单数量多、生产历史长)优化查询性能,降低首页接口响应时间和内存占用。
## 2. æ¶‰åŠæŽ¥å£
1. `GET /home/productionOrderProgress`
2. `GET /home/todayProductionPlan`
## 3. å‰ç«¯æ˜¯å¦éœ€è¦æ”¹ä»£ç 
结论:**无强制改动,接口入参与返回结构保持兼容**。
你现有页面可以直接联调,不需要改字段映射。
## 4. æŽ¥å£è¯´æ˜Žï¼ˆä¿æŒä¸å˜ï¼‰
### 4.1 ç”Ÿäº§è®¢å•进度
请求:
```http
GET /home/productionOrderProgress?tab=all&pageNum=1&pageSize=10
```
参数:
- `tab`:`all` / `inProgress` / `completed` / `paused`
- `pageNum`:页码,默认 `1`
- `pageSize`:每页条数,默认 `10`,最大 `50`
返回 `data`(结构不变):
```json
{
  "tab": "all",
  "total": 1200,
  "pageNum": 1,
  "pageSize": 10,
  "inProgressCount": 180,
  "completedCount": 900,
  "pausedCount": 20,
  "records": [
    {
      "orderNo": "MO-20260518-001",
      "productName": "智能控制器",
      "plannedQuantity": 1000.00,
      "completedQuantity": 860.00,
      "completionRate": 86.00,
      "dueDate": "2026-05-20",
      "status": 2,
      "statusLabel": "进行中"
    }
  ]
}
```
### 4.2 ä»Šæ—¥ç”Ÿäº§è®¡åˆ’
请求:
```http
GET /home/todayProductionPlan?limit=4
```
参数:
- `limit`:返回条数,默认 `4`,最大 `20`
返回 `data`(结构不变):
```json
{
  "total": 230,
  "records": [
    {
      "orderNo": "MO-20260518-004",
      "productName": "结构件A",
      "plannedQuantity": 1200.00,
      "dueDate": "2026-05-15",
      "status": 2,
      "statusLabel": "进行中"
    }
  ]
}
```
## 5. åŽç«¯ä¼˜åŒ–点(供前端知悉)
1. è®¢å•进度与今日计划改为轻量 SQL,仅查询首页必需字段。
2. åŽ»æŽ‰äº†é¦–é¡µæŸ¥è¯¢è·¯å¾„ä¸­ä¸å¿…è¦çš„å¤§å…³è”ã€å›¾ç‰‡å¡«å……å’Œå¯¹è±¡è£…é…ã€‚
3. çŠ¶æ€ç»Ÿè®¡æ”¹ä¸ºæ•°æ®åº“èšåˆè®¡æ•°ï¼Œä¸å†é€æ¡æ‹‰å–è®¡ç®—ã€‚
4. åˆ†é¡µä¸Žæ¡æ•°ä¸Šé™ä¿ç•™ï¼ˆ`pageSize <= 50`, `limit <= 20`)。
## 6. å‰ç«¯å»ºè®®
1. åˆ‡æ¢ `tab` æ—¶ä¿ç•™çŽ°æœ‰è°ƒç”¨æ–¹å¼å³å¯ã€‚
2. `dueDate` å¯èƒ½ä¸ºç©ºï¼Œç»§ç»­æŒ‰ `--` å…œåº•展示。
3. ç™¾åˆ†æ¯”字段仍为数值,前端继续追加 `%` å±•示。
doc/20260521_²É¹ºÖÇÄÜÌåÓÅ»¯Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
# é‡‡è´­æ™ºèƒ½ä½“优化前端变更文档
## 1. å˜æ›´èƒŒæ™¯
本次针对采购智能体做了对齐优化(参考销售/审批/制造智能体):
1. æå‡ `quickPrompts` å‘½ä¸­ç¨³å®šæ€§ã€‚
2. å¢žå¼ºç›¸å¯¹æ—¶é—´è¯†åˆ«ï¼ˆä»Šå¤©/昨天/本周/上周/本月/上月/今年/去年/近N天等)。
3. å¢žåŠ ä¸šåŠ¡æ„å›¾æœªè¯†åˆ«æ—¶çš„ç»“æž„åŒ–å…œåº•å“åº”ï¼Œé¿å…ç¼–é€ æ•°æ®ã€‚
4. è¡¥å……待付款查询的汇总字段,便于前端直接渲染统计卡片。
## 2. æŽ¥å£å½±å“æ¦‚览
| æŽ¥å£ | æ–¹æ³• | æ˜¯å¦æ”¹è·¯å¾„ | æ˜¯å¦æ”¹å…¥å‚ | æ˜¯å¦æ”¹è¿”回结构 |
| --- | --- | --- | --- | --- |
| `/purchase-ai/chat` | POST(SSE) | å¦ | å¦ | æ˜¯ï¼ˆæ–°å¢žå…œåº• JSON ç±»åž‹ï¼‰ |
| `/purchase-ai/analyze-files` | POST(SSE) | å¦ | å¦ | å¦ï¼ˆä»…内部提示词增强) |
## 3. æ–°å¢žå…œåº•响应(重点)
当用户明显在问采购业务,但条件不充分且未命中可执行意图时,`/purchase-ai/chat` ä¼šç›´æŽ¥è¿”回结构化 JSON(而不是自由文本):
```json
{
  "success": false,
  "type": "purchase_intent_not_recognized",
  "description": "未识别到可执行的采购查询条件。为保证结果准确,当前不会推测或编造数据,请补充明确时间范围、供应商、采购合同号或物料后再查询。",
  "summary": {},
  "data": {
    "quickPrompts": [
      "本月采购金额排名前十的物料有哪些?",
      "哪些采购订单还未入库?",
      "最近7天供应商到货异常有哪些?",
      "帮我统计待付款采购单!",
      "列出本月采购退货情况"
    ]
  },
  "charts": {}
}
```
前端处理建议:
1. å½“ `type === "purchase_intent_not_recognized"` æ—¶ï¼Œå±•示 `description`。
2. è¯»å– `data.quickPrompts` ä½œä¸ºå¿«æ·æé—®æŒ‰é’®ï¼ˆå¯ç›´æŽ¥å›žå¡«è¾“入框)。
## 4. å¾…付款返回新增汇总字段
接口类型:`type = "purchase_pending_payment_list"`
位置:`summary`
新增字段:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
| --- | --- | --- |
| pendingOrderCount | number | å¾…付款订单数 |
| totalContractAmount | number | å¾…付款订单合同总额 |
| totalPaidAmount | number | å·²ä»˜æ¬¾æ€»é¢ |
| totalPendingAmount | number | å¾…付款总额 |
说明:原有字段仍保留(兼容),本次为增量字段,不破坏现有渲染。
## 5. æ—¶é—´å£å¾„优化
采购智能体现在统一按中国时区动态日期换算相对时间,支持:
- ä»Šå¤©ã€æ˜¨å¤©
- æœ¬å‘¨ã€ä¸Šå‘¨
- æœ¬æœˆã€ä¸Šæœˆ
- ä»Šå¹´ã€å޻年
- è¿‘N天/周/月/年、近半年、近半个月
前端无需改传参,但展示时间范围时请以后端返回 `summary.startDate/endDate/timeRange` ä¸ºå‡†ã€‚
## 6. å‰ç«¯è”调检查清单
1. `chat` æµå¼ç»“果拼接后,优先按 JSON è§£æžã€‚
2. è¦†ç›–新类型 `purchase_intent_not_recognized` çš„ UI å¤„理。
3. å¾…付款页面读取并展示 `summary.totalPendingAmount` ç­‰æ–°å¢žå­—段。
4. éªŒè¯ä»¥ä¸‹å¿«æ·é—®é¢˜å¯ç¨³å®šè¿”回结构化结果:
   - æœ¬æœˆé‡‡è´­é‡‘额排名前十的物料有哪些?
   - å“ªäº›é‡‡è´­è®¢å•还未入库?
   - æœ€è¿‘7天供应商到货异常有哪些?
   - å¸®æˆ‘统计待付款采购单!
   - åˆ—出本月采购退货情况
doc/20260521_Ê×Ò³HomeController½Ó¿ÚÉý¼¶Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
# é¦–页 HomeController æŽ¥å£å‡çº§å‰ç«¯å˜æ›´æ–‡æ¡£
更新时间:2026-05-21
适用模块:首页(`/home`)
## 1. å˜æ›´æ¦‚览
本次为 **兼容式升级**,旧调用方式仍可用。
重点是给生产看板接口增加更明确的筛选参数,便于前端按日期和状态查询。
涉及接口:
1. `GET /home/productionOrderProgress`
2. `GET /home/todayProductionPlan`
## 2. å‚数变更
### 2.1 ç”Ÿäº§è®¢å•进度 `GET /home/productionOrderProgress`
旧参数(仍兼容):
- `tab`:`all` / `inProgress` / `completed` / `paused`
- `pageNum`:默认 `1`
- `pageSize`:默认 `10`,最大 `50`
新增参数:
- `status`(可选):状态筛选,优先级高于 `tab`
  å¯é€‰å€¼ï¼š`all` / `waiting` / `inProgress` / `completed` / `paused` / `1` / `2` / `3` / `4`
- `bizDate`(可选):业务日期筛选,格式 `yyyy-MM-dd`(按订单创建时间过滤)
参数优先级:
1. å¦‚果传了 `status`,后端优先按 `status` è§£æžï¼›
2. æœªä¼  `status` æ—¶ï¼Œæ²¿ç”¨åŽŸæœ‰ `tab` è¡Œä¸ºï¼›
3. `status` æˆ– `bizDate` æ ¼å¼é”™è¯¯æ—¶è¿”回失败信息。
请求示例:
```http
GET /home/productionOrderProgress?status=completed&bizDate=2026-05-20&pageNum=1&pageSize=10
```
### 2.2 ä»Šæ—¥ç”Ÿäº§è®¡åˆ’ `GET /home/todayProductionPlan`
旧参数(仍兼容):
- `limit`:默认 `4`,最大 `20`
新增参数:
- `planDate`(可选):计划日期筛选,格式 `yyyy-MM-dd`(按 `plan_complete_time` è¿‡æ»¤ï¼‰
请求示例:
```http
GET /home/todayProductionPlan?limit=6&planDate=2026-05-21
```
## 3. è¿”回结构变更
### 3.1 `productionOrderProgress` è¿”回新增字段
新增:
- `status`:标准化状态回显(`all` / `waiting` / `inProgress` / `completed` / `paused`)
- `bizDate`:日期筛选回显(未传时为 `null`)
- `waitingCount`:待开始订单数量
兼容保留:
- `tab` å­—段继续返回(老页面无需改动可继续使用)
返回示例:
```json
{
  "tab": "completed",
  "status": "completed",
  "bizDate": "2026-05-20",
  "total": 24,
  "pageNum": 1,
  "pageSize": 10,
  "waitingCount": 3,
  "inProgressCount": 6,
  "completedCount": 12,
  "pausedCount": 2,
  "records": []
}
```
### 3.2 `todayProductionPlan` è¿”回新增字段
新增:
- `planDate`:日期筛选回显(未传时为 `null`)
返回示例:
```json
{
  "planDate": "2026-05-21",
  "total": 9,
  "records": []
}
```
## 4. å‰ç«¯æ”¹é€ å»ºè®®
1. æ–°é¡µé¢å»ºè®®ä¼˜å…ˆä¼  `status`,逐步替代 `tab`。
2. éœ€è¦æŒ‰æ—¥æœŸå¤ç›˜çœ‹æ¿æ—¶ï¼Œä½¿ç”¨ `bizDate` / `planDate`。
3. è€é¡µé¢å¯ä¸æ”¹ï¼Œç»§ç»­æ²¿ç”¨åŽŸå‚æ•°ä¹Ÿèƒ½æ­£å¸¸è”è°ƒã€‚
doc/20260522_StockInRecordÁбíÔ´µ¥ºÅǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
# StockInRecord åˆ—表源单号前端联调文档
更新时间:2026-05-22
适用接口:`GET /stockInRecord/listPage`
## 1. å˜æ›´è¯´æ˜Ž
本次对入库管理列表接口增加返回字段 `sourceOrderNo`(源单号),用于原材料场景展示采购来源单号。
生效条件:
- å½“请求参数 `topParentProductId = 278` æ—¶ï¼ŒåŽç«¯è¿”回 `sourceOrderNo`。
- å…¶ä»– `topParentProductId` åœºæ™¯ä¸‹ï¼Œè¯¥å­—段返回 `null`(或不展示)。
## 2. å­—段定义
新增字段:
- `sourceOrderNo`:`string`,源单号(采购合同号)。
## 3. å–值规则
仅在 `topParentProductId = 278` æ—¶æŒ‰â€œæ¥æºâ€è®¡ç®—:
1. æ¥æº = `采购-入库`(`recordType = 7`)
   - å…ˆæŒ‰ `recordId` æŸ¥é‡‡è´­äº§å“è¡¨ `sales_ledger_product`(`type=2`);
   - å†é€šè¿‡ `sales_ledger_product.sales_ledger_id` æŸ¥é‡‡è´­å°è´¦è¡¨ `purchase_ledger`;
   - è¿”回 `purchase_ledger.purchase_contract_number` ä½œä¸º `sourceOrderNo`。
   - å…¼å®¹å…œåº•:若未命中采购产品链路,则按 `recordId` ç›´æŽ¥æŸ¥ `purchase_ledger.id` å–单号。
2. æ¥æº = `采购-质检-合格入库`(`recordType = 10`)
   - å…ˆæŒ‰ `recordId` æŸ¥è´¨æ£€è¡¨ `quality_inspect`;
   - å†é€šè¿‡ `quality_inspect.purchase_ledger_id` æŸ¥é‡‡è´­å°è´¦è¡¨ `purchase_ledger`;
   - è¿”回 `purchase_ledger.purchase_contract_number` ä½œä¸º `sourceOrderNo`。
## 4. è¿”回示例
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "id": 1024,
        "recordType": "7",
        "productName": "铜排",
        "model": "T2-30x3",
        "sourceOrderNo": "CG-2026-00128"
      },
      {
        "id": 1025,
        "recordType": "10",
        "productName": "铜排",
        "model": "T2-30x3",
        "sourceOrderNo": "CG-2026-00131"
      }
    ],
    "total": 2
  }
}
```
## 5. å‰ç«¯è”调建议
1. åˆ—表列新增“源单号”,读取字段 `sourceOrderNo`。
2. å»ºè®®ä»…在 `topParentProductId = 278` çš„页面/筛选条件下展示该列。
3. å½“ `sourceOrderNo` ä¸ºç©ºæ—¶å±•示 `--`,避免空白。
## 6. å›žå½’清单
1. `topParentProductId=278` + `recordType=7`:应返回采购合同号。
2. `topParentProductId=278` + `recordType=10`:应返回采购合同号(经质检链路)。
3. `topParentProductId!=278`:`sourceOrderNo` åº”为 `null` æˆ–前端不展示。
4. åŽŸæœ‰å­—æ®µï¼ˆ`productName/model/unit/createBy` ç­‰ï¼‰ä¸å—影响。
doc/20260522_StockInRecordÁбíÔ´µ¥ºÅǰ¶ËÁªµ÷Îĵµ_276²¹³ä.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
# StockInRecord åˆ—表源单号前端联调文档(`topParentProductId=276`补充)
更新时间:2026-05-22
适用接口:`GET /stockInRecord/listPage`
## 1. å˜æ›´è¯´æ˜Ž
在已有 `sourceOrderNo` åŸºç¡€ä¸Šï¼Œæ–°å¢ž `topParentProductId = 276` çš„æºå•号溯源逻辑:
- æ ¹æ®â€œæ¥æºï¼ˆrecordType)+ recordId”溯源;
- ä¼˜å…ˆè¿”回销售单号(销售合同号);
- è‹¥é”€å”®å•号为空,则回退返回生产订单号;
- ä¸è€ƒè™‘自定义入库(`recordType=0/9`)。
## 2. è¿”回字段
字段无新增,继续使用:
- `sourceOrderNo`:`string`,源单号。
## 3. 276 åœºæ™¯å–值规则
请求参数满足 `topParentProductId = 276` æ—¶ï¼š
1. `recordType = 14/15`(销售退货-合格/不合格入库)
   - `stock_in_record.record_id -> return_sale_product.id -> return_management.shipping_id -> shipping_info.sales_ledger_id -> sales_ledger.sales_contract_no`
   - è¿”回销售合同号。
2. `recordType = 2/5`(生产报工-入库/报废)
   - `stock_in_record.record_id -> production_product_main.id -> production_operation_task.production_order_id -> production_order`
   - å…ˆå–该生产订单关联销售合同号(由生产计划关联销售台账聚合);
   - é”€å”®åˆåŒå·ä¸ºç©ºæ—¶ï¼Œè¿”回 `production_order.nps_no`。
3. `recordType = 6`(质检-合格入库)
   - `stock_in_record.record_id -> quality_inspect.id -> quality_inspect.product_main_id -> production_product_main -> production_operation_task -> production_order`
   - å…ˆå–销售合同号,空则回退 `production_order.nps_no`。
4. `recordType = 4/11`(不合格处理-报废/让步放行)
   - `stock_in_record.record_id -> quality_unqualified.id -> quality_unqualified.inspect_id -> quality_inspect -> production_product_main -> production_operation_task -> production_order`
   - å…ˆå–销售合同号,空则回退 `production_order.nps_no`。
5. `recordType = 20/22`(领料退料/生产退料-合格入库)
   - `stock_in_record.record_id -> production_order_pick.id -> production_order`
   - å…ˆå–销售合同号,空则回退 `production_order.nps_no`。
6. `recordType = 0/9`(自定义入库)
   - ä¸å‚与溯源,`sourceOrderNo = null`。
## 4. å…¶ä»–场景说明
- `topParentProductId = 278` çš„采购链路源单号逻辑保持不变。
- å…¶ä»– `topParentProductId` ä¸è§¦å‘本次 276 è§„则,`sourceOrderNo` ä¸ºç©ºã€‚
## 5. å‰ç«¯è”调建议
1. åœ¨ `topParentProductId=276` çš„列表场景展示“源单号”列,读取 `sourceOrderNo`。
2. å»ºè®®ç©ºå€¼ç»Ÿä¸€å±•示 `--`。
3. ä¸éœ€è¦æ–°å¢žè¯·æ±‚参数,沿用现有 `/stockInRecord/listPage`。
## 6. å›žå½’清单
1. `topParentProductId=276` + `recordType=14/15`:应返回销售合同号。
2. `topParentProductId=276` + `recordType=2/5/6/4/11/20/22`:优先销售合同号,缺失时返回生产订单号。
3. `topParentProductId=276` + `recordType=0/9`:`sourceOrderNo` ä¸ºç©ºã€‚
4. `topParentProductId=278`:仍按采购链路返回采购合同号。
doc/20260522_²ÆÎñÖúÊÖÌáÎÊÓÅ»¯Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
# è´¢åŠ¡åŠ©æ‰‹æé—®ä¼˜åŒ–å‰ç«¯å˜æ›´æ–‡æ¡£
更新时间:2026-05-22
适用模块:财务智能助手(`/financial-ai`)
## 1. èƒŒæ™¯
当前首页财务助手快捷提问为:
1. `生成本周经营周报`
2. `为什么利润下降`
3. `哪个客户最赚钱`
问题点:
- ç¬¬ 3 æ¡é—®æ³•在部分场景下意图命中不稳定,容易走普通文本回答,导致图表链接以原始 Markdown æ–‡æœ¬å±•示(如 `![...](https://local/generate_chart?options=...)`)。
- å¿«æ·æé—®ç¼ºå°‘时间范围和分析目标,结果稳定性与可解释性较弱。
## 2. å‰ç«¯å¿«æ·æé—®æ–‡æ¡ˆä¼˜åŒ–(必改)
建议将默认三条快捷提问调整为:
1. `生成本周经营周报(利润与现金流)`
2. `分析本月利润下降原因`
3. `近30天哪个客户利润贡献最高`
说明:
- ä¸‰æ¡é—®æ³•均带时间范围或分析目标,后端命中更稳定。
- ç¬¬ 3 æ¡ä¸Žâ€œæœ€èµšé’±å®¢æˆ·â€è¯­ä¹‰ä¸€è‡´ï¼Œä½†â€œåˆ©æ¶¦è´¡çŒ®æœ€é«˜â€æ›´æ˜Žç¡®ï¼Œé€‚合直接驱动利润分析结果页。
## 3. ä¸ŽåŽç«¯èƒ½åŠ›æ˜ å°„
| å¿«æ·æé—® | é¢„期命中能力 | é¢„期 `type` |
| --- | --- | --- |
| ç”Ÿæˆæœ¬å‘¨ç»è¥å‘¨æŠ¥ï¼ˆåˆ©æ¶¦ä¸ŽçŽ°é‡‘æµï¼‰ | ç»è¥æŠ¥å‘Šç”Ÿæˆ | `financial_operation_report` |
| åˆ†æžæœ¬æœˆåˆ©æ¶¦ä¸‹é™åŽŸå›  | è®¢å•利润分析 | `financial_order_profit_analysis` |
| è¿‘30天哪个客户利润贡献最高 | è®¢å•利润分析 | `financial_order_profit_analysis` |
后端已同步增强“最赚钱客户/客户利润最高/利润贡献最高”等同义问法识别,前端按以上文案改造后可直接联调。
## 4. èŠå¤©å†…容渲染兜底(建议改)
针对聊天返回文本中出现的图表 Markdown é“¾æŽ¥ï¼ˆ`https://local/generate_chart?options=...`),建议前端增加兜底处理:
1. è¯†åˆ« Markdown å›¾ç‰‡è¯­æ³•中的 `local/generate_chart` é“¾æŽ¥ã€‚
2. è§£æž `options` å‚数并转换为 ECharts `option` åŽæ¸²æŸ“图表组件。
3. è§£æžå¤±è´¥æ—¶ä¸å±•示原始长链接文本,展示统一空态提示。
## 5. è”调回归清单
1. ç‚¹å‡»å¿«æ·æé—® `生成本周经营周报(利润与现金流)`
   - æ ¡éªŒè¿”回 `type=financial_operation_report`,并正常渲染摘要/建议/图表。
2. ç‚¹å‡»å¿«æ·æé—® `分析本月利润下降原因`
   - æ ¡éªŒè¿”回 `type=financial_order_profit_analysis`,并展示亏损订单与客户利润排行。
3. ç‚¹å‡»å¿«æ·æé—® `近30天哪个客户利润贡献最高`
   - æ ¡éªŒè¿”回 `type=financial_order_profit_analysis`,`summary.topCustomerByProfit` æœ‰å€¼ã€‚
4. æ‰‹å·¥è¾“å…¥ `哪个客户最赚钱`、`哪个客户利润最高`
   - æ ¡éªŒä»å‘½ä¸­ `financial_order_profit_analysis`,不再出现原始图表 Markdown é“¾æŽ¥ç›´å‡ºã€‚
doc/20260522_²ÆÎñÉý¼¶AIÄ£¿éǰ¶Ë±ä¸üÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,150 @@
# è´¢åŠ¡æ¨¡å—å‡çº§åŽ AI æ¨¡å—前端变更联调文档(采购/销售/生产/待办)
更新日期:2026-05-22
适用范围:`/sales-ai`、`/purchase-ai`、`/manufacturing-ai`、`/xiaozhi`(审批待办)
## 1. å˜æ›´æ€»è§ˆ
| æ¨¡å— | å¯¹å¤–接口 | æ˜¯å¦éœ€è¦å‰ç«¯æ”¹é€  | ç»“论 |
| --- | --- | --- | --- |
| é”€å”® AI | `POST /sales-ai/chat` | æ˜¯ | è´¢åŠ¡å£å¾„åˆ‡æ¢åˆ°æ–°æ”¶æ¬¾æ¨¡åž‹ï¼Œéƒ¨åˆ† `type` çš„字段语义变化 |
| é‡‡è´­ AI | `POST /purchase-ai/chat` | æ˜¯ | ä»˜æ¬¾/发票/待付款计算切换到新财务链路,统计值从占位改为真实值 |
| ç”Ÿäº§ AI | `POST /manufacturing-ai/chat` | å¦ | å·²æ ¸æŸ¥ï¼Œæ— æ—§è´¢åŠ¡é€»è¾‘ä¾èµ–ï¼Œæ— å­—æ®µå˜æ›´ |
| å¾…办 AI | `POST /xiaozhi/chat` | å¦ | å·²æ ¸æŸ¥ï¼Œæ— æ—§è´¢åŠ¡é€»è¾‘ä¾èµ–ï¼Œæ— å­—æ®µå˜æ›´ |
## 2. é”€å”® AI å˜æ›´ï¼ˆ`/sales-ai/chat`)
### 2.1 `type = sales_return_list`(销售退款/回款记录)
当前返回数据来源统一为新财务表 `account_sales_collection`,不再走旧收款退货逻辑。
`data.items[]` å…³é”®å­—段:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
| --- | --- | --- |
| id | number | æ”¶æ¬¾è®°å½•ID |
| refundId | string | æ˜ å°„ `collectionNumber`,前端可继续作为“退款/回款单号”展示 |
| collectionNumber | string | æ”¶æ¬¾å•号 |
| paymentMethod | string | æ”¶æ¬¾æ–¹å¼ |
| actualAmount | number | æ”¶æ¬¾é‡‘额(与 `collectionAmount` åŒå€¼ï¼‰ |
| collectionAmount | number | æ”¶æ¬¾é‡‘额(推荐主展示字段) |
| customerId | number | å®¢æˆ·ID |
| remark | string | å¤‡æ³¨ |
| createTime | string | æ”¶æ¬¾æ—¥æœŸï¼ˆyyyy-MM-dd) |
`summary` å¢žé‡å…³æ³¨ï¼š
- `returnAmount`:时间范围内金额汇总(按 `collectionAmount` ç»Ÿè®¡ï¼‰
### 2.2 `type = sales_customer_interaction_list`(客户往来)
当前返回基于新链路:
`account_sales_collection.stock_out_record_ids -> stock_out_record(record_type=13) -> shipping_info -> sales_ledger`
返回约定:
- æ— æ•°æ®æ—¶ï¼š`description = "no_customer_interactions"`
- æœ‰æ•°æ®æ—¶ï¼š`description = "ok"`
`summary` å…³é”®å­—段:
- `totalReceiptAmount`
- `customerCount`
`data.items[]` å…³é”®å­—段:
- `salesLedgerId`
- `salesContractNo`
- `customerName`
- `projectName`
- `receiptPaymentDate`
- `receiptPaymentAmount`
- `receiptPaymentType`
- `collectionNumber`
- `registrant`
- `remark`
### 2.3 `type = sales_ledger_list`(销售台账)
字段结构不变,但金额口径已切换:
- `receivedAmount` ç”±æ–°æ”¶æ¬¾æ¨¡åž‹æ±‡æ€»å¾—到;
- `pendingAmount = max(0, invoicedAmount - receivedAmount)`;
- è‹¥æ”¶æ¬¾è®°å½•未显式关联台账,则按客户维度兜底归集。
前端改造建议:
- ä¸æ”¹å­—段名;
- é‡ç‚¹å›žå½’“已收金额/待回款金额”是否与财务台账一致。
## 3. é‡‡è´­ AI å˜æ›´ï¼ˆ`/purchase-ai/chat`)
### 3.1 `type = purchase_stats`(采购统计)
以下字段已从占位值改为真实统计值:
- `summary.paymentCount`
- `summary.invoiceCount`
- `summary.paymentAmount`
- `summary.invoiceAmount`
发票金额口径:
- ä¼˜å…ˆ `taxInclusivePrice`
- è‹¥ä¸ºç©º/0,则使用 `taxExclusivelPrice + taxPrice`
### 3.2 `type = purchase_pending_payment_list`(待付款采购单)
核心计算已切换到新财务链路:
`account_purchase_payment -> account_payment_application -> stock_in_record -> (purchase_ledger / quality_inspect) -> purchase_ledger_id`
映射规则:
1. `stock_in_record.record_type = 7`:`record_id` ç›´æŽ¥è§†ä¸º `purchase_ledger_id`
2. `stock_in_record.record_type = 10`:通过 `quality_inspect.id = record_id` å– `quality_inspect.purchase_ledger_id`
金额字段口径:
- `paidAmount`:新链路累计已付款金额
- `pendingAmount = contractAmount - paidAmount`(<=0 çš„记录不返回)
`summary` å…³é”®å­—段(均为真实值):
- `pendingOrderCount`
- `totalContractAmount`
- `totalPaidAmount`
- `totalPendingAmount`
### 3.3 æ•°æ®æ¸…洗修复
已修复 `record_type` å¸¦ç©ºæ ¼å¯¼è‡´çš„æ˜ å°„丢失问题(后端统一 `trim()` åŽå†åˆ¤æ–­ `7/10`)。
## 4. ç”Ÿäº§ AI / å¾…办 AI æ ¸æŸ¥ç»“论
已核查以下模块代码,未发现旧财务逻辑耦合点:
- `ManufacturingAgentTools`(生产)
- `ApproveTodoTools`(待办审批)
结论:
- å¯¹å¤– `type` ä¸Žå­—段结构无变更;
- å‰ç«¯æ— éœ€åšå…¼å®¹æ”¹é€ ï¼Œä»…需做一次回归验证。
## 5. å‰ç«¯è”调要点
1. `/sales-ai/chat`、`/purchase-ai/chat` ç»§ç»­æŒ‰ SSE æ–‡æœ¬æµæ‹¼æŽ¥åŽåš JSON è§£æžã€‚
2. æŒ‰ `type` è·¯ç”±æ¸²æŸ“,不要仅依赖 `description` æ–‡æ¡ˆã€‚
3. `sales_customer_interaction_list` éœ€å…¼å®¹ `description` æžšä¸¾ï¼š`ok` / `no_customer_interactions`。
4. `sales_return_list` é‡‘额展示统一用 `collectionAmount`(`actualAmount` ä¿ç•™å…¼å®¹ï¼‰ã€‚
5. `purchase_pending_payment_list` çš„æ±‡æ€»å¡ç‰‡è¯·ç›´æŽ¥è¯»å– `summary.totalPendingAmount` ç­‰å­—段,不再前端二次估算。
## 6. å›žå½’清单(建议)
### é”€å”®
1. æé—®ï¼šâ€œè¿‘30天哪个订单回款最少”
   - æ ¡éªŒ `sales_ledger_list` çš„ `receivedAmount/pendingAmount`。
2. æé—®ï¼šâ€œæŸ¥è¯¢æœ¬æœˆé”€å”®é€€æ¬¾â€
   - æ ¡éªŒ `sales_return_list` çš„ `collectionNumber/collectionAmount/returnAmount`。
3. æé—®ï¼šâ€œæŸ¥è¯¢æœ¬æœˆå®¢æˆ·å¾€æ¥â€
   - æ ¡éªŒ `sales_customer_interaction_list` çš„ `totalReceiptAmount/customerCount`。
### é‡‡è´­
1. æé—®ï¼šâ€œç»Ÿè®¡æœ¬æœˆé‡‡è´­æ•°æ®â€
   - æ ¡éªŒ `purchase_stats` çš„ `paymentCount/invoiceCount/paymentAmount/invoiceAmount` éžå›ºå®š0。
2. æé—®ï¼šâ€œåˆ—出待付款采购单”
   - æ ¡éªŒ `purchase_pending_payment_list` çš„ `paidAmount/pendingAmount` ä¸Žè´¢åŠ¡å®žé™…ä¸€è‡´ã€‚
### ç”Ÿäº§/待办
1. ç”Ÿäº§æé—®ï¼šâ€œæŸ¥è¯¢æœ¬å‘¨è®¾å¤‡ç»´ä¿®è®°å½•”
2. å¾…办提问:“查询我的待审批列表”
   - æ ¡éªŒè¿”回结构与升级前一致(无字段破坏)。
doc/20260522_²É¹ºÌ¨ÕËÈë¿â״̬_ÏúÊÛ²úÆ·Èë¿âÉóºË״̬ǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,127 @@
# é‡‡è´­å…¥åº“状态前端联调文档
更新时间:2026-05-22
适用版本:本次后端变更后
## 1. å˜æ›´èŒƒå›´
1. `GET /purchaseLedger/listPage`
   - æ–°å¢žæŸ¥è¯¢æ¡ä»¶ï¼š`stockInStatus`(入库状态)
   - æ–°å¢žè¿”回字段:`stockInStatus`(入库状态)
2. `GET /salesLedgerProduct/list`
   - æ–°å¢žè¿”回字段:`stockInApprovalStatus`(每个产品的入库审核状态)
---
## 2. å…¥åº“状态枚举(两接口一致)
- `待入库`
- `入库中`
- `完全入库`
说明:前端筛选值请直接使用以上中文枚举值。
---
## 3. æŽ¥å£ä¸€ï¼š`GET /purchaseLedger/listPage`
### 3.1 æ–°å¢žè¯·æ±‚参数
- `stockInStatus`:`string`,可选
  å¯ä¼ å€¼ï¼š`待入库` / `入库中` / `完全入库`
### 3.2 æ–°å¢žè¿”回字段
- `stockInStatus`:`string`,采购台账维度入库状态
### 3.3 çŠ¶æ€è®¡ç®—è§„åˆ™ï¼ˆé‡‡è´­å°è´¦ç»´åº¦ï¼‰
以该采购台账下 `sales_ledger_product.type = 2` çš„采购产品为计算范围:
1. å…¨éƒ¨äº§å“éƒ½è¾¾åˆ°â€œå®Œå…¨å…¥åº“” => å°è´¦çŠ¶æ€ `完全入库`
2. è‡³å°‘有一个产品存在“审核通过入库”,但未全部完全入库 => å°è´¦çŠ¶æ€ `入库中`
3. æ²¡æœ‰ä»»ä½•产品存在“审核通过入库” => å°è´¦çŠ¶æ€ `待入库`
“审核通过入库”统计口径:`stock_in_record.approval_status = 1`,并按以下来源溯源:
- `record_type = 7`(采购-入库):按采购台账+产品关联统计
- `record_type = 10`(采购-质检-合格入库):通过 `quality_inspect` å›žæº¯åˆ°é‡‡è´­å°è´¦+产品统计
### 3.4 è¿”回示例(节选)
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "id": 1201,
        "purchaseContractNumber": "CG20260522001",
        "supplierName": "XX供应商",
        "stockInStatus": "入库中"
      }
    ],
    "total": 1
  }
}
```
---
## 4. æŽ¥å£äºŒï¼š`GET /salesLedgerProduct/list`
### 4.1 æ–°å¢žè¿”回字段
- `stockInApprovalStatus`:`string`,当前产品行的入库审核状态
### 4.2 çŠ¶æ€è®¡ç®—è§„åˆ™ï¼ˆäº§å“ç»´åº¦ï¼‰
仅当产品 `type = 2`(采购产品)时计算并返回:
1. å®¡æ ¸é€šè¿‡å…¥åº“数量 `<= 0` => `待入库`
2. å®¡æ ¸é€šè¿‡å…¥åº“数量 `>= äº§å“é‡‡è´­æ•°é‡` => `完全入库`
3. å…¶ä»–情况 => `入库中`
其中“审核通过入库数量”统计同样基于:
- `stock_in_record.approval_status = 1`
- æ¥æº `record_type = 7 / 10` çš„æº¯æºå…³è”逻辑
`type != 2` çš„产品,该字段返回 `null`。
### 4.3 è¿”回示例(节选)
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": [
    {
      "id": 5566,
      "type": 2,
      "productCategory": "铜材",
      "specificationModel": "T2-30x3",
      "quantity": 100,
      "stockInApprovalStatus": "待入库"
    }
  ]
}
```
---
## 5. å‰ç«¯æ”¹é€ å»ºè®®
1. é‡‡è´­å°è´¦åˆ—表新增“入库状态”筛选项,值固定:`待入库/入库中/完全入库`。
2. é‡‡è´­å°è´¦åˆ—表新增“入库状态”列,展示 `stockInStatus`。
3. é‡‡è´­äº§å“åˆ—表新增“入库审核状态”列,展示 `stockInApprovalStatus`。
4. ç©ºå€¼ï¼ˆå¦‚ `type != 2`)建议展示为 `--`。
---
## 6. è”调检查清单
1. `/purchaseLedger/listPage` ä¸ä¼  `stockInStatus`:应正常返回全部数据,并带 `stockInStatus`。
2. `/purchaseLedger/listPage?stockInStatus=待入库`:仅返回待入库台账。
3. `/purchaseLedger/listPage?stockInStatus=入库中`:仅返回入库中台账。
4. `/purchaseLedger/listPage?stockInStatus=完全入库`:仅返回完全入库台账。
5. `/salesLedgerProduct/list` åœ¨é‡‡è´­äº§å“ï¼ˆ`type=2`)下返回 `stockInApprovalStatus`。
6. `/salesLedgerProduct/list` åœ¨éžé‡‡è´­äº§å“ï¼ˆ`type!=2`)下 `stockInApprovalStatus` ä¸º `null`。
doc/20260522_Ê×Ò³²ÆÎñ½Ó¿ÚÉý¼¶Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
# é¦–页财务接口升级前端变更文档
更新时间:2026-05-22
适用模块:首页(`/home`)
## 1. å˜æ›´æ¦‚览
本次为 **兼容式升级**,接口 URL、请求参数、返回字段保持不变。
主要变更是首页财务数据从占位/半成品逻辑,切换为按财务真实数据口径计算。
涉及接口:
1. `GET /home/statisticsReceivablePayable`
2. `GET /home/monthlyIncome`
3. `GET /home/monthlyExpenditure`
## 2. å‚数说明(无新增)
### 2.1 `GET /home/statisticsReceivablePayable`
- `type`:`1` æœ¬å‘¨ï¼Œ`2` æœ¬æœˆï¼Œ`3` æœ¬å­£åº¦ï¼ˆé»˜è®¤ `1`)
### 2.2 `GET /home/monthlyIncome`
- æ— å‚æ•°
### 2.3 `GET /home/monthlyExpenditure`
- æ— å‚æ•°
## 3. è¿”回字段口径变更
### 3.1 åº”收应付统计 `statisticsReceivablePayable`
返回字段不变:
- `receivableMoney`
- `payableMoney`
- `advanceMoney`
- `prepayMoney`
新口径:
- `receivableMoney = max(销售合同金额合计 - æ”¶æ¬¾é‡‘额合计, 0)`
- `payableMoney = max(采购合同金额合计 - ä»˜æ¬¾é‡‘额合计, 0)`
- `advanceMoney = æ”¶æ¬¾é‡‘额合计`
- `prepayMoney = ä»˜æ¬¾é‡‘额合计`
以上金额均按 `type` å¯¹åº”时间范围统计,保留两位小数。
返回示例:
```json
{
  "receivableMoney": 128000.00,
  "payableMoney": 76000.00,
  "advanceMoney": 42000.00,
  "prepayMoney": 31000.00
}
```
### 3.2 æœˆåº¦æ”¶å…¥ `monthlyIncome`
返回字段不变:
- `monthlyIncome`
- `collectionRate`
- `overdueNum`
- `overdueRate`
新口径:
- `monthlyIncome`:当月收款合计
- `collectionRate`:`当月收款合计 / å½“月销售合同金额合计 * 100`
- `overdueNum`:历史应收对账单(`account_statement.account_type=1`)中,早于当月且 `closing_balance > 0` çš„æ•°é‡
- `overdueRate`:`overdueNum / åŽ†å²åº”æ”¶å¯¹è´¦å•æ€»æ•° * 100`
返回示例:
```json
{
  "monthlyIncome": 89500.00,
  "collectionRate": "62.80",
  "overdueNum": 4,
  "overdueRate": "18.18"
}
```
### 3.3 æœˆåº¦æ”¯å‡º `monthlyExpenditure`
返回字段不变:
- `monthlyExpenditure`
- `paymentRate`
- `grossProfit`
- `profitMarginRate`
新口径:
- `monthlyExpenditure`:当月付款合计
- `paymentRate`:`当月付款合计 / å½“月采购合同金额合计 * 100`
- `grossProfit`:`当月收款合计 - å½“月付款合计`
- `profitMarginRate`:`grossProfit / å½“月收款合计 * 100`
返回示例:
```json
{
  "monthlyExpenditure": 73400.00,
  "paymentRate": "57.34",
  "grossProfit": 16100.00,
  "profitMarginRate": "17.99"
}
```
## 4. å‰ç«¯è”调说明
1. å‰ç«¯å­—段映射无需调整,可直接沿用现有解析逻辑。
2. ç™¾åˆ†æ¯”字段仍为不带 `%` çš„字符串,前端如需展示 `%` è¯·ç»§ç»­å‰ç«¯æ‹¼æŽ¥ã€‚
3. æœ¬æ¬¡åŽç«¯è¿”回值由真实财务数据驱动,建议重点回归卡片汇总与趋势图的数值联动。
doc/20260523_ ЭͬÉóÅúÐÂÔö³ö²îʱ¼äºÍ½áÊøÊ±¼ä.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
ALTER TABLE approve_process
    ADD COLUMN start_date_time datetime DEFAULT NULL COMMENT '出差开始时间',
    ADD COLUMN end_date_time   datetime DEFAULT NULL COMMENT '出差结束时间';
doc/20260523_ÐÂÔöÈë¿â¼Ç¼±íµÄÔ¤¾¯ÊýÁ¿.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
alter table stock_in_record
    add warn_num decimal(16, 4) null comment '预警数量';
doc/add_rating_evaluation_fields.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
ALTER TABLE after_sales_service
    ADD COLUMN rating DECIMAL(2, 1) COMMENT '评分(1-5分,可保留1位小数)' AFTER dis_date,
ADD COLUMN evaluation VARCHAR(1000) COMMENT '评价内容描述' AFTER rating;
doc/alter_table_measuring_instrument_ledger.sql
ÎļþÒÑɾ³ý
doc/financial-ai-front-integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,192 @@
# è´¢åŠ¡æ™ºèƒ½ä½“å‰ç«¯è”è°ƒæ–‡æ¡£
## 1. æ¨¡å—说明
财务智能体后端已新增统一入口 `financial-ai`,用于业财一体化分析,覆盖:
- æ™ºèƒ½æˆæœ¬æ ¸ç®—
- è®¢å•利润分析
- åº“存资金分析
- åº”收应付与现金流预测
- ç»è¥å¼‚常预警
- AI ç»è¥é©¾é©¶èˆ±
- æ—¥æŠ¥/周报自动生成
- è´¢åŠ¡çŸ¥è¯†æ£€ç´¢ï¼ˆè½»é‡ RAG ä¸Šä¸‹æ–‡ï¼‰
接口采用 **SSE æµå¼è¾“出**,工具命中时返回结构化 JSON å­—符串。
## 2. æŽ¥å£æ¸…单
### 2.1 å¯¹è¯æŽ¥å£ï¼ˆSSE)
- `POST /financial-ai/chat`
- `Content-Type: application/json`
- `Accept: text/stream;charset=utf-8`
请求体:
```json
{
  "memoryId": "finance-uuid-001",
  "message": "查询近30天亏损订单"
}
```
字段说明:
- `memoryId`:会话唯一标识(前端生成 UUID,单会话复用)
- `message`:自然语言问题
---
### 2.2 ä¼šè¯åˆ—表
- `GET /financial-ai/history/sessions`
---
### 2.3 ä¼šè¯æ¶ˆæ¯
- `GET /financial-ai/history/messages/{memoryId}`
---
### 2.4 åˆ é™¤ä¼šè¯
- `DELETE /financial-ai/history/{memoryId}`
## 3. SSE è¿”回处理规范
### 3.1 è¿”回形态
- æ™®é€šé—®ç­”:流式文本片段
- å·¥å…·å‘½ä¸­ï¼šå®Œæ•´ JSON å­—符串(通常一次性输出,也可能分片)
前端建议处理流程:
1. å°† SSE åˆ†ç‰‡æŒ‰é¡ºåºæ‹¼æŽ¥æˆ `rawText`
2. å¯¹ `rawText` å°è¯• `JSON.parse`
3. è‹¥å¯è§£æžï¼ŒæŒ‰ `type` åˆ†å‘渲染图表/表格
4. è‹¥ä¸å¯è§£æžï¼ŒæŒ‰æ™®é€šæ–‡æœ¬å±•示
### 3.2 ç»“构化 JSON é€šç”¨æ ¼å¼
```json
{
  "success": true,
  "type": "financial_order_profit_analysis",
  "description": "已完成订单利润分析",
  "summary": {},
  "data": {},
  "charts": {}
}
```
字段说明:
- `type`:结果类型(前端渲染分发键)
- `summary`:头部指标
- `data`:表格明细/建议列表
- `charts`:ECharts `option` æ•°æ®
## 4. type ä¸Žå‰ç«¯é¡µé¢æ˜ å°„
建议按 `type` å»ºç«‹æ¸²æŸ“策略:
- `financial_cost_accounting`:成本核算页
- `financial_order_profit_analysis`:订单利润页
- `financial_inventory_capital_analysis`:库存资金页
- `financial_cashflow_forecast`:现金流页
- `financial_business_anomaly_warning`:风险预警页
- `financial_business_cockpit`:经营驾驶舱
- `financial_operation_report`:日报周报页
- `financial_rag_knowledge`:知识检索/口径说明卡片
## 5. å…³é”®æ•°æ®å­—段(联调重点)
### 5.1 æˆæœ¬/利润类
- `data.orders[]`:
  - `salesContractNo`
  - `customerName`
  - `revenue`
  - `materialCost`
  - `laborCost`
  - `depreciationCost`
  - `scrapCost`
  - `totalCost`
  - `profit`
  - `profitRate`
  - `riskLevel`
  - `reasons`
  - `suggestion`
### 5.2 åº“存资金类
- `data.items[]`:
  - `productName`
  - `model`
  - `quantity`
  - `inventoryValue`
  - `stagnantDays`
  - `overstock`
  - `riskLevel`
### 5.3 çŽ°é‡‘æµç±»
- `data.actualMonthly[]` / `data.forecastMonthly[]`:
  - `month`
  - `income`
  - `expense`
  - `netFlow`
- `data.receivableRiskTop[]` / `data.payablePressureTop[]`
### 5.4 å¼‚常预警类
- `data.items[]`:
  - `riskLevel`
  - `type`
  - `message`
  - `detail`
### 5.5 æŠ¥å‘Šç±»
- `data.headline`
- `data.conclusions[]`
- `data.riskSuggestions[]`
- `data.orderProfitTop[]`
## 6. å›¾è¡¨è”调规范
`charts` å†…字段均为 ECharts `option`,可直接喂给图表组件。
常见字段:
- æŸ±çŠ¶å›¾ï¼š`orderProfitBarOption` / `processCostBarOption` / `inventoryValueTopOption`
- é¥¼å›¾ï¼š`costCompositionPieOption` / `inventoryAgingPieOption` / `anomalyLevelPieOption`
- è¶‹åŠ¿å›¾ï¼š`cashFlowTrendOption`
- ä»ªè¡¨ç›˜ï¼š`fundGapGaugeOption` / `inventoryTurnoverGauge`
## 7. æŽ¨èå‰ç«¯é—®å¥ï¼ˆå›žå½’测试)
1. `查看本月经营驾驶舱`
2. `查询近30天亏损订单`
3. `分析近30天库存资金占用`
4. `预测未来3个月现金流`
5. `生成本周经营周报`
6. `为什么利润下降`
7. `哪个客户最赚钱`
8. `哪个工序成本最高`
## 8. å¼‚常与兜底处理
- `memoryId` ä¸ºç©ºï¼šè¿”回文本 `memoryId不能为空`
- `message` ä¸ºç©ºï¼šè¿”回文本 `message不能为空`
- æ— æ•°æ®åœºæ™¯ï¼š`success=true` ä¸” `data.items=[]`,前端按空态展示
- éž JSON æµè¿”回:按普通聊天文本展示
## 9. è”调建议
1. å…ˆåš `type` åˆ†å‘器(保证所有结构化结果可落地)
2. å†åšæ‘˜è¦å¡ç‰‡ï¼ˆ`summary`)+ è¡¨æ ¼ï¼ˆ`data`)+ å›¾è¡¨ï¼ˆ`charts`)
3. æœ€åŽè¡¥ä¼šè¯åŽ†å²ä¸Žåˆ é™¤èƒ½åŠ›ï¼Œå½¢æˆå®Œæ•´å¯¹è¯é—­çŽ¯
doc/update_after_sales_service.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
ALTER TABLE `after_sales_service`
    ADD COLUMN `product_model_quantities` varchar(255) DEFAULT NULL COMMENT '产品型号对应售后数量';
pom.xml
@@ -50,28 +50,13 @@
        <!--        <spring-security.version>5.7.12</spring-security.version>-->
        <!--        <spring-framework.version>5.3.39</spring-framework.version>-->
        <mybatis-plus.version>3.5.16</mybatis-plus.version>
        <getui-sdk.version>1.0.7.0</getui-sdk.version>
        <jsqlparser.version>4.9</jsqlparser.version>
        <thumbnailator.version>0.4.20</thumbnailator.version>
        <langchain4j.version>1.0.0-beta3</langchain4j.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-bom</artifactId>
                <version>${langchain4j.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-community-bom</artifactId>
                <version>${langchain4j.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
@@ -133,10 +118,6 @@
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <!-- pool å¯¹è±¡æ±  -->
        <dependency>
@@ -144,38 +125,7 @@
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-pinecone</artifactId>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-reactor</artifactId>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-mcp</artifactId>
        </dependency>
        <!-- Mysql驱动包 -->
        <dependency>
@@ -403,12 +353,6 @@
            <version>3.3.3</version>
        </dependency>
        <dependency>
            <groupId>com.getui.push</groupId>
            <artifactId>restful-sdk</artifactId>
            <version>${getui-sdk.version}</version>
            <scope>compile</scope>
        </dependency>
        <!--hutool工具包-->
        <dependency>
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/AccountDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/bean/dto/AccountDto2.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/bean/dto/AccountDto3.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/bean/dto/AccountReportDto.java
ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/ReportDateDto.java ÐÞ¸Ä
@@ -1,17 +1,16 @@
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;
/**
 * @author :yys
 * @date : 2026/1/16 16:57
 */
@Data
public class ReportDateDto {
@Schema(name = "AccountReportDto", description = "财务报表--日期参数")
public class AccountReportDto {
    /**
     * å¼€å§‹æ—¶é—´
@@ -26,15 +25,5 @@
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate entryDateEnd;
    /**
     * å¼€å§‹æœˆä»½
     */
    private Integer startMonth;
    /**
     * ç»“束月份
     */
    private Integer endMonth;
}
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/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
@@ -5,7 +5,7 @@
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
@@ -5,7 +5,7 @@
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
@@ -5,7 +5,7 @@
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
@@ -5,7 +5,7 @@
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/AccountReportVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package com.ruoyi.account.bean.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
@Schema(name = "AccountReportVo", description = "财务报表--返回参数")
public class AccountReportVo {
    @Schema(description = "总营收")
    private BigDecimal totalIncome;
    @Schema(description = "总支出")
    private BigDecimal totalExpense;
    @Schema(description = "应收账款")
    private BigDecimal accountsReceivable;
    @Schema(description = "应付账款")
    private BigDecimal accountsPayable;
    @Schema(description = "净收入")
    private BigDecimal netRevenue;
    // --- æŠ˜çº¿å›¾ï¼šæœˆåº¦è¶‹åŠ¿æ•°æ® ---
    @Schema(description = "月度趋势数据列表")
    private List<MonthlyTrendVO> monthlyTrendList;
    // --- æŸ±çŠ¶å›¾ï¼šåº”æ”¶åº”ä»˜æœˆåº¦æ•°æ® ---
    @Schema(description = "应收应付月度数据列表")
    private List<ReceivablePayableVO> receivablePayableList;
    @Data
    @Schema(description = "月度趋势VO(折线图用)")
    public static class MonthlyTrendVO {
        @Schema(description = "月份,格式:yyyy-MM")
        private String month;
        @Schema(description = "当月营收")
        private BigDecimal income;
        @Schema(description = "当月支出")
        private BigDecimal expense;
        @Schema(description = "当月净利润")
        private BigDecimal profit;
    }
    @Data
    @Schema(description = "应收应付月度VO(柱状图用)")
    public static class ReceivablePayableVO {
        @Schema(description = "月份,格式:yyyy-MM")
        private String month;
        @Schema(description = "应收账款金额")
        private BigDecimal receivable;
        @Schema(description = "应付账款金额")
        private BigDecimal payable;
    }
}
src/main/java/com/ruoyi/account/bean/vo/StatementAccountVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.ruoyi.account.bean.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.account.pojo.AccountStatementDetails;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(name = "StatementAccountVo", description = "财务管理--对账单详情(返回)")
@ExcelIgnoreUnannotated
public class StatementAccountVo extends AccountStatement {
    //客户名称(应收是客户,应付是供应商)
    @Schema(description = "客户名称")
    @Excel(name = "客户名称")
    private String customerName;
    //对账明细
    @Schema(description = "对账明细")
    private List<AccountStatementDetails> accountStatementDetails;
}
src/main/java/com/ruoyi/account/bean/vo/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
@@ -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/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
@@ -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/controller/AccountExpenseController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/controller/AccountFileController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/controller/AccountIncomeController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/controller/AccountStatementController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,71 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.StatementAccountDto;
import com.ruoyi.account.bean.vo.StatementAccountVo;
import com.ruoyi.account.service.AccountStatementService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
/**
 * <p>
 *  å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 09:42:47
 */
@RestController
@RequestMapping("/accountStatement")
@RequiredArgsConstructor
@Tag(name = "财务管理-对账单")
public class AccountStatementController {
    private final AccountStatementService accountStatementService;
    @GetMapping("/getAccountStatementDetailsByMonth")
    @Log(title = "根据客户和月份查询对账单明细", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--根据客户和月份查询对账单明细")
    public R getAccountStatementDetailsByMonth(StatementAccountDto statementAccountDto) {
        return R.ok(accountStatementService.getAccountStatementDetailsByMonth(statementAccountDto));
    }
    @PostMapping("/addAccountStatement")
    @Log(title = "新增对账单", businessType = BusinessType.INSERT)
    @Operation(summary = "财务管理--新增对账单")
    public R addAccountStatement(@RequestBody StatementAccountVo statementAccountVo) {
        return R.ok(accountStatementService.addAccountStatement(statementAccountVo));
    }
    @DeleteMapping("/deleteAccountStatement")
    @Log(title = "删除对账单", businessType = BusinessType.DELETE)
    @Operation(summary = "财务管理--删除对账单")
    public R deleteAccountStatement(@RequestParam("ids") Long[] ids) {
        return R.ok(accountStatementService.deleteAccountStatement(Arrays.asList(ids)));
    }
    @GetMapping("/listPageAccountStatement")
    @Log(title = "对账单台账", businessType = BusinessType.OTHER)
    @Operation(summary = "财务管理--对账单台账")
    public R<IPage<StatementAccountVo>> listPageAccountStatement(Page page, StatementAccountDto statementAccountDto) {
        IPage<StatementAccountVo> listPage = accountStatementService.listPageAccountStatement(page,statementAccountDto);
        return R.ok(listPage);
    }
    @PostMapping("/exportAccountStatement")
    @Operation(summary = "导出对账单文件")
    @Log(title = "导出对账单文件", businessType = BusinessType.EXPORT)
    public void exportAccountStatement(HttpServletResponse response, StatementAccountDto statementAccountDto) {
        accountStatementService.exportAccountStatement(response,statementAccountDto);
    }
}
src/main/java/com/ruoyi/account/controller/AccountingController.java
@@ -1,12 +1,16 @@
package com.ruoyi.account.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.service.impl.AccountingServiceImpl;
import com.ruoyi.account.bean.dto.AccountReportDto;
import com.ruoyi.account.service.AccountingService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -19,11 +23,11 @@
@Tag(name = "会计核算")
@RestController
@RequestMapping("/accounting")
@AllArgsConstructor
@RequiredArgsConstructor
public class AccountingController extends BaseController {
    private AccountingServiceImpl accountingService;
    private final AccountingService accountingService;
    @Operation(summary = "总计")
    @GetMapping("/total")
@@ -43,4 +47,14 @@
        return accountingService.calculateDepreciation(page,year);
    }
    /*****************************************财务报表******************************************************************************/
    @GetMapping("/accountStatementDetailsByMonth")
    @Log(title = "财务报表", businessType = BusinessType.OTHER)
    @Operation(summary = "财务报表")
    public R getAccountStatementDetailsByMonth(AccountReportDto accountReportDto) {
        return R.ok(accountingService.getAccountStatementDetailsByMonth(accountReportDto));
    }
}
src/main/java/com/ruoyi/account/controller/BorrowInfoController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/controller/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/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,28 @@
package com.ruoyi.account.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.StatementAccountDto;
import com.ruoyi.account.bean.vo.StatementAccountVo;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.purchase.dto.VatDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 *  Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 09:42:47
 */
@Mapper
public interface AccountStatementMapper extends BaseMapper<AccountStatement> {
    IPage<StatementAccountVo> listPageAccountStatement(Page page, @Param("req") StatementAccountDto statementAccountDto);
    IPage<VatDto> selectVatDtoPage(Page page, @Param("month") String month);
}
src/main/java/com/ruoyi/account/mapper/BorrowInfoMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/mapper/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
@@ -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/AccountingService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.account.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.AccountReportDto;
import com.ruoyi.account.bean.vo.AccountReportVo;
import com.ruoyi.framework.web.domain.AjaxResult;
public interface AccountingService {
    AjaxResult total(Integer year);
    AjaxResult deviceTypeDistribution(Integer year);
    AjaxResult calculateDepreciation(Page page, Integer year);
    AccountReportVo getAccountStatementDetailsByMonth(AccountReportDto accountReportDto);
}
src/main/java/com/ruoyi/account/service/BorrowInfoService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/impl/AccountExpenseServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/impl/AccountFileServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/impl/AccountIncomeServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/impl/AccountStatementDetailsServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.account.service.impl;
import com.ruoyi.account.pojo.AccountStatementDetails;
import com.ruoyi.account.mapper.AccountStatementDetailsMapper;
import com.ruoyi.account.service.AccountStatementDetailsService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * è´¢åŠ¡ç®¡ç†--对账单明细 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 10:12:42
 */
@Service
public class AccountStatementDetailsServiceImpl extends ServiceImpl<AccountStatementDetailsMapper, AccountStatementDetails> implements AccountStatementDetailsService {
}
src/main/java/com/ruoyi/account/service/impl/AccountStatementServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,298 @@
package com.ruoyi.account.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.account.bean.dto.StatementAccountDto;
import com.ruoyi.account.bean.dto.purchase.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto;
import com.ruoyi.account.bean.dto.sales.SalesOutboundDto;
import com.ruoyi.account.bean.dto.sales.SalesReturnDto;
import com.ruoyi.account.bean.vo.StatementAccountVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.bean.vo.sales.SalesReturnVo;
import com.ruoyi.account.mapper.AccountStatementDetailsMapper;
import com.ruoyi.account.mapper.AccountStatementMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.account.pojo.AccountStatementDetails;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.account.service.AccountStatementService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
 * <p>
 *  æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-19 09:42:47
 */
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class AccountStatementServiceImpl extends ServiceImpl<AccountStatementMapper, AccountStatement> implements AccountStatementService {
    private final AccountStatementMapper accountStatementMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final StockOutRecordMapper stockOutRecordMapper;
    private final StockInRecordMapper stockInRecordMapper;
    private final ReturnManagementMapper returnManagementMapper;
    private final AccountStatementDetailsMapper accountStatementDetailsMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
    private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd");
    @Override
    public StatementAccountVo getAccountStatementDetailsByMonth(StatementAccountDto statementAccountDto) {
        //对账月份转换成开始日期和结束日期区间
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
        YearMonth yearMonth = YearMonth.parse(statementAccountDto.getStatementMonth(), formatter);
        statementAccountDto.setStartDate(yearMonth.atDay(1));
        statementAccountDto.setEndDate(yearMonth.atEndOfMonth());
        if (statementAccountDto.getAccountType() == 1){
            //应收对账--Customer
            return getAccountStatementDetailsByCustomerAndMonth(statementAccountDto);
        }else {
            //应付对账--SupplierManage
            return getAccountStatementDetailsBySupplierAndMonth(statementAccountDto);
        }
    }
    @Override
    public boolean addAccountStatement(StatementAccountVo statementAccountVo) {
        //同一客户或者同一供应商,一个月份只能有一个对账单
        List<AccountStatement> accountStatements = accountStatementMapper.selectList(Wrappers.<AccountStatement>lambdaQuery()
                .eq(AccountStatement::getStatementMonth, statementAccountVo.getStatementMonth())
                .eq(AccountStatement::getAccountType, statementAccountVo.getAccountType())
                .eq(AccountStatement::getCustomerId, statementAccountVo.getCustomerId()));
        if (CollectionUtils.isNotEmpty(accountStatements)){
            throw new ServiceException("同一客户或者同一供应商,一个月份只能有一个对账单");
        }
        AccountStatement accountStatement = new AccountStatement();
        BeanUtils.copyProperties(statementAccountVo, accountStatement);
        accountStatement.setStatementNumber(genStatementAccountNo());
        boolean save = save(accountStatement);
        statementAccountVo.getAccountStatementDetails().stream().forEach(accountStatementDetails -> {
            accountStatementDetails.setAccountStatementId(accountStatement.getId());
            //添加对账单明细
            accountStatementDetailsMapper.insert(accountStatementDetails);
        });
        return save;
    }
    @Override
    public boolean deleteAccountStatement(List<Long> ids) {
        //删除对账单明细
        accountStatementDetailsMapper.delete(Wrappers.<AccountStatementDetails>lambdaQuery().in(AccountStatementDetails::getAccountStatementId, ids));
        return removeByIds(ids);
    }
    @Override
    public IPage<StatementAccountVo> listPageAccountStatement(Page page, StatementAccountDto statementAccountDto) {
        return accountStatementMapper.listPageAccountStatement(page, statementAccountDto);
    }
    @Override
    public void exportAccountStatement(HttpServletResponse response, StatementAccountDto statementAccountDto) {
        List<StatementAccountVo> list = accountStatementMapper.listPageAccountStatement(new Page(1,-1),statementAccountDto).getRecords();
        ExcelUtil<StatementAccountVo> util = new ExcelUtil<>(StatementAccountVo.class);
        util.exportExcel(response, list , "对账单");
    }
    //根据客户和月份获取对账详情(销售)
    private StatementAccountVo getAccountStatementDetailsByCustomerAndMonth(StatementAccountDto statementAccountDto) {
        StatementAccountVo statementAccountVo = new StatementAccountVo();
        statementAccountVo.setAccountType(1);//应收对账
        List<AccountStatementDetails> accountStatementDetailsList = new ArrayList<>();
        /*查询出库明细*/
        SalesOutboundDto salesOutboundDto = new SalesOutboundDto();
        salesOutboundDto.setCustomerId(statementAccountDto.getCustomerId());
        salesOutboundDto.setStartDate(statementAccountDto.getStartDate());
        salesOutboundDto.setEndDate(statementAccountDto.getEndDate());
        List<SalesOutboundVo> salesOutboundVos = stockOutRecordMapper.listPageAccountSales(new Page(1, -1), salesOutboundDto).getRecords();
        salesOutboundVos.stream().forEach(salesOutboundVo -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=出库日期
            accountStatementDetails.setOccurrenceDate(salesOutboundVo.getShippingDate());
            //单据编号=出库单号
            accountStatementDetails.setReceiptNumber(salesOutboundVo.getOutboundBatches());
            //类型=出库
            accountStatementDetails.setType(1);
            //金额=出库金额
            accountStatementDetails.setAmount(salesOutboundVo.getOutboundAmount());
            //备注
            accountStatementDetails.setRemark("产品销售出库,产品:"+salesOutboundVo.getProductName());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        /*查询收款明细*/
        List<AccountSalesCollection> accountSalesCollections = accountSalesCollectionMapper.selectList(Wrappers.<AccountSalesCollection>lambdaQuery()
                .eq(AccountSalesCollection::getCustomerId, statementAccountDto.getCustomerId())
                .between(AccountSalesCollection::getCollectionDate, statementAccountDto.getStartDate(), statementAccountDto.getEndDate()));
        accountSalesCollections.stream().forEach(accountSalesCollection -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=收款日期
            accountStatementDetails.setOccurrenceDate(accountSalesCollection.getCollectionDate());
            //单据编号=收款单号
            accountStatementDetails.setReceiptNumber(accountSalesCollection.getCollectionNumber());
            //类型=收款
            accountStatementDetails.setType(3);
            //金额=收款金额
            accountStatementDetails.setAmount(accountSalesCollection.getCollectionAmount());
            //备注
            accountStatementDetails.setRemark("客户回款,备注:"+accountSalesCollection.getRemark());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        /*查询退货明细*/
        SalesReturnDto salesReturnDto = new SalesReturnDto();
        salesReturnDto.setCustomerId(statementAccountDto.getCustomerId());
        salesReturnDto.setStartDate(statementAccountDto.getStartDate());
        salesReturnDto.setEndDate(statementAccountDto.getEndDate());
        List<SalesReturnVo> salesReturnVos = returnManagementMapper.listPageAccountSalesReturn(new Page(1, -1), salesReturnDto).getRecords();
        salesReturnVos.stream().forEach(salesReturnVo -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=退货日期
            accountStatementDetails.setOccurrenceDate(salesReturnVo.getMakeTime().toLocalDate());
            //单据编号=退货单号
            accountStatementDetails.setReceiptNumber(salesReturnVo.getReturnNo());
            //类型=退货
            accountStatementDetails.setType(5);
            //金额=退款金额
            accountStatementDetails.setAmount(salesReturnVo.getRefundAmount());
            //备注
            accountStatementDetails.setRemark("产品退货,原因:"+salesReturnVo.getReturnReason());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        //期初余额=上个月的期末余额
        statementAccountVo.setOpeningBalance(BigDecimal.ZERO);
        List<AccountStatement> accountStatements = accountStatementMapper.selectList(Wrappers.<AccountStatement>lambdaQuery()
                .eq(AccountStatement::getAccountType, 1)
                .eq(AccountStatement::getCustomerId, statementAccountDto.getCustomerId())
                .eq(AccountStatement::getStatementMonth,
                        YearMonth.parse(statementAccountDto.getStatementMonth()).minusMonths(1).toString()));
        if (CollectionUtils.isNotEmpty(accountStatements)){
            statementAccountVo.setOpeningBalance(accountStatements.get(accountStatements.size() - 1).getClosingBalance());
        }
        //本期应收=出库-退货金额累计
        statementAccountVo.setCurrentPlan(salesOutboundVos.stream().map(SalesOutboundVo::getOutboundAmount).reduce(BigDecimal.ZERO, BigDecimal::add)
                .subtract(salesReturnVos.stream().map(SalesReturnVo::getRefundAmount).reduce(BigDecimal.ZERO, BigDecimal::add)));
        //本期收款=收款金额累计
        statementAccountVo.setCurrentActually(accountSalesCollections.stream().map(AccountSalesCollection::getCollectionAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
        //期末余额=期初+应收-收款
        statementAccountVo.setClosingBalance(statementAccountVo.getOpeningBalance().add(statementAccountVo.getCurrentPlan()).subtract(statementAccountVo.getCurrentActually()));
        statementAccountVo.setAccountStatementDetails(accountStatementDetailsList);
        return statementAccountVo;
    }
    //根据供应商和月份获取对账详情(采购)
    private StatementAccountVo getAccountStatementDetailsBySupplierAndMonth(StatementAccountDto statementAccountDto) {
        StatementAccountVo statementAccountVo = new StatementAccountVo();
        statementAccountVo.setAccountType(2);//应付对账
        List<AccountStatementDetails> accountStatementDetailsList = new ArrayList<>();
        /*查询入库明细*/
        PurchaseInboundDto purchaseInboundDto = new PurchaseInboundDto();
        purchaseInboundDto.setSupplierId(statementAccountDto.getCustomerId());
        purchaseInboundDto.setStartDate(statementAccountDto.getStartDate());
        purchaseInboundDto.setEndDate(statementAccountDto.getEndDate());
        List<PurchaseInboundVo> purchaseInboundVos = stockInRecordMapper.listPageAccountPurchase(new Page(1, -1), purchaseInboundDto).getRecords();
        purchaseInboundVos.stream().forEach(purchaseInboundVo -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=入库日期
            accountStatementDetails.setOccurrenceDate(purchaseInboundVo.getInboundDate());
            //单据编号=入库单号
            accountStatementDetails.setReceiptNumber(purchaseInboundVo.getInboundBatches());
            //类型=入库
            accountStatementDetails.setType(2);
            //金额=入库金额
            accountStatementDetails.setAmount(purchaseInboundVo.getInboundAmount());
            //备注
            accountStatementDetails.setRemark("产品采购入库,产品:"+purchaseInboundVo.getProductName());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        /*查询付款明细*/
        List<AccountPurchasePayment> accountPurchasePayments = accountPurchasePaymentMapper.selectList(Wrappers.<AccountPurchasePayment>lambdaQuery()
                .eq(AccountPurchasePayment::getSupplierId, statementAccountDto.getCustomerId())
                .between(AccountPurchasePayment::getPaymentDate, statementAccountDto.getStartDate(), statementAccountDto.getEndDate()));
        accountPurchasePayments.stream().forEach(accountPurchasePayment -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=付款日期
            accountStatementDetails.setOccurrenceDate(accountPurchasePayment.getPaymentDate());
            //单据编号=付款单号
            accountStatementDetails.setReceiptNumber(accountPurchasePayment.getPaymentNumber());
            //类型=付款
            accountStatementDetails.setType(4);
            //金额=付款金额
            accountStatementDetails.setAmount(accountPurchasePayment.getPaymentAmount());
            //备注
            accountStatementDetails.setRemark("支付货款,备注:"+accountPurchasePayment.getRemark());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        /*查询退货明细*/
        PurchaseReturnDto purchaseReturnDto = new PurchaseReturnDto();
        purchaseReturnDto.setSupplierId(statementAccountDto.getCustomerId());
        purchaseReturnDto.setStartDate(statementAccountDto.getStartDate());
        purchaseReturnDto.setEndDate(statementAccountDto.getEndDate());
        List<PurchaseReturnVo> purchaseReturnVos = purchaseReturnOrdersMapper.listPageAccountPurchaseReturn(new Page(1, -1), purchaseReturnDto).getRecords();
        purchaseReturnVos.stream().forEach(purchaseReturnVo -> {
            AccountStatementDetails accountStatementDetails = new AccountStatementDetails();
            //数据日期=退货日期
            accountStatementDetails.setOccurrenceDate(purchaseReturnVo.getPreparedAt().toLocalDate());
            //单据编号=退货单号
            accountStatementDetails.setReceiptNumber(purchaseReturnVo.getReturnNo());
            //类型=退货
            accountStatementDetails.setType(5);
            //金额=退款金额
            accountStatementDetails.setAmount(purchaseReturnVo.getTotalAmount());
            //备注
            accountStatementDetails.setRemark("产品退货,退货方式:"+purchaseReturnVo.getReturnType());
            accountStatementDetailsList.add(accountStatementDetails);
        });
        //期初余额=上个月的期末余额
        statementAccountVo.setOpeningBalance(BigDecimal.ZERO);
        List<AccountStatement> accountStatements = accountStatementMapper.selectList(Wrappers.<AccountStatement>lambdaQuery()
                .eq(AccountStatement::getAccountType, 2)
                .eq(AccountStatement::getCustomerId, statementAccountDto.getCustomerId())
                .eq(AccountStatement::getStatementMonth,
                        YearMonth.parse(statementAccountDto.getStatementMonth()).minusMonths(1).toString()));
        if (CollectionUtils.isNotEmpty(accountStatements)){
            statementAccountVo.setOpeningBalance(accountStatements.get(accountStatements.size() - 1).getClosingBalance());
        }
        //本期应付=入库-退货金额累计
        statementAccountVo.setCurrentPlan(purchaseInboundVos.stream().map(PurchaseInboundVo::getInboundAmount).reduce(BigDecimal.ZERO, BigDecimal::add)
                .subtract(purchaseReturnVos.stream().map(PurchaseReturnVo::getTotalAmount).reduce(BigDecimal.ZERO, BigDecimal::add)));
        //本期付款=付款金额累计
        statementAccountVo.setCurrentActually(accountPurchasePayments.stream().map(AccountPurchasePayment::getPaymentAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
        //期末余额=期初+应收-收款
        statementAccountVo.setClosingBalance(statementAccountVo.getOpeningBalance().add(statementAccountVo.getCurrentPlan()).subtract(statementAccountVo.getCurrentActually()));
        statementAccountVo.setAccountStatementDetails(accountStatementDetailsList);
        return statementAccountVo;
    }
    private String genStatementAccountNo() {
        return "DZ" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
}
src/main/java/com/ruoyi/account/service/impl/AccountingServiceImpl.java
@@ -3,27 +3,47 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.AccountReportDto;
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.account.bean.dto.purchase.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto;
import com.ruoyi.account.bean.dto.sales.SalesOutboundDto;
import com.ruoyi.account.bean.dto.sales.SalesReturnDto;
import com.ruoyi.account.bean.vo.AccountReportVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo;
import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.bean.vo.sales.SalesReturnVo;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.account.service.AccountingService;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.mapper.CustomStorageMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.procurementrecord.pojo.CustomStorage;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Year;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
@@ -35,14 +55,21 @@
@Service
@Slf4j
@RequiredArgsConstructor
public class AccountingServiceImpl {
public class AccountingServiceImpl implements AccountingService {
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final BorrowInfoMapper borrowInfoMapper;
    private final CustomStorageMapper customStorageMapper;
    private final ProcurementRecordMapper procurementRecordMapper;
    private final ProcurementRecordOutMapper procurementRecordOutMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final StockOutRecordMapper stockOutRecordMapper;
    private final ReturnManagementMapper returnManagementMapper;
    private final StockInRecordMapper stockInRecordMapper;
    private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
    @Override
    public AjaxResult total(Integer year) {
        Map<String,Object> map = new HashMap<>();
        map.put("deprAmount",0); // æŠ˜æ—§é‡‘额
@@ -75,17 +102,8 @@
            map.put("netValue",reduce.subtract(total));
        }
        // è´Ÿå€º
        LambdaQueryWrapper<BorrowInfo> borrowInfoLambdaQueryWrapper = new LambdaQueryWrapper<>();
        borrowInfoLambdaQueryWrapper.like(BorrowInfo::getCreateTime,year)
                .eq(BorrowInfo::getStatus,1);
        List<BorrowInfo> borrowInfos = borrowInfoMapper.selectList(borrowInfoLambdaQueryWrapper);
        if(CollectionUtils.isNotEmpty(borrowInfos)){
            BigDecimal reduce = borrowInfos.stream()
                    .map(BorrowInfo::getBorrowAmount)
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            map.put("debt",reduce);
        }
        map.put("debt",BigDecimal.ZERO);
        // åº“存资产
        LambdaQueryWrapper<ProcurementRecordStorage> procurementRecordStorageLambdaQueryWrapper = new LambdaQueryWrapper<>();
        procurementRecordStorageLambdaQueryWrapper.like(ProcurementRecordStorage::getCreateTime,year);
@@ -245,6 +263,7 @@
        return totalDepreciation.setScale(2, BigDecimal.ROUND_HALF_UP);
    }
    @Override
    public AjaxResult deviceTypeDistribution(Integer year) {
        // 2. ç»„装返回VO
       DeviceTypeDistributionVO vo = new DeviceTypeDistributionVO();
@@ -268,6 +287,7 @@
        return AjaxResult.success(vo);
    }
    @Override
    public AjaxResult calculateDepreciation(Page page, Integer year) {
        LambdaQueryWrapper<DeviceLedger> deviceLedgerLambdaQueryWrapper = new LambdaQueryWrapper<>();
        deviceLedgerLambdaQueryWrapper.like(DeviceLedger::getCreateTime,year)
@@ -279,4 +299,159 @@
        }
        return AjaxResult.success(deviceLedgerIPage);
    }
    @Override
    public AccountReportVo getAccountStatementDetailsByMonth(AccountReportDto accountReportDto) {
        AccountReportVo accountReportVo = new AccountReportVo();
        LocalDate start = accountReportDto.getEntryDateStart();
        LocalDate end = accountReportDto.getEntryDateEnd();
        DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyy-MM");
        // ========== 1. é¡¶éƒ¨å¡ç‰‡æ•°æ® ==========
        // 1.1 æ€»è¥æ”¶ = æ”¶æ¬¾å•总金额
        List<AccountSalesCollection> accountSalesCollections = accountSalesCollectionMapper.selectList(
                Wrappers.<AccountSalesCollection>lambdaQuery()
                        .between(AccountSalesCollection::getCollectionDate, start, end)
        );
        BigDecimal totalIncome = Optional.of(
                accountSalesCollections.stream()
                        .map(AccountSalesCollection::getCollectionAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setTotalIncome(totalIncome);
        // 1.2 æ€»æ”¯å‡º = ä»˜æ¬¾å•总金额
        List<AccountPurchasePayment> accountPurchasePayments = accountPurchasePaymentMapper.selectList(
                Wrappers.<AccountPurchasePayment>lambdaQuery()
                        .between(AccountPurchasePayment::getPaymentDate, start, end)
        );
        BigDecimal totalExpense = Optional.of(
                accountPurchasePayments.stream()
                        .map(AccountPurchasePayment::getPaymentAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setTotalExpense(totalExpense);
        // 1.3 åº”收账款 = é”€å”®å‡ºåº“金额合计 - é”€å”®é€€è´§é‡‘额合计
        SalesOutboundDto salesOutboundDto = new SalesOutboundDto();
        salesOutboundDto.setStartDate(accountReportDto.getEntryDateStart());
        salesOutboundDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<SalesOutboundVo> salesOutboundVos = stockOutRecordMapper.listPageAccountSales(new Page(1, -1), salesOutboundDto).getRecords();
        BigDecimal salesOutAmount = Optional.of(
                salesOutboundVos.stream()
                        .map(SalesOutboundVo::getOutboundAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        SalesReturnDto salesReturnDto = new SalesReturnDto();
        salesReturnDto.setStartDate(accountReportDto.getEntryDateStart());
        salesReturnDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<SalesReturnVo> salesReturnVos = returnManagementMapper.listPageAccountSalesReturn(new Page(1, -1), salesReturnDto).getRecords();
        BigDecimal salesReturnAmount = Optional.of(
                salesReturnVos.stream()
                        .map(SalesReturnVo::getRefundAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setAccountsReceivable(salesOutAmount.subtract(salesReturnAmount));
        // 1.4 åº”付账款 = é‡‡è´­å…¥åº“金额合计 - é‡‡è´­é€€è´§é‡‘额合计
        PurchaseInboundDto purchaseInboundDto = new PurchaseInboundDto();
        purchaseInboundDto.setStartDate(accountReportDto.getEntryDateStart());
        purchaseInboundDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<PurchaseInboundVo> purchaseInboundVos = stockInRecordMapper.listPageAccountPurchase(new Page(1, -1), purchaseInboundDto).getRecords();
        BigDecimal purchaseInAmount = Optional.of(
                purchaseInboundVos.stream()
                        .map(PurchaseInboundVo::getInboundAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        PurchaseReturnDto purchaseReturnDto = new PurchaseReturnDto();
        purchaseReturnDto.setStartDate(accountReportDto.getEntryDateStart());
        purchaseReturnDto.setEndDate(accountReportDto.getEntryDateEnd());
        List<PurchaseReturnVo> purchaseReturnVos = purchaseReturnOrdersMapper.listPageAccountPurchaseReturn(new Page(1, -1), purchaseReturnDto).getRecords();
        BigDecimal purchaseReturnAmount = Optional.of(
                purchaseReturnVos.stream()
                        .map(PurchaseReturnVo::getTotalAmount)
                        .filter(Objects::nonNull)
                        .reduce(BigDecimal.ZERO, BigDecimal::add)
        ).orElse(BigDecimal.ZERO);
        accountReportVo.setAccountsPayable(purchaseInAmount.subtract(purchaseReturnAmount));
        // 1.5 å‡€åˆ©æ¶¦ = æ€»è¥æ”¶ - æ€»æ”¯å‡º
        BigDecimal netProfit = totalIncome.subtract(totalExpense);
        accountReportVo.setNetRevenue(netProfit);
        // ========== 2. æŠ˜çº¿å›¾ï¼šæœˆåº¦è¥æ”¶/支出/净利润趋势 ==========
        Map<String, BigDecimal> monthIncomeMap = new HashMap<>();
        Map<String, BigDecimal> monthExpenseMap = new HashMap<>();
        // æœˆåº¦è¥æ”¶
        accountSalesCollections.forEach(item -> {
            String month = item.getCollectionDate().format(monthFormatter);
            monthIncomeMap.put(month, monthIncomeMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getCollectionAmount()).orElse(BigDecimal.ZERO)));
        });
        // æœˆåº¦æ”¯å‡º
        accountPurchasePayments.forEach(item -> {
            String month = item.getPaymentDate().format(monthFormatter);
            monthExpenseMap.put(month, monthExpenseMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getPaymentAmount()).orElse(BigDecimal.ZERO)));
        });
        // ç”Ÿæˆè¿žç»­æœˆä»½åˆ—表
        List<String> monthList = new ArrayList<>();
        LocalDate current = start.withDayOfMonth(1);
        while (!current.isAfter(end.withDayOfMonth(1))) {
            monthList.add(current.format(monthFormatter));
            current = current.plusMonths(1);
        }
        // ç»„装趋势数据
        List<AccountReportVo.MonthlyTrendVO> trendList = new ArrayList<>();
        for (String month : monthList) {
            BigDecimal income = monthIncomeMap.getOrDefault(month, BigDecimal.ZERO);
            BigDecimal expense = monthExpenseMap.getOrDefault(month, BigDecimal.ZERO);
            AccountReportVo.MonthlyTrendVO trend = new AccountReportVo.MonthlyTrendVO();
            trend.setMonth(month);
            trend.setIncome(income);
            trend.setExpense(expense);
            trend.setProfit(income.subtract(expense));
            trendList.add(trend);
        }
        accountReportVo.setMonthlyTrendList(trendList);
        // ========== 3. æŸ±çŠ¶å›¾ï¼šæœˆåº¦åº”æ”¶/应付数据 ==========
        Map<String, BigDecimal> monthReceivableMap = new HashMap<>();
        Map<String, BigDecimal> monthPayableMap = new HashMap<>();
        // æœˆåº¦åº”收(销售出库-退货)
        salesOutboundVos.forEach(item -> {
            String month = item.getShippingDate().format(monthFormatter);
            monthReceivableMap.put(month, monthReceivableMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getOutboundAmount()).orElse(BigDecimal.ZERO)));
        });
        salesReturnVos.forEach(item -> {
            String month = item.getMakeTime().format(monthFormatter);
            monthReceivableMap.put(month, monthReceivableMap.getOrDefault(month, BigDecimal.ZERO)
                    .subtract(Optional.ofNullable(item.getRefundAmount()).orElse(BigDecimal.ZERO)));
        });
        // æœˆåº¦åº”付(采购入库-退货)
        purchaseInboundVos.forEach(item -> {
            String month = item.getInboundDate().format(monthFormatter);
            monthPayableMap.put(month, monthPayableMap.getOrDefault(month, BigDecimal.ZERO)
                    .add(Optional.ofNullable(item.getInboundAmount()).orElse(BigDecimal.ZERO)));
        });
        purchaseReturnVos.forEach(item -> {
            String month = item.getPreparedAt().format(monthFormatter);
            monthPayableMap.put(month, monthPayableMap.getOrDefault(month, BigDecimal.ZERO)
                    .subtract(Optional.ofNullable(item.getTotalAmount()).orElse(BigDecimal.ZERO)));
        });
        // ç»„装应收应付数据
        List<AccountReportVo.ReceivablePayableVO> rpList = new ArrayList<>();
        for (String month : monthList) {
            AccountReportVo.ReceivablePayableVO rp = new AccountReportVo.ReceivablePayableVO();
            rp.setMonth(month);
            rp.setReceivable(monthReceivableMap.getOrDefault(month, BigDecimal.ZERO));
            rp.setPayable(monthPayableMap.getOrDefault(month, BigDecimal.ZERO));
            rpList.add(rp);
        }
        accountReportVo.setReceivablePayableList(rpList);
        return accountReportVo;
    }
}
src/main/java/com/ruoyi/account/service/impl/BorrowInfoServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java
@@ -174,7 +174,7 @@
        if (accountSubjectDto != null && accountSubjectDto.getStatus() != null) {
            queryWrapper.eq(AccountSubject::getStatus, accountSubjectDto.getStatus());
        }
        queryWrapper.orderByAsc(AccountSubject::getSubjectCode).orderByAsc(AccountSubject::getId);
        queryWrapper.orderByDesc(AccountSubject::getSubjectCode).orderByDesc(AccountSubject::getId);
        return queryWrapper;
    }
@@ -296,8 +296,8 @@
        }
        List<AccountSubject> sortedSubjects = new ArrayList<>(subjects);
        sortedSubjects.sort(Comparator
                .comparing(AccountSubject::getSubjectCode, Comparator.nullsLast(String::compareTo))
                .thenComparing(AccountSubject::getId, Comparator.nullsLast(Long::compareTo)));
                .comparing(AccountSubject::getSubjectCode, Comparator.nullsFirst(String::compareTo)).reversed()
                .thenComparing(AccountSubject::getId, Comparator.nullsFirst(Long::compareTo)).reversed());
        Map<Long, AccountSubjectVo> subjectVoMap = new LinkedHashMap<>();
        for (AccountSubject subject : sortedSubjects) {
src/main/java/com/ruoyi/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/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/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/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/aftersalesservice/dto/AfterSalesServiceExeclDto.java
@@ -62,4 +62,12 @@
    @Excel(name = "关联部门")
    private String deptName;
    @Schema(description = "评分")
    @Excel(name = "评分")
    private String rating;
    @Schema(description = "评价内容描述")
    @Excel(name = "评价内容描述")
    private String evaluation;
}
src/main/java/com/ruoyi/aftersalesservice/pojo/AfterSalesService.java
@@ -115,7 +115,6 @@
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
@@ -157,8 +156,16 @@
    @Schema(description = "产品型号IDs")
    private String productModelIds;
    @Schema(description = "产品型号数量,逗号分隔")
    private String productModelQuantities;
    @Schema(description = "评分(1-5分)")
    @Excel(name = "评分")
    private BigDecimal rating;
    @Schema(description = "评价内容描述")
    @Excel(name = "评价内容描述")
    private String evaluation;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
src/main/java/com/ruoyi/aftersalesservice/service/impl/AfterSalesServiceServiceImpl.java
@@ -11,6 +11,8 @@
import com.ruoyi.aftersalesservice.service.AfterSalesServiceService;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import java.time.LocalDateTime;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
@@ -44,6 +46,7 @@
    private final SysUserMapper sysUserMapper;
    private final ISalesLedgerProductService salesLedgerProductService;
    private final ISalesLedgerService salesLedgerService;
    private final com.ruoyi.sales.mapper.ShippingInfoMapper shippingInfoMapper;
    @Override
    public IPage<AfterSalesServiceNewDto> listPage(Page page, AfterSalesServiceNewDto afterSalesService) {
@@ -69,7 +72,7 @@
        if(sysUser == null) throw new RuntimeException("审核人不存在");
        afterSalesServiceNewDto.setCheckNickName(sysUser.getNickName());
        if (StringUtils.isEmpty(afterSalesServiceNewDto.getAfterSalesServiceNo())) {
            String string = OrderUtils.countAfterServiceTodayByCreateTime(afterSalesServiceMapper, "SH_");
            String string = OrderUtils.countAfterServiceTodayByCreateTime(afterSalesServiceMapper, "SH_", afterSalesServiceNewDto.getCreateTime() != null ? afterSalesServiceNewDto.getCreateTime() : LocalDateTime.now());
            afterSalesServiceNewDto.setAfterSalesServiceNo(string);
        }
        return this.save(afterSalesServiceNewDto);
@@ -81,6 +84,38 @@
        SalesLedger byId = salesLedgerService.getById(afterSalesService.getSalesLedgerId());
        List<Long> collect = Arrays.stream(afterSalesService.getProductModelIds().split(",")).map(Long::valueOf).collect(Collectors.toList());
        List<SalesLedgerProduct> list = salesLedgerProductService.list(new QueryWrapper<SalesLedgerProduct>().lambda().in(SalesLedgerProduct::getId, collect));
        if (StringUtils.isNotEmpty(afterSalesService.getProductModelQuantities())) {
            String[] quantities = afterSalesService.getProductModelQuantities().split(",");
            for (int i = 0; i < collect.size(); i++) {
                Long productId = collect.get(i);
                for (SalesLedgerProduct product : list) {
                    if (product.getId().equals(productId)) {
                        if (i < quantities.length && StringUtils.isNotEmpty(quantities[i])) {
                            product.setQuantity(new java.math.BigDecimal(quantities[i]));
                        }
                        break;
                    }
                }
            }
        }
        for (SalesLedgerProduct product : list) {
            com.ruoyi.sales.pojo.ShippingInfo shippingInfo = shippingInfoMapper.selectOne(
                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.ruoyi.sales.pojo.ShippingInfo>()
                    .eq(com.ruoyi.sales.pojo.ShippingInfo::getSalesLedgerProductId, product.getId())
                    .orderByDesc(com.ruoyi.sales.pojo.ShippingInfo::getCreateTime)
                    .last("limit 1")
            );
            if (shippingInfo != null) {
                product.setShippingCarNumber(shippingInfo.getShippingCarNumber());
                product.setShippingDate(shippingInfo.getShippingDate());
                product.setShippingStatus(shippingInfo.getStatus());
                product.setExpressCompany(shippingInfo.getExpressCompany());
                product.setExpressNumber(shippingInfo.getExpressNumber());
            }
        }
        AfterSalesServiceNewDto afterSalesServiceNewDto = new AfterSalesServiceNewDto();
        BeanUtils.copyProperties(afterSalesService, afterSalesServiceNewDto);
        SalesLedgerDto salesLedgerDto = new SalesLedgerDto();
src/main/java/com/ruoyi/ai/assistant/ApproveTodoAgent.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/assistant/Assistant.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/assistant/FileAnalyzeAgent.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/assistant/FinancialIntentExecutor.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,284 @@
package com.ruoyi.ai.assistant;
import com.ruoyi.ai.tools.FinancialAgentTools;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class FinancialIntentExecutor {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final Pattern LIMIT_PATTERN = Pattern.compile("(?:前|最近|展示|返回)?\\s*(\\d{1,2})\\s*(?:条|个|名)");
    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
    private static final Pattern RELATIVE_DAY_PATTERN = Pattern.compile("(?:近|最近)\\s*(\\d{1,3})\\s*天");
    private static final Pattern FUTURE_MONTH_PATTERN = Pattern.compile("(?:未来|后续|接下来)\\s*(\\d{1,2})\\s*(?:个)?月");
    private final FinancialAgentTools financialAgentTools;
    public FinancialIntentExecutor(FinancialAgentTools financialAgentTools) {
        this.financialAgentTools = financialAgentTools;
    }
    public String tryExecute(String memoryId, String message) {
        if (!StringUtils.hasText(message)) {
            return null;
        }
        String text = message.trim();
        String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text);
        if (StringUtils.hasText(quickPromptResponse)) {
            return quickPromptResponse;
        }
        DateRange dateRange = extractDateRange(text);
        Integer limit = extractLimit(text);
        String keyword = extractKeyword(text);
        String startDate = dateRange.startDate();
        String endDate = dateRange.endDate();
        String timeRange = dateRange.label();
        if (containsAny(text, "成本核算", "产品成本", "工序成本", "人工成本", "折旧", "材料损耗", "成本最高")) {
            return financialAgentTools.calculateIntelligentCost(memoryId, startDate, endDate, timeRange, keyword, limit);
        }
        if (containsAny(text, "利润分析", "订单利润", "亏损订单", "低利润", "最赚钱客户", "哪个客户最赚钱",
                "客户最赚钱", "利润最高客户", "利润贡献最高", "利润下降")) {
            return financialAgentTools.analyzeOrderProfit(memoryId, startDate, endDate, timeRange, keyword, limit);
        }
        if (containsAny(text, "库存资金", "库存积压", "呆滞库存", "资金占用", "周转率", "库存周转")) {
            return financialAgentTools.analyzeInventoryCapital(memoryId, startDate, endDate, timeRange, keyword, limit);
        }
        if (containsAny(text, "现金流", "回款风险", "付款压力", "资金缺口", "应收", "应付", "回款预测")) {
            return financialAgentTools.forecastCashFlow(memoryId, startDate, endDate, timeRange, extractForecastMonths(text));
        }
        if (containsAny(text, "异常预警", "经营异常", "风险预警", "成本异常", "利润异常", "回款异常", "订单风险")) {
            return financialAgentTools.detectBusinessAnomalies(memoryId, startDate, endDate, timeRange, limit);
        }
        if (containsAny(text, "驾驶舱", "经营看板", "经营总览", "经营仪表盘", "经营大盘")) {
            return financialAgentTools.getBusinessCockpit(memoryId, startDate, endDate, timeRange);
        }
        if (containsAny(text, "日报", "周报", "经营报告", "分析报告")) {
            return financialAgentTools.generateOperationReport(memoryId, startDate, endDate, timeRange,
                    containsAny(text, "周报") ? "weekly" : "daily");
        }
        if (containsAny(text, "业财融合", "业财联动", "口径", "指标解释", "为什么")) {
            return financialAgentTools.retrieveFinancialKnowledge(memoryId, text);
        }
        return null;
    }
    private String tryExecuteQuickPrompt(String memoryId, String text) {
        String normalized = normalizeForMatch(text);
        if ("生成本周经营周报利润与现金流".equals(normalized) || "生成本周经营周报".equals(normalized) || "生成周报".equals(normalized)) {
            DateRange range = weekRange();
            return financialAgentTools.generateOperationReport(memoryId, range.startDate(), range.endDate(), range.label(), "weekly");
        }
        if ("分析本月利润下降原因".equals(normalized)) {
            DateRange range = monthRange();
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("近30天哪个客户利润贡献最高".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("查看本月经营驾驶舱".equals(normalized) || "查看经营驾驶舱".equals(normalized)) {
            DateRange range = monthRange();
            return financialAgentTools.getBusinessCockpit(memoryId, range.startDate(), range.endDate(), range.label());
        }
        if ("查询近30天亏损订单".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return financialAgentTools.analyzeOrderProfit(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("分析近30天库存资金占用".equals(normalized)) {
            DateRange range = recentDaysRange(30);
            return financialAgentTools.analyzeInventoryCapital(memoryId, range.startDate(), range.endDate(), range.label(), null, null);
        }
        if ("预测未来3个月现金流".equals(normalized)) {
            return financialAgentTools.forecastCashFlow(memoryId, null, null, null, 3);
        }
        if ("哪个工序成本最高".equals(normalized)) {
            return financialAgentTools.calculateIntelligentCost(memoryId, null, null, null, null, null);
        }
        return null;
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.contains(keyword)) {
                return true;
            }
        }
        return false;
    }
    private Integer extractLimit(String text) {
        Matcher matcher = LIMIT_PATTERN.matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(1)) : null;
    }
    private Integer extractForecastMonths(String text) {
        Matcher matcher = FUTURE_MONTH_PATTERN.matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(1)) : null;
    }
    private DateRange extractDateRange(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        if (matcher.find()) {
            String first = matcher.group(1);
            String second = matcher.find() ? matcher.group(1) : first;
            return buildDateRange(first, second, first + "至" + second);
        }
        if (text.contains("本月")) {
            return monthRange();
        }
        if (text.contains("上月")) {
            return lastMonthRange();
        }
        if (text.contains("今年") || text.contains("本年")) {
            return yearRange();
        }
        if (text.contains("本周")) {
            return weekRange();
        }
        Matcher relativeDayMatcher = RELATIVE_DAY_PATTERN.matcher(text);
        if (relativeDayMatcher.find()) {
            int days = Integer.parseInt(relativeDayMatcher.group(1));
            return recentDaysRange(days);
        }
        return new DateRange(null, null, null);
    }
    private DateRange buildDateRange(String start, String end, String label) {
        LocalDate startDate = parseDate(start);
        LocalDate endDate = parseDate(end);
        if (startDate == null || endDate == null) {
            return new DateRange(null, null, null);
        }
        if (startDate.isAfter(endDate)) {
            LocalDate temp = startDate;
            startDate = endDate;
            endDate = temp;
        }
        return new DateRange(formatDate(startDate), formatDate(endDate), label);
    }
    private DateRange recentDaysRange(int days) {
        LocalDate end = LocalDate.now();
        int safeDays = Math.max(days, 1);
        LocalDate start = end.minusDays(safeDays - 1L);
        return new DateRange(formatDate(start), formatDate(end), "近" + safeDays + "天");
    }
    private DateRange monthRange() {
        LocalDate today = LocalDate.now();
        return new DateRange(formatDate(today.withDayOfMonth(1)), formatDate(today), "本月");
    }
    private DateRange weekRange() {
        LocalDate today = LocalDate.now();
        LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
        return new DateRange(formatDate(start), formatDate(today), "本周");
    }
    private DateRange lastMonthRange() {
        YearMonth lastMonth = YearMonth.now().minusMonths(1);
        return new DateRange(formatDate(lastMonth.atDay(1)), formatDate(lastMonth.atEndOfMonth()), "上月");
    }
    private DateRange yearRange() {
        LocalDate today = LocalDate.now();
        return new DateRange(formatDate(today.withDayOfYear(1)), formatDate(today), "今年");
    }
    private LocalDate parseDate(String text) {
        try {
            return LocalDate.parse(text, DATE_FMT);
        } catch (Exception ignored) {
            return null;
        }
    }
    private String formatDate(LocalDate date) {
        return date == null ? null : date.format(DATE_FMT);
    }
    private String normalizeForMatch(String text) {
        if (!StringUtils.hasText(text)) {
            return "";
        }
        return text.replace("(", "")
                .replace(")", "")
                .replace("(", "")
                .replace(")", "")
                .replace(",", "")
                .replace(",", "")
                .replace("。", "")
                .replace(".", "")
                .replace("!", "")
                .replace("!", "")
                .replace("?", "")
                .replace("?", "")
                .replace(":", "")
                .replace(":", "")
                .replace(";", "")
                .replace(";", "")
                .replace(" ", "")
                .trim();
    }
    private String extractKeyword(String text) {
        String cleaned = text
                .replaceAll("\\d{4}-\\d{2}-\\d{2}", "")
                .replaceAll("(?:近|最近)\\s*\\d{1,3}\\s*天", "")
                .replaceAll("(?:前|最近|展示|返回)?\\s*\\d{1,2}\\s*(?:条|个|名)", "")
                .replace("查询", "")
                .replace("查看", "")
                .replace("看下", "")
                .replace("看看", "")
                .replace("帮我", "")
                .replace("请", "")
                .replace("一个", "")
                .replace("为什么", "")
                .replace("哪个客户最赚钱", "")
                .replace("最近哪个客户最赚钱", "")
                .replace("本月哪个客户最赚钱", "")
                .replace("近30天哪个客户最赚钱", "")
                .replace("最赚钱客户", "")
                .replace("客户最赚钱", "")
                .replace("哪个客户利润最高", "")
                .replace("利润最高客户", "")
                .replace("哪个客户利润贡献最高", "")
                .replace("利润贡献最高客户", "")
                .replace("本月", "")
                .replace("本周", "")
                .replace("本年", "")
                .replace("今年", "")
                .replace("上月", "")
                .replace("近30天", "")
                .replace("近7天", "")
                .replace("近90天", "")
                .replace("前10条", "")
                .replace("最近10条", "")
                .replace("前20条", "")
                .replace("最近20条", "")
                .replace("订单利润分析", "")
                .replace("利润分析", "")
                .replace("库存资金分析", "")
                .replace("现金流预测", "")
                .replace("经营驾驶舱", "")
                .replace("日报", "")
                .replace("周报", "")
                .replace("异常预警", "")
                .replace("条", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
    private record DateRange(String startDate, String endDate, String label) {
    }
}
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/assistant/PurchaseAgent.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/assistant/SalesAgent.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/assistant/SalesIntentExecutor.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/assistant/SeparateChatAssistant.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/bean/ChatForm.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/bean/PurchaseAiConfirmRequest.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/config/ApproveTodoAgentConfig.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/config/EmbeddingStoreConfig.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/config/SalesAgentConfig.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/config/SeparateChatAssistantConfig.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/config/XiaozhiAgentConfig.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java
@@ -4,6 +4,8 @@
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -11,18 +13,26 @@
public class AiSessionUserContext {
    private final Map<String, LoginUser> loginUserByMemoryId = new ConcurrentHashMap<>();
    private final Map<String, Instant> lastAccessTimeByMemoryId = new ConcurrentHashMap<>();
    private static final Duration SESSION_TIMEOUT = Duration.ofHours(24);
    public void bind(String memoryId, LoginUser loginUser) {
        if (!StringUtils.hasText(memoryId) || loginUser == null) {
            return;
        }
        loginUserByMemoryId.put(memoryId, loginUser);
        lastAccessTimeByMemoryId.put(memoryId, Instant.now());
    }
    public LoginUser get(String memoryId) {
        if (!StringUtils.hasText(memoryId)) {
            return null;
        }
        if (isExpired(memoryId)) {
            remove(memoryId);
            return null;
        }
        lastAccessTimeByMemoryId.put(memoryId, Instant.now());
        return loginUserByMemoryId.get(memoryId);
    }
@@ -31,5 +41,25 @@
            return;
        }
        loginUserByMemoryId.remove(memoryId);
        lastAccessTimeByMemoryId.remove(memoryId);
    }
    public void cleanExpiredSessions() {
        Instant now = Instant.now();
        lastAccessTimeByMemoryId.entrySet().removeIf(entry -> {
            boolean expired = Duration.between(entry.getValue(), now).compareTo(SESSION_TIMEOUT) > 0;
            if (expired) {
                loginUserByMemoryId.remove(entry.getKey());
            }
            return expired;
        });
    }
    private boolean isExpired(String memoryId) {
        Instant lastAccess = lastAccessTimeByMemoryId.get(memoryId);
        if (lastAccess == null) {
            return true;
        }
        return Duration.between(lastAccess, Instant.now()).compareTo(SESSION_TIMEOUT) > 0;
    }
}
src/main/java/com/ruoyi/ai/controller/FinancialAiController.java
ÎļþÃû´Ó src/main/java/com/ruoyi/ai/controller/SalesAiController.java ÐÞ¸Ä
@@ -1,7 +1,7 @@
package com.ruoyi.ai.controller;
import com.ruoyi.ai.assistant.SalesAgent;
import com.ruoyi.ai.assistant.SalesIntentExecutor;
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;
@@ -24,32 +24,38 @@
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 = "销售助手智能体")
@Tag(name = "财务智能体")
@RestController
@RequestMapping("/sales-ai")
public class SalesAiController extends BaseController {
@RequestMapping("/financial-ai")
public class FinancialAiController extends BaseController {
    private final SalesAgent salesAgent;
    private final SalesIntentExecutor salesIntentExecutor;
    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 SalesAiController(SalesAgent salesAgent,
                             SalesIntentExecutor salesIntentExecutor,
                             AiSessionUserContext aiSessionUserContext,
                             MongoChatMemoryStore mongoChatMemoryStore,
                             AiChatSessionService aiChatSessionService) {
        this.salesAgent = salesAgent;
        this.salesIntentExecutor = salesIntentExecutor;
    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 = "销售助手对话")
    @Operation(summary = "财务智能体对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (!StringUtils.hasText(chatForm.getMemoryId())) {
@@ -66,7 +72,7 @@
        aiSessionUserContext.bind(memoryId, loginUser);
        aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
        String directResponse = salesIntentExecutor.tryExecute(memoryId, userMessage);
        String directResponse = financialIntentExecutor.tryExecute(memoryId, userMessage);
        if (StringUtils.isNotEmpty(directResponse)) {
            mongoChatMemoryStore.appendMessages(
                    memoryId,
@@ -77,7 +83,7 @@
        }
        if (isBusinessDataIntent(userMessage)) {
            String noGuessResponse = "未识别到可执行的数据查询条件。为保证结果准确,当前不会推测或编造数据,请补充明确时间范围、客户或单号后再查询。";
            String noGuessResponse = "未识别到可执行的数据查询条件。为保证结果准确,当前不会推测或编造数据,请补充明确时间范围、客户、供应商或单号后再查询。";
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(noGuessResponse))
@@ -86,28 +92,32 @@
            return Flux.just(noGuessResponse);
        }
        return salesAgent.chat(memoryId, userMessage)
        return financialAgent.chat(memoryId, userMessage, currentDateForPrompt())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
    @Operation(summary = "销售助手会话列表")
    @Operation(summary = "财务智能体会话列表")
    @GetMapping("/history/sessions")
    public AjaxResult listSessions() {
        return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "销售助手会话消息")
    @Operation(summary = "财务智能体会话消息")
    @GetMapping("/history/messages/{memoryId}")
    public AjaxResult listMessages(@PathVariable String memoryId) {
        return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "删除销售助手会话")
    @Operation(summary = "删除财务智能体会话")
    @DeleteMapping("/history/{memoryId}")
    public AjaxResult deleteSession(@PathVariable String memoryId) {
        aiSessionUserContext.remove(memoryId);
        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
    }
    private String currentDateForPrompt() {
        return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
    }
    private boolean isBusinessDataIntent(String message) {
@@ -116,8 +126,11 @@
        }
        String text = message.trim();
        return containsAny(text,
                "查询", "查看", "统计", "分析", "建议", "客户档案", "私海", "公海",
                "销售报价", "销售台账", "销售退货", "客户往来", "发货台账", "回款", "报价", "风险");
                "查询", "查看", "统计", "分析", "建议", "成本核算", "产品成本", "工序成本",
                "订单利润", "亏损订单", "低利润", "库存资金", "库存积压", "呆滞库存",
                "现金流", "回款风险", "付款压力", "资金缺口", "应收", "应付",
                "异常预警", "经营异常", "风险预警", "驾驶舱", "经营看板", "经营总览",
                "日报", "周报", "经营报告", "分析报告", "业财融合", "口径", "指标解释");
    }
    private boolean containsAny(String text, String... keywords) {
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/dto/AiChatSessionDto.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/mapper/AiChatSessionMapper.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/pojo/AiChatSession.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/schedule/AiSessionCleanupTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.ai.schedule;
import com.ruoyi.ai.context.AiSessionUserContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class AiSessionCleanupTask {
    private final AiSessionUserContext aiSessionUserContext;
    public AiSessionCleanupTask(AiSessionUserContext aiSessionUserContext) {
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Scheduled(cron = "0 0 2 * * ?")
    public void cleanupExpiredSessions() {
        try {
            aiSessionUserContext.cleanExpiredSessions();
        } catch (Exception e) {
            System.err.println("清理过期AI会话失败: " + e.getMessage());
        }
    }
}
src/main/java/com/ruoyi/ai/service/AiChatSessionService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/service/PurchaseAiService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
@@ -22,11 +22,10 @@
import java.io.IOException;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
@@ -46,7 +45,7 @@
    private static final int DEFAULT_LIMIT = 10;
    private static final int MAX_LIMIT = 20;
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private final ApproveProcessMapper approveProcessMapper;
@@ -740,7 +739,10 @@
    }
    private String formatDate(Date value) {
        return value == null ? "" : DATE_FORMAT.format(value);
        if (value == null) {
            return "";
        }
        return value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().format(DATE_FORMATTER);
    }
    private long countByStatus(List<ApproveProcess> processes, int status) {
@@ -824,8 +826,9 @@
    private Date parseDate(String dateText) {
        try {
            return DATE_FORMAT.parse(dateText);
        } catch (ParseException e) {
            LocalDate localDate = LocalDate.parse(dateText, DATE_FORMATTER);
            return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
        } catch (Exception e) {
            throw new IllegalArgumentException("日期格式必须是 yyyy-MM-dd");
        }
    }
src/main/java/com/ruoyi/ai/tools/FinancialAgentTools.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2311 @@
package com.ruoyi.ai.tools;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.ruoyi.account.mapper.AccountStatementMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.AccountStatement;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.account.service.impl.AccountingServiceImpl;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.basic.mapper.CustomerMapper;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.mapper.SupplierManageMapper;
import com.ruoyi.basic.pojo.Customer;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.pojo.DeviceRepair;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordOut;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
import com.ruoyi.production.mapper.ProductionAccountMapper;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionPlanMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.mapper.ProductionProductOutputMapper;
import com.ruoyi.production.pojo.ProductionAccount;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionPlan;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.pojo.ProductionProductOutput;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
import com.ruoyi.technology.pojo.TechnologyOperation;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Component
public class FinancialAgentTools {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final Pattern RELATIVE_PATTERN = Pattern.compile("(近|最近)?\\s*(\\d+)\\s*(天|周|个月|月|å¹´)");
    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
    private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
    private static final int DEFAULT_LIMIT = 10;
    private static final int MAX_LIMIT = 50;
    private static final BigDecimal DEFAULT_FALLBACK_MATERIAL_COST_RATE = new BigDecimal("0.60");
    private static final BigDecimal DEFAULT_LABOR_COST_RATE = new BigDecimal("0.15");
    private static final BigDecimal DEFAULT_OVERHEAD_COST_RATE = new BigDecimal("0.10");
    private static final BigDecimal SME_RECEIVABLE_RISK_THRESHOLD = new BigDecimal("500000");
    private static final BigDecimal SME_INVENTORY_RISK_THRESHOLD = new BigDecimal("1000000");
    private static final BigDecimal SME_PROFIT_WARNING_RATE = new BigDecimal("0.08");
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final ProductionAccountMapper productionAccountMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionProductOutputMapper productionProductOutputMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final DeviceRepairMapper deviceRepairMapper;
    private final ProcurementRecordMapper procurementRecordMapper;
    private final ProcurementRecordOutMapper procurementRecordOutMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final AccountStatementMapper accountStatementMapper;
    private final CustomerMapper customerMapper;
    private final SupplierManageMapper supplierManageMapper;
    private final ProductModelMapper productModelMapper;
    private final ProductMapper productMapper;
    private final AiSessionUserContext aiSessionUserContext;
    public FinancialAgentTools(SalesLedgerMapper salesLedgerMapper,
                               SalesLedgerProductMapper salesLedgerProductMapper,
                               ProductionAccountMapper productionAccountMapper,
                               ProductionProductMainMapper productionProductMainMapper,
                               ProductionOperationTaskMapper productionOperationTaskMapper,
                               ProductionOrderMapper productionOrderMapper,
                               ProductionPlanMapper productionPlanMapper,
                               ProductionProductOutputMapper productionProductOutputMapper,
                               TechnologyOperationMapper technologyOperationMapper,
                               DeviceLedgerMapper deviceLedgerMapper,
                               DeviceRepairMapper deviceRepairMapper,
                               ProcurementRecordMapper procurementRecordMapper,
                               ProcurementRecordOutMapper procurementRecordOutMapper,
                               StockInventoryMapper stockInventoryMapper,
                               AccountSalesCollectionMapper accountSalesCollectionMapper,
                               AccountPurchasePaymentMapper accountPurchasePaymentMapper,
                               AccountStatementMapper accountStatementMapper,
                               CustomerMapper customerMapper,
                               SupplierManageMapper supplierManageMapper,
                               ProductModelMapper productModelMapper,
                               ProductMapper productMapper,
                               AiSessionUserContext aiSessionUserContext) {
        this.salesLedgerMapper = salesLedgerMapper;
        this.salesLedgerProductMapper = salesLedgerProductMapper;
        this.productionAccountMapper = productionAccountMapper;
        this.productionProductMainMapper = productionProductMainMapper;
        this.productionOperationTaskMapper = productionOperationTaskMapper;
        this.productionOrderMapper = productionOrderMapper;
        this.productionPlanMapper = productionPlanMapper;
        this.productionProductOutputMapper = productionProductOutputMapper;
        this.technologyOperationMapper = technologyOperationMapper;
        this.deviceLedgerMapper = deviceLedgerMapper;
        this.deviceRepairMapper = deviceRepairMapper;
        this.procurementRecordMapper = procurementRecordMapper;
        this.procurementRecordOutMapper = procurementRecordOutMapper;
        this.stockInventoryMapper = stockInventoryMapper;
        this.accountSalesCollectionMapper = accountSalesCollectionMapper;
        this.accountPurchasePaymentMapper = accountPurchasePaymentMapper;
        this.accountStatementMapper = accountStatementMapper;
        this.customerMapper = customerMapper;
        this.supplierManageMapper = supplierManageMapper;
        this.productModelMapper = productModelMapper;
        this.productMapper = productMapper;
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Tool(name = "财务知识检索", value = "按财务经营问题检索业财融合知识片段与指标口径,作为RAG上下文。")
    public String retrieveFinancialKnowledge(@ToolMemoryId String memoryId,
                                             @P(value = "问题或关键词,例如利润下降、库存周转、资金缺口") String question) {
        List<KnowledgeDoc> knowledgeDocs = financeKnowledgeBase();
        String normalized = normalizeForMatch(question);
        List<KnowledgeDoc> ranked = knowledgeDocs.stream()
                .sorted(Comparator.comparingInt((KnowledgeDoc doc) -> keywordHitCount(doc.keywords(), normalized)).reversed())
                .filter(doc -> keywordHitCount(doc.keywords(), normalized) > 0 || !StringUtils.hasText(normalized))
                .limit(5)
                .toList();
        List<Map<String, Object>> items = ranked.stream().map(doc -> {
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("topic", doc.topic());
            map.put("knowledge", doc.knowledge());
            map.put("relatedTables", doc.relatedTables());
            map.put("suggestedQuestions", doc.suggestedQuestions());
            return map;
        }).toList();
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("question", safe(question));
        summary.put("hitCount", items.size());
        summary.put("retrievalMode", "keyword_rag");
        return jsonResponse(true, "financial_rag_knowledge", "已返回财务知识检索结果", summary, Map.of("items", items), Map.of());
    }
    @Tool(name = "智能成本核算", value = "自动核算产品成本、工序成本、人工成本、设备折旧、材料损耗与订单利润。")
    public String calculateIntelligentCost(@ToolMemoryId String memoryId,
                                           @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                           @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                           @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange,
                                           @P(value = "关键词,可匹配合同号/客户/项目", required = false) String keyword,
                                           @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        if (loginUser == null) {
            return jsonResponse(false, "financial_cost_accounting", "用户信息获取失败", Map.of(), Map.of(), Map.of());
        }
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit);
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("orderCount", bundle.orderMetrics().size());
        summary.put("totalRevenue", bundle.totalRevenue());
        summary.put("totalMaterialCost", bundle.totalMaterialCost());
        summary.put("totalLaborCost", bundle.totalLaborCost());
        summary.put("totalDepreciationCost", bundle.totalDepreciationCost());
        summary.put("totalScrapCost", bundle.totalScrapCost());
        summary.put("totalCost", bundle.totalCost());
        summary.put("totalProfit", bundle.totalProfit());
        summary.put("profitRate", toPercent(rate(bundle.totalProfit(), bundle.totalRevenue())));
        List<Map<String, Object>> orderItems = bundle.orderMetrics().stream()
                .map(this::toOrderCostItem)
                .toList();
        List<Map<String, Object>> processItems = bundle.processCostRanking().entrySet().stream()
                .map(entry -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("processName", entry.getKey());
                    map.put("cost", entry.getValue());
                    return map;
                }).toList();
        List<Map<String, Object>> topCustomerItems = buildCustomerProfitTop(bundle.orderMetrics(), 5);
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("costCompositionPieOption",
                buildCostCompositionPie(bundle.totalMaterialCost(), bundle.totalLaborCost(), bundle.totalDepreciationCost(), bundle.totalScrapCost()));
        charts.put("orderProfitBarOption", buildOrderProfitBar(bundle.orderMetrics()));
        charts.put("processCostBarOption", buildProcessCostBar(bundle.processCostRanking()));
        return jsonResponse(true, "financial_cost_accounting", "已完成智能成本核算", summary,
                Map.of(
                        "orders", orderItems,
                        "processCostRanking", processItems,
                        "topCustomers", topCustomerItems
                ),
                charts
        );
    }
    @Tool(name = "订单利润分析", value = "识别低利润/亏损订单,输出原因分析和优化建议。")
    public String analyzeOrderProfit(@ToolMemoryId String memoryId,
                                     @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                     @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                     @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange,
                                     @P(value = "关键词,可匹配合同号/客户/项目", required = false) String keyword,
                                     @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        if (loginUser == null) {
            return jsonResponse(false, "financial_order_profit_analysis", "用户信息获取失败", Map.of(), Map.of(), Map.of());
        }
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, keyword, limit);
        List<OrderProfitMetric> metrics = bundle.orderMetrics();
        List<OrderProfitMetric> riskyOrders = metrics.stream()
                .filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0 || item.profitRate().compareTo(SME_PROFIT_WARNING_RATE) < 0)
                .sorted(Comparator.comparing(OrderProfitMetric::profitRate)
                        .thenComparing(OrderProfitMetric::profit))
                .toList();
        Map<String, BigDecimal> customerProfitMap = new LinkedHashMap<>();
        for (OrderProfitMetric metric : metrics) {
            customerProfitMap.merge(metric.customerName(), metric.profit(), BigDecimal::add);
        }
        Map.Entry<String, BigDecimal> topCustomer = customerProfitMap.entrySet().stream()
                .max(Map.Entry.comparingByValue())
                .orElse(Map.entry("暂无数据", BigDecimal.ZERO));
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("orderCount", metrics.size());
        summary.put("lossOrderCount", metrics.stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count());
        summary.put("lowProfitOrderCount", riskyOrders.size());
        summary.put("avgProfitRate", toPercent(avgRate(metrics)));
        summary.put("topCustomerByProfit", topCustomer.getKey());
        summary.put("topCustomerProfit", topCustomer.getValue());
        List<Map<String, Object>> riskyItems = riskyOrders.stream()
                .limit(normalizeLimit(limit))
                .map(this::toRiskOrderItem)
                .toList();
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("profitDistributionOption", buildProfitDistributionBar(metrics));
        charts.put("lossOrderTrendOption", buildLossOrderTrendLine(metrics));
        charts.put("customerProfitTopOption", buildCustomerProfitBar(customerProfitMap));
        return jsonResponse(true, "financial_order_profit_analysis", "已完成订单利润分析", summary,
                Map.of(
                        "riskOrders", riskyItems,
                        "allOrders", metrics.stream().map(this::toOrderCostItem).toList(),
                        "customerProfitTop", buildCustomerProfitTop(metrics, 10)
                ),
                charts
        );
    }
    @Tool(name = "库存资金分析", value = "分析库存积压、呆滞库存、资金占用与周转率。")
    public String analyzeInventoryCapital(@ToolMemoryId String memoryId,
                                          @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                          @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                          @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange,
                                          @P(value = "关键词,可匹配产品名称/型号", required = false) String keyword,
                                          @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        int finalLimit = normalizeLimit(limit);
        List<StockInventory> inventoryRows = queryStockInventory(loginUser);
        if (inventoryRows.isEmpty()) {
            return jsonResponse(true, "financial_inventory_capital_analysis", "当前无库存数据",
                    rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
        }
        Set<Long> modelIds = inventoryRows.stream()
                .map(StockInventory::getProductModelId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Long, ProductModel> productModelMap = queryProductModels(modelIds);
        Map<Long, Product> productMap = queryProducts(productModelMap.values());
        Map<Long, BigDecimal> avgUnitCostByModelId = queryAverageUnitCostByModel(loginUser, modelIds);
        OutboundStats outboundStats = queryOutboundStats(loginUser, modelIds, range);
        List<InventoryMetric> metrics = buildInventoryMetrics(inventoryRows, productModelMap, productMap, avgUnitCostByModelId, outboundStats)
                .stream()
                .filter(metric -> matchInventoryKeyword(metric, keyword))
                .sorted(Comparator.comparing(InventoryMetric::inventoryValue).reversed())
                .toList();
        BigDecimal totalInventoryValue = metrics.stream().map(InventoryMetric::inventoryValue).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal stagnantValue = metrics.stream()
                .filter(metric -> metric.stagnantDays() >= 90)
                .map(InventoryMetric::inventoryValue)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        long stagnantCount = metrics.stream().filter(metric -> metric.stagnantDays() >= 90).count();
        long overstockCount = metrics.stream().filter(InventoryMetric::overstock).count();
        BigDecimal totalOutboundCost = outboundStats.totalOutboundCost();
        BigDecimal turnoverDays = totalOutboundCost.compareTo(BigDecimal.ZERO) > 0
                ? totalInventoryValue.multiply(BigDecimal.valueOf(daysBetween(range.start(), range.end()) + 1L))
                .divide(totalOutboundCost, 2, RoundingMode.HALF_UP)
                : BigDecimal.ZERO;
        List<Map<String, Object>> items = metrics.stream()
                .limit(finalLimit)
                .map(this::toInventoryItem)
                .toList();
        Map<String, Object> summary = rangeSummary(range, metrics.size(), keyword);
        summary.put("totalInventoryValue", totalInventoryValue);
        summary.put("stagnantValue", stagnantValue);
        summary.put("stagnantCount", stagnantCount);
        summary.put("overstockCount", overstockCount);
        summary.put("turnoverDays", turnoverDays);
        summary.put("capitalOccupation", totalInventoryValue);
        summary.put("totalOutboundCost", totalOutboundCost);
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("inventoryValueTopOption", buildInventoryTopBar(metrics));
        charts.put("inventoryAgingPieOption", buildInventoryAgingPie(metrics));
        charts.put("inventoryTurnoverGauge", buildTurnoverGauge(turnoverDays));
        return jsonResponse(true, "financial_inventory_capital_analysis", "已完成库存资金分析", summary, Map.of("items", items), charts);
    }
    @Tool(name = "应收应付与现金流预测", value = "预测未来现金流、回款风险、付款压力与资金缺口。")
    public String forecastCashFlow(@ToolMemoryId String memoryId,
                                   @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                   @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                   @P(value = "时间范围描述,如近90天、本年", required = false) String timeRange,
                                   @P(value = "预测月份数,默认3,最大6", required = false) Integer forecastMonths) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近90天");
        int months = forecastMonths == null || forecastMonths <= 0 ? 3 : Math.min(forecastMonths, 6);
        List<AccountSalesCollection> collections = queryCollections(loginUser, range);
        List<AccountPurchasePayment> payments = queryPayments(loginUser, range);
        List<MonthlyCashFlow> monthlyActual = buildMonthlyCashFlow(range, collections, payments);
        List<MonthlyCashFlow> monthlyForecast = forecastMonthlyCashFlow(monthlyActual, months);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        BigDecimal receivableTotal = snapshot.receivableTotal();
        BigDecimal payableTotal = snapshot.payableTotal();
        BigDecimal forecastNetSum = monthlyForecast.stream().map(MonthlyCashFlow::netFlow).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal coverage = receivableTotal.add(maxZero(forecastNetSum));
        BigDecimal fundGap = maxZero(payableTotal.subtract(coverage));
        Map<String, String> customerNameMap = queryCustomerNameMap(snapshot.receivableTop().stream().map(StatementMetric::entityId).collect(Collectors.toSet()));
        Map<String, String> supplierNameMap = querySupplierNameMap(snapshot.payableTop().stream().map(StatementMetric::entityId).collect(Collectors.toSet()));
        List<Map<String, Object>> receivableRiskItems = snapshot.receivableTop().stream().map(item -> toStatementRiskItem(item, customerNameMap, "customer")).toList();
        List<Map<String, Object>> payablePressureItems = snapshot.payableTop().stream().map(item -> toStatementRiskItem(item, supplierNameMap, "supplier")).toList();
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("actualIncomeTotal", collections.stream().map(AccountSalesCollection::getCollectionAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
        summary.put("actualExpenseTotal", payments.stream().map(AccountPurchasePayment::getPaymentAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
        summary.put("receivableBalance", receivableTotal);
        summary.put("payableBalance", payableTotal);
        summary.put("forecastNetSum", forecastNetSum);
        summary.put("fundGap", fundGap);
        summary.put("forecastMonths", months);
        summary.put("collectionRiskLevel", riskLevelByAmount(receivableTotal));
        summary.put("paymentPressureLevel", riskLevelByAmount(payableTotal));
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("cashFlowTrendOption", buildCashflowTrend(monthlyActual, monthlyForecast));
        charts.put("receivablePayableBarOption", buildReceivablePayableBar(receivableTotal, payableTotal));
        charts.put("fundGapGaugeOption", buildFundGapGauge(fundGap));
        return jsonResponse(true, "financial_cashflow_forecast", "已完成应收应付与现金流预测", summary,
                Map.of(
                        "actualMonthly", monthlyActual.stream().map(this::toMonthlyCashFlowItem).toList(),
                        "forecastMonthly", monthlyForecast.stream().map(this::toMonthlyCashFlowItem).toList(),
                        "receivableRiskTop", receivableRiskItems,
                        "payablePressureTop", payablePressureItems
                ),
                charts
        );
    }
    @Tool(name = "经营异常预警", value = "识别成本异常、利润异常、回款异常、订单风险、库存异常。")
    public String detectBusinessAnomalies(@ToolMemoryId String memoryId,
                                          @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                          @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                          @P(value = "时间范围描述,如近30天", required = false) String timeRange,
                                          @P(value = "返回条数,默认10,最大50", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "近30天");
        int finalLimit = normalizeLimit(limit);
        AnalysisBundle currentBundle = buildOrderProfitBundle(loginUser, range, null, Math.max(finalLimit, 30));
        DateRange prevRange = previousSameLengthRange(range);
        AnalysisBundle prevBundle = buildOrderProfitBundle(loginUser, prevRange, null, 50);
        BigDecimal currentCostRate = rate(currentBundle.totalCost(), currentBundle.totalRevenue());
        BigDecimal prevCostRate = rate(prevBundle.totalCost(), prevBundle.totalRevenue());
        BigDecimal costRateDiff = currentCostRate.subtract(prevCostRate);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        List<InventoryMetric> inventoryMetrics = buildInventoryMetrics(
                queryStockInventory(loginUser),
                queryProductModels(Collections.emptySet()),
                Map.of(),
                queryAverageUnitCostByModel(loginUser, Collections.emptySet()),
                queryOutboundStats(loginUser, Collections.emptySet(), range)
        );
        List<Map<String, Object>> anomalyItems = new ArrayList<>();
        if (costRateDiff.compareTo(new BigDecimal("0.10")) > 0) {
            anomalyItems.add(anomalyItem("high", "成本异常", "单位收入成本率较上周期上升超过10%", Map.of(
                    "currentCostRate", toPercent(currentCostRate),
                    "previousCostRate", toPercent(prevCostRate),
                    "delta", toPercent(costRateDiff)
            )));
        }
        long lossCount = currentBundle.orderMetrics().stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count();
        if (lossCount > 0) {
            anomalyItems.add(anomalyItem("high", "利润异常", "检测到亏损订单", Map.of("lossOrderCount", lossCount)));
        }
        if (snapshot.receivableTotal().compareTo(snapshot.payableTotal().multiply(new BigDecimal("1.2"))) > 0) {
            anomalyItems.add(anomalyItem("medium", "回款异常", "应收余额显著高于应付,回款压力偏大", Map.of(
                    "receivableBalance", snapshot.receivableTotal(),
                    "payableBalance", snapshot.payableTotal()
            )));
        }
        long overdueOrderCount = currentBundle.orderMetrics().stream()
                .filter(item -> item.deliveryDate() != null && item.deliveryDate().isBefore(LocalDate.now()) && item.profitRate().compareTo(new BigDecimal("0.10")) < 0)
                .count();
        if (overdueOrderCount > 0) {
            anomalyItems.add(anomalyItem("medium", "订单风险", "存在低利润且交付已逾期订单", Map.of("overdueRiskOrderCount", overdueOrderCount)));
        }
        long stagnantCount = inventoryMetrics.stream().filter(item -> item.stagnantDays() >= 90).count();
        if (stagnantCount > 0) {
            anomalyItems.add(anomalyItem("medium", "库存异常", "存在超过90天未周转库存", Map.of("stagnantCount", stagnantCount)));
        }
        List<Map<String, Object>> topAnomalies = anomalyItems.stream().limit(finalLimit).toList();
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("anomalyCount", topAnomalies.size());
        summary.put("highRiskCount", topAnomalies.stream().filter(item -> "high".equals(item.get("riskLevel"))).count());
        summary.put("mediumRiskCount", topAnomalies.stream().filter(item -> "medium".equals(item.get("riskLevel"))).count());
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("anomalyLevelPieOption", buildAnomalyLevelPie(topAnomalies));
        charts.put("anomalyTypeBarOption", buildAnomalyTypeBar(topAnomalies));
        return jsonResponse(true, "financial_business_anomaly_warning", "已完成经营异常预警分析", summary,
                Map.of("items", topAnomalies), charts);
    }
    @Tool(name = "AI经营驾驶舱", value = "实时展示产值、利润、库存、回款、设备利用率、订单利润率等核心经营指标。")
    public String getBusinessCockpit(@ToolMemoryId String memoryId,
                                     @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                     @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                     @P(value = "时间范围描述,如本月、近30天", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange, "本月");
        AnalysisBundle profitBundle = buildOrderProfitBundle(loginUser, range, null, 30);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        List<StockInventory> inventories = queryStockInventory(loginUser);
        BigDecimal inventoryValue = estimateInventoryValue(loginUser, inventories);
        long deviceTotal = countDevices(loginUser);
        long repairingCount = countRepairingDevices(loginUser);
        BigDecimal deviceUtilization = deviceTotal > 0
                ? new BigDecimal(deviceTotal - repairingCount).multiply(ONE_HUNDRED).divide(new BigDecimal(deviceTotal), 2, RoundingMode.HALF_UP)
                : BigDecimal.ZERO;
        BigDecimal outputValue = profitBundle.totalRevenue();
        BigDecimal profitRate = rate(profitBundle.totalProfit(), profitBundle.totalRevenue());
        BigDecimal collectionRate = snapshot.receivableTotal().compareTo(BigDecimal.ZERO) > 0
                ? ONE_HUNDRED.subtract(rate(snapshot.receivableTotal(), snapshot.receivableTotal().add(snapshot.payableTotal())).multiply(ONE_HUNDRED))
                : BigDecimal.ZERO;
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("outputValue", outputValue);
        summary.put("profit", profitBundle.totalProfit());
        summary.put("profitRate", toPercent(profitRate));
        summary.put("inventoryValue", inventoryValue);
        summary.put("receivableBalance", snapshot.receivableTotal());
        summary.put("payableBalance", snapshot.payableTotal());
        summary.put("collectionRate", toPercent(collectionRate.divide(ONE_HUNDRED, 4, RoundingMode.HALF_UP)));
        summary.put("deviceUtilizationRate", deviceUtilization + "%");
        summary.put("orderProfitRate", toPercent(avgRate(profitBundle.orderMetrics())));
        Map<String, Object> indicators = new LinkedHashMap<>();
        indicators.put("产值", outputValue);
        indicators.put("利润", profitBundle.totalProfit());
        indicators.put("库存资金占用", inventoryValue);
        indicators.put("应收余额", snapshot.receivableTotal());
        indicators.put("设备利用率", deviceUtilization + "%");
        indicators.put("订单平均利润率", toPercent(avgRate(profitBundle.orderMetrics())));
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("kpiCardData", indicators);
        charts.put("profitTrendOption", buildOrderProfitBar(profitBundle.orderMetrics()));
        charts.put("receivablePayableBarOption", buildReceivablePayableBar(snapshot.receivableTotal(), snapshot.payableTotal()));
        charts.put("inventoryProfitGaugeOption", buildInventoryProfitGauge(inventoryValue, profitBundle.totalProfit()));
        return jsonResponse(true, "financial_business_cockpit", "已生成AI经营驾驶舱数据", summary,
                Map.of(
                        "orderProfitTop", profitBundle.orderMetrics().stream()
                                .sorted(Comparator.comparing(OrderProfitMetric::profit).reversed())
                                .limit(10)
                                .map(this::toOrderCostItem)
                                .toList(),
                        "indicators", indicators
                ),
                charts
        );
    }
    @Tool(name = "日报周报自动生成", value = "自动输出经营分析日报/周报与风险建议。")
    public String generateOperationReport(@ToolMemoryId String memoryId,
                                          @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                          @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                          @P(value = "时间范围描述,如今天、本周", required = false) String timeRange,
                                          @P(value = "报告类型 daily/weekly", required = false) String reportType) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange,
                "weekly".equalsIgnoreCase(reportType) ? "本周" : "今天");
        String type = "weekly".equalsIgnoreCase(reportType) ? "weekly" : "daily";
        AnalysisBundle bundle = buildOrderProfitBundle(loginUser, range, null, 30);
        StatementSnapshot snapshot = buildStatementSnapshot(loginUser);
        BigDecimal inventoryValue = estimateInventoryValue(loginUser, queryStockInventory(loginUser));
        long lossCount = bundle.orderMetrics().stream().filter(item -> item.profit().compareTo(BigDecimal.ZERO) < 0).count();
        List<String> conclusions = new ArrayList<>();
        conclusions.add("营收" + bundle.totalRevenue() + ",利润" + bundle.totalProfit() + ",利润率" + toPercent(rate(bundle.totalProfit(), bundle.totalRevenue())) + "。");
        conclusions.add("应收余额" + snapshot.receivableTotal() + ",应付余额" + snapshot.payableTotal() + ",库存资金占用" + inventoryValue + "。");
        if (lossCount > 0) {
            conclusions.add("发现亏损订单" + lossCount + "个,建议优先复核材料损耗和工序人工效率。");
        } else {
            conclusions.add("当前未发现亏损订单,建议持续跟踪低于8%利润率订单。");
        }
        if (snapshot.receivableTotal().compareTo(snapshot.payableTotal()) > 0) {
            conclusions.add("回款压力偏高,建议针对高应收客户执行分层催收与账期优化。");
        } else {
            conclusions.add("资金压力可控,建议保持付款计划与采购节奏联动。");
        }
        List<Map<String, Object>> riskSuggestions = new ArrayList<>();
        if (lossCount > 0) {
            riskSuggestions.add(riskSuggestion("利润风险", "高", "复核亏损订单BOM和工序工资定额,必要时调整报价与交付节奏。"));
        }
        if (snapshot.receivableTotal().compareTo(SME_RECEIVABLE_RISK_THRESHOLD) > 0) {
            riskSuggestions.add(riskSuggestion("回款风险", "中", "对应收TOP客户建立周度回款计划,并设置预警阈值。"));
        }
        if (inventoryValue.compareTo(SME_INVENTORY_RISK_THRESHOLD) > 0) {
            riskSuggestions.add(riskSuggestion("库存风险", "中", "对高金额呆滞库存执行降价、替代和生产消耗策略。"));
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("reportType", type);
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("orderCount", bundle.orderMetrics().size());
        summary.put("lossOrderCount", lossCount);
        summary.put("riskSuggestionCount", riskSuggestions.size());
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("headline", "weekly".equals(type) ? "经营周报" : "经营日报");
        data.put("conclusions", conclusions);
        data.put("riskSuggestions", riskSuggestions);
        data.put("orderProfitTop", bundle.orderMetrics().stream()
                .sorted(Comparator.comparing(OrderProfitMetric::profitRate))
                .limit(10)
                .map(this::toRiskOrderItem)
                .toList());
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("reportProfitBarOption", buildOrderProfitBar(bundle.orderMetrics()));
        charts.put("reportReceivablePayableOption", buildReceivablePayableBar(snapshot.receivableTotal(), snapshot.payableTotal()));
        return jsonResponse(true, "financial_operation_report", "已自动生成经营分析报告", summary, data, charts);
    }
    private AnalysisBundle buildOrderProfitBundle(LoginUser loginUser, DateRange range, String keyword, Integer limit) {
        List<SalesLedger> ledgers = querySalesLedgers(loginUser, range, keyword, limit);
        if (ledgers.isEmpty()) {
            return AnalysisBundle.empty();
        }
        List<Long> ledgerIds = ledgers.stream().map(SalesLedger::getId).filter(Objects::nonNull).toList();
        List<SalesLedgerProduct> ledgerProducts = queryLedgerProducts(loginUser, ledgerIds);
        Map<Long, List<SalesLedgerProduct>> productsByLedgerId = ledgerProducts.stream()
                .collect(Collectors.groupingBy(SalesLedgerProduct::getSalesLedgerId));
        MaterialCostResult materialCostResult = calculateMaterialCost(loginUser, range, ledgerProducts);
        ProductionCostContext productionCostContext = calculateProductionCost(loginUser, range, ledgers, ledgerProducts, materialCostResult.avgUnitCostByModelId());
        BigDecimal totalDepreciation = calculateTotalDepreciation(loginUser);
        BigDecimal totalRevenue = ledgers.stream()
                .map(SalesLedger::getContractAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        Map<Long, BigDecimal> depreciationCostByLedger = allocateDepreciation(ledgers, totalDepreciation, totalRevenue);
        List<OrderProfitMetric> metrics = new ArrayList<>();
        for (SalesLedger ledger : ledgers) {
            BigDecimal revenue = defaultDecimal(ledger.getContractAmount());
            BigDecimal materialCost = materialCostResult.materialCostByLedgerId().getOrDefault(ledger.getId(), fallbackMaterialCost(productsByLedgerId.get(ledger.getId()), revenue));
            BigDecimal laborCost = productionCostContext.laborCostByLedgerId().getOrDefault(ledger.getId(), BigDecimal.ZERO);
            BigDecimal scrapCost = productionCostContext.scrapCostByLedgerId().getOrDefault(ledger.getId(), BigDecimal.ZERO);
            BigDecimal depreciationCost = depreciationCostByLedger.getOrDefault(ledger.getId(), BigDecimal.ZERO);
            BigDecimal totalCost = materialCost.add(laborCost).add(scrapCost).add(depreciationCost);
            BigDecimal profit = revenue.subtract(totalCost);
            BigDecimal profitRate = rate(profit, revenue);
            String riskLevel = profit.compareTo(BigDecimal.ZERO) < 0
                    ? "high"
                    : (profitRate.compareTo(new BigDecimal("0.08")) < 0 ? "medium" : "low");
            List<String> reasons = buildProfitReasons(revenue, materialCost, laborCost, scrapCost, profit, profitRate);
            String suggestion = buildProfitSuggestion(riskLevel, reasons);
            metrics.add(new OrderProfitMetric(
                    ledger.getId(),
                    safe(ledger.getSalesContractNo()),
                    safe(ledger.getCustomerName()),
                    safe(ledger.getProjectName()),
                    toLocalDate(ledger.getEntryDate()),
                    ledger.getDeliveryDate(),
                    revenue,
                    materialCost,
                    laborCost,
                    depreciationCost,
                    scrapCost,
                    totalCost,
                    profit,
                    profitRate,
                    riskLevel,
                    reasons,
                    suggestion
            ));
        }
        metrics.sort(Comparator.comparing(OrderProfitMetric::entryDate, Comparator.nullsLast(Comparator.reverseOrder()))
                .thenComparing(OrderProfitMetric::ledgerId, Comparator.nullsLast(Comparator.reverseOrder())));
        BigDecimal totalMaterialCost = metrics.stream().map(OrderProfitMetric::materialCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalLaborCost = metrics.stream().map(OrderProfitMetric::laborCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalScrapCost = metrics.stream().map(OrderProfitMetric::scrapCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalDepreciationCost = metrics.stream().map(OrderProfitMetric::depreciationCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalCost = metrics.stream().map(OrderProfitMetric::totalCost).reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalProfit = metrics.stream().map(OrderProfitMetric::profit).reduce(BigDecimal.ZERO, BigDecimal::add);
        return new AnalysisBundle(
                metrics,
                productionCostContext.processCostRanking(),
                totalRevenue,
                totalMaterialCost,
                totalLaborCost,
                totalDepreciationCost,
                totalScrapCost,
                totalCost,
                totalProfit
        );
    }
    private MaterialCostResult calculateMaterialCost(LoginUser loginUser, DateRange range, List<SalesLedgerProduct> ledgerProducts) {
        if (ledgerProducts.isEmpty()) {
            return new MaterialCostResult(Map.of(), Map.of());
        }
        List<Long> ledgerProductIds = ledgerProducts.stream().map(SalesLedgerProduct::getId).filter(Objects::nonNull).toList();
        Set<Long> productModelIds = ledgerProducts.stream().map(SalesLedgerProduct::getProductModelId).filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, Long> productIdToLedgerId = ledgerProducts.stream()
                .filter(item -> item.getId() != null && item.getSalesLedgerId() != null)
                .collect(Collectors.toMap(SalesLedgerProduct::getId, SalesLedgerProduct::getSalesLedgerId, (a, b) -> a));
        Map<Long, BigDecimal> avgUnitCostByModelId = queryAverageUnitCostByModel(loginUser, productModelIds);
        LambdaQueryWrapper<ProcurementRecordOut> outWrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(outWrapper, loginUser.getTenantId(), ProcurementRecordOut::getTenantId);
        applyDeptFilter(outWrapper, loginUser.getCurrentDeptId(), ProcurementRecordOut::getDeptId);
        outWrapper.eq(ProcurementRecordOut::getType, 2)
                .in(ProcurementRecordOut::getSalesLedgerProductId, ledgerProductIds);
        if (range.hasDateFilter()) {
            outWrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
                    .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(outWrapper));
        Set<Integer> storageIds = outList.stream()
                .map(ProcurementRecordOut::getProcurementRecordStorageId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Integer, ProcurementRecordStorage> storageMap = storageIds.isEmpty()
                ? Map.of()
                : defaultList(procurementRecordMapper.selectBatchIds(storageIds)).stream()
                .collect(Collectors.toMap(ProcurementRecordStorage::getId, item -> item, (a, b) -> a));
        Map<Long, BigDecimal> materialCostByLedgerId = new HashMap<>();
        for (ProcurementRecordOut out : outList) {
            Long ledgerId = productIdToLedgerId.get(out.getSalesLedgerProductId());
            if (ledgerId == null) {
                continue;
            }
            ProcurementRecordStorage storage = storageMap.get(out.getProcurementRecordStorageId());
            BigDecimal unitPrice = storage == null ? BigDecimal.ZERO : defaultDecimal(storage.getUnitPrice());
            BigDecimal quantity = defaultDecimal(out.getInboundNum());
            BigDecimal cost = quantity.multiply(unitPrice);
            materialCostByLedgerId.merge(ledgerId, cost, BigDecimal::add);
        }
        return new MaterialCostResult(materialCostByLedgerId, avgUnitCostByModelId);
    }
    private ProductionCostContext calculateProductionCost(LoginUser loginUser,
                                                          DateRange range,
                                                          List<SalesLedger> ledgers,
                                                          List<SalesLedgerProduct> ledgerProducts,
                                                          Map<Long, BigDecimal> avgUnitCostByModelId) {
        if (ledgers.isEmpty()) {
            return ProductionCostContext.empty();
        }
        Set<Long> ledgerIds = ledgers.stream().map(SalesLedger::getId).filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, Set<Long>> productModelToLedgerIds = new HashMap<>();
        for (SalesLedgerProduct product : ledgerProducts) {
            if (product.getProductModelId() == null || product.getSalesLedgerId() == null) {
                continue;
            }
            productModelToLedgerIds.computeIfAbsent(product.getProductModelId(), key -> new HashSet<>()).add(product.getSalesLedgerId());
        }
        LambdaQueryWrapper<ProductionPlan> planWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(planWrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        planWrapper.in(ProductionPlan::getSalesLedgerId, ledgerIds);
        List<ProductionPlan> plans = defaultList(productionPlanMapper.selectList(planWrapper));
        Map<Long, Long> planIdToLedgerId = plans.stream()
                .filter(item -> item.getId() != null && item.getSalesLedgerId() != null)
                .collect(Collectors.toMap(ProductionPlan::getId, ProductionPlan::getSalesLedgerId, (a, b) -> a));
        LambdaQueryWrapper<ProductionOrder> orderWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(orderWrapper, loginUser.getCurrentDeptId(), ProductionOrder::getDeptId);
        if (range.hasDateFilter()) {
            orderWrapper.ge(ProductionOrder::getCreateTime, range.start().atStartOfDay().minusMonths(2))
                    .lt(ProductionOrder::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
        }
        List<ProductionOrder> orders = defaultList(productionOrderMapper.selectList(orderWrapper));
        Map<Long, Set<Long>> orderIdToLedgerIds = new HashMap<>();
        for (ProductionOrder order : orders) {
            Set<Long> orderLedgers = new HashSet<>();
            for (Long planId : parseIdList(order.getProductionPlanIds())) {
                Long ledgerId = planIdToLedgerId.get(planId);
                if (ledgerId != null) {
                    orderLedgers.add(ledgerId);
                }
            }
            if (orderLedgers.isEmpty() && order.getProductModelId() != null) {
                orderLedgers.addAll(productModelToLedgerIds.getOrDefault(order.getProductModelId(), Set.of()));
            }
            if (!orderLedgers.isEmpty()) {
                orderIdToLedgerIds.put(order.getId(), orderLedgers);
            }
        }
        if (orderIdToLedgerIds.isEmpty()) {
            return ProductionCostContext.empty();
        }
        LambdaQueryWrapper<ProductionOperationTask> taskWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(taskWrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        taskWrapper.in(ProductionOperationTask::getProductionOrderId, orderIdToLedgerIds.keySet());
        List<ProductionOperationTask> tasks = defaultList(productionOperationTaskMapper.selectList(taskWrapper));
        Map<Long, Long> taskIdToOrderId = tasks.stream()
                .filter(item -> item.getId() != null && item.getProductionOrderId() != null)
                .collect(Collectors.toMap(ProductionOperationTask::getId, ProductionOperationTask::getProductionOrderId, (a, b) -> a));
        if (taskIdToOrderId.isEmpty()) {
            return ProductionCostContext.empty();
        }
        LambdaQueryWrapper<ProductionProductMain> mainWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(mainWrapper, loginUser.getCurrentDeptId(), ProductionProductMain::getDeptId);
        mainWrapper.in(ProductionProductMain::getProductionOperationTaskId, taskIdToOrderId.keySet());
        if (range.hasDateFilter()) {
            mainWrapper.ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay().minusMonths(2))
                    .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay().plusMonths(1));
        }
        List<ProductionProductMain> mainList = defaultList(productionProductMainMapper.selectList(mainWrapper));
        Map<Long, Set<Long>> mainIdToLedgers = new HashMap<>();
        for (ProductionProductMain main : mainList) {
            Long orderId = taskIdToOrderId.get(main.getProductionOperationTaskId());
            if (orderId == null) {
                continue;
            }
            Set<Long> ledgerSet = orderIdToLedgerIds.get(orderId);
            if (ledgerSet == null || ledgerSet.isEmpty()) {
                continue;
            }
            mainIdToLedgers.put(main.getId(), ledgerSet);
        }
        if (mainIdToLedgers.isEmpty()) {
            return ProductionCostContext.empty();
        }
        LambdaQueryWrapper<ProductionAccount> accountWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(accountWrapper, loginUser.getCurrentDeptId(), ProductionAccount::getDeptId);
        accountWrapper.in(ProductionAccount::getProductionProductMainId, mainIdToLedgers.keySet());
        if (range.hasDateFilter()) {
            accountWrapper.ge(ProductionAccount::getSchedulingDate, range.start().atStartOfDay())
                    .lt(ProductionAccount::getSchedulingDate, range.end().plusDays(1).atStartOfDay());
        }
        List<ProductionAccount> accountList = defaultList(productionAccountMapper.selectList(accountWrapper));
        Map<String, BigDecimal> salaryQuotaByOperation = defaultList(technologyOperationMapper.selectList(new LambdaQueryWrapper<TechnologyOperation>()
                        .select(TechnologyOperation::getName, TechnologyOperation::getSalaryQuota)))
                .stream()
                .filter(item -> StringUtils.hasText(item.getName()))
                .collect(Collectors.toMap(TechnologyOperation::getName, item -> defaultDecimal(item.getSalaryQuota()), (a, b) -> a));
        Map<Long, BigDecimal> laborCostByLedger = new HashMap<>();
        Map<String, BigDecimal> processCostMap = new HashMap<>();
        for (ProductionAccount account : accountList) {
            Set<Long> ledgerSet = mainIdToLedgers.get(account.getProductionProductMainId());
            if (ledgerSet == null || ledgerSet.isEmpty()) {
                continue;
            }
            BigDecimal cost = estimateLaborCost(account, salaryQuotaByOperation);
            if (cost.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal split = cost.divide(new BigDecimal(ledgerSet.size()), 4, RoundingMode.HALF_UP);
            for (Long ledgerId : ledgerSet) {
                laborCostByLedger.merge(ledgerId, split, BigDecimal::add);
            }
            processCostMap.merge(safe(account.getTechnologyOperationName()), cost, BigDecimal::add);
        }
        LambdaQueryWrapper<ProductionProductOutput> outputWrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(outputWrapper, loginUser.getCurrentDeptId(), ProductionProductOutput::getDeptId);
        outputWrapper.in(ProductionProductOutput::getProductionProductMainId, mainIdToLedgers.keySet());
        if (range.hasDateFilter()) {
            outputWrapper.ge(ProductionProductOutput::getCreateTime, range.start().atStartOfDay())
                    .lt(ProductionProductOutput::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        List<ProductionProductOutput> outputList = defaultList(productionProductOutputMapper.selectList(outputWrapper));
        Map<Long, BigDecimal> scrapCostByLedger = new HashMap<>();
        for (ProductionProductOutput output : outputList) {
            Set<Long> ledgerSet = mainIdToLedgers.get(output.getProductionProductMainId());
            if (ledgerSet == null || ledgerSet.isEmpty()) {
                continue;
            }
            BigDecimal scrapQty = defaultDecimal(output.getScrapQty());
            if (scrapQty.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal unitCost = avgUnitCostByModelId.getOrDefault(output.getProductModelId(), BigDecimal.ZERO);
            BigDecimal scrapCost = scrapQty.multiply(unitCost);
            BigDecimal split = scrapCost.divide(new BigDecimal(ledgerSet.size()), 4, RoundingMode.HALF_UP);
            for (Long ledgerId : ledgerSet) {
                scrapCostByLedger.merge(ledgerId, split, BigDecimal::add);
            }
        }
        Map<String, BigDecimal> processCostRanking = processCostMap.entrySet().stream()
                .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
                .limit(10)
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
        return new ProductionCostContext(laborCostByLedger, scrapCostByLedger, processCostRanking);
    }
    private List<SalesLedger> querySalesLedgers(LoginUser loginUser, DateRange range, String keyword, Integer limit) {
        LambdaQueryWrapper<SalesLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), SalesLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedger::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(SalesLedger::getSalesContractNo, keyword)
                    .or().like(SalesLedger::getCustomerContractNo, keyword)
                    .or().like(SalesLedger::getCustomerName, keyword)
                    .or().like(SalesLedger::getProjectName, keyword)
                    .or().like(SalesLedger::getSalesman, keyword));
        }
        if (range.hasDateFilter()) {
            wrapper.ge(SalesLedger::getEntryDate, toDate(range.start()))
                    .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end()));
        }
        wrapper.orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId);
        if (limit != null && limit > 0) {
            wrapper.last("limit " + normalizeLimit(limit));
        }
        return defaultList(salesLedgerMapper.selectList(wrapper));
    }
    private List<SalesLedgerProduct> queryLedgerProducts(LoginUser loginUser, List<Long> ledgerIds) {
        if (ledgerIds == null || ledgerIds.isEmpty()) {
            return List.of();
        }
        LambdaQueryWrapper<SalesLedgerProduct> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedgerProduct::getDeptId);
        wrapper.in(SalesLedgerProduct::getSalesLedgerId, ledgerIds)
                .eq(SalesLedgerProduct::getType, 1);
        return defaultList(salesLedgerProductMapper.selectList(wrapper));
    }
    private List<StockInventory> queryStockInventory(LoginUser loginUser) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        return defaultList(stockInventoryMapper.selectList(wrapper));
    }
    private Map<Long, ProductModel> queryProductModels(Set<Long> modelIds) {
        if (modelIds == null || modelIds.isEmpty()) {
            return defaultList(productModelMapper.selectList(null)).stream()
                    .filter(item -> item.getId() != null)
                    .collect(Collectors.toMap(ProductModel::getId, item -> item, (a, b) -> a));
        }
        LambdaQueryWrapper<ProductModel> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(ProductModel::getId, modelIds);
        return defaultList(productModelMapper.selectList(wrapper)).stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(ProductModel::getId, item -> item, (a, b) -> a));
    }
    private Map<Long, Product> queryProducts(Collection<ProductModel> models) {
        Set<Long> productIds = models == null ? Set.of() : models.stream()
                .map(ProductModel::getProductId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (productIds.isEmpty()) {
            return Map.of();
        }
        LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(Product::getId, productIds);
        return defaultList(productMapper.selectList(wrapper)).stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(Product::getId, item -> item, (a, b) -> a));
    }
    private Map<Long, BigDecimal> queryAverageUnitCostByModel(LoginUser loginUser, Set<Long> productModelIds) {
        LambdaQueryWrapper<ProcurementRecordStorage> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementRecordStorage::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementRecordStorage::getDeptId);
        wrapper.in(ProcurementRecordStorage::getType, List.of(1, 2));
        if (productModelIds != null && !productModelIds.isEmpty()) {
            wrapper.in(ProcurementRecordStorage::getProductModelId, productModelIds);
        }
        List<ProcurementRecordStorage> rows = defaultList(procurementRecordMapper.selectList(wrapper));
        Map<Long, BigDecimal> amountByModel = new HashMap<>();
        Map<Long, BigDecimal> qtyByModel = new HashMap<>();
        for (ProcurementRecordStorage row : rows) {
            if (row.getProductModelId() == null) {
                continue;
            }
            BigDecimal qty = defaultDecimal(row.getInboundNum());
            if (qty.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal amount = defaultDecimal(row.getUnitPrice()).multiply(qty);
            amountByModel.merge(row.getProductModelId(), amount, BigDecimal::add);
            qtyByModel.merge(row.getProductModelId(), qty, BigDecimal::add);
        }
        Map<Long, BigDecimal> result = new HashMap<>();
        for (Map.Entry<Long, BigDecimal> entry : amountByModel.entrySet()) {
            BigDecimal qty = qtyByModel.get(entry.getKey());
            if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            result.put(entry.getKey(), entry.getValue().divide(qty, 6, RoundingMode.HALF_UP));
        }
        return result;
    }
    private OutboundStats queryOutboundStats(LoginUser loginUser, Set<Long> productModelIds, DateRange range) {
        LambdaQueryWrapper<ProcurementRecordOut> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementRecordOut::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementRecordOut::getDeptId);
        if (productModelIds != null && !productModelIds.isEmpty()) {
            wrapper.in(ProcurementRecordOut::getProductModelId, productModelIds);
        }
        if (range.hasDateFilter()) {
            wrapper.ge(ProcurementRecordOut::getCreateTime, range.start().atStartOfDay())
                    .lt(ProcurementRecordOut::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        List<ProcurementRecordOut> outList = defaultList(procurementRecordOutMapper.selectList(wrapper));
        if (outList.isEmpty()) {
            return OutboundStats.empty();
        }
        Set<Integer> storageIds = outList.stream()
                .map(ProcurementRecordOut::getProcurementRecordStorageId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        Map<Integer, ProcurementRecordStorage> storageMap = storageIds.isEmpty()
                ? Map.of()
                : defaultList(procurementRecordMapper.selectBatchIds(storageIds)).stream()
                .collect(Collectors.toMap(ProcurementRecordStorage::getId, item -> item, (a, b) -> a));
        Map<Long, BigDecimal> outboundQtyByModel = new HashMap<>();
        Map<Long, LocalDateTime> lastOutboundTimeByModel = new HashMap<>();
        BigDecimal totalOutboundCost = BigDecimal.ZERO;
        for (ProcurementRecordOut out : outList) {
            Long modelId = out.getProductModelId();
            if (modelId == null) {
                continue;
            }
            BigDecimal qty = defaultDecimal(out.getInboundNum());
            outboundQtyByModel.merge(modelId, qty, BigDecimal::add);
            if (out.getCreateTime() != null) {
                LocalDateTime existing = lastOutboundTimeByModel.get(modelId);
                if (existing == null || out.getCreateTime().isAfter(existing)) {
                    lastOutboundTimeByModel.put(modelId, out.getCreateTime());
                }
            }
            ProcurementRecordStorage storage = storageMap.get(out.getProcurementRecordStorageId());
            BigDecimal unitPrice = storage == null ? BigDecimal.ZERO : defaultDecimal(storage.getUnitPrice());
            totalOutboundCost = totalOutboundCost.add(unitPrice.multiply(qty));
        }
        return new OutboundStats(outboundQtyByModel, lastOutboundTimeByModel, totalOutboundCost);
    }
    private List<InventoryMetric> buildInventoryMetrics(List<StockInventory> inventoryRows,
                                                        Map<Long, ProductModel> productModelMap,
                                                        Map<Long, Product> productMap,
                                                        Map<Long, BigDecimal> avgUnitCostByModelId,
                                                        OutboundStats outboundStats) {
        Map<Long, InventoryMetricBuilder> metricBuilderByModel = new HashMap<>();
        for (StockInventory row : inventoryRows) {
            if (row.getProductModelId() == null) {
                continue;
            }
            InventoryMetricBuilder builder = metricBuilderByModel.computeIfAbsent(row.getProductModelId(), InventoryMetricBuilder::new);
            builder.addQuantity(maxZero(defaultDecimal(row.getQualitity()).subtract(defaultDecimal(row.getLockedQuantity()))));
            builder.addLockedQuantity(defaultDecimal(row.getLockedQuantity()));
            builder.addWarnNum(defaultDecimal(row.getWarnNum()));
            if (row.getCreateTime() != null) {
                builder.updateFirstInTime(row.getCreateTime());
            }
        }
        List<InventoryMetric> result = new ArrayList<>();
        LocalDate today = LocalDate.now();
        for (InventoryMetricBuilder builder : metricBuilderByModel.values()) {
            Long modelId = builder.modelId();
            ProductModel model = productModelMap.get(modelId);
            Product product = model == null ? null : productMap.get(model.getProductId());
            BigDecimal unitCost = avgUnitCostByModelId.getOrDefault(modelId, BigDecimal.ZERO);
            BigDecimal value = builder.quantity().multiply(unitCost);
            LocalDateTime lastOutTime = outboundStats.lastOutboundTimeByModel().get(modelId);
            long stagnantDays;
            if (lastOutTime != null) {
                stagnantDays = daysBetween(lastOutTime.toLocalDate(), today);
            } else if (builder.firstInTime() != null) {
                stagnantDays = daysBetween(builder.firstInTime().toLocalDate(), today);
            } else {
                stagnantDays = 0;
            }
            BigDecimal outQty = outboundStats.outboundQtyByModel().getOrDefault(modelId, BigDecimal.ZERO);
            boolean overstock = builder.warnNum().compareTo(BigDecimal.ZERO) > 0
                    && builder.quantity().compareTo(builder.warnNum().multiply(new BigDecimal("3"))) > 0;
            result.add(new InventoryMetric(
                    modelId,
                    product == null ? "未知产品" : safe(product.getProductName()),
                    model == null ? "未知型号" : safe(model.getModel()),
                    builder.quantity(),
                    builder.lockedQuantity(),
                    unitCost,
                    value,
                    outQty,
                    stagnantDays,
                    overstock
            ));
        }
        return result;
    }
    private List<AccountSalesCollection> queryCollections(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId);
        if (range.hasDateFilter()) {
            wrapper.ge(AccountSalesCollection::getCollectionDate, range.start())
                    .le(AccountSalesCollection::getCollectionDate, range.end());
        }
        wrapper.orderByAsc(AccountSalesCollection::getCollectionDate);
        return defaultList(accountSalesCollectionMapper.selectList(wrapper));
    }
    private List<AccountPurchasePayment> queryPayments(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountPurchasePayment> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchasePayment::getDeptId);
        if (range.hasDateFilter()) {
            wrapper.ge(AccountPurchasePayment::getPaymentDate, range.start())
                    .le(AccountPurchasePayment::getPaymentDate, range.end());
        }
        wrapper.orderByAsc(AccountPurchasePayment::getPaymentDate);
        return defaultList(accountPurchasePaymentMapper.selectList(wrapper));
    }
    private List<MonthlyCashFlow> buildMonthlyCashFlow(DateRange range,
                                                       List<AccountSalesCollection> collections,
                                                       List<AccountPurchasePayment> payments) {
        Map<YearMonth, BigDecimal> incomeByMonth = new LinkedHashMap<>();
        Map<YearMonth, BigDecimal> expenseByMonth = new LinkedHashMap<>();
        DateRange monthlyRange = range.hasDateFilter() ? range : inferCashFlowRange(collections, payments);
        if (!monthlyRange.hasDateFilter()) {
            return List.of();
        }
        YearMonth startMonth = YearMonth.from(monthlyRange.start());
        YearMonth endMonth = YearMonth.from(monthlyRange.end());
        for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) {
            incomeByMonth.put(month, BigDecimal.ZERO);
            expenseByMonth.put(month, BigDecimal.ZERO);
        }
        for (AccountSalesCollection row : collections) {
            if (row.getCollectionDate() == null) {
                continue;
            }
            YearMonth month = YearMonth.from(row.getCollectionDate());
            if (incomeByMonth.containsKey(month)) {
                incomeByMonth.put(month, incomeByMonth.get(month).add(defaultDecimal(row.getCollectionAmount())));
            }
        }
        for (AccountPurchasePayment row : payments) {
            if (row.getPaymentDate() == null) {
                continue;
            }
            YearMonth month = YearMonth.from(row.getPaymentDate());
            if (expenseByMonth.containsKey(month)) {
                expenseByMonth.put(month, expenseByMonth.get(month).add(defaultDecimal(row.getPaymentAmount())));
            }
        }
        List<MonthlyCashFlow> result = new ArrayList<>();
        for (YearMonth month : incomeByMonth.keySet()) {
            BigDecimal income = incomeByMonth.get(month);
            BigDecimal expense = expenseByMonth.getOrDefault(month, BigDecimal.ZERO);
            result.add(new MonthlyCashFlow(month.toString(), income, expense, income.subtract(expense)));
        }
        return result;
    }
    private DateRange inferCashFlowRange(List<AccountSalesCollection> collections,
                                         List<AccountPurchasePayment> payments) {
        LocalDate min = null;
        LocalDate max = null;
        for (AccountSalesCollection row : defaultList(collections)) {
            if (row.getCollectionDate() == null) {
                continue;
            }
            min = min == null || row.getCollectionDate().isBefore(min) ? row.getCollectionDate() : min;
            max = max == null || row.getCollectionDate().isAfter(max) ? row.getCollectionDate() : max;
        }
        for (AccountPurchasePayment row : defaultList(payments)) {
            if (row.getPaymentDate() == null) {
                continue;
            }
            min = min == null || row.getPaymentDate().isBefore(min) ? row.getPaymentDate() : min;
            max = max == null || row.getPaymentDate().isAfter(max) ? row.getPaymentDate() : max;
        }
        return min == null || max == null ? new DateRange(null, null, "全部") : new DateRange(min, max, "全部");
    }
    private List<MonthlyCashFlow> forecastMonthlyCashFlow(List<MonthlyCashFlow> actual, int forecastMonths) {
        if (actual.isEmpty()) {
            List<MonthlyCashFlow> defaults = new ArrayList<>();
            YearMonth now = YearMonth.now();
            for (int i = 1; i <= forecastMonths; i++) {
                YearMonth month = now.plusMonths(i);
                defaults.add(new MonthlyCashFlow(month.toString(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
            }
            return defaults;
        }
        List<BigDecimal> series = actual.stream().map(MonthlyCashFlow::netFlow).toList();
        BigDecimal avg = series.stream().reduce(BigDecimal.ZERO, BigDecimal::add)
                .divide(new BigDecimal(series.size()), 4, RoundingMode.HALF_UP);
        BigDecimal slope = BigDecimal.ZERO;
        if (series.size() > 1) {
            slope = series.get(series.size() - 1).subtract(series.get(0))
                    .divide(new BigDecimal(series.size() - 1), 4, RoundingMode.HALF_UP);
        }
        YearMonth lastMonth = YearMonth.parse(actual.get(actual.size() - 1).month());
        List<MonthlyCashFlow> forecast = new ArrayList<>();
        for (int i = 1; i <= forecastMonths; i++) {
            YearMonth month = lastMonth.plusMonths(i);
            BigDecimal net = avg.add(slope.multiply(new BigDecimal(i))).setScale(2, RoundingMode.HALF_UP);
            BigDecimal income = net.compareTo(BigDecimal.ZERO) >= 0 ? net : BigDecimal.ZERO;
            BigDecimal expense = net.compareTo(BigDecimal.ZERO) >= 0 ? BigDecimal.ZERO : net.abs();
            forecast.add(new MonthlyCashFlow(month.toString(), income, expense, net));
        }
        return forecast;
    }
    private StatementSnapshot buildStatementSnapshot(LoginUser loginUser) {
        LambdaQueryWrapper<AccountStatement> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountStatement::getDeptId);
        wrapper.orderByDesc(AccountStatement::getStatementMonth);
        List<AccountStatement> rows = defaultList(accountStatementMapper.selectList(wrapper));
        if (rows.isEmpty()) {
            return StatementSnapshot.empty();
        }
        Map<String, AccountStatement> latestByEntity = new HashMap<>();
        for (AccountStatement row : rows) {
            if (row.getAccountType() == null || row.getCustomerId() == null || !StringUtils.hasText(row.getStatementMonth())) {
                continue;
            }
            String key = row.getAccountType() + "::" + row.getCustomerId();
            AccountStatement existing = latestByEntity.get(key);
            if (existing == null || row.getStatementMonth().compareTo(existing.getStatementMonth()) > 0) {
                latestByEntity.put(key, row);
            }
        }
        BigDecimal receivableTotal = BigDecimal.ZERO;
        BigDecimal payableTotal = BigDecimal.ZERO;
        List<StatementMetric> receivableMetrics = new ArrayList<>();
        List<StatementMetric> payableMetrics = new ArrayList<>();
        for (AccountStatement row : latestByEntity.values()) {
            BigDecimal closing = defaultDecimal(row.getClosingBalance());
            if (Objects.equals(row.getAccountType(), 1)) {
                receivableTotal = receivableTotal.add(closing);
                receivableMetrics.add(new StatementMetric(String.valueOf(row.getCustomerId()), closing,
                        defaultDecimal(row.getCurrentPlan()), defaultDecimal(row.getCurrentActually()), safe(row.getStatementMonth())));
            } else if (Objects.equals(row.getAccountType(), 2)) {
                payableTotal = payableTotal.add(closing);
                payableMetrics.add(new StatementMetric(String.valueOf(row.getCustomerId()), closing,
                        defaultDecimal(row.getCurrentPlan()), defaultDecimal(row.getCurrentActually()), safe(row.getStatementMonth())));
            }
        }
        receivableMetrics.sort(Comparator.comparing(StatementMetric::closingBalance).reversed());
        payableMetrics.sort(Comparator.comparing(StatementMetric::closingBalance).reversed());
        return new StatementSnapshot(
                receivableTotal,
                payableTotal,
                receivableMetrics.stream().limit(10).toList(),
                payableMetrics.stream().limit(10).toList()
        );
    }
    private BigDecimal calculateTotalDepreciation(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
        wrapper.eq(DeviceLedger::getIsDepr, 1);
        List<DeviceLedger> devices = defaultList(deviceLedgerMapper.selectList(wrapper));
        BigDecimal total = BigDecimal.ZERO;
        for (DeviceLedger device : devices) {
            total = total.add(defaultDecimal(AccountingServiceImpl.calculatePreciseDepreciation(device)));
        }
        return total;
    }
    private Map<Long, BigDecimal> allocateDepreciation(List<SalesLedger> ledgers, BigDecimal totalDepreciation, BigDecimal totalRevenue) {
        if (ledgers.isEmpty() || totalDepreciation.compareTo(BigDecimal.ZERO) <= 0) {
            return Map.of();
        }
        Map<Long, BigDecimal> result = new HashMap<>();
        if (totalRevenue.compareTo(BigDecimal.ZERO) <= 0) {
            BigDecimal avg = totalDepreciation.divide(new BigDecimal(ledgers.size()), 4, RoundingMode.HALF_UP);
            for (SalesLedger ledger : ledgers) {
                result.put(ledger.getId(), avg);
            }
            return result;
        }
        for (SalesLedger ledger : ledgers) {
            BigDecimal revenue = defaultDecimal(ledger.getContractAmount());
            BigDecimal ratio = revenue.divide(totalRevenue, 6, RoundingMode.HALF_UP);
            result.put(ledger.getId(), totalDepreciation.multiply(ratio));
        }
        return result;
    }
    private BigDecimal fallbackMaterialCost(List<SalesLedgerProduct> products, BigDecimal revenue) {
        if (products != null && !products.isEmpty()) {
            BigDecimal productAmount = products.stream()
                    .map(SalesLedgerProduct::getTaxExclusiveTotalPrice)
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            if (productAmount.compareTo(BigDecimal.ZERO) > 0) {
                return productAmount;
            }
        }
        BigDecimal materialCost = revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE);
        BigDecimal laborCost = revenue.multiply(DEFAULT_LABOR_COST_RATE);
        BigDecimal overheadCost = revenue.multiply(DEFAULT_OVERHEAD_COST_RATE);
        return materialCost.add(laborCost).add(overheadCost);
    }
    private BigDecimal estimateTotalCost(BigDecimal revenue, List<SalesLedgerProduct> products) {
        if (revenue == null || revenue.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        BigDecimal materialCost = BigDecimal.ZERO;
        if (products != null && !products.isEmpty()) {
            materialCost = products.stream()
                    .map(SalesLedgerProduct::getTaxExclusiveTotalPrice)
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
        }
        if (materialCost.compareTo(BigDecimal.ZERO) <= 0) {
            materialCost = revenue.multiply(DEFAULT_FALLBACK_MATERIAL_COST_RATE);
        }
        BigDecimal laborCost = revenue.multiply(DEFAULT_LABOR_COST_RATE);
        BigDecimal overheadCost = revenue.multiply(DEFAULT_OVERHEAD_COST_RATE);
        return materialCost.add(laborCost).add(overheadCost);
    }
    private Map<String, String> queryCustomerNameMap(Set<String> idSet) {
        if (idSet == null || idSet.isEmpty()) {
            return Map.of();
        }
        Set<Long> ids = idSet.stream()
                .map(this::toLongOrNull)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (ids.isEmpty()) {
            return Map.of();
        }
        LambdaQueryWrapper<Customer> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(Customer::getId, ids);
        return defaultList(customerMapper.selectList(wrapper)).stream()
                .collect(Collectors.toMap(item -> String.valueOf(item.getId()), Customer::getCustomerName, (a, b) -> a));
    }
    private Map<String, String> querySupplierNameMap(Set<String> idSet) {
        if (idSet == null || idSet.isEmpty()) {
            return Map.of();
        }
        Set<Long> ids = idSet.stream()
                .map(this::toLongOrNull)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (ids.isEmpty()) {
            return Map.of();
        }
        LambdaQueryWrapper<SupplierManage> wrapper = new LambdaQueryWrapper<>();
        wrapper.in(SupplierManage::getId, ids);
        return defaultList(supplierManageMapper.selectList(wrapper)).stream()
                .collect(Collectors.toMap(item -> String.valueOf(item.getId()), SupplierManage::getSupplierName, (a, b) -> a));
    }
    private String riskLevelByAmount(BigDecimal amount) {
        if (amount.compareTo(new BigDecimal("5000000")) >= 0) {
            return "high";
        }
        if (amount.compareTo(new BigDecimal("1000000")) >= 0) {
            return "medium";
        }
        return "low";
    }
    private long countDevices(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
        return deviceLedgerMapper.selectCount(wrapper);
    }
    private long countRepairingDevices(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
        wrapper.in(DeviceRepair::getStatus, List.of(0, 3));
        return deviceRepairMapper.selectCount(wrapper);
    }
    private BigDecimal estimateInventoryValue(LoginUser loginUser, List<StockInventory> inventories) {
        if (inventories == null || inventories.isEmpty()) {
            return BigDecimal.ZERO;
        }
        Set<Long> modelIds = inventories.stream().map(StockInventory::getProductModelId).filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, BigDecimal> costMap = queryAverageUnitCostByModel(loginUser, modelIds);
        BigDecimal total = BigDecimal.ZERO;
        for (StockInventory inventory : inventories) {
            BigDecimal qty = maxZero(defaultDecimal(inventory.getQualitity()).subtract(defaultDecimal(inventory.getLockedQuantity())));
            BigDecimal unit = costMap.getOrDefault(inventory.getProductModelId(), BigDecimal.ZERO);
            total = total.add(qty.multiply(unit));
        }
        return total;
    }
    private DateRange previousSameLengthRange(DateRange range) {
        if (!range.hasDateFilter()) {
            return new DateRange(null, null, "全部");
        }
        long days = daysBetween(range.start(), range.end()) + 1L;
        LocalDate prevEnd = range.start().minusDays(1);
        LocalDate prevStart = prevEnd.minusDays(days - 1L);
        return new DateRange(prevStart, prevEnd, prevStart + "至" + prevEnd);
    }
    private List<String> buildProfitReasons(BigDecimal revenue,
                                            BigDecimal materialCost,
                                            BigDecimal laborCost,
                                            BigDecimal scrapCost,
                                            BigDecimal profit,
                                            BigDecimal profitRate) {
        List<String> reasons = new ArrayList<>();
        BigDecimal materialRate = rate(materialCost, revenue);
        if (materialRate.compareTo(new BigDecimal("0.70")) >= 0) {
            reasons.add("材料成本占比超过70%");
        } else if (materialRate.compareTo(new BigDecimal("0.55")) >= 0) {
            reasons.add("材料成本占比偏高");
        }
        BigDecimal laborRate = rate(laborCost, revenue);
        if (laborRate.compareTo(new BigDecimal("0.20")) >= 0) {
            reasons.add("人工成本占比超过20%");
        } else if (laborRate.compareTo(new BigDecimal("0.12")) >= 0) {
            reasons.add("人工成本增长偏快");
        }
        BigDecimal scrapRate = rate(scrapCost, revenue);
        if (scrapRate.compareTo(new BigDecimal("0.05")) >= 0) {
            reasons.add("报废损耗占比偏高");
        }
        if (profit.compareTo(BigDecimal.ZERO) < 0) {
            reasons.add("订单处于亏损状态");
        } else if (profitRate.compareTo(new BigDecimal("0.08")) < 0) {
            reasons.add("利润率低于8%");
        }
        if (reasons.isEmpty()) {
            reasons.add("成本结构处于合理区间");
        }
        return reasons;
    }
    private String buildProfitSuggestion(String riskLevel, List<String> reasons) {
        if ("high".equals(riskLevel)) {
            return "优先复核BOM用量与工序定额,必要时调整报价和付款条款,并限制超账期交付。";
        }
        if ("medium".equals(riskLevel)) {
            return "建议优化采购批次和工序排产,提升一次合格率并同步执行毛利预警。";
        }
        if (reasons.stream().anyMatch(item -> item.contains("材料"))) {
            return "保持材料采购成本看板,按周跟踪主要材料单价波动。";
        }
        return "维持当前经营节奏,继续跟踪订单利润率和回款效率。";
    }
    private Map<String, Object> toOrderCostItem(OrderProfitMetric metric) {
        Map<String, Object> item = new LinkedHashMap<>();
        item.put("ledgerId", metric.ledgerId());
        item.put("salesContractNo", metric.salesContractNo());
        item.put("customerName", metric.customerName());
        item.put("projectName", metric.projectName());
        item.put("entryDate", formatDate(metric.entryDate()));
        item.put("deliveryDate", formatDate(metric.deliveryDate()));
        item.put("revenue", metric.revenue());
        item.put("materialCost", metric.materialCost());
        item.put("laborCost", metric.laborCost());
        item.put("depreciationCost", metric.depreciationCost());
        item.put("scrapCost", metric.scrapCost());
        item.put("totalCost", metric.totalCost());
        item.put("profit", metric.profit());
        item.put("profitRate", toPercent(metric.profitRate()));
        item.put("riskLevel", metric.riskLevel());
        item.put("reasons", metric.reasons());
        item.put("suggestion", metric.suggestion());
        return item;
    }
    private Map<String, Object> toRiskOrderItem(OrderProfitMetric metric) {
        Map<String, Object> map = toOrderCostItem(metric);
        map.put("priority", "high".equals(metric.riskLevel()) ? "high" : ("medium".equals(metric.riskLevel()) ? "medium" : "low"));
        return map;
    }
    private List<Map<String, Object>> buildCustomerProfitTop(List<OrderProfitMetric> metrics, int topN) {
        Map<String, BigDecimal> customerProfitMap = new HashMap<>();
        Map<String, BigDecimal> customerRevenueMap = new HashMap<>();
        for (OrderProfitMetric metric : metrics) {
            customerProfitMap.merge(metric.customerName(), metric.profit(), BigDecimal::add);
            customerRevenueMap.merge(metric.customerName(), metric.revenue(), BigDecimal::add);
        }
        return customerProfitMap.entrySet().stream()
                .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
                .limit(topN)
                .map(entry -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    BigDecimal revenue = customerRevenueMap.getOrDefault(entry.getKey(), BigDecimal.ZERO);
                    map.put("customerName", entry.getKey());
                    map.put("profit", entry.getValue());
                    map.put("revenue", revenue);
                    map.put("profitRate", toPercent(rate(entry.getValue(), revenue)));
                    return map;
                })
                .toList();
    }
    private Map<String, Object> toInventoryItem(InventoryMetric metric) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("productModelId", metric.modelId());
        map.put("productName", metric.productName());
        map.put("model", metric.modelName());
        map.put("quantity", metric.quantity());
        map.put("lockedQuantity", metric.lockedQuantity());
        map.put("avgUnitCost", metric.avgUnitCost());
        map.put("inventoryValue", metric.inventoryValue());
        map.put("outboundQuantity", metric.outboundQuantity());
        map.put("stagnantDays", metric.stagnantDays());
        map.put("overstock", metric.overstock());
        map.put("riskLevel", metric.stagnantDays() >= 90 ? "high" : (metric.stagnantDays() >= 30 ? "medium" : "low"));
        return map;
    }
    private boolean matchInventoryKeyword(InventoryMetric metric, String keyword) {
        if (!StringUtils.hasText(keyword)) {
            return true;
        }
        return metric.productName().contains(keyword.trim()) || metric.modelName().contains(keyword.trim());
    }
    private Map<String, Object> toMonthlyCashFlowItem(MonthlyCashFlow flow) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("month", flow.month());
        map.put("income", flow.income());
        map.put("expense", flow.expense());
        map.put("netFlow", flow.netFlow());
        return map;
    }
    private Map<String, Object> toStatementRiskItem(StatementMetric metric, Map<String, String> nameMap, String type) {
        BigDecimal actualRate = rate(metric.actualAmount(), metric.planAmount());
        Map<String, Object> map = new LinkedHashMap<>();
        map.put(type + "Id", metric.entityId());
        map.put(type + "Name", safe(nameMap.get(metric.entityId())));
        map.put("statementMonth", metric.statementMonth());
        map.put("closingBalance", metric.closingBalance());
        map.put("planAmount", metric.planAmount());
        map.put("actualAmount", metric.actualAmount());
        map.put("actualRate", toPercent(actualRate));
        map.put("riskLevel", metric.closingBalance().compareTo(new BigDecimal("1000000")) > 0 || actualRate.compareTo(new BigDecimal("0.50")) < 0 ? "high" : "medium");
        return map;
    }
    private Map<String, Object> anomalyItem(String level, String type, String message, Map<String, Object> detail) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("riskLevel", level);
        map.put("type", type);
        map.put("message", message);
        map.put("detail", detail);
        return map;
    }
    private Map<String, Object> riskSuggestion(String type, String level, String suggestion) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("type", type);
        map.put("level", level);
        map.put("suggestion", suggestion);
        return map;
    }
    private Map<String, Object> buildCostCompositionPie(BigDecimal material, BigDecimal labor, BigDecimal depreciation, BigDecimal scrap) {
        List<Map<String, Object>> data = List.of(
                Map.of("name", "材料成本", "value", material),
                Map.of("name", "人工成本", "value", labor),
                Map.of("name", "折旧成本", "value", depreciation),
                Map.of("name", "损耗成本", "value", scrap)
        );
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "成本构成", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("series", List.of(Map.of("name", "成本构成", "type", "pie", "radius", "60%", "data", data)));
        return option;
    }
    private Map<String, Object> buildOrderProfitBar(List<OrderProfitMetric> metrics) {
        List<OrderProfitMetric> top = metrics.stream()
                .sorted(Comparator.comparing(OrderProfitMetric::profit))
                .limit(10)
                .toList();
        List<String> xData = top.stream().map(OrderProfitMetric::salesContractNo).toList();
        List<BigDecimal> yData = top.stream().map(OrderProfitMetric::profit).toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "订单利润分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", xData));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "利润", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildProcessCostBar(Map<String, BigDecimal> processCosts) {
        List<String> xData = new ArrayList<>(processCosts.keySet());
        List<BigDecimal> yData = new ArrayList<>(processCosts.values());
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "工序成本排名", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", xData));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "成本", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildProfitDistributionBar(List<OrderProfitMetric> metrics) {
        List<OrderProfitMetric> sorted = metrics.stream()
                .sorted(Comparator.comparing(OrderProfitMetric::profitRate))
                .limit(15)
                .toList();
        List<String> xData = sorted.stream().map(OrderProfitMetric::salesContractNo).toList();
        List<BigDecimal> yData = sorted.stream().map(metric -> metric.profitRate().multiply(ONE_HUNDRED).setScale(2, RoundingMode.HALF_UP)).toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "订单利润率分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", xData));
        option.put("yAxis", Map.of("type", "value", "name", "%"));
        option.put("series", List.of(Map.of("name", "利润率", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildLossOrderTrendLine(List<OrderProfitMetric> metrics) {
        Map<String, Long> lossByDate = new LinkedHashMap<>();
        List<OrderProfitMetric> sorted = metrics.stream()
                .filter(metric -> metric.entryDate() != null)
                .sorted(Comparator.comparing(OrderProfitMetric::entryDate))
                .toList();
        for (OrderProfitMetric metric : sorted) {
            String day = formatDate(metric.entryDate());
            long inc = metric.profit().compareTo(BigDecimal.ZERO) < 0 ? 1L : 0L;
            lossByDate.merge(day, inc, Long::sum);
        }
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "亏损订单趋势", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", new ArrayList<>(lossByDate.keySet())));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "亏损订单数", "type", "line", "smooth", true, "data", new ArrayList<>(lossByDate.values()))));
        return option;
    }
    private Map<String, Object> buildCustomerProfitBar(Map<String, BigDecimal> customerProfitMap) {
        List<Map.Entry<String, BigDecimal>> top = customerProfitMap.entrySet().stream()
                .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
                .limit(10)
                .toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "客户利润贡献TOP10", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", top.stream().map(Map.Entry::getKey).toList()));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "利润", "type", "bar", "data", top.stream().map(Map.Entry::getValue).toList())));
        return option;
    }
    private Map<String, Object> buildInventoryTopBar(List<InventoryMetric> metrics) {
        List<InventoryMetric> top = metrics.stream()
                .sorted(Comparator.comparing(InventoryMetric::inventoryValue).reversed())
                .limit(10)
                .toList();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "库存资金占用TOP10", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", top.stream().map(item -> item.productName() + "/" + item.modelName()).toList()));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "资金占用", "type", "bar", "data", top.stream().map(InventoryMetric::inventoryValue).toList())));
        return option;
    }
    private Map<String, Object> buildInventoryAgingPie(List<InventoryMetric> metrics) {
        long normal = metrics.stream().filter(item -> item.stagnantDays() < 30).count();
        long slow = metrics.stream().filter(item -> item.stagnantDays() >= 30 && item.stagnantDays() < 90).count();
        long stagnant = metrics.stream().filter(item -> item.stagnantDays() >= 90).count();
        List<Map<String, Object>> data = List.of(
                Map.of("name", "正常", "value", normal),
                Map.of("name", "缓慢", "value", slow),
                Map.of("name", "呆滞", "value", stagnant)
        );
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "库存库龄分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("series", List.of(Map.of("type", "pie", "radius", "60%", "data", data)));
        return option;
    }
    private Map<String, Object> buildTurnoverGauge(BigDecimal turnoverDays) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "库存周转天数", "left", "center"));
        option.put("series", List.of(Map.of(
                "type", "gauge",
                "min", 0,
                "max", 180,
                "detail", Map.of("formatter", "{value}天"),
                "data", List.of(Map.of("value", turnoverDays, "name", "周转天数"))
        )));
        return option;
    }
    private Map<String, Object> buildCashflowTrend(List<MonthlyCashFlow> actual, List<MonthlyCashFlow> forecast) {
        List<String> labels = new ArrayList<>();
        List<BigDecimal> netActual = new ArrayList<>();
        List<BigDecimal> netForecast = new ArrayList<>();
        for (MonthlyCashFlow point : actual) {
            labels.add(point.month());
            netActual.add(point.netFlow());
            netForecast.add(null);
        }
        for (MonthlyCashFlow point : forecast) {
            labels.add(point.month());
            netActual.add(null);
            netForecast.add(point.netFlow());
        }
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "现金流趋势(实际+预测)", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", labels));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(
                Map.of("name", "实际净现金流", "type", "line", "smooth", true, "data", netActual),
                Map.of("name", "预测净现金流", "type", "line", "smooth", true, "data", netForecast)
        ));
        return option;
    }
    private Map<String, Object> buildReceivablePayableBar(BigDecimal receivable, BigDecimal payable) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "应收应付余额对比", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", List.of("应收", "应付")));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "余额", "type", "bar", "data", List.of(receivable, payable))));
        return option;
    }
    private Map<String, Object> buildFundGapGauge(BigDecimal fundGap) {
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "资金缺口", "left", "center"));
        option.put("series", List.of(Map.of(
                "type", "gauge",
                "min", 0,
                "max", 10000000,
                "detail", Map.of("formatter", "{value}"),
                "data", List.of(Map.of("value", fundGap, "name", "资金缺口"))
        )));
        return option;
    }
    private Map<String, Object> buildAnomalyLevelPie(List<Map<String, Object>> anomalies) {
        long high = anomalies.stream().filter(item -> "high".equals(item.get("riskLevel"))).count();
        long medium = anomalies.stream().filter(item -> "medium".equals(item.get("riskLevel"))).count();
        long low = anomalies.stream().filter(item -> "low".equals(item.get("riskLevel"))).count();
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "异常等级分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("series", List.of(Map.of("type", "pie", "radius", "60%", "data", List.of(
                Map.of("name", "高风险", "value", high),
                Map.of("name", "中风险", "value", medium),
                Map.of("name", "低风险", "value", low)
        ))));
        return option;
    }
    private Map<String, Object> buildAnomalyTypeBar(List<Map<String, Object>> anomalies) {
        Map<String, Long> countByType = new LinkedHashMap<>();
        for (Map<String, Object> anomaly : anomalies) {
            countByType.merge(String.valueOf(anomaly.get("type")), 1L, Long::sum);
        }
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "异常类型分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", new ArrayList<>(countByType.keySet())));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "异常数", "type", "bar", "data", new ArrayList<>(countByType.values()))));
        return option;
    }
    private Map<String, Object> buildInventoryProfitGauge(BigDecimal inventoryValue, BigDecimal profit) {
        BigDecimal ratio = inventoryValue.compareTo(BigDecimal.ZERO) <= 0
                ? BigDecimal.ZERO
                : profit.divide(inventoryValue, 4, RoundingMode.HALF_UP).multiply(ONE_HUNDRED);
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "利润/库存资金比", "left", "center"));
        option.put("series", List.of(Map.of(
                "type", "gauge",
                "min", -100,
                "max", 100,
                "detail", Map.of("formatter", "{value}%"),
                "data", List.of(Map.of("value", ratio.setScale(2, RoundingMode.HALF_UP), "name", "利润资金比"))
        )));
        return option;
    }
    private int normalizeLimit(Integer limit) {
        if (limit == null || limit <= 0) {
            return DEFAULT_LIMIT;
        }
        return Math.min(limit, MAX_LIMIT);
    }
    private DateRange resolveDateRange(String startDate, String endDate, String timeRange, String defaultLabel) {
        LocalDate today = LocalDate.now();
        LocalDate explicitStart = parseLocalDate(startDate);
        LocalDate explicitEnd = parseLocalDate(endDate);
        if (explicitStart != null || explicitEnd != null) {
            LocalDate start = explicitStart != null ? explicitStart : explicitEnd;
            LocalDate end = explicitEnd != null ? explicitEnd : explicitStart;
            if (start.isAfter(end)) {
                LocalDate temp = start;
                start = end;
                end = temp;
            }
            return new DateRange(start, end, start + "至" + end);
        }
        if (!StringUtils.hasText(timeRange)) {
            return new DateRange(null, null, "全部");
        }
        String text = timeRange.trim();
        if (text.contains("今天")) {
            return new DateRange(today, today, "今天");
        }
        if (text.contains("昨天") || text.contains("昨日")) {
            LocalDate day = today.minusDays(1);
            return new DateRange(day, day, "昨天");
        }
        if (text.contains("本周")) {
            LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            return new DateRange(start, today, "本周");
        }
        if (text.contains("上周")) {
            LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            LocalDate start = thisWeekStart.minusWeeks(1);
            LocalDate end = thisWeekStart.minusDays(1);
            return new DateRange(start, end, "上周");
        }
        if (text.contains("本月")) {
            return new DateRange(today.withDayOfMonth(1), today, "本月");
        }
        if (text.contains("上月")) {
            YearMonth lastMonth = YearMonth.from(today).minusMonths(1);
            return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth(), "上月");
        }
        if (text.contains("今年") || text.contains("本年")) {
            return new DateRange(today.withDayOfYear(1), today, "今年");
        }
        Matcher relativeMatcher = RELATIVE_PATTERN.matcher(text);
        if (relativeMatcher.find()) {
            int amount = Integer.parseInt(relativeMatcher.group(2));
            String unit = relativeMatcher.group(3);
            LocalDate start = switch (unit) {
                case "天" -> today.minusDays(Math.max(amount - 1L, 0));
                case "周" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
                case "个月", "月" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
                case "å¹´" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
                default -> today.minusDays(29);
            };
            return new DateRange(start, today, "近" + amount + unit);
        }
        Matcher dateMatcher = DATE_PATTERN.matcher(text);
        if (dateMatcher.find()) {
            LocalDate start = parseLocalDate(dateMatcher.group(1));
            LocalDate end = dateMatcher.find() ? parseLocalDate(dateMatcher.group(1)) : start;
            if (start != null && end != null) {
                if (start.isAfter(end)) {
                    LocalDate temp = start;
                    start = end;
                    end = temp;
                }
                return new DateRange(start, end, start + "至" + end);
            }
        }
        return new DateRange(null, null, "全部");
    }
    private LocalDate parseLocalDate(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        try {
            return LocalDate.parse(text.trim(), DATE_FMT);
        } catch (Exception ignored) {
            return null;
        }
    }
    private Date toDate(LocalDate localDate) {
        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private Date toExclusiveEndDate(LocalDate localDate) {
        return Date.from(localDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private LocalDate toLocalDate(Date date) {
        if (date == null) {
            return null;
        }
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
    }
    private String formatDate(LocalDate date) {
        return date == null ? "" : date.format(DATE_FMT);
    }
    private String displayDate(LocalDate date) {
        return date == null ? "" : date.format(DATE_FMT);
    }
    private long daysBetween(LocalDate start, LocalDate end) {
        if (start == null || end == null || start.isAfter(end)) {
            return 0;
        }
        return end.toEpochDay() - start.toEpochDay();
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private BigDecimal maxZero(BigDecimal value) {
        return value == null || value.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : value;
    }
    private BigDecimal rate(BigDecimal numerator, BigDecimal denominator) {
        if (denominator == null || denominator.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        return defaultDecimal(numerator).divide(denominator, 6, RoundingMode.HALF_UP);
    }
    private String toPercent(BigDecimal decimal) {
        if (decimal == null) {
            return "0.00%";
        }
        BigDecimal rate = decimal.multiply(ONE_HUNDRED).setScale(2, RoundingMode.HALF_UP);
        return rate.toPlainString() + "%";
    }
    private BigDecimal avgRate(List<OrderProfitMetric> metrics) {
        if (metrics == null || metrics.isEmpty()) {
            return BigDecimal.ZERO;
        }
        BigDecimal sum = metrics.stream().map(OrderProfitMetric::profitRate).reduce(BigDecimal.ZERO, BigDecimal::add);
        return sum.divide(new BigDecimal(metrics.size()), 6, RoundingMode.HALF_UP);
    }
    private BigDecimal estimateLaborCost(ProductionAccount account, Map<String, BigDecimal> salaryQuotaByOperation) {
        BigDecimal salaryQuota = salaryQuotaByOperation.getOrDefault(safe(account.getTechnologyOperationName()), BigDecimal.ZERO);
        BigDecimal finishedNum = defaultDecimal(account.getFinishedNum());
        BigDecimal workHours = defaultDecimal(account.getWorkHours());
        if (salaryQuota.compareTo(BigDecimal.ZERO) > 0 && finishedNum.compareTo(BigDecimal.ZERO) > 0) {
            return finishedNum.multiply(salaryQuota);
        }
        if (salaryQuota.compareTo(BigDecimal.ZERO) > 0 && workHours.compareTo(BigDecimal.ZERO) > 0) {
            return workHours.multiply(salaryQuota);
        }
        if (workHours.compareTo(BigDecimal.ZERO) > 0) {
            return workHours;
        }
        return finishedNum;
    }
    private List<Long> parseIdList(String raw) {
        if (!StringUtils.hasText(raw)) {
            return List.of();
        }
        String text = raw.replace("[", "").replace("]", "").replace(" ", "");
        if (!StringUtils.hasText(text)) {
            return List.of();
        }
        List<Long> result = new ArrayList<>();
        for (String part : text.split(",")) {
            if (!StringUtils.hasText(part)) {
                continue;
            }
            try {
                result.add(Long.parseLong(part.trim()));
            } catch (Exception ignored) {
            }
        }
        return result;
    }
    private int keywordHitCount(List<String> keywords, String question) {
        if (!StringUtils.hasText(question) || keywords == null) {
            return 0;
        }
        int count = 0;
        for (String keyword : keywords) {
            if (question.contains(keyword)) {
                count++;
            }
        }
        return count;
    }
    private String normalizeForMatch(String text) {
        if (!StringUtils.hasText(text)) {
            return "";
        }
        return text.replace(",", "")
                .replace(",", "")
                .replace("。", "")
                .replace(".", "")
                .replace("!", "")
                .replace("!", "")
                .replace("?", "")
                .replace("?", "")
                .replace(":", "")
                .replace(":", "")
                .replace(";", "")
                .replace(";", "")
                .replace(" ", "")
                .trim();
    }
    private String safe(Object value) {
        return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ').trim();
    }
    private LoginUser currentLoginUser(String memoryId) {
        LoginUser loginUser = aiSessionUserContext.get(memoryId);
        if (loginUser != null) {
            return loginUser;
        }
        return SecurityUtils.getLoginUser();
    }
    private Map<String, Object> rangeSummary(DateRange range, int count, String keyword) {
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", displayDate(range.start()));
        summary.put("endDate", displayDate(range.end()));
        summary.put("count", count);
        summary.put("keyword", safe(keyword));
        return summary;
    }
    private Long toLongOrNull(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        try {
            return Long.valueOf(value.trim());
        } catch (Exception ignored) {
            return null;
        }
    }
    private <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    private <T> void applyTenantFilter(LambdaQueryWrapper<T> wrapper, Long tenantId, SFunction<T, Long> field) {
        if (tenantId != null) {
            wrapper.eq(field, tenantId);
        }
    }
    private <T> void applyDeptFilter(LambdaQueryWrapper<T> wrapper, Long deptId, SFunction<T, Long> field) {
        if (deptId != null) {
            wrapper.eq(field, deptId);
        }
    }
    private List<KnowledgeDoc> financeKnowledgeBase() {
        return List.of(
                new KnowledgeDoc(
                        "利润下降分析框架",
                        List.of("利润下降", "亏损订单", "毛利率", "净利率"),
                        "先看收入端(订单结构、单价、交付延迟),再看成本端(材料、人工、折旧、损耗),最后看现金端(回款、账期、坏账风险)。",
                        List.of("sales_ledger", "sales_ledger_product", "production_account", "device_ledger", "account_statement"),
                        List.of("为什么本月利润下降?", "哪些订单亏损最严重?", "成本上升来自哪个工序?")
                ),
                new KnowledgeDoc(
                        "库存资金占用诊断",
                        List.of("库存积压", "呆滞库存", "周转率", "资金占用"),
                        "库存资金诊断重点看:库存价值、近30天出库成本、呆滞天数、超储比例,形成去库存与采购节奏联动策略。",
                        List.of("stock_inventory", "procurement_record_storage", "procurement_record_out"),
                        List.of("哪些物料资金占用最高?", "哪些库存超过90天未周转?", "库存周转天数是否异常?")
                ),
                new KnowledgeDoc(
                        "现金流与账款风险",
                        List.of("现金流", "应收", "应付", "回款", "资金缺口"),
                        "现金流判断要结合收款、付款、应收应付余额与预测净流量,重点关注高余额客户和高集中付款供应商。",
                        List.of("account_sales_collection", "account_purchase_payment", "account_statement"),
                        List.of("未来三个月是否有资金缺口?", "哪个客户回款风险最高?", "付款压力最大的是哪些供应商?")
                ),
                new KnowledgeDoc(
                        "业财一体化口径",
                        List.of("业财融合", "业财联动", "口径", "驾驶舱"),
                        "订单利润口径=销售收入-材料成本-人工成本-设备折旧-损耗成本;经营驾驶舱联动订单、生产、库存、设备、账款数据。",
                        List.of("sales_ledger", "production_operation_task", "production_product_main", "device_ledger", "stock_inventory", "account_statement"),
                        List.of("订单利润率如何计算?", "经营驾驶舱核心指标有哪些?")
                )
        );
    }
    private String jsonResponse(boolean success,
                                String type,
                                String description,
                                Map<String, Object> summary,
                                Map<String, Object> data,
                                Map<String, Object> charts) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("success", success);
        result.put("type", type);
        result.put("description", description);
        result.put("summary", summary == null ? Map.of() : summary);
        result.put("data", data == null ? Map.of() : data);
        result.put("charts", charts == null ? Map.of() : charts);
        return JSON.toJSONString(result);
    }
    private record DateRange(LocalDate start, LocalDate end, String label) {
        private boolean hasDateFilter() {
            return start != null && end != null;
        }
    }
    private record OrderProfitMetric(Long ledgerId,
                                     String salesContractNo,
                                     String customerName,
                                     String projectName,
                                     LocalDate entryDate,
                                     LocalDate deliveryDate,
                                     BigDecimal revenue,
                                     BigDecimal materialCost,
                                     BigDecimal laborCost,
                                     BigDecimal depreciationCost,
                                     BigDecimal scrapCost,
                                     BigDecimal totalCost,
                                     BigDecimal profit,
                                     BigDecimal profitRate,
                                     String riskLevel,
                                     List<String> reasons,
                                     String suggestion) {
    }
    private record AnalysisBundle(List<OrderProfitMetric> orderMetrics,
                                  Map<String, BigDecimal> processCostRanking,
                                  BigDecimal totalRevenue,
                                  BigDecimal totalMaterialCost,
                                  BigDecimal totalLaborCost,
                                  BigDecimal totalDepreciationCost,
                                  BigDecimal totalScrapCost,
                                  BigDecimal totalCost,
                                  BigDecimal totalProfit) {
        private static AnalysisBundle empty() {
            return new AnalysisBundle(List.of(), Map.of(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO,
                    BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
        }
    }
    private record MaterialCostResult(Map<Long, BigDecimal> materialCostByLedgerId,
                                      Map<Long, BigDecimal> avgUnitCostByModelId) {
    }
    private record ProductionCostContext(Map<Long, BigDecimal> laborCostByLedgerId,
                                         Map<Long, BigDecimal> scrapCostByLedgerId,
                                         Map<String, BigDecimal> processCostRanking) {
        private static ProductionCostContext empty() {
            return new ProductionCostContext(Map.of(), Map.of(), Map.of());
        }
    }
    private record InventoryMetric(Long modelId,
                                   String productName,
                                   String modelName,
                                   BigDecimal quantity,
                                   BigDecimal lockedQuantity,
                                   BigDecimal avgUnitCost,
                                   BigDecimal inventoryValue,
                                   BigDecimal outboundQuantity,
                                   long stagnantDays,
                                   boolean overstock) {
    }
    private static class InventoryMetricBuilder {
        private final Long modelId;
        private BigDecimal quantity = BigDecimal.ZERO;
        private BigDecimal lockedQuantity = BigDecimal.ZERO;
        private BigDecimal warnNum = BigDecimal.ZERO;
        private LocalDateTime firstInTime;
        private InventoryMetricBuilder(Long modelId) {
            this.modelId = modelId;
        }
        private void addQuantity(BigDecimal quantity) {
            this.quantity = this.quantity.add(quantity);
        }
        private void addLockedQuantity(BigDecimal lockedQuantity) {
            this.lockedQuantity = this.lockedQuantity.add(lockedQuantity);
        }
        private void addWarnNum(BigDecimal warnNum) {
            this.warnNum = this.warnNum.max(warnNum);
        }
        private void updateFirstInTime(LocalDateTime createTime) {
            if (this.firstInTime == null || createTime.isBefore(this.firstInTime)) {
                this.firstInTime = createTime;
            }
        }
        private Long modelId() {
            return modelId;
        }
        private BigDecimal quantity() {
            return quantity;
        }
        private BigDecimal lockedQuantity() {
            return lockedQuantity;
        }
        private BigDecimal warnNum() {
            return warnNum;
        }
        private LocalDateTime firstInTime() {
            return firstInTime;
        }
    }
    private record OutboundStats(Map<Long, BigDecimal> outboundQtyByModel,
                                 Map<Long, LocalDateTime> lastOutboundTimeByModel,
                                 BigDecimal totalOutboundCost) {
        private static OutboundStats empty() {
            return new OutboundStats(Map.of(), Map.of(), BigDecimal.ZERO);
        }
    }
    private record MonthlyCashFlow(String month, BigDecimal income, BigDecimal expense, BigDecimal netFlow) {
    }
    private record StatementMetric(String entityId,
                                   BigDecimal closingBalance,
                                   BigDecimal planAmount,
                                   BigDecimal actualAmount,
                                   String statementMonth) {
    }
    private record StatementSnapshot(BigDecimal receivableTotal,
                                     BigDecimal payableTotal,
                                     List<StatementMetric> receivableTop,
                                     List<StatementMetric> payableTop) {
        private static StatementSnapshot empty() {
            return new StatementSnapshot(BigDecimal.ZERO, BigDecimal.ZERO, List.of(), List.of());
        }
    }
    private record KnowledgeDoc(String topic,
                                List<String> keywords,
                                String knowledge,
                                List<String> relatedTables,
                                List<String> suggestedQuestions) {
    }
}
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/approve/bean/dto/ApprovalInstanceDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.basic.dto.StorageBlobDTO;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalInstanceDto extends ApprovalInstance {
    private String approveAction;
    private String approveComment;
    private String createTimeEnd;
    private String createTimeStart;
    private List<StorageBlobDTO> storageBlobDTOs;
}
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalTemplateDto  extends ApprovalTemplate {
    private List<ApprovalTemplateNodeDto> nodes;
}
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeApproverDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import lombok.Data;
@Data
public class ApprovalTemplateNodeApproverDto extends ApprovalTemplateNodeApprover {
}
src/main/java/com/ruoyi/approve/bean/dto/ApprovalTemplateNodeDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalTemplateNodeDto extends ApprovalTemplateNode {
    private List<ApprovalTemplateNodeApproverDto> approvers;
}
src/main/java/com/ruoyi/approve/bean/dto/FinReimbursementDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.approve.bean.dto;
import com.ruoyi.approve.pojo.FinReimbursement;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import com.ruoyi.basic.dto.StorageBlobDTO;
import lombok.Data;
import java.util.List;
@Data
public class FinReimbursementDto extends FinReimbursement {
    private String createTimeStart;
    private String createTimeEnd;
    private FinReimbursementTravel  travel;
    private List<FinReimbursementDetail> details;
    private List<ApprovalTemplateNodeDto> nodes;
    private List<StorageBlobDTO> storageBlobDTOs;
}
src/main/java/com/ruoyi/approve/bean/vo/ApprovalInstanceVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.approve.bean.vo;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalRecord;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.basic.dto.StorageBlobVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalInstanceVo extends ApprovalInstance {
    //当前用户是否可以审批
    @Schema(description = "当前用户是否可以审批")
    private Boolean isApprove;
    //审批流程
    private List<ApprovalTask> tasks;
    //审批记录
    private List<ApprovalRecord>  records;
    @Schema(description = "业务名称")
    private String businessName;
    private List<StorageBlobVO> storageBlobVOList;
}
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeApproverVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.approve.bean.vo;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import lombok.Data;
@Data
public class ApprovalTemplateNodeApproverVo extends ApprovalTemplateNodeApprover {
}
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateNodeVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package com.ruoyi.approve.bean.vo;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalTemplateNodeVo extends ApprovalTemplateNode {
    private List<ApprovalTemplateNodeApproverVo> approvers;
}
src/main/java/com/ruoyi/approve/bean/vo/ApprovalTemplateVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.approve.bean.vo;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import lombok.Data;
import java.util.List;
@Data
public class ApprovalTemplateVo extends ApprovalTemplate {
    private List<ApprovalTemplateNodeVo> nodes;
    private String createdUserName;
}
src/main/java/com/ruoyi/approve/bean/vo/ApproveGetAndUpdateVo.java
@@ -9,6 +9,7 @@
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
@@ -45,6 +46,14 @@
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
    @Schema(description = "出差开始时间")
    private LocalDateTime startDateTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
    @Schema(description = "出差结束时间")
    private LocalDateTime endDateTime;
    private BigDecimal price;
    private String location;
src/main/java/com/ruoyi/approve/bean/vo/ApproveProcessVO.java
@@ -8,6 +8,7 @@
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
@@ -58,6 +59,14 @@
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
    @Schema(description = "出差开始时间")
    private LocalDateTime startDateTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
    @Schema(description = "出差结束时间")
    private LocalDateTime endDateTime;
    private BigDecimal price;
    private String location;
src/main/java/com/ruoyi/approve/bean/vo/FinReimbursementVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.approve.bean.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.approve.pojo.*;
import com.ruoyi.basic.dto.StorageBlobVO;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
@Data
public class FinReimbursementVo extends FinReimbursement {
    private String createTimeStart;
    private String createTimeEnd;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;
    private FinReimbursementTravel travel;
    private List<FinReimbursementDetail> details;
    //审批流程
    private List<ApprovalTask> tasks;
    //审批记录
    private List<ApprovalRecord>  records;
    private List<StorageBlobVO> storageBlobVOList;
}
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
package com.ruoyi.approve.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.vo.ApprovalInstanceVo;
import com.ruoyi.approve.service.ApprovalInstanceService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹å®žä¾‹è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:46
 */
@RestController
@RequestMapping("/approvalInstance")
@Tag(name = "审批实例表")
@AllArgsConstructor
public class ApprovalInstanceController extends BaseController {
    private final ApprovalInstanceService approvalInstanceService;
    @GetMapping("/listPage")
    @Operation(summary = "分页查询")
    @Log(title = "审批列表分页查询", businessType = BusinessType.OTHER)
    public R listPage(Page<ApprovalInstanceVo> page, ApprovalInstanceDto approvalInstanceDto) {
        return approvalInstanceService.listPage(page, approvalInstanceDto);
    }
    @PostMapping("/save")
    @Operation(summary = "保存")
    @Log(title = "审批列表保存", businessType = BusinessType.INSERT)
    public R save(@RequestBody ApprovalInstanceDto approvalInstanceDto) {
        return approvalInstanceService.add(approvalInstanceDto) ? R.ok() : R.fail();
    }
    @PutMapping("/update")
    @Operation(summary = "更新")
    @Log(title = "审批列表更新", businessType = BusinessType.UPDATE)
    public R update(@RequestBody ApprovalInstanceDto approvalInstanceDto) {
        return approvalInstanceService.update(approvalInstanceDto) ? R.ok() : R.fail();
    }
    @DeleteMapping("/delete")
    @Log(title = "审批列表删除", businessType = BusinessType.DELETE)
    @Operation(summary = "删除")
    public R delete(@RequestBody List<Long> ids) {
        return approvalInstanceService.delete(ids) ? R.ok() : R.fail();
    }
    @Operation(summary = "审批")
    @PostMapping("/approve")
    @Log(title = "审批列表审批", businessType = BusinessType.UPDATE)
    public R approve(@RequestBody ApprovalInstanceDto approvalInstanceDto) {
        return approvalInstanceService.approve(approvalInstanceDto);
    }
}
src/main/java/com/ruoyi/approve/controller/ApprovalInstanceNodeController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.approve.controller;
import com.ruoyi.framework.web.controller.BaseController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å®¡æ‰¹èŠ‚ç‚¹å®žä¾‹è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:54
 */
@RestController
@RequestMapping("/approvalInstanceNode")
public class ApprovalInstanceNodeController extends BaseController {
}
src/main/java/com/ruoyi/approve/controller/ApprovalRecordController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å®¡æ‰¹è®°å½•表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:28:21
 */
@RestController
@RequestMapping("/approvalRecord")
public class ApprovalRecordController {
}
src/main/java/com/ruoyi/approve/controller/ApprovalTaskController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å®¡æ‰¹ä»»åŠ¡è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:32:37
 */
@RestController
@RequestMapping("/approvalTask")
public class ApprovalTaskController {
}
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
package com.ruoyi.approve.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalTemplateDto;
import com.ruoyi.approve.bean.vo.ApprovalTemplateVo;
import com.ruoyi.approve.service.ApprovalTemplateService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ‹é“è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:08
 */
@RestController
@RequestMapping("/approvalTemplate")
@Tag(name = "审批模板表")
@AllArgsConstructor
public class ApprovalTemplateController  extends BaseController {
    private final ApprovalTemplateService approvalTemplateService;
    @GetMapping("/listPage")
    @Operation(summary = "分页查询")
    @Log(title = "审批模板分页查询", businessType = BusinessType.OTHER)
    public R listPage(Page<ApprovalTemplateVo> page, ApprovalTemplateDto approvalTemplateDto) {
        return R.ok(approvalTemplateService.listPage(page, approvalTemplateDto));
    }
    @PostMapping("/add")
    @Operation(summary = "添加")
    @Log(title = "添加审批模板", businessType = BusinessType.INSERT)
    public R add(@RequestBody ApprovalTemplateDto approvalTemplateDto) {
        return R.ok(approvalTemplateService.saveApprovalTemplateDto(approvalTemplateDto));
    }
    @PutMapping("/update")
    @Operation(summary = "修改")
    @Log(title = "修改审批模板", businessType = BusinessType.UPDATE)
    public R update(@RequestBody ApprovalTemplateDto approvalTemplateDto) {
        return R.ok(approvalTemplateService.updateApprovalTemplateDto(approvalTemplateDto));
    }
    @PostMapping("/delete")
    @Operation(summary = "删除")
    @Log(title = "删除审批模板", businessType = BusinessType.DELETE)
    public R delete(@RequestBody List<Long> ids) {
        return R.ok(approvalTemplateService.delete(ids));
    }
    @GetMapping("/list/{type}")
    @Operation(summary = "查询所有审批模板")
    public R list(@PathVariable("type") Integer type) {
        return R.ok(approvalTemplateService.listApprovalTemplateVo(type));
    }
    @GetMapping("/detail/{id}")
    @Operation(summary = "查询审批模板详情")
    @Log(title = "查询审批模板详情", businessType = BusinessType.OTHER)
    public R detail(@PathVariable("id") Long id) {
        return R.ok(approvalTemplateService.getApprovalTemplateVoById(id));
    }
}
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeApproverController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹å®¡æ‰¹äººè¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:30
 */
@RestController
@RequestMapping("/approvalTemplateNodeApprover")
public class ApprovalTemplateNodeApproverController {
}
src/main/java/com/ruoyi/approve/controller/ApprovalTemplateNodeController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:19
 */
@RestController
@RequestMapping("/approvalTemplateNode")
public class ApprovalTemplateNodeController {
}
src/main/java/com/ruoyi/approve/controller/ApproveNodeController.java
@@ -2,6 +2,8 @@
import com.ruoyi.approve.pojo.ApproveNode;
import com.ruoyi.approve.service.IApproveNodeService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -35,6 +37,7 @@
     * @param approveNode
     * @return
     */
    @Log(title = "审批节点", businessType = BusinessType.UPDATE)
    @PostMapping("/updateApproveNode")
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "审批节点")
@@ -48,6 +51,7 @@
     * @param id
     * @return
     */
    @Log(title = "初始化审批节点", businessType = BusinessType.INSERT)
    @PostMapping("/init")
    public AjaxResult init(String id) {
        approveNodeService.initApproveNodes("",id,1L);
src/main/java/com/ruoyi/approve/controller/ApproveProcessConfigNodeController.java
@@ -2,6 +2,8 @@
import com.ruoyi.approve.pojo.ApproveProcessConfigNode;
import com.ruoyi.approve.service.ApproveProcessConfigNodeService;
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.annotations.ApiOperation;
import lombok.AllArgsConstructor;
@@ -41,6 +43,7 @@
     * @return
     */
    @ApiOperation("添加审批节点")
    @Log(title = "审批流程节点", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public R addApproveProcessConfigNodes(@RequestBody List<ApproveProcessConfigNode> approveProcessConfigNodes) {
        return R.ok(approveProcessConfigNodeService.addApproveProcessConfigNodes(approveProcessConfigNodes));
src/main/java/com/ruoyi/approve/controller/ApproveProcessController.java
@@ -8,6 +8,8 @@
import com.ruoyi.approve.bean.vo.ApproveProcessVO;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.SysDept;
@@ -47,6 +49,7 @@
     * @param approveProcessVO
     * @return
     */
    @Log(title = "添加审批", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "添加审批")
@@ -78,6 +81,7 @@
     * @param approveGetAndUpdateVo
     * @return
     */
    @Log(title = "更新审批", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "更新审批")
@@ -103,6 +107,7 @@
     * @param ids
     * @return
     */
    @Log(title = "删除审批", businessType = BusinessType.DELETE)
    @DeleteMapping("/deleteIds")
    @Operation(summary = "删除审批")
    @Transactional(rollbackFor = Exception.class)
@@ -115,6 +120,7 @@
    }
    @Operation(summary = "公出管理导出")
    @Log(title = "公出管理导出", businessType = BusinessType.EXPORT)
    @PostMapping("/exportOne")
    public void exportOne(HttpServletResponse response) {
        List<ApproveProcess> accountExpenses = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
@@ -125,6 +131,7 @@
    }
    @Operation(summary = "请假管理导出")
    @Log(title = "请假管理导出", businessType = BusinessType.EXPORT)
    @PostMapping("/exportTwo")
    public void exportTwo(HttpServletResponse response) {
        List<ApproveProcess> accountExpenses = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
@@ -135,6 +142,7 @@
    }
    @Operation(summary = "出差管理导出")
    @Log(title = "出差管理导出", businessType = BusinessType.EXPORT)
    @PostMapping("/exportThree")
    public void exportThree(HttpServletResponse response) {
        List<ApproveProcess> accountExpenses = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
@@ -145,6 +153,7 @@
    }
    @Operation(summary = "报销管理导出")
    @Log(title = "报销管理导出", businessType = BusinessType.EXPORT)
    @PostMapping("/exportFour")
    public void exportFour(HttpServletResponse response) {
        List<ApproveProcess> accountExpenses = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
@@ -155,6 +164,7 @@
    }
    @Operation(summary = "采购申请导出")
    @Log(title = "采购申请导出", businessType = BusinessType.EXPORT)
    @PostMapping("/exportFive")
    public void exportFive(HttpServletResponse response) {
        List<ApproveProcess> accountExpenses = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
@@ -165,6 +175,7 @@
    }
    @Operation(summary = "协同审批导出")
    @Log(title = "协同审批导出", businessType = BusinessType.EXPORT)
    @PostMapping("/exportZero")
    public void exportZero(HttpServletResponse response) {
        List<ApproveProcess> accountExpenses = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
@@ -175,6 +186,7 @@
    }
    @Operation(summary = "危险作业审批导出")
    @Log(title = "危险作业审批导出", businessType = BusinessType.EXPORT)
    @PostMapping("/exportEight")
    public void exportEight(HttpServletResponse response) {
        List<ApproveProcess> accountExpenses = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
src/main/java/com/ruoyi/approve/controller/FinReimbursementController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
package com.ruoyi.approve.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.service.FinReimbursementService;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * <p>
 * æŠ¥é”€å•主表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@RestController
@RequestMapping("/finReimbursement")
@Tag(name = "报销单主表", description = "报销单主表")
@AllArgsConstructor
public class FinReimbursementController {
    private final FinReimbursementService finReimbursementService;
    @GetMapping("/listPage")
    @Operation(summary = "分页查询")
    public R listPage(Page<FinReimbursementVo> page, FinReimbursementDto finReimbursementDto) {
        return R.ok(finReimbursementService.listPage(finReimbursementDto, page));
    }
    @PostMapping("/save")
    @Operation(summary = "保存")
    public R save(@RequestBody FinReimbursementDto finReimbursementDto) {
        return R.ok(finReimbursementService.add(finReimbursementDto));
    }
    @GetMapping("/detail")
    @Operation(summary = "详情")
    public R detail(Long id) {
        return R.ok(finReimbursementService.detail(id));
    }
    @PostMapping("/update")
    @Operation(summary = "修改")
    public R update(@RequestBody FinReimbursementDto finReimbursementDto) {
        return R.ok(finReimbursementService.update(finReimbursementDto));
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除")
    public R delete(@RequestBody List<Long> ids) {
        return R.ok(finReimbursementService.delete(ids));
    }
}
src/main/java/com/ruoyi/approve/controller/FinReimbursementDetailController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * æŠ¥é”€å•明细表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@RestController
@RequestMapping("/finReimbursementDetail")
public class FinReimbursementDetailController {
}
src/main/java/com/ruoyi/approve/controller/FinReimbursementTravelController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * å·®æ—…报销扩展表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@RestController
@RequestMapping("/finReimbursementTravel")
public class FinReimbursementTravelController {
}
src/main/java/com/ruoyi/approve/controller/HolidaySettingsController.java
@@ -7,6 +7,8 @@
import com.ruoyi.approve.mapper.WorkingHoursSettingMapper;
import com.ruoyi.approve.pojo.*;
import com.ruoyi.approve.service.HolidaySettingsService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
@@ -37,6 +39,7 @@
     * å¢žæ·»
     * @return
     */
    @Log(title = "增添假期设置", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody HolidaySettings holidaySettings){
        return AjaxResult.success(holidaySettingsService.save(holidaySettings));
@@ -45,6 +48,7 @@
     * æ›´æ–°
     * @return
     */
    @Log(title = "更新假期设置", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    public AjaxResult update(@RequestBody HolidaySettings holidaySettings){
        return AjaxResult.success(holidaySettingsService.updateById(holidaySettings));
@@ -53,6 +57,7 @@
     * åˆ é™¤
     * @return
     */
    @Log(title = "删除假期设置", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    public AjaxResult delete(@RequestBody List<Long> ids){
        if(CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
@@ -72,6 +77,7 @@
     * å¢žæ·»å¹´å‡è§„则
     * @return
     */
    @Log(title = "增添年假规则", businessType = BusinessType.INSERT)
    @PostMapping("/addAnnualLeaveSetting")
    public AjaxResult addAnnualLeaveSetting(@RequestBody AnnualLeaveSetting annualLeaveSetting){
        return AjaxResult.success(annualLeaveSettingMapper.insert(annualLeaveSetting));
@@ -80,6 +86,7 @@
     * æ›´æ–°å¹´å‡è§„则
     * @return
     */
    @Log(title = "更新年假规则", businessType = BusinessType.UPDATE)
    @PostMapping("/updateAnnualLeaveSetting")
    public AjaxResult updateAnnualLeaveSetting(@RequestBody AnnualLeaveSetting annualLeaveSetting){
        return AjaxResult.success(annualLeaveSettingMapper.updateById(annualLeaveSetting));
@@ -88,6 +95,7 @@
     * åˆ é™¤å¹´å‡è§„则
     * @return
     */
    @Log(title = "删除年假规则", businessType = BusinessType.DELETE)
    @DeleteMapping("/deleteAnnualLeaveSetting")
    public AjaxResult deleteAnnualLeaveSetting(@RequestBody List<Long> ids){
        if(CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
@@ -108,6 +116,7 @@
     * å¢žæ·»åŠ ç­è§„åˆ™
     * @return
     */
    @Log(title = "增添加班规则", businessType = BusinessType.INSERT)
    @PostMapping("/addOvertimeSetting")
    public AjaxResult addOvertimeSetting(@RequestBody OvertimeSetting overtimeSetting){
        return AjaxResult.success(overtimeSettingMapper.insert(overtimeSetting));
@@ -116,6 +125,7 @@
     * æ›´æ–°åŠ ç­è§„åˆ™
     * @return
     */
    @Log(title = "更新加班规则", businessType = BusinessType.UPDATE)
    @PostMapping("/updateOvertimeSetting")
    public AjaxResult updateOvertimeSetting(@RequestBody OvertimeSetting overtimeSetting){
        return AjaxResult.success(overtimeSettingMapper.updateById(overtimeSetting));
@@ -124,6 +134,7 @@
     * åˆ é™¤åŠ ç­è§„åˆ™
     * @return
     */
    @Log(title = "删除加班规则", businessType = BusinessType.DELETE)
    @DeleteMapping("/deleteOvertimeSetting")
    public AjaxResult deleteOvertimeSetting(@RequestBody List<Long> ids){
        if(CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
@@ -143,6 +154,7 @@
     * å¢žæ·»ç­åˆ¶è§„则
     * @return
     */
    @Log(title = "增添班制规则", businessType = BusinessType.INSERT)
    @PostMapping("/addWorkingHoursSetting")
    public AjaxResult addWorkingHoursSetting(@RequestBody WorkingHoursSetting workingHoursSetting){
        return AjaxResult.success(workingHoursSettingMapper.insert(workingHoursSetting));
@@ -151,6 +163,7 @@
     * æ›´æ–°ç­åˆ¶è§„则
     * @return
     */
    @Log(title = "更新班制规则", businessType = BusinessType.UPDATE)
    @PostMapping("/updateWorkingHoursSetting")
    public AjaxResult updateWorkingHoursSetting(@RequestBody WorkingHoursSetting workingHoursSetting){
        return AjaxResult.success(workingHoursSettingMapper.updateById(workingHoursSetting));
@@ -159,6 +172,7 @@
     * åˆ é™¤ç­åˆ¶è§„则
     * @return
     */
    @Log(title = "删除班制规则", businessType = BusinessType.DELETE)
    @DeleteMapping("/deleteWorkingHoursSetting")
    public AjaxResult deleteWorkingHoursSetting(@RequestBody List<Long> ids){
        if(CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
src/main/java/com/ruoyi/approve/controller/KnowledgeBaseController.java
@@ -5,6 +5,8 @@
import com.ruoyi.approve.pojo.KnowledgeBase;
import com.ruoyi.approve.service.KnowledgeBaseService;
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.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -35,6 +37,7 @@
     * å¢žæ·»
     * @return
     */
    @Log(title = "知识库", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody KnowledgeBase knowledgeBase){
        return AjaxResult.success(knowledgeBaseService.save(knowledgeBase));
@@ -43,6 +46,7 @@
     * æ›´æ–°
     * @return
     */
    @Log(title = "知识库", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    public AjaxResult update(@RequestBody KnowledgeBase knowledgeBase){
        return AjaxResult.success(knowledgeBaseService.updateById(knowledgeBase));
@@ -51,12 +55,14 @@
     * åˆ é™¤
     * @return
     */
    @Log(title = "知识库", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    public AjaxResult delete(@RequestBody List<Long> ids){
        if(CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
        return AjaxResult.success(knowledgeBaseService.removeByIds(ids));
    }
    @Log(title = "知识库", businessType = BusinessType.EXPORT)
    @Operation(summary = "知识库管理导出")
    @PostMapping("/export")
    public void export(HttpServletResponse response) {
src/main/java/com/ruoyi/approve/controller/NotificationManagementController.java
@@ -8,6 +8,8 @@
import com.ruoyi.approve.pojo.NotificationManagement;
import com.ruoyi.approve.pojo.OnlineMeeting;
import com.ruoyi.approve.service.NotificationManagementService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
@@ -36,6 +38,7 @@
     * å¢žæ·»
     * @return
     */
    @Log(title = "增添通知管理", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody NotificationManagement notificationManagement){
        return AjaxResult.success(notificationManagementService.save(notificationManagement));
@@ -44,6 +47,7 @@
     * æ›´æ–°
     * @return
     */
    @Log(title = "更新通知管理", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    public AjaxResult update(@RequestBody NotificationManagement notificationManagement){
        return AjaxResult.success(notificationManagementService.updateById(notificationManagement));
@@ -52,6 +56,7 @@
     * åˆ é™¤
     * @return
     */
    @Log(title = "删除通知管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    public AjaxResult delete(@RequestBody List<Long> ids){
        if(CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
@@ -62,6 +67,7 @@
     * @param onlineMeeting
     * @return
     */
    @Log(title = "新增线上会议", businessType = BusinessType.INSERT)
    @PostMapping("/addOnlineMeeting")
    public AjaxResult addOnlineMeeting(@RequestBody OnlineMeeting onlineMeeting){
        return AjaxResult.success(onlineMeetingMapper.insert(onlineMeeting));
@@ -70,6 +76,7 @@
     *新增文件共享
     *
     */
    @Log(title = "新增文件共享", businessType = BusinessType.INSERT)
    @PostMapping("/addFileSharing")
    public AjaxResult addFileSharing(@RequestBody FileSharing fileSharing){
        return AjaxResult.success(fileSharingMapper.insert(fileSharing));
src/main/java/com/ruoyi/approve/controller/RpaProcessAutomationController.java
@@ -5,6 +5,8 @@
import com.ruoyi.approve.pojo.RpaProcessAutomation;
import com.ruoyi.approve.service.RpaProcessAutomationService;
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.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -34,6 +36,7 @@
     * å¢žæ·»
     * @return
     */
    @Log(title = "RPA流程自动化", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody RpaProcessAutomation rpaProcessAutomation){
        return AjaxResult.success(rpaProcessAutomationService.save(rpaProcessAutomation));
@@ -42,6 +45,7 @@
     * æ›´æ–°
     * @return
     */
    @Log(title = "RPA流程自动化", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    public AjaxResult update(@RequestBody RpaProcessAutomation rpaProcessAutomation){
        return AjaxResult.success(rpaProcessAutomationService.updateById(rpaProcessAutomation));
@@ -50,12 +54,14 @@
     * åˆ é™¤
     * @return
     */
    @Log(title = "RPA流程自动化", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    public AjaxResult delete(@RequestBody List<Long> ids){
        if(CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
        return AjaxResult.success(rpaProcessAutomationService.removeByIds(ids));
    }
    @Log(title = "RPA流程自动化", businessType = BusinessType.EXPORT)
    @Operation(summary = "RPA流程自动化导出")
    @PostMapping("/export")
    public void export(HttpServletResponse response) {
src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.vo.ApprovalInstanceVo;
import com.ruoyi.approve.pojo.ApprovalInstance;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 * å®¡æ‰¹å®žä¾‹è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:46
 */
@Mapper
public interface ApprovalInstanceMapper extends BaseMapper<ApprovalInstance> {
    IPage<ApprovalInstanceVo> listPage(Page<ApprovalInstanceVo> page,@Param("ew") ApprovalInstanceDto approvalInstanceDto);
}
src/main/java/com/ruoyi/approve/mapper/ApprovalInstanceNodeMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.ApprovalInstanceNode;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å®¡æ‰¹èŠ‚ç‚¹å®žä¾‹è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:54
 */
@Mapper
public interface ApprovalInstanceNodeMapper extends BaseMapper<ApprovalInstanceNode> {
}
src/main/java/com/ruoyi/approve/mapper/ApprovalRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.ApprovalRecord;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å®¡æ‰¹è®°å½•表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:28:21
 */
@Mapper
public interface ApprovalRecordMapper extends BaseMapper<ApprovalRecord> {
}
src/main/java/com/ruoyi/approve/mapper/ApprovalTaskMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.ApprovalTask;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å®¡æ‰¹ä»»åŠ¡è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:32:37
 */
@Mapper
public interface ApprovalTaskMapper extends BaseMapper<ApprovalTask> {
}
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalTemplateDto;
import com.ruoyi.approve.bean.vo.ApprovalTemplateVo;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:08
 */
@Mapper
public interface ApprovalTemplateMapper extends BaseMapper<ApprovalTemplate> {
    IPage<ApprovalTemplateVo> listPage(Page<ApprovalTemplateVo> page,@Param("ew") ApprovalTemplateDto approvalTemplateDto);
}
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeApproverMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹å®¡æ‰¹äººè¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:30
 */
@Mapper
public interface ApprovalTemplateNodeApproverMapper extends BaseMapper<ApprovalTemplateNodeApprover> {
}
src/main/java/com/ruoyi/approve/mapper/ApprovalTemplateNodeMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:19
 */
@Mapper
public interface ApprovalTemplateNodeMapper extends BaseMapper<ApprovalTemplateNode> {
}
src/main/java/com/ruoyi/approve/mapper/FinReimbursementDetailMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * æŠ¥é”€å•明细表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@Mapper
public interface FinReimbursementDetailMapper extends BaseMapper<FinReimbursementDetail> {
}
src/main/java/com/ruoyi/approve/mapper/FinReimbursementMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.pojo.FinReimbursement;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 * æŠ¥é”€å•主表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@Mapper
public interface FinReimbursementMapper extends BaseMapper<FinReimbursement> {
    IPage<FinReimbursementVo> listPage(@Param("ew") FinReimbursementDto finReimbursementDto, Page<FinReimbursementVo> page);
}
src/main/java/com/ruoyi/approve/mapper/FinReimbursementTravelMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.approve.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * å·®æ—…报销扩展表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@Mapper
public interface FinReimbursementTravelMapper extends BaseMapper<FinReimbursementTravel> {
}
src/main/java/com/ruoyi/approve/pojo/ApprovalInstance.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,151 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹å®žä¾‹è¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:46
 */
@Getter
@Setter
@ToString
@TableName("approval_instance")
@ApiModel(value = "ApprovalInstance对象", description = "审批实例表")
public class ApprovalInstance implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * å®¡æ‰¹å®žä¾‹ID
     */
    @Schema(description ="审批实例ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹ç¼–号
     */
    @Schema(description ="审批编号")
    private String instanceNo;
    /**
     * æ¨¡æ¿ID
     */
    @Schema(description ="模板ID")
    private Long templateId;
    /**
     * æ¨¡æ¿åç§°
     */
    @Schema(description ="模板名称")
    private String templateName;
    /**
     * ä¸šåŠ¡ID
     */
    @Schema(description ="业务ID")
    private Long businessId;
    /**
     * ä¸šåŠ¡ç±»åž‹
     */
    @Schema(description ="业务类型")
    private Long businessType;
    /**
     * å®¡æ‰¹æ ‡é¢˜
     */
    @Schema(description ="审批标题")
    private String title;
    /**
     * å®¡æ‰¹çŠ¶æ€
     */
    @Schema(description ="审批状态 PENDING - å¾…审批/进行中  APPROVED - å·²é€šè¿‡/已完成  REJECTED - å·²é©³å›ž")
    private String status;
    /**
     * å½“前审批级别
     */
    @Schema(description ="当前审批级别")
    private Integer currentLevel;
    /**
     * ç”³è¯·äººID
     */
    @Schema(description ="申请人ID")
    private Long applicantId;
    /**
     * ç”³è¯·äººåç§°
     */
    @Schema(description ="申请人名称")
    private String applicantName;
    /**
     * ç”³è¯·æ—¶é—´
     */
    @Schema(description ="申请时间")
    private LocalDateTime applyTime;
    /**
     * å®Œæˆæ—¶é—´
     */
    @Schema(description ="完成时间")
    private LocalDateTime finishTime;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description ="创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description ="创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description ="更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description ="更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * é€»è¾‘删除
     */
    @Schema(description ="逻辑删除")
    private Byte deleted;
    @Schema(description = "表单数据")
    private String formConfig;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalInstanceNode.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹èŠ‚ç‚¹å®žä¾‹è¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:54
 */
@Getter
@Setter
@ToString
@TableName("approval_instance_node")
@ApiModel(value = "ApprovalInstanceNode对象", description = "审批节点实例表")
public class ApprovalInstanceNode implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * èŠ‚ç‚¹å®žä¾‹ID
     */
    @Schema(description ="节点实例ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹å®žä¾‹ID
     */
    @Schema(description ="审批实例ID")
    private Long instanceId;
    /**
     * å®¡æ‰¹çº§åˆ«
     */
    @Schema(description ="审批级别")
    private Integer levelNo;
    /**
     * å®¡æ‰¹ç±»åž‹
     */
    @Schema(description ="审批类型")
    private String approveType;
    /**
     * èŠ‚ç‚¹çŠ¶æ€
     */
    @Schema(description ="节点状态 PENDING - å¾…处理 APPROVED - å·²é€šè¿‡ REJECTED - å·²é©³å›ž")
    private String status;
    /**
     * å¼€å§‹æ—¶é—´
     */
    @Schema(description ="开始时间")
    private LocalDateTime startTime;
    /**
     * å®Œæˆæ—¶é—´
     */
    @Schema(description ="完成时间")
    private LocalDateTime finishTime;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description ="创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description ="创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description ="更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description ="更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * é€»è¾‘删除
     */
    @Schema(description ="逻辑删除")
    private Byte deleted;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹è®°å½•表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:28:21
 */
@Getter
@Setter
@ToString
@TableName("approval_record")
@ApiModel(value = "ApprovalRecord对象", description = "审批记录表")
public class ApprovalRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * å®¡æ‰¹è®°å½•ID
     */
    @Schema(description ="审批记录ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹å®žä¾‹ID
     */
    @Schema(description ="审批实例ID")
    private Long instanceId;
    /**
     * èŠ‚ç‚¹å®žä¾‹ID
     */
    @Schema(description ="节点实例ID")
    private Long nodeId;
    /**
     * å®¡æ‰¹ä»»åŠ¡ID
     */
    @Schema(description ="审批任务ID")
    private Long taskId;
    /**
     * æ“ä½œäººID
     */
    @Schema(description ="操作人ID")
    private Long operatorId;
    /**
     * æ“ä½œäººåç§°
     */
    @Schema(description ="操作人名称")
    private String operatorName;
    /**
     * æ“ä½œç±»åž‹
     */
    @Schema(description ="操作类型")
    private String action;
    /**
     * å®¡æ‰¹æ„è§
     */
    @Schema(description ="审批意见")
    private String comment;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description ="创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description ="创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * é€»è¾‘删除
     */
    @Schema(description ="逻辑删除")
    private Byte deleted;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,128 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹ä»»åŠ¡è¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:32:37
 */
@Getter
@Setter
@ToString
@TableName("approval_task")
@ApiModel(value = "ApprovalTask对象", description = "审批任务表")
public class ApprovalTask implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * å®¡æ‰¹ä»»åŠ¡ID
     */
    @Schema(description ="审批任务ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹å®žä¾‹ID
     */
    @Schema(description ="审批实例ID")
    private Long instanceId;
    /**
     * èŠ‚ç‚¹å®žä¾‹ID
     */
    @Schema(description ="节点实例ID")
    private Long nodeId;
    /**
     * å®¡æ‰¹çº§åˆ«
     */
    @Schema(description ="审批级别")
    private Integer levelNo;
    /**
     * å®¡æ‰¹äººID
     */
    @Schema(description ="审批人ID")
    private Long approverId;
    /**
     * å®¡æ‰¹äººåç§°
     */
    @Schema(description ="审批人名称")
    private String approverName;
    /**
     * ä»»åŠ¡çŠ¶æ€
     */
    @Schema(description ="任务状态 PENDING - å¾…审批 APPROVED - å·²åŒæ„  REJECTED - å·²æ‹’绝")
    private String taskStatus;
    /**
     * å®¡æ‰¹æ—¶é—´
     */
    @Schema(description ="审批时间")
    private LocalDateTime approveTime;
    /**
     * å®¡æ‰¹æ„è§
     */
    @Schema(description ="审批意见")
    private String comment;
    /**
     * æ˜¯å¦å·²è¯»
     */
    @Schema(description ="是否已读")
    private Byte isRead;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description ="创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description ="创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description ="更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description ="更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * é€»è¾‘删除
     */
    @Schema(description ="逻辑删除")
    private Byte deleted;
    @Schema(description ="部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplate.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿è¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:08
 */
@Getter
@Setter
@ToString
@TableName("approval_template")
@ApiModel(value = "ApprovalTemplate对象", description = "审批模板表")
public class ApprovalTemplate implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * æ¨¡æ¿ID
     */
    @Schema(description ="模板ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * æ¨¡æ¿åç§°
     */
    @Schema(description ="模板名称")
    private String templateName;
    /**
     * å¯ç”¨çŠ¶æ€ï¼š1启用,0停用
     */
    @Schema(description ="启用状态:1启用,0停用")
    private Byte enabled;
    /**
     * æ¨¡æ¿è¯´æ˜Ž
     */
    @Schema(description ="模板说明")
    private String description;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description ="创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description ="创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description ="更新人")
    @TableField(fill = FieldFill.UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description ="更新时间")
    @TableField(fill = FieldFill.UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    /**
     * é€»è¾‘删除:0否,1是
     */
    @Schema(description ="逻辑删除:0否,1是")
    private Integer deleted;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @Schema(description = "表单配置")
    private String formConfig;
    @Schema(description = "模板类型:0系统内置,1自定义")
    private Integer templateType;
    @Schema(description = "业务类型")
    private Long businessType;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNode.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹è¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:19
 */
@Getter
@Setter
@ToString
@TableName("approval_template_node")
@ApiModel(value = "ApprovalTemplateNode对象", description = "审批模板节点表")
public class ApprovalTemplateNode implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * èŠ‚ç‚¹ID
     */
    @Schema(description ="节点ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹æ¨¡æ¿ID
     */
    @Schema(description ="审批模板ID")
    private Long templateId;
    /**
     * å®¡æ‰¹çº§åˆ«ï¼Œä»Ž1开始
     */
    @Schema(description ="审批级别,从1开始")
    private Integer levelNo;
    /**
     * å®¡æ‰¹æ–¹å¼ï¼šAND会签,OR或签
     */
    @Schema(description ="审批方式:AND会签,OR或签")
    private String approveType;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/approve/pojo/ApprovalTemplateNodeApprover.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹å®¡æ‰¹äººè¡¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:30
 */
@Getter
@Setter
@ToString
@TableName("approval_template_node_approver")
@ApiModel(value = "ApprovalTemplateNodeApprover对象", description = "审批模板节点审批人表")
public class ApprovalTemplateNodeApprover implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @Schema(description = "主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * å®¡æ‰¹èŠ‚ç‚¹ID
     */
    @Schema(description ="审批节点ID")
    private Long nodeId;
    /**
     * å®¡æ‰¹æ¨¡æ¿ID
     */
    @Schema(description ="审批模板ID")
    private Long templateId;
    /**
     * å®¡æ‰¹äººID
     */
    @Schema(description ="审批人ID")
    private Long approverId;
    /**
     * å®¡æ‰¹äººåç§°å†—ä½™
     */
    @Schema(description ="审批人名称冗余")
    private String approverName;
    /**
     * å®¡æ‰¹äººæŽ’序
     */
    @Schema(description ="审批人排序")
    private Integer sortNo;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT)
    private Long deleted ;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/approve/pojo/ApproveProcess.java
@@ -151,6 +151,16 @@
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
    @Excel(name = "出差开始时间", dateFormat = "yyyy-MM-dd HH:mm", width = 30)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
    @Schema(description = "出差开始时间")
    private LocalDateTime startDateTime;
    @Excel(name = "出差结束时间", dateFormat = "yyyy-MM-dd HH:mm", width = 30)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
    @Schema(description = "出差结束时间")
    private LocalDateTime endDateTime;
    private BigDecimal price;
    private String location;
src/main/java/com/ruoyi/approve/pojo/FinReimbursement.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,209 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
 * æŠ¥é”€å•主表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@Getter
@Setter
@ToString
@TableName("fin_reimbursement")
@ApiModel(value = "FinReimbursement对象", description = "报销单主表")
public class FinReimbursement implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @Schema(description = "主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * æŠ¥é”€å•号
     */
    @Schema(description = "报销单号")
    private String billNo;
    /**
     * æŠ¥é”€ç±»åž‹ï¼š1-差旅报销,2-费用报销
     */
    @Schema(description = "报销类型:1-差旅报销,2-费用报销")
    private Byte reimbursementType;
    /**
     * è´¹ç”¨ç±»åž‹ï¼šå·®æ—…è´¹/办公采购/业务招待/交通费/通讯费/其他
     */
    @Schema(description = "费用类型:差旅费/办公采购/业务招待/交通费/通讯费/其他")
    private String expenseType;
    /**
     * ç”³è¯·äººID
     */
    @Schema(description = "申请人ID")
    private Long applicantId;
    /**
     * å‘˜å·¥ç¼–号
     */
    @Schema(description = "员工编号")
    private String applicantCode;
    /**
     * å‘˜å·¥å§“名
     */
    @Schema(description = "员工姓名")
    private String applicantName;
    /**
     * ç”³è¯·éƒ¨é—¨ID
     */
    @Schema(description = "申请部门ID")
    private Long applicantDeptId;
    /**
     * ç”³è¯·éƒ¨é—¨åç§°
     */
    @Schema(description = "申请部门名称")
    private String applicantDeptName;
    /**
     * æŠ¥é”€åŽŸå› 
     */
    @Schema(description = "报销原因")
    private String reason;
    /**
     * ç”³è¯·é‡‘额
     */
    @Schema(description = "申请金额")
    private BigDecimal applyAmount;
    /**
     * æ˜Žç»†æ±‡æ€»é‡‘额
     */
    @Schema(description = "明细汇总金额")
    private BigDecimal detailTotalAmount;
    /**
     * æ”¶æ¬¾äºº
     */
    @Schema(description = "收款人")
    private String payeeName;
    /**
     * æ”¶æ¬¾è´¦å·
     */
    @Schema(description = "收款账号")
    private String payeeAccount;
    /**
     * å¼€æˆ·æ”¯è¡Œ
     */
    @Schema(description = "开户支行")
    private String payeeBank;
    /**
     * å®¡æ‰¹å®žä¾‹ID,对应 approval_instance.id
     */
    @Schema(description = "审批实例ID,对应 approval_instance.id")
    private Long approvalInstanceId;
    /**
     * å®¡æ‰¹æµç¨‹ID,对应 approve_process.id
     */
    @Schema(description = "审批流程ID,对应 approve_process.id")
    private Long approveProcessId;
    /**
     * å•据状态:DRAFT-草稿,IN_APPROVAL-审批中,APPROVED-审批通过,REJECTED-审批驳回,WITHDRAWN-已撤回,PAID-已付款
     */
    @Schema(description = "单据状态:DRAFT-草稿,IN_APPROVAL-审批中,APPROVED-审批通过,REJECTED-审批驳回,WITHDRAWN-已撤回,PAID-已付款")
    private String billStatus;
    /**
     * å®¡æ‰¹é€šè¿‡æ—¶é—´
     */
    @Schema(description = "审批通过时间")
    private LocalDateTime approvedTime;
    /**
     * ä»˜æ¬¾æ—¶é—´
     */
    @Schema(description = "付款时间")
    private LocalDateTime paidTime;
    /**
     * ç”Ÿæˆçš„财务支出记录ID,对应 account_expense.id
     */
    @Schema(description = "生成的财务支出记录ID,对应 account_expense.id")
    private Long accountExpenseId;
    /**
     * å¤‡æ³¨
     */
    @Schema(description = "备注")
    private String remark;
    /**
     * ç§Ÿæˆ·ID
     */
    @Schema(description = "租户ID")
    private Long tenantId;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description = "更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * å½’属部门ID
     */
    @Schema(description = "归属部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * é€»è¾‘删除:0-否,1-是
     */
    @Schema(description = "逻辑删除:0-否,1-是")
    private Byte deleted;
}
src/main/java/com/ruoyi/approve/pojo/FinReimbursementDetail.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,157 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * <p>
 * æŠ¥é”€å•明细表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@Getter
@Setter
@ToString
@TableName("fin_reimbursement_detail")
@ApiModel(value = "FinReimbursementDetail对象", description = "报销单明细表")
public class FinReimbursementDetail implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @Schema(description = "主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * æŠ¥é”€å•ID,对应 fin_reimbursement.id
     */
    @Schema(description = "报销单ID,对应 fin_reimbursement.id")
    private Long reimbursementId;
    /**
     * æ˜Žç»†è¡Œå·
     */
    @Schema(description = "明细行号")
    private Integer rowNo;
    /**
     * å‘票日期
     */
    @Schema(description = "发票日期")
    private LocalDate invoiceDate;
    /**
     * è´¹ç”¨ç§‘ç›®
     */
    @Schema(description = "费用科目")
    private String expenseCategory;
    /**
     * é‡‘额
     */
    @Schema(description = "金额")
    private BigDecimal amount;
    /**
     * æè¿°
     */
    @Schema(description = "描述")
    private String description;
    /**
     * å‘票号码
     */
    @Schema(description = "发票号码")
    private String invoiceNo;
    /**
     * å‘票类型
     */
    @Schema(description = "发票类型")
    private String invoiceType;
    /**
     * ç¥¨é¢é‡‘额
     */
    @Schema(description = "票面金额")
    private BigDecimal invoiceAmount;
    /**
     * ç¨Žçއ
     */
    @Schema(description = "税率")
    private BigDecimal taxRate;
    /**
     * ç¨Žé¢
     */
    @Schema(description = "税额")
    private BigDecimal taxAmount;
    /**
     * å¤‡æ³¨
     */
    @Schema(description = "备注")
    private String remark;
    /**
     * ç§Ÿæˆ·ID
     */
    @Schema(description = "租户ID")
    private Long tenantId;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description = "更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * å½’属部门ID
     */
    @Schema(description = "归属部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    /**
     * é€»è¾‘删除:0-否,1-是
     */
    @Schema(description = "逻辑删除:0-否,1-是")
    private Byte deleted;
}
src/main/java/com/ruoyi/approve/pojo/FinReimbursementTravel.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,162 @@
package com.ruoyi.approve.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * <p>
 * å·®æ—…报销扩展表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@Getter
@Setter
@ToString
@TableName("fin_reimbursement_travel")
@ApiModel(value = "FinReimbursementTravel对象", description = "差旅报销扩展表")
public class FinReimbursementTravel implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ä¸»é”®ID
     */
    @Schema(description = "主键ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * æŠ¥é”€å•ID,对应 fin_reimbursement.id
     */
    @Schema(description = "报销单ID,对应 fin_reimbursement.id")
    private Long reimbursementId;
    /**
     * å‡ºå·®å¼€å§‹æ—¶é—´
     */
    @Schema(description = "出差开始时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;
    /**
     * å‡ºå·®ç»“束时间
     */
    @Schema(description = "出差结束时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;
    /**
     * å‡ºå·®å¤©æ•°
     */
    @Schema(description = "出差天数")
    private BigDecimal travelDays;
    /**
     * å‡ºå·®åœ°/出发城市
     */
    @Schema(description = "出差地/出发城市")
    private String departureCity;
    /**
     * ç›®çš„地/目的城市
     */
    @Schema(description = "目的地/目的城市")
    private String destinationCity;
    /**
     * é…’店标准
     */
    @Schema(description = "酒店标准")
    private BigDecimal hotelStandard;
    /**
     * ä½å®¿å¤©æ•°
     */
    @Schema(description = "住宿天数")
    private BigDecimal lodgingDays;
    /**
     * ç”Ÿæ´»è¡¥è´´
     */
    @Schema(description = "生活补贴")
    private BigDecimal mealAllowance;
    /**
     * äº¤é€šè¡¥è´´
     */
    @Schema(description = "交通补贴")
    private BigDecimal transportAllowance;
    /**
     * ä½å®¿é™é¢
     */
    @Schema(description = "住宿限额")
    private BigDecimal lodgingLimit;
    /**
     * ç‰¹æ‰¹æ ‡è®°æ–‡æœ¬ï¼Œå¦‚在标准范围内/超标特批
     */
    @Schema(description = "特批标记文本,如在标准范围内/超标特批")
    private String standardTag;
    /**
     * æ˜¯å¦åœ¨æ ‡å‡†å†…:1-是,0-否
     */
    @Schema(description = "是否在标准内:1-是,0-否")
    private Byte withinStandard;
    /**
     * ç§Ÿæˆ·ID
     */
    @Schema(description = "租户ID")
    private Long tenantId;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @Schema(description = "更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * å½’属部门ID
     */
    @Schema(description = "归属部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/approve/service/ApprovalInstanceNodeService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.ApprovalInstanceNode;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * å®¡æ‰¹èŠ‚ç‚¹å®žä¾‹è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:54
 */
public interface ApprovalInstanceNodeService extends IService<ApprovalInstanceNode> {
}
src/main/java/com/ruoyi/approve/service/ApprovalInstanceService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.approve.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.vo.ApprovalInstanceVo;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.framework.web.domain.R;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹å®žä¾‹è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:46
 */
public interface ApprovalInstanceService extends IService<ApprovalInstance> {
    R listPage(Page<ApprovalInstanceVo> page, ApprovalInstanceDto approvalInstanceDto);
    Boolean add(ApprovalInstanceDto approvalInstanceDto);
    Boolean update(ApprovalInstanceDto approvalInstanceDto);
    Boolean delete(List<Long> ids);
    R approve(ApprovalInstanceDto approvalInstanceDto);
    R autoApprove(Long instanceId);
}
src/main/java/com/ruoyi/approve/service/ApprovalRecordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.ApprovalRecord;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * å®¡æ‰¹è®°å½•表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:28:21
 */
public interface ApprovalRecordService extends IService<ApprovalRecord> {
}
src/main/java/com/ruoyi/approve/service/ApprovalTaskService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * å®¡æ‰¹ä»»åŠ¡è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:32:37
 */
public interface ApprovalTaskService extends IService<ApprovalTask> {
}
src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeApproverService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹å®¡æ‰¹äººè¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:30
 */
public interface ApprovalTemplateNodeApproverService extends IService<ApprovalTemplateNodeApprover> {
}
src/main/java/com/ruoyi/approve/service/ApprovalTemplateNodeService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeDto;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:19
 */
public interface ApprovalTemplateNodeService extends IService<ApprovalTemplateNode> {
    Boolean saveApprovalTemplateNode(Long id, List<ApprovalTemplateNodeDto> nodes);
}
src/main/java/com/ruoyi/approve/service/ApprovalTemplateService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.approve.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.ApprovalTemplateDto;
import com.ruoyi.approve.bean.vo.ApprovalTemplateVo;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:08
 */
public interface ApprovalTemplateService extends IService<ApprovalTemplate> {
    IPage<ApprovalTemplateVo> listPage(Page<ApprovalTemplateVo> page, ApprovalTemplateDto approvalTemplateDto);
    Boolean saveApprovalTemplateDto(ApprovalTemplateDto approvalTemplateDto);
    Boolean updateApprovalTemplateDto(ApprovalTemplateDto approvalTemplateDto);
    Boolean delete(List<Long> ids);
    List<ApprovalTemplateVo> listApprovalTemplateVo(Integer type);
    ApprovalTemplateVo getApprovalTemplateVoById(Long id);
}
src/main/java/com/ruoyi/approve/service/FinReimbursementDetailService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * æŠ¥é”€å•明细表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
public interface FinReimbursementDetailService extends IService<FinReimbursementDetail> {
}
src/main/java/com/ruoyi/approve/service/FinReimbursementService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.approve.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.pojo.FinReimbursement;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
 * <p>
 * æŠ¥é”€å•主表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
public interface FinReimbursementService extends IService<FinReimbursement> {
    IPage<FinReimbursementVo> listPage(FinReimbursementDto finReimbursementDto, Page<FinReimbursementVo> page);
    Boolean add(FinReimbursementDto finReimbursementDto);
    FinReimbursementVo detail(Long id);
    Boolean update(FinReimbursementDto finReimbursementDto);
    Boolean delete(List<Long> ids);
}
src/main/java/com/ruoyi/approve/service/FinReimbursementTravelService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.approve.service;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * å·®æ—…报销扩展表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
public interface FinReimbursementTravelService extends IService<FinReimbursementTravel> {
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceNodeServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.ApprovalInstanceNode;
import com.ruoyi.approve.mapper.ApprovalInstanceNodeMapper;
import com.ruoyi.approve.service.ApprovalInstanceNodeService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * å®¡æ‰¹èŠ‚ç‚¹å®žä¾‹è¡¨ æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:27:54
 */
@Service
public class ApprovalInstanceNodeServiceImpl extends ServiceImpl<ApprovalInstanceNodeMapper, ApprovalInstanceNode> implements ApprovalInstanceNodeService {
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,846 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.vo.ApprovalInstanceVo;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.mapper.ApprovalTemplateNodeApproverMapper;
import com.ruoyi.approve.mapper.FinReimbursementMapper;
import com.ruoyi.approve.pojo.*;
import com.ruoyi.approve.service.*;
import com.ruoyi.approve.utils.ApproveProcessConfigNodeUtils;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsMapper;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeDeptMapper;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeUserMapper;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser;
import com.ruoyi.common.enums.*;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.mapper.SysUserDeptMapper;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.project.system.service.ISysNoticeService;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.quality.utils.QualityInspectHelper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.mapper.SalesQuotationMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.SalesQuotation;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.staff.mapper.HolidayApplicationMapper;
import com.ruoyi.staff.pojo.HolidayApplication;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
 * <p>
 * å®¡æ‰¹å®žä¾‹æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @since 2026-05-18 03:27:46
 */
@Service
@RequiredArgsConstructor
public class ApprovalInstanceServiceImpl extends ServiceImpl<ApprovalInstanceMapper, ApprovalInstance> implements ApprovalInstanceService {
    private static final String ENTERPRISE_NEWS_STATUS_PUBLISHED = "PUBLISHED";
    private static final String ENTERPRISE_NEWS_STATUS_REJECTED = "REJECTED";
    private final ApprovalInstanceMapper approvalInstanceMapper;
    private final ApproveProcessConfigNodeUtils approveProcessConfigNodeUtils;
    private final ApprovalInstanceNodeService approvalInstanceNodeService;
    private final ApprovalTaskService approvalTaskService;
    private final ApprovalRecordService approvalRecordService;
    private final ApprovalTemplateNodeService approvalTemplateNodeService;
    private final FinReimbursementMapper finReimbursementMapper;
    private final FileUtil fileUtil;
    private final ISysNoticeService sysNoticeService;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final StockUtils stockUtils;
    private final SalesQuotationMapper salesQuotationMapper;
    private final ShippingInfoMapper shippingInfoMapper;
    private final QualityInspectHelper qualityInspectHelper;
    private final EnterpriseNewsScopeUserMapper enterpriseNewsScopeUserMapper;
    private final SysUserMapper sysUserMapper;
    private final SysUserDeptMapper sysUserDeptMapper;
    private final SysDeptMapper sysDeptMapper;
    private final HolidayApplicationMapper holidayApplicationMapper;
    private final EnterpriseNewsMapper enterpriseNewsMapper;
    private final EnterpriseNewsScopeDeptMapper enterpriseNewsScopeDeptMapper;
    private final ApprovalTemplateNodeApproverMapper approvalTemplateNodeApproverMapper;
    @Override
    public R listPage(Page<ApprovalInstanceVo> page, ApprovalInstanceDto approvalInstanceDto) {
        IPage<ApprovalInstanceVo> approvalInstanceVoIPage = approvalInstanceMapper.listPage(page, approvalInstanceDto);
        List<ApprovalInstanceVo> records = approvalInstanceVoIPage.getRecords();
        if (records == null || records.isEmpty()) {
            return R.ok(approvalInstanceVoIPage);
        }
        records.forEach(vo -> {
            vo.setBusinessName(TypeEnums.getLabelByValue(vo.getBusinessType()));
        });
        Long currentUserId = SecurityUtils.getUserId();
        List<Long> instanceIds = records.stream()
                .map(ApprovalInstanceVo::getId)
                .filter(id -> id != null)
                .distinct()
                .collect(Collectors.toList());
        if (!instanceIds.isEmpty()) {
            Map<Long, List<ApprovalRecord>> recordMap = approvalRecordService.list(
                    Wrappers.<ApprovalRecord>lambdaQuery()
                            .in(ApprovalRecord::getInstanceId, instanceIds)
                            .eq(ApprovalRecord::getDeleted, 0)
            ).stream().collect(Collectors.groupingBy(ApprovalRecord::getInstanceId));
            Map<Long, List<ApprovalTask>> taskMap = approvalTaskService.list(
                    Wrappers.<ApprovalTask>lambdaQuery()
                            .in(ApprovalTask::getInstanceId, instanceIds)
                            .eq(ApprovalTask::getDeleted, 0)
            ).stream().collect(Collectors.groupingBy(ApprovalTask::getInstanceId));
            for (ApprovalInstanceVo vo : records) {
                vo.setIsApprove(approveProcessConfigNodeUtils.isCurrentApprover(vo.getId(), currentUserId));
                vo.setRecords(recordMap.getOrDefault(vo.getId(), new ArrayList<>()));
                vo.setTasks(taskMap.getOrDefault(vo.getId(), new ArrayList<>()));
                vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.APPROVAL_INSTANCE, vo.getId()));
            }
        }
        return R.ok(approvalInstanceVoIPage);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean add(ApprovalInstanceDto approvalInstanceDto) {
        String instanceNo = OrderUtils.countTodayByCreateTime(approvalInstanceMapper, "SP", "instance_no", approvalInstanceDto.getCreateTime() != null ? approvalInstanceDto.getCreateTime() : LocalDateTime.now());
        approvalInstanceDto.setInstanceNo(instanceNo);
        approvalInstanceDto.setStatus("PENDING");
        approvalInstanceDto.setCurrentLevel(1);
        boolean saved = this.save(approvalInstanceDto);
        if (!saved) {
            return false;
        }
        approveProcessConfigNodeUtils.createCurrentNodeAndTasks(approvalInstanceDto);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVAL_INSTANCE, approvalInstanceDto.getId(), approvalInstanceDto.getStorageBlobDTOs());
        sendApproveNotice(approvalInstanceDto, approveProcessConfigNodeUtils.getCurrentPendingTasks(approvalInstanceDto.getId()));
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean update(ApprovalInstanceDto approvalInstanceDto) {
        if (approvalInstanceDto == null || approvalInstanceDto.getId() == null) {
            return false;
        }
        // åˆ¤æ–­æ˜¯å¦æœ‰æ­£åœ¨è¿›è¡Œçš„审批任务,有则不允许修改
        long pendingTaskCount = approvalTaskService.count(
                Wrappers.<ApprovalTask>lambdaQuery()
                        .eq(ApprovalTask::getInstanceId, approvalInstanceDto.getId())
                        .eq(ApprovalTask::getTaskStatus, "PENDING")
                        .eq(ApprovalTask::getDeleted, 0)
        );
        if (pendingTaskCount > 0) {
            throw new ServiceException("该审批单有正在进行的审批任务,不允许修改");
        }
        boolean updated = this.updateById(approvalInstanceDto);
        if (!updated) {
            return false;
        }
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVAL_INSTANCE, approvalInstanceDto.getId(), approvalInstanceDto.getStorageBlobDTOs());
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean delete(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return false;
        }
        fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVAL_INSTANCE, ids);
        int instanceRows = approvalInstanceMapper.update(
                null,
                Wrappers.<ApprovalInstance>lambdaUpdate()
                        .in(ApprovalInstance::getId, ids)
                        .eq(ApprovalInstance::getDeleted, 0)
                        .set(ApprovalInstance::getDeleted, (byte) 1)
        );
        LambdaUpdateWrapper<ApprovalInstanceNode> nodeUpdateWrapper = Wrappers.lambdaUpdate();
        nodeUpdateWrapper.in(ApprovalInstanceNode::getInstanceId, ids)
                .eq(ApprovalInstanceNode::getDeleted, 0)
                .set(ApprovalInstanceNode::getDeleted, (byte) 1);
        approvalInstanceNodeService.update(nodeUpdateWrapper);
        LambdaUpdateWrapper<ApprovalTask> taskUpdateWrapper = Wrappers.lambdaUpdate();
        taskUpdateWrapper.in(ApprovalTask::getInstanceId, ids)
                .eq(ApprovalTask::getDeleted, 0)
                .set(ApprovalTask::getDeleted, (byte) 1);
        approvalTaskService.update(taskUpdateWrapper);
        LambdaUpdateWrapper<ApprovalRecord> recordUpdateWrapper = Wrappers.lambdaUpdate();
        recordUpdateWrapper.in(ApprovalRecord::getInstanceId, ids)
                .eq(ApprovalRecord::getDeleted, 0)
                .set(ApprovalRecord::getDeleted, (byte) 1);
        approvalRecordService.update(recordUpdateWrapper);
        return instanceRows > 0;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R approve(ApprovalInstanceDto approvalInstanceDto) {
        if (approvalInstanceDto == null || approvalInstanceDto.getId() == null) {
            return R.fail("审批实例 ID ä¸èƒ½ä¸ºç©º");
        }
        String approveAction = normalizeApproveAction(approvalInstanceDto.getApproveAction());
        if (approveAction == null) {
            return R.fail("审批动作只支持 APPROVED æˆ– REJECTED");
        }
        ApprovalInstance instance = getPendingApprovalInstance(approvalInstanceDto.getId());
        if (instance == null) {
            return R.fail("审批实例不存在");
        }
        ApprovalInstanceNode currentNode = approveProcessConfigNodeUtils.getCurrentNode(instance.getId());
        if (currentNode == null) {
            return R.fail("当前没有待处理的审批节点");
        }
        Long currentUserId = SecurityUtils.getUserId();
        ApprovalTask currentTask = approveProcessConfigNodeUtils.getCurrentUserTask(instance.getId(), currentUserId);
        if (currentTask == null) {
            return R.fail("当前用户没有可审批任务");
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String operatorName = loginUser.getUser() != null ? loginUser.getUser().getNickName() : SecurityUtils.getUsername();
        LocalDateTime now = LocalDateTime.now();
        if (!updateCurrentTask(approvalInstanceDto, approveAction, currentTask, now)) {
            return R.fail("当前任务已被处理,请刷新后重试");
        }
        saveApprovalRecord(
                instance.getId(),
                currentNode.getId(),
                currentTask.getId(),
                currentUserId,
                operatorName,
                approveAction,
                approvalInstanceDto.getApproveComment()
        );
        //审批拒绝的处理
        if ("REJECTED".equals(approveAction)) {
            return rejectCurrentNode(instance, currentNode, now);
        }
        if (!approveProcessConfigNodeUtils.canProceedToNextLevel(instance.getId(), currentNode.getApproveType())) {
            return R.ok("审批成功,等待其他审批人处理");
        }
        return approveAndMoveNext(instance, currentNode, approvalInstanceDto, now);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R autoApprove(Long instanceId) {
        if (instanceId == null) {
            return R.fail("审批实例 ID ä¸èƒ½ä¸ºç©º");
        }
        ApprovalInstance instance = getPendingApprovalInstance(instanceId);
        if (instance == null) {
            return R.fail("审批实例不存在");
        }
        if ("REJECTED".equals(instance.getStatus())) {
            return R.fail("审批已驳回,无法自动通过");
        }
        if ("APPROVED".equals(instance.getStatus())) {
            return R.ok("审批已完成");
        }
        ApprovalInstanceDto autoApproveDto = new ApprovalInstanceDto();
        autoApproveDto.setId(instanceId);
        autoApproveDto.setApproveComment("系统自动审批");
        int loopCount = 0;
        while (loopCount++ < 20) {
            ApprovalInstance currentInstance = getPendingApprovalInstance(instanceId);
            if (currentInstance == null) {
                return R.fail("审批实例不存在");
            }
            if ("APPROVED".equals(currentInstance.getStatus())) {
                return R.ok("审批已完成");
            }
            if ("REJECTED".equals(currentInstance.getStatus())) {
                return R.fail("审批已驳回,无法自动通过");
            }
            ApprovalInstanceNode currentNode = approveProcessConfigNodeUtils.getCurrentNode(currentInstance.getId());
            if (currentNode == null) {
                currentInstance.setStatus("APPROVED");
                currentInstance.setFinishTime(LocalDateTime.now());
                this.updateById(currentInstance);
                handleBusinessAfterApprovalFinished(currentInstance);
                return R.ok("审批已完成");
            }
            List<ApprovalTask> pendingTasks = approvalTaskService.list(
                    Wrappers.<ApprovalTask>lambdaQuery()
                            .eq(ApprovalTask::getInstanceId, currentInstance.getId())
                            .eq(ApprovalTask::getNodeId, currentNode.getId())
                            .eq(ApprovalTask::getTaskStatus, "PENDING")
                            .eq(ApprovalTask::getDeleted, 0)
            );
            LocalDateTime now = LocalDateTime.now();
            for (ApprovalTask currentTask : pendingTasks) {
                if (!updateCurrentTask(autoApproveDto, "APPROVED", currentTask, now)) {
                    return R.fail("当前任务已被处理,请刷新后重试");
                }
                saveApprovalRecord(
                        currentInstance.getId(),
                        currentNode.getId(),
                        currentTask.getId(),
                        0L,
                        "系统自动审批",
                        "APPROVED",
                        autoApproveDto.getApproveComment()
                );
            }
            if (!approveProcessConfigNodeUtils.canProceedToNextLevel(currentInstance.getId(), currentNode.getApproveType())) {
                return R.ok("审批成功,等待其他审批人处理");
            }
            R moveResult = moveToNextLevel(currentInstance, currentNode, autoApproveDto, now, false);
            if (!R.isSuccess(moveResult)) {
                return moveResult;
            }
        }
        return R.fail("自动审批循环次数超限");
    }
    private String normalizeApproveAction(String approveAction) {
        if (!StringUtils.hasText(approveAction)) {
            return null;
        }
        String normalizedAction = approveAction.trim().toUpperCase(Locale.ROOT);
        return "APPROVED".equals(normalizedAction) || "REJECTED".equals(normalizedAction)
                ? normalizedAction
                : null;
    }
    private ApprovalInstance getPendingApprovalInstance(Long instanceId) {
        return this.getOne(
                new LambdaQueryWrapper<ApprovalInstance>()
                        .eq(ApprovalInstance::getId, instanceId)
                        .eq(ApprovalInstance::getDeleted, 0)
                        .last("LIMIT 1")
        );
    }
    private boolean updateCurrentTask(ApprovalInstanceDto approvalInstanceDto,
                                      String approveAction,
                                      ApprovalTask currentTask,
                                      LocalDateTime now) {
        // ä»…允许待审批任务被成功处理一次,避免并发下重复审批成功。
        return approvalTaskService.update(
                Wrappers.<ApprovalTask>lambdaUpdate()
                        .eq(ApprovalTask::getId, currentTask.getId())
                        .eq(ApprovalTask::getTaskStatus, "PENDING")
                        .eq(ApprovalTask::getDeleted, 0)
                        .set(ApprovalTask::getTaskStatus, approveAction)
                        .set(ApprovalTask::getComment, approvalInstanceDto.getApproveComment())
                        .set(ApprovalTask::getApproveTime, now)
                        .set(ApprovalTask::getIsRead, (byte) 1)
        );
    }
    private R rejectCurrentNode(ApprovalInstance instance, ApprovalInstanceNode currentNode, LocalDateTime now) {
        if (!updateCurrentNodeStatus(currentNode.getId(), "REJECTED", now)) {
            return R.ok("当前节点已处理完成");
        }
        closePendingTasks(instance.getId(), currentNode.getId());
        instance.setStatus("REJECTED");
        instance.setFinishTime(now);
        this.updateById(instance);
        // é©³å›žå¯¹åº”的企业新闻, å·®æ—…报销
        if (instance.getBusinessType().equals(TypeEnums.ENTERPRISE_NEWS_APPROVAL.getCode())) {
            enterpriseNewsMapper.update(
                    new LambdaUpdateWrapper<EnterpriseNews>()
                            .eq(EnterpriseNews::getId, instance.getBusinessId())
                            .set(EnterpriseNews::getStatus, "REJECTED")
            );
        }else if (instance.getBusinessType().equals(TypeEnums.TRAVEL_REIMBURSEMENT_APPROVAL.getCode())||instance.getBusinessType().equals(TypeEnums.EXPENSE_APPROVAL.getCode())) {
            finReimbursementMapper.update(
                    new LambdaUpdateWrapper<FinReimbursement>()
                            .eq(FinReimbursement::getId, instance.getBusinessId())
                            .set(FinReimbursement::getBillStatus, "REJECTED")
            );
        }
        return R.ok("审批已驳回");
    }
    private R approveAndMoveNext(ApprovalInstance instance,
                                 ApprovalInstanceNode currentNode,
                                 ApprovalInstanceDto approvalInstanceDto,
                                 LocalDateTime now) {
        return moveToNextLevel(instance, currentNode, approvalInstanceDto, now, true);
    }
    private R moveToNextLevel(ApprovalInstance instance,
                              ApprovalInstanceNode currentNode,
                              ApprovalInstanceDto approvalInstanceDto,
                              LocalDateTime now,
                              boolean notifyNextNode) {
        if (!updateCurrentNodeStatus(currentNode.getId(), "APPROVED", now)) {
            return R.ok("当前节点已处理完成");
        }
        closePendingTasks(instance.getId(), currentNode.getId());
        int nextLevel = currentNode.getLevelNo() + 1;
        ApprovalInstanceNode nextInstanceNode = approvalInstanceNodeService.getOne(
                new LambdaQueryWrapper<ApprovalInstanceNode>()
                        .eq(ApprovalInstanceNode::getInstanceId, instance.getId())
                        .eq(ApprovalInstanceNode::getLevelNo, nextLevel)
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .orderByAsc(ApprovalInstanceNode::getId)
                        .last("LIMIT 1")
        );
        if (nextInstanceNode != null) {
            if (!activateNextInstanceNode(nextInstanceNode.getId(), now)) {
                return R.ok("下一审批节点已被激活,请刷新后重试");
            }
            instance.setCurrentLevel(nextLevel);
            instance.setStatus("PENDING");
            this.updateById(instance);
            if (notifyNextNode) {
                List<ApprovalTask> nextTasks = approvalTaskService.list(
                        Wrappers.<ApprovalTask>lambdaQuery()
                                .eq(ApprovalTask::getInstanceId, instance.getId())
                                .eq(ApprovalTask::getNodeId, nextInstanceNode.getId())
                                .eq(ApprovalTask::getTaskStatus, "PENDING")
                                .eq(ApprovalTask::getDeleted, 0)
                );
                sendApproveNotice(instance, nextTasks);
            }
            return R.ok("审批成功,已流转到下一节点");
        }
        ApprovalTemplateNode nextTemplateNode = approvalTemplateNodeService.getOne(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .eq(ApprovalTemplateNode::getTemplateId, instance.getTemplateId())
                        .eq(ApprovalTemplateNode::getLevelNo, nextLevel)
                        .orderByAsc(ApprovalTemplateNode::getId)
                        .last("LIMIT 1")
        );
        if (nextTemplateNode == null) {
            instance.setStatus("APPROVED");
            instance.setFinishTime(now);
            this.updateById(instance);
            handleBusinessAfterApprovalFinished(instance);
            return R.ok("审批已完成");
        }
        instance.setCurrentLevel(nextLevel);
        instance.setStatus("PENDING");
        this.updateById(instance);
        approveProcessConfigNodeUtils.createCurrentNodeAndTasks(instance, false);
        if (notifyNextNode) {
            sendApproveNotice(instance, approveProcessConfigNodeUtils.getCurrentPendingTasks(approvalInstanceDto.getId()));
        }
        return R.ok("审批成功,已流转到下一节点");
    }
    private boolean activateNextInstanceNode(Long nodeId, LocalDateTime now) {
        return approvalInstanceNodeService.update(
                Wrappers.<ApprovalInstanceNode>lambdaUpdate()
                        .eq(ApprovalInstanceNode::getId, nodeId)
                        .eq(ApprovalInstanceNode::getStatus, "WAITING")
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .set(ApprovalInstanceNode::getStatus, "PENDING")
                        .set(ApprovalInstanceNode::getStartTime, now)
        );
    }
    private boolean updateCurrentNodeStatus(Long nodeId, String targetStatus, LocalDateTime now) {
        // ä»…允许一个请求将当前节点从待处理推进到目标状态,避免重复流转。
        return approvalInstanceNodeService.update(
                Wrappers.<ApprovalInstanceNode>lambdaUpdate()
                        .eq(ApprovalInstanceNode::getId, nodeId)
                        .eq(ApprovalInstanceNode::getStatus, "PENDING")
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .set(ApprovalInstanceNode::getStatus, targetStatus)
                        .set(ApprovalInstanceNode::getFinishTime, now)
        );
    }
    private void handleBusinessAfterApprovalFinished(ApprovalInstance instance) {
        String status = instance.getStatus();
        Long businessType = instance.getBusinessType();
        if (TypeEnums.PURCHASE_APPROVAL.getCode().equals(businessType)) {
            handlePurchaseApprovalFinished(instance, status);
            return;
        }
        if (TypeEnums.QUOTATION_APPROVAL.getCode().equals(businessType)) {
            handleSalesQuotationApprovalFinished(instance, status);
            return;
        }
        if (TypeEnums.SHIPPING_APPROVAL.getCode().equals(businessType)) {
            handleShippingApprovalFinished(instance, status);
            return;
        }
        if (TypeEnums.TRAVEL_REIMBURSEMENT_APPROVAL.getCode().equals(businessType)
                || TypeEnums.EXPENSE_APPROVAL.getCode().equals(businessType)) {
            handleReimbursementApprovalFinished(instance, status);
            return;
        }
        // é©³å›žå¯¹åº”的企业新闻、加班申请、请假申请可以重新再提交
        if (TypeEnums.LEAVE_APPROVAL.getCode().equals(businessType)
                || TypeEnums.OVERTIME_APPROVAL.getCode().equals(businessType)) {
            handleHolidayApplicationApprovalFinished(instance, status);
            return;
        }
        if (TypeEnums.ENTERPRISE_NEWS_APPROVAL.getCode().equals(businessType)) {
            handleNewsApprovalFinished(instance, status);
        }
    }
    private void handleReimbursementApprovalFinished(ApprovalInstance instance, String status) {
        if (instance == null || instance.getBusinessId() == null) {
            return;
        }
        FinReimbursement reimbursement = new FinReimbursement();
        reimbursement.setId(instance.getBusinessId());
        if ("APPROVED".equals(status)) {
            reimbursement.setBillStatus("APPROVED");
            reimbursement.setApprovedTime(instance.getFinishTime());
        } else if ("REJECTED".equals(status)) {
            reimbursement.setBillStatus("REJECTED");
        } else if ("PENDING".equals(status)) {
            reimbursement.setBillStatus("IN_APPROVAL");
        } else {
            return;
        }
        finReimbursementMapper.updateById(reimbursement);
    }
    private void handleNewsApprovalFinished(ApprovalInstance instance, String status) {
        if (instance == null || instance.getBusinessId() == null) {
            return;
        }
        EnterpriseNews enterpriseNews = new EnterpriseNews();
        enterpriseNews.setId(instance.getBusinessId());
        if ("APPROVED".equals(status)) {
            enterpriseNews.setStatus(ENTERPRISE_NEWS_STATUS_PUBLISHED);
            enterpriseNewsMapper.updateById(enterpriseNews);
            sendEnterpriseNewsNotice(instance.getBusinessId());
            return;
        }
        if ("REJECTED".equals(status)) {
            enterpriseNews.setStatus(ENTERPRISE_NEWS_STATUS_REJECTED);
            enterpriseNewsMapper.updateById(enterpriseNews);
        }
    }
    private void handleHolidayApplicationApprovalFinished(ApprovalInstance instance, String status) {
        if (instance == null || instance.getBusinessId() == null) {
            return;
        }
        HolidayApplication holidayApplication = new HolidayApplication();
        holidayApplication.setId(instance.getBusinessId());
        if ("APPROVED".equals(status)) {
            holidayApplication.setStatus("APPROVED");
        } else if ("REJECTED".equals(status)) {
            holidayApplication.setStatus("REJECTED");
        } else if ("PENDING".equals(status)) {
            holidayApplication.setStatus("PENDING");
        } else {
            return;
        }
        holidayApplicationMapper.updateById(holidayApplication);
    }
    private void handlePurchaseApprovalFinished(ApprovalInstance instance, String status) {
        PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectOne(
                new LambdaQueryWrapper<PurchaseLedger>()
                        .eq(PurchaseLedger::getId, instance.getBusinessId())
                        .last("limit 1")
        );
        if (purchaseLedger == null) {
            return;
        }
        if ("APPROVED".equals(status)) {
            purchaseLedger.setApprovalStatus(ApprovalStatusEnum.APPROVED.getCode());
            List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(
                    new QueryWrapper<SalesLedgerProduct>().lambda()
                            .eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedger.getId())
                            .eq(SalesLedgerProduct::getType, 2)
            );
            for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) {
                if (salesLedgerProduct.getIsChecked()) {
                    qualityInspectHelper.addQualityInspect(purchaseLedger, salesLedgerProduct);
                } else {
                    stockUtils.addStockWithBatchNo(
                            salesLedgerProduct.getProductModelId(),
                            salesLedgerProduct.getQuantity(),
                            StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(),
                            purchaseLedger.getId(),
                            purchaseLedger.getPurchaseContractNumber() + "-" + salesLedgerProduct.getId()
                    );
                }
            }
        } else if ("REJECTED".equals(status)) {
            purchaseLedger.setApprovalStatus(ApprovalStatusEnum.REJECTED.getCode());
        } else if ("PENDING".equals(status)) {
            purchaseLedger.setApprovalStatus(ApprovalStatusEnum.IN_PROGRESS.getCode());
        }
        purchaseLedgerMapper.updateById(purchaseLedger);
    }
    private void handleSalesQuotationApprovalFinished(ApprovalInstance instance, String status) {
        SalesQuotation salesQuote = salesQuotationMapper.selectOne(
                new LambdaQueryWrapper<SalesQuotation>()
                        .eq(SalesQuotation::getId, instance.getBusinessId())
                        .last("limit 1")
        );
        if (salesQuote == null) {
            return;
        }
        if ("APPROVED".equals(status)) {
            salesQuote.setStatus(SalesQuotationStatusEnum.APPROVED.getCode());
        } else if ("REJECTED".equals(status)) {
            salesQuote.setStatus(SalesQuotationStatusEnum.REJECTED.getCode());
        } else if ("PENDING".equals(status)) {
            salesQuote.setStatus(SalesQuotationStatusEnum.IN_PROGRESS.getCode());
        }
        salesQuotationMapper.updateById(salesQuote);
    }
    private void handleShippingApprovalFinished(ApprovalInstance instance, String status) {
        ShippingInfo shippingInfo = shippingInfoMapper.selectOne(
                new LambdaQueryWrapper<ShippingInfo>()
                        .eq(ShippingInfo::getId, instance.getTitle())
                        .orderByDesc(ShippingInfo::getCreateTime)
                        .last("limit 1")
        );
        if (shippingInfo == null) {
            return;
        }
        if ("APPROVED".equals(status)) {
            shippingInfo.setStatus(ShippingStatusEnum.APPROVED.getCode());
            shippingInfo.setShippingDate(new Date());
            stockUtils.shipmentStatus(StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), shippingInfo.getId());
        } else if ("REJECTED".equals(status)) {
            stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
            shippingInfo.setStatus(ShippingStatusEnum.REJECTED.getCode());
        } else if ("PENDING".equals(status)) {
            shippingInfo.setStatus(ShippingStatusEnum.IN_PROGRESS.getCode());
        }
        shippingInfoMapper.updateById(shippingInfo);
    }
    private List<ApprovalTask> createNodeAndTasks(ApprovalInstance instance, ApprovalTemplateNode templateNode) {
        List<ApprovalTemplateNodeApprover> approvers = approvalTemplateNodeApproverMapper.selectList(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .eq(ApprovalTemplateNodeApprover::getTemplateId, instance.getTemplateId())
                        .eq(ApprovalTemplateNodeApprover::getNodeId, templateNode.getId())
                        .eq(ApprovalTemplateNodeApprover::getDeleted, 0L)
                        .orderByAsc(ApprovalTemplateNodeApprover::getSortNo)
        );
        if (approvers == null || approvers.isEmpty()) {
            throw new RuntimeException("下一审批节点未配置审批人");
        }
        ApprovalInstanceNode instanceNode = new ApprovalInstanceNode();
        instanceNode.setInstanceId(instance.getId());
        instanceNode.setLevelNo(templateNode.getLevelNo());
        instanceNode.setApproveType(templateNode.getApproveType());
        instanceNode.setStatus("PENDING");
        instanceNode.setStartTime(LocalDateTime.now());
        instanceNode.setDeleted((byte) 0);
        approvalInstanceNodeService.save(instanceNode);
        List<ApprovalTask> taskList = new ArrayList<>(approvers.size());
        for (ApprovalTemplateNodeApprover approver : approvers) {
            ApprovalTask task = new ApprovalTask();
            task.setInstanceId(instance.getId());
            task.setNodeId(instanceNode.getId());
            task.setLevelNo(instanceNode.getLevelNo());
            task.setApproverId(approver.getApproverId());
            task.setApproverName(approver.getApproverName());
            task.setTaskStatus("PENDING");
            task.setIsRead((byte) 0);
            task.setDeleted((byte) 0);
            taskList.add(task);
        }
        approvalTaskService.saveBatch(taskList);
        return taskList;
    }
    private void sendApproveNotice(ApprovalInstance instance, List<ApprovalTask> tasks) {
        if (instance == null || tasks == null || tasks.isEmpty()) {
            return;
        }
        List<Long> approverIds = tasks.stream()
                .map(ApprovalTask::getApproverId)
                .filter(id -> id != null && id > 0)
                .distinct()
                .collect(Collectors.toList());
        if (approverIds.isEmpty()) {
            return;
        }
        String title = StringUtils.hasText(instance.getTemplateName()) ? instance.getTemplateName() : "审批提醒";
        String message = "审批单号 " + instance.getInstanceNo() + " éœ€è¦æ‚¨å®¡æ‰¹";
        String jumpPath = "/officeProcessAutomation/ApproveManage/approve-list?id=" + instance.getId();
        sysNoticeService.simpleNoticeByUser(title, message, approverIds, jumpPath);
    }
    private void sendEnterpriseNewsNotice(Long newsId) {
        EnterpriseNews enterpriseNews = enterpriseNewsMapper.selectById(newsId);
        if (enterpriseNews == null) {
            return;
        }
        List<Long> userIds = getEnterpriseNewsNoticeUserIds(enterpriseNews);
        if (userIds == null || userIds.isEmpty()) {
            return;
        }
        String title = "企业新闻";
        String message = "您有新的企业新闻《" + enterpriseNews.getTitle() + "》请及时查阅";
        String jumpPath = "/officeProcessAutomation/EnterpriseNews?id=" + newsId;
        sysNoticeService.simpleNoticeByUser(title, message, userIds, jumpPath);
    }
    private List<Long> getEnterpriseNewsNoticeUserIds(EnterpriseNews enterpriseNews) {
        if (enterpriseNews == null || !org.springframework.util.StringUtils.hasText(enterpriseNews.getReadScope())) {
            return Collections.emptyList();
        }
        String readScope = enterpriseNews.getReadScope().trim();
        if ("all".equals(readScope)) {
            return sysUserMapper.selectList(new LambdaQueryWrapper<SysUser>()
                            .select(SysUser::getUserId)
                            .eq(SysUser::getDelFlag, "0"))
                    .stream()
                    .map(SysUser::getUserId)
                    .filter(id -> id != null && id > 0)
                    .distinct()
                    .collect(Collectors.toList());
        }
        if ("dept".equals(readScope)) {
            List<Long> deptIds = enterpriseNewsScopeDeptMapper.selectList(
                            new LambdaQueryWrapper<EnterpriseNewsScopeDept>()
                                    .eq(EnterpriseNewsScopeDept::getNewsId, enterpriseNews.getId()))
                    .stream()
                    .map(EnterpriseNewsScopeDept::getDeptId)
                    .filter(id -> id != null && id > 0)
                    .distinct()
                    .collect(Collectors.toList());
            if (deptIds.isEmpty()) {
                return Collections.emptyList();
            }
            return sysUserDeptMapper.selectDistinctUserIdsByDeptIds(collectDeptIdsWithChildren(deptIds));
        }
        if ("custom".equals(readScope)) {
            return enterpriseNewsScopeUserMapper.selectList(
                            new LambdaQueryWrapper<EnterpriseNewsScopeUser>()
                                    .eq(EnterpriseNewsScopeUser::getNewsId, enterpriseNews.getId()))
                    .stream()
                    .map(EnterpriseNewsScopeUser::getUserId)
                    .filter(id -> id != null && id > 0)
                    .distinct()
                    .collect(Collectors.toList());
        }
        return Collections.emptyList();
    }
    private List<Long> collectDeptIdsWithChildren(List<Long> deptIds) {
        Set<Long> allDeptIds = new LinkedHashSet<>();
        for (Long deptId : deptIds) {
            if (deptId == null) {
                continue;
            }
            allDeptIds.add(deptId);
            List<SysDept> children = sysDeptMapper.selectChildrenDeptById(deptId);
            if (children != null && !children.isEmpty()) {
                for (SysDept child : children) {
                    if (child != null && child.getDeptId() != null) {
                        allDeptIds.add(child.getDeptId());
                    }
                }
            }
        }
        return new ArrayList<>(allDeptIds);
    }
    private void closePendingTasks(Long instanceId, Long nodeId) {
        LambdaUpdateWrapper<ApprovalTask> updateWrapper = Wrappers.lambdaUpdate();
        updateWrapper.eq(ApprovalTask::getInstanceId, instanceId)
                .eq(ApprovalTask::getNodeId, nodeId)
                .eq(ApprovalTask::getTaskStatus, "PENDING")
                .eq(ApprovalTask::getDeleted, 0)
                .set(ApprovalTask::getDeleted, (byte) 1);
        approvalTaskService.update(updateWrapper);
    }
    private void saveApprovalRecord(Long instanceId,
                                    Long nodeId,
                                    Long taskId,
                                    Long operatorId,
                                    String operatorName,
                                    String action,
                                    String comment) {
        ApprovalRecord record = new ApprovalRecord();
        record.setInstanceId(instanceId);
        record.setNodeId(nodeId);
        record.setTaskId(taskId);
        record.setOperatorId(operatorId);
        record.setOperatorName(operatorName);
        record.setAction(action);
        record.setComment(comment);
        record.setDeleted((byte) 0);
        approvalRecordService.save(record);
    }
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalRecordServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.ApprovalRecord;
import com.ruoyi.approve.mapper.ApprovalRecordMapper;
import com.ruoyi.approve.service.ApprovalRecordService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * å®¡æ‰¹è®°å½•表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:28:21
 */
@Service
public class ApprovalRecordServiceImpl extends ServiceImpl<ApprovalRecordMapper, ApprovalRecord> implements ApprovalRecordService {
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalTaskServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.approve.mapper.ApprovalTaskMapper;
import com.ruoyi.approve.service.ApprovalTaskService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * å®¡æ‰¹ä»»åŠ¡è¡¨ æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 03:32:37
 */
@Service
public class ApprovalTaskServiceImpl extends ServiceImpl<ApprovalTaskMapper, ApprovalTask> implements ApprovalTaskService {
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeApproverServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import com.ruoyi.approve.mapper.ApprovalTemplateNodeApproverMapper;
import com.ruoyi.approve.service.ApprovalTemplateNodeApproverService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹å®¡æ‰¹äººè¡¨ æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:30
 */
@Service
public class ApprovalTemplateNodeApproverServiceImpl extends ServiceImpl<ApprovalTemplateNodeApproverMapper, ApprovalTemplateNodeApprover> implements ApprovalTemplateNodeApproverService {
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateNodeServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeApproverDto;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeDto;
import com.ruoyi.approve.mapper.ApprovalTemplateNodeMapper;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import com.ruoyi.approve.service.ApprovalTemplateNodeApproverService;
import com.ruoyi.approve.service.ApprovalTemplateNodeService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿èŠ‚ç‚¹è¡¨ æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-18 11:20:19
 */
@Service
@RequiredArgsConstructor
public class ApprovalTemplateNodeServiceImpl extends ServiceImpl<ApprovalTemplateNodeMapper, ApprovalTemplateNode> implements ApprovalTemplateNodeService {
    private final ApprovalTemplateNodeMapper approvalTemplateNodeMapper;
    private final ApprovalTemplateNodeApproverService approvalTemplateNodeApproverService;
    @Override
    public Boolean saveApprovalTemplateNode(Long templateId, List<ApprovalTemplateNodeDto> nodes) {
        if (nodes == null || nodes.isEmpty()) {
            throw new RuntimeException("节点列表不能为空");
        }
        List<ApprovalTemplateNodeApprover> approverList = new ArrayList<>();
        for (ApprovalTemplateNodeDto nodeDto : nodes) {
            ApprovalTemplateNode node = new ApprovalTemplateNode();
            BeanUtils.copyProperties(nodeDto, node);
            node.setTemplateId(templateId);
            approvalTemplateNodeMapper.insert(node);
            List<ApprovalTemplateNodeApproverDto> approvers = nodeDto.getApprovers();
            if (approvers == null || approvers.isEmpty()) {
                throw new RuntimeException("节点审批人不能为空");
            }
            for (ApprovalTemplateNodeApproverDto approverDto : approvers) {
                ApprovalTemplateNodeApprover approver = new ApprovalTemplateNodeApprover();
                BeanUtils.copyProperties(approverDto, approver);
                approver.setNodeId(node.getId());
                approver.setTemplateId(templateId);
                approver.setDeleted(0L);
                approverList.add(approver);
            }
        }
        approvalTemplateNodeApproverService.saveBatch(approverList);
        return true;
    }
}
src/main/java/com/ruoyi/approve/service/impl/ApprovalTemplateServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,250 @@
package com.ruoyi.approve.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.dto.ApprovalTemplateDto;
import com.ruoyi.approve.bean.vo.ApprovalTemplateNodeApproverVo;
import com.ruoyi.approve.bean.vo.ApprovalTemplateNodeVo;
import com.ruoyi.approve.bean.vo.ApprovalTemplateVo;
import com.ruoyi.approve.mapper.ApprovalTemplateMapper;
import com.ruoyi.approve.mapper.ApprovalTemplateNodeApproverMapper;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import com.ruoyi.approve.service.ApprovalTemplateNodeService;
import com.ruoyi.approve.service.ApprovalTemplateService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * <p>
 * å®¡æ‰¹æ¨¡æ¿æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @since 2026-05-18 11:20:08
 */
@Service
@RequiredArgsConstructor
public class ApprovalTemplateServiceImpl extends ServiceImpl<ApprovalTemplateMapper, ApprovalTemplate> implements ApprovalTemplateService {
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final ApprovalTemplateNodeService approvalTemplateNodeService;
    private final ApprovalTemplateNodeApproverMapper approvalTemplateNodeApproverMapper;
    @Override
    public IPage<ApprovalTemplateVo> listPage(Page<ApprovalTemplateVo> page, ApprovalTemplateDto approvalTemplateDto) {
        IPage<ApprovalTemplateVo> approvalTemplateVoIPage = approvalTemplateMapper.listPage(page, approvalTemplateDto);
        fillTemplateVoNodes(approvalTemplateVoIPage.getRecords());
        return approvalTemplateVoIPage;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean saveApprovalTemplateDto(ApprovalTemplateDto approvalTemplateDto) {
        approvalTemplateMapper.insert(approvalTemplateDto);
        approvalTemplateNodeService.remove(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .eq(ApprovalTemplateNode::getTemplateId, approvalTemplateDto.getId())
        );
        approvalTemplateNodeApproverMapper.delete(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .eq(ApprovalTemplateNodeApprover::getTemplateId, approvalTemplateDto.getId())
        );
        return approvalTemplateNodeService.saveApprovalTemplateNode(
                approvalTemplateDto.getId(),
                approvalTemplateDto.getNodes()
        );
    }
    @Override
    public Boolean updateApprovalTemplateDto(ApprovalTemplateDto approvalTemplateDto) {
        approvalTemplateMapper.updateById(approvalTemplateDto);
        approvalTemplateNodeService.remove(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .eq(ApprovalTemplateNode::getTemplateId, approvalTemplateDto.getId())
        );
        approvalTemplateNodeApproverMapper.delete(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .eq(ApprovalTemplateNodeApprover::getTemplateId, approvalTemplateDto.getId())
        );
        return approvalTemplateNodeService.saveApprovalTemplateNode(
                approvalTemplateDto.getId(),
                approvalTemplateDto.getNodes()
        );
    }
    @Transactional(rollbackFor = Exception.class)
    public Boolean delete(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return false;
        }
        ApprovalTemplate updateEntity = new ApprovalTemplate();
        updateEntity.setDeleted(1);
        LambdaUpdateWrapper<ApprovalTemplate> updateWrapper = Wrappers.lambdaUpdate();
        updateWrapper.in(ApprovalTemplate::getId, ids)
                .eq(ApprovalTemplate::getDeleted, 0);
        int rows = approvalTemplateMapper.update(updateEntity, updateWrapper);
        return rows == ids.size();
    }
    @Override
    public List<ApprovalTemplateVo> listApprovalTemplateVo(Integer type) {
        List<ApprovalTemplate> templateList = this.list(
                new LambdaQueryWrapper<ApprovalTemplate>()
                        .eq(ApprovalTemplate::getDeleted, 0)
                        .eq(ApprovalTemplate::getEnabled, 1)
                        .orderByDesc(ApprovalTemplate::getTemplateType)
                        .orderByDesc(ApprovalTemplate::getId)
        );
        if (CollUtil.isEmpty(templateList)) {
            return Collections.emptyList();
        }
        List<ApprovalTemplateVo> templateVos = templateList.stream()
                .map(template -> {
                    ApprovalTemplateVo templateVo = new ApprovalTemplateVo();
                    BeanUtils.copyProperties(template, templateVo);
                    return templateVo;
                })
                .collect(Collectors.toList());
        fillTemplateVoNodes(templateVos);
        return templateVos;
    }
    @Override
    public ApprovalTemplateVo getApprovalTemplateVoById(Long id) {
        if (id == null) {
            throw new IllegalArgumentException("参数 id ä¸èƒ½ä¸ºç©º");
        }
        ApprovalTemplate template = this.getOne(
                new LambdaQueryWrapper<ApprovalTemplate>()
                        .eq(ApprovalTemplate::getId, id)
                        .eq(ApprovalTemplate::getDeleted, 0)
        );
        if (template == null) {
            throw new IllegalArgumentException("模板不存在");
        }
        List<ApprovalTemplateNode> nodeList = approvalTemplateNodeService.list(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .eq(ApprovalTemplateNode::getTemplateId, id)
                        .orderByAsc(ApprovalTemplateNode::getLevelNo)
        );
        List<ApprovalTemplateNodeApprover> approverList = approvalTemplateNodeApproverMapper.selectList(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .eq(ApprovalTemplateNodeApprover::getTemplateId, id)
                        .eq(ApprovalTemplateNodeApprover::getDeleted, 0L)
        );
        Map<Long, List<ApprovalTemplateNode>> nodeMap = nodeList.stream()
                .collect(Collectors.groupingBy(ApprovalTemplateNode::getTemplateId));
        Map<Long, List<ApprovalTemplateNodeApprover>> approverMap = approverList.stream()
                .collect(Collectors.groupingBy(ApprovalTemplateNodeApprover::getNodeId));
        return buildTemplateVo(template, nodeMap, approverMap);
    }
    /**
     * æ‰¹é‡å¡«å……模板节点及节点审批人,避免循环查库。
     */
    private void fillTemplateVoNodes(List<ApprovalTemplateVo> templateVos) {
        if (CollUtil.isEmpty(templateVos)) {
            return;
        }
        List<Long> templateIds = templateVos.stream()
                .map(ApprovalTemplateVo::getId)
                .collect(Collectors.toList());
        List<ApprovalTemplateNode> nodeList = approvalTemplateNodeService.list(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .in(ApprovalTemplateNode::getTemplateId, templateIds)
                        .orderByAsc(ApprovalTemplateNode::getLevelNo)
        );
        List<ApprovalTemplateNodeApprover> approverList = approvalTemplateNodeApproverMapper.selectList(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .in(ApprovalTemplateNodeApprover::getTemplateId, templateIds)
                        .eq(ApprovalTemplateNodeApprover::getDeleted, 0L)
        );
        Map<Long, List<ApprovalTemplateNode>> nodeMap = nodeList.stream()
                .collect(Collectors.groupingBy(ApprovalTemplateNode::getTemplateId));
        Map<Long, List<ApprovalTemplateNodeApprover>> approverMap = approverList.stream()
                .collect(Collectors.groupingBy(ApprovalTemplateNodeApprover::getNodeId));
        templateVos.forEach(templateVo -> templateVo.setNodes(
                nodeMap.getOrDefault(templateVo.getId(), Collections.emptyList())
                        .stream()
                        .sorted(Comparator.comparing(
                                ApprovalTemplateNode::getLevelNo,
                                Comparator.nullsLast(Integer::compareTo)
                        ))
                        .map(node ->  buildNodeVo(node, approverMap))
                        .collect(Collectors.toList())
        ));
    }
    private ApprovalTemplateVo buildTemplateVo(ApprovalTemplate template,
                                               Map<Long, List<ApprovalTemplateNode>> nodeMap,
                                               Map<Long, List<ApprovalTemplateNodeApprover>> approverMap) {
        ApprovalTemplateVo templateVo = new ApprovalTemplateVo();
        BeanUtils.copyProperties(template, templateVo);
        List<ApprovalTemplateNodeVo> nodeVos = nodeMap
                .getOrDefault(template.getId(), Collections.emptyList())
                .stream()
                .sorted(Comparator.comparing(
                        ApprovalTemplateNode::getLevelNo,
                        Comparator.nullsLast(Integer::compareTo)
                ))
                .map(node -> buildNodeVo(node, approverMap))
                .collect(Collectors.toList());
        templateVo.setNodes(nodeVos);
        return templateVo;
    }
    private ApprovalTemplateNodeVo buildNodeVo(ApprovalTemplateNode node,
                                               Map<Long, List<ApprovalTemplateNodeApprover>> approverMap) {
        ApprovalTemplateNodeVo nodeVo = new ApprovalTemplateNodeVo();
        BeanUtils.copyProperties(node, nodeVo);
        List<ApprovalTemplateNodeApproverVo> approverVos = approverMap
                .getOrDefault(node.getId(), Collections.emptyList())
                .stream()
                .sorted(Comparator.comparing(
                        ApprovalTemplateNodeApprover::getSortNo,
                        Comparator.nullsLast(Integer::compareTo)
                ))
                .map(this::buildApproverVo)
                .collect(Collectors.toList());
        nodeVo.setApprovers(approverVos);
        return nodeVo;
    }
    private ApprovalTemplateNodeApproverVo buildApproverVo(ApprovalTemplateNodeApprover approver) {
        ApprovalTemplateNodeApproverVo approverVo = new ApprovalTemplateNodeApproverVo();
        BeanUtils.copyProperties(approver, approverVo);
        return approverVo;
    }
}
src/main/java/com/ruoyi/approve/service/impl/ApproveBusinessStatusService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,175 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityInspectParamMapper;
import com.ruoyi.quality.mapper.QualityTestStandardMapper;
import com.ruoyi.quality.mapper.QualityTestStandardParamMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.mapper.SalesQuotationMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.SalesQuotation;
import com.ruoyi.sales.pojo.ShippingInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ApproveBusinessStatusService {
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesQuotationMapper salesQuotationMapper;
    private final ShippingInfoMapper shippingInfoMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final StockUtils stockUtils;
    private final QualityInspectMapper qualityInspectMapper;
    private final QualityTestStandardMapper qualityTestStandardMapper;
    private final QualityTestStandardParamMapper qualityTestStandardParamMapper;
    private final QualityInspectParamMapper qualityInspectParamMapper;
    /**
     * ç»Ÿä¸€åŒæ­¥å®¡æ‰¹ç»“果对应的业务单据状态。
     * status:1-审核中,2-审核完成,3-审核未通过。
     */
    public void syncBusinessStatus(Integer approveType, String approveReason, Integer status) {
        if (approveType == null || status == null || !StringUtils.hasText(approveReason)) {
            return;
        }
        switch (approveType) {
            case 5:
                syncPurchaseStatus(approveReason, status);
                break;
            case 6:
                syncSalesQuotationStatus(approveReason, status);
                break;
            case 7:
                syncShippingStatus(approveReason, status);
                break;
            default:
                break;
        }
    }
    // é‡‡è´­å®¡æ‰¹é€šè¿‡æ—¶ï¼ŒæŒ‰äº§å“è´¨æ£€é…ç½®å†³å®šç”Ÿæˆè´¨æ£€å•或直接入库。
    private void syncPurchaseStatus(String approveReason, Integer status) {
        PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectOne(new LambdaQueryWrapper<PurchaseLedger>()
                .eq(PurchaseLedger::getPurchaseContractNumber, approveReason)
                .last("limit 1"));
        if (purchaseLedger == null) {
            return;
        }
        if (status.equals(2)) {
            purchaseLedger.setApprovalStatus(3);
            List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(new QueryWrapper<SalesLedgerProduct>()
                    .lambda().eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedger.getId()).eq(SalesLedgerProduct::getType, 2));
            for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) {
                if (Boolean.TRUE.equals(salesLedgerProduct.getIsChecked())) {
                    addQualityInspect(purchaseLedger, salesLedgerProduct);
                } else {
                    stockUtils.addStockWithBatchNo(
                            salesLedgerProduct.getProductModelId(),
                            salesLedgerProduct.getQuantity(),
                            StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(),
                            purchaseLedger.getId(),
                            purchaseLedger.getPurchaseContractNumber() + "-" + salesLedgerProduct.getId());
                }
            }
        } else if (status.equals(3)) {
            purchaseLedger.setApprovalStatus(4);
        } else if (status.equals(1)) {
            purchaseLedger.setApprovalStatus(2);
        } else {
            return;
        }
        purchaseLedgerMapper.updateById(purchaseLedger);
    }
    // æŠ¥ä»·å®¡æ‰¹çŠ¶æ€å›žå†™åˆ°é”€å”®æŠ¥ä»·å•çŠ¶æ€ã€‚
    private void syncSalesQuotationStatus(String approveReason, Integer status) {
        SalesQuotation salesQuote = salesQuotationMapper.selectOne(new LambdaQueryWrapper<SalesQuotation>()
                .eq(SalesQuotation::getQuotationNo, approveReason)
                .last("limit 1"));
        if (salesQuote == null) {
            return;
        }
        if (status.equals(2)) {
            salesQuote.setStatus("通过");
        } else if (status.equals(3)) {
            salesQuote.setStatus("拒绝");
        } else if (status.equals(1)) {
            salesQuote.setStatus("审核中");
        } else {
            return;
        }
        salesQuotationMapper.updateById(salesQuote);
    }
    // å‘货审批通过时同步发货状态和出库审批状态;拒绝时删除待确认出库记录。
    private void syncShippingStatus(String approveReason, Integer status) {
        ShippingInfo shippingInfo = shippingInfoMapper.selectOne(new LambdaQueryWrapper<ShippingInfo>()
                .eq(ShippingInfo::getShippingNo, approveReason)
                .orderByDesc(ShippingInfo::getCreateTime)
                .last("limit 1"));
        if (shippingInfo == null) {
            return;
        }
        if (status.equals(2)) {
            shippingInfo.setStatus("审核通过");
            shippingInfo.setShippingDate(new Date());
            stockUtils.shipmentStatus(StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), shippingInfo.getId());
        } else if (status.equals(3)) {
            stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
            shippingInfo.setStatus("审核拒绝");
        } else if (status.equals(1)) {
            shippingInfo.setStatus("审核中");
        } else {
            return;
        }
        shippingInfoMapper.updateById(shippingInfo);
    }
    // ç”Ÿæˆé‡‡è´­è´¨æ£€å•,并按产品质检标准初始化质检参数。
    private void addQualityInspect(PurchaseLedger purchaseLedger, SalesLedgerProduct saleProduct) {
        QualityInspect qualityInspect = new QualityInspect();
        qualityInspect.setInspectType(0);
        qualityInspect.setSupplier(purchaseLedger.getSupplierName());
        qualityInspect.setPurchaseLedgerId(purchaseLedger.getId());
        qualityInspect.setProductId(saleProduct.getProductId());
        qualityInspect.setProductName(saleProduct.getProductCategory());
        qualityInspect.setModel(saleProduct.getSpecificationModel());
        qualityInspect.setProductModelId(saleProduct.getProductModelId());
        qualityInspect.setUnit(saleProduct.getUnit());
        qualityInspect.setQuantity(saleProduct.getQuantity());
        qualityInspectMapper.insert(qualityInspect);
        List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(saleProduct.getProductId(), 0, null);
        if (qualityTestStandard.size() > 0) {
            qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
            qualityInspectMapper.updateById(qualityInspect);
            qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
                            .eq(QualityTestStandardParam::getTestStandardId, qualityTestStandard.get(0).getId()))
                    .forEach(qualityTestStandardParam -> {
                        QualityInspectParam param = new QualityInspectParam();
                        com.ruoyi.common.utils.bean.BeanUtils.copyProperties(qualityTestStandardParam, param);
                        param.setId(null);
                        param.setInspectId(qualityInspect.getId());
                        qualityInspectParamMapper.insert(param);
                    });
        }
    }
}
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java
@@ -1,7 +1,6 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -14,26 +13,12 @@
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.project.system.service.ISysNoticeService;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityInspectParamMapper;
import com.ruoyi.quality.mapper.QualityTestStandardMapper;
import com.ruoyi.quality.mapper.QualityTestStandardParamMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
import com.ruoyi.sales.mapper.*;
import com.ruoyi.sales.pojo.*;
import com.ruoyi.sales.mapper.CommonFileMapper;
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.service.impl.CommonFileServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -55,18 +40,8 @@
    private final SysUserMapper sysUserMapper;
    private final ISysNoticeService sysNoticeService;
    private final CommonFileMapper fileMapper;
    private final DeviceRepairMapper deviceRepairMapper;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesQuotationMapper salesQuotationMapper;
    private final ShippingInfoMapper shippingInfoMapper;
    private final ShippingProductDetailMapper shippingProductDetailMapper;
    private final CommonFileServiceImpl commonFileService;
    private final StockUtils stockUtils;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final QualityTestStandardMapper qualityTestStandardMapper;
    private final QualityTestStandardParamMapper qualityTestStandardParamMapper;
    private final QualityInspectParamMapper qualityInspectParamMapper;
    private final ApproveBusinessStatusService approveBusinessStatusService;
    private final FileUtil fileUtil;
@@ -162,74 +137,7 @@
        }
        approveProcessMapper.updateById(approveProcess);
        //采购审核
        if (approveProcess.getApproveType().equals(5)) {
            PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectOne(new LambdaQueryWrapper<PurchaseLedger>()
                    .eq(PurchaseLedger::getPurchaseContractNumber, approveProcess.getApproveReason())
                    .last("limit 1"));
            if (purchaseLedger != null) {
                if (status.equals(2)) {
                    // åŒæ„
                    purchaseLedger.setApprovalStatus(3);
                    List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(new QueryWrapper<SalesLedgerProduct>()
                            .lambda().eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedger.getId()).eq(SalesLedgerProduct::getType, 2));
                    for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) {
                        // è´¨æ£€
                        if (salesLedgerProduct.getIsChecked()) {
                            addQualityInspect(purchaseLedger, salesLedgerProduct);
                        } else {
                            //直接入库
                            stockUtils.addStockWithBatchNo(salesLedgerProduct.getProductModelId(), salesLedgerProduct.getQuantity(), StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(), purchaseLedger.getId(), purchaseLedger.getPurchaseContractNumber() + "-" + salesLedgerProduct.getId());
                        }
                    }
                } else if (status.equals(3)) {
                    // æ‹’绝
                    purchaseLedger.setApprovalStatus(4);
                } else if (status.equals(1)) {
                    // å®¡æ ¸ä¸­
                    purchaseLedger.setApprovalStatus(2);
                }
                purchaseLedgerMapper.updateById(purchaseLedger);
            }
        }
        // é”€å”®æŠ¥ä»·çŠ¶æ€ä¿®æ”¹
        if (approveProcess.getApproveType().equals(6)) {
            SalesQuotation salesQuote = salesQuotationMapper.selectOne(new LambdaQueryWrapper<SalesQuotation>()
                    .eq(SalesQuotation::getQuotationNo, approveProcess.getApproveReason())
                    .last("limit 1"));
            // åŒæ„
            if (status.equals(2) && salesQuote != null) {
                salesQuote.setStatus("通过");
            } else if (status.equals(3) && salesQuote != null) {
                salesQuote.setStatus("拒绝");
            } else if (status.equals(1) && salesQuote != null) {
                salesQuote.setStatus("审核中");
            }
            salesQuotationMapper.updateById(salesQuote);
        }
        // å‡ºåº“审批修改=发货审批
        if (approveProcess.getApproveType().equals(7)) {
            ShippingInfo shippingInfo = shippingInfoMapper.selectOne(new LambdaQueryWrapper<ShippingInfo>()
                    .eq(ShippingInfo::getShippingNo, approveProcess.getApproveReason())
                    .orderByDesc(ShippingInfo::getCreateTime)
                    .last("limit 1"));
            if (shippingInfo != null) {
                if (status.equals(2)) {
                    shippingInfo.setStatus("审核通过");
                    shippingInfo.setShippingDate(new Date());
                    //更改出库审核状态(待确认改成待审核)
                    stockUtils.shipmentStatus(StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode(), shippingInfo.getId());
                } else if (status.equals(3)) {
                    //删除原本(待确认)的出库审核状态
                    stockUtils.deleteStockOutRecord(shippingInfo.getId(), StockOutQualifiedRecordTypeEnum.SALE_SHIP_STOCK_OUT.getCode());
                    shippingInfo.setStatus("审核拒绝");
                } else if (status.equals(1)) {
                    shippingInfo.setStatus("审核中");
                }
                shippingInfoMapper.updateById(shippingInfo);
            }
        }
        approveBusinessStatusService.syncBusinessStatus(approveProcess.getApproveType(), approveProcess.getApproveReason(), status);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVE_NODE, approveNode.getId(), approveNode.getStorageBlobDTOS());
    }
@@ -305,34 +213,6 @@
                return "办公用品审批";
        }
        return null;
    }
    private void addQualityInspect(PurchaseLedger purchaseLedger, SalesLedgerProduct saleProduct) {
        QualityInspect qualityInspect = new QualityInspect();
        qualityInspect.setInspectType(0);
        qualityInspect.setSupplier(purchaseLedger.getSupplierName());
        qualityInspect.setPurchaseLedgerId(purchaseLedger.getId());
        qualityInspect.setProductId(saleProduct.getProductId());
        qualityInspect.setProductName(saleProduct.getProductCategory());
        qualityInspect.setModel(saleProduct.getSpecificationModel());
        qualityInspect.setProductModelId(saleProduct.getProductModelId());
        qualityInspect.setUnit(saleProduct.getUnit());
        qualityInspect.setQuantity(saleProduct.getQuantity());
        qualityInspectMapper.insert(qualityInspect);
        List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(saleProduct.getProductId(), 0, null);
        if (qualityTestStandard.size() > 0) {
            qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
            qualityInspectMapper.updateById(qualityInspect);
            qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
                            .eq(QualityTestStandardParam::getTestStandardId, qualityTestStandard.get(0).getId()))
                    .forEach(qualityTestStandardParam -> {
                        QualityInspectParam param = new QualityInspectParam();
                        com.ruoyi.common.utils.bean.BeanUtils.copyProperties(qualityTestStandardParam, param);
                        param.setId(null);
                        param.setInspectId(qualityInspect.getId());
                        qualityInspectParamMapper.insert(param);
                    });
        }
    }
}
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
@@ -1,8 +1,6 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -25,10 +23,8 @@
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysNotice;
import com.ruoyi.project.system.domain.SysUser;
@@ -38,10 +34,8 @@
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.sales.mapper.CommonFileMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.mapper.ShippingInfoMapper;
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.service.impl.CommonFileServiceImpl;
import lombok.RequiredArgsConstructor;
@@ -53,15 +47,12 @@
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ApproveProcessServiceImpl extends ServiceImpl<ApproveProcessMapper, ApproveProcess> implements IApproveProcessService {
    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
    private final SysDeptMapper sysDeptMapper;
    private final IApproveNodeService approveNodeService;
    private final SysUserMapper sysUserMapper;
@@ -70,11 +61,10 @@
    private final CommonFileServiceImpl commonFileService;
    private final ISysNoticeService sysNoticeService;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final StockUtils stockUtils;
    private final ShippingInfoMapper shippingInfoMapper;
    private final ApproveNodeMapper approveNodeMapper;
    private final ApproveProcessConfigNodeService approveProcessConfigNodeService;
    private final ApproveBusinessStatusService approveBusinessStatusService;
    private final FileUtil fileUtil;
    private final ApproveProcessConfigNodeMapper approveProcessConfigNodeMapper;
@@ -82,61 +72,34 @@
    public void addApprove(ApproveProcessVO approveProcessVO) throws Exception {
        SysUser sysUser = SecurityUtils.getLoginUser().getUser();
        SysDept sysDept = sysDeptMapper.selectDeptById(SecurityUtils.getLoginUser().getCurrentDeptId());
        List<ApproveProcessConfigNodeVo> list = approveProcessConfigNodeService.listNode(approveProcessVO.getApproveType());
        if (sysDept == null) throw new RuntimeException("部门不存在");
        if (sysUser == null) throw new RuntimeException("申请人不存在");
        List<ApproveProcessConfigNodeVo> list = Optional.ofNullable(approveProcessConfigNodeService.listNode(approveProcessVO.getApproveType()))
                .orElse(Collections.emptyList());
        List<Long> nodeIds = list.stream()
                .map(ApproveProcessConfigNodeVo::getApproverId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        // æ— å®¡æ ¸äººé€»è¾‘添加
        // å®¡æ‰¹é…ç½®æ²¡æœ‰æœ‰æ•ˆå®¡æ ¸äººæ—¶ï¼Œä¸æ–°å¢žååŒå®¡æ‰¹æµç¨‹ï¼Œç›´æŽ¥æ‰§è¡Œä¸šåŠ¡å®¡æ ¸é€šè¿‡é€»è¾‘ã€‚
        if (CollectionUtils.isEmpty(nodeIds)) {
            autoPassPurchaseApproveIfNoApprover(approveProcessVO); // é‡‡è´­å•无审核人逻辑
            approveBusinessStatusService.syncBusinessStatus(approveProcessVO.getApproveType(), approveProcessVO.getApproveReason(), 2);
            return;
        }
        List<SysUser> sysUsers = sysUserMapper.selectUserByIds(nodeIds);
        if (CollectionUtils.isEmpty(sysUsers)) throw new RuntimeException("审核用户不存在");
        if (sysDept == null) throw new RuntimeException("部门不存在");
        if (sysUser == null) throw new RuntimeException("申请人不存在");
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        ApproveProcess approveProcess = new ApproveProcess();
        String no = OrderUtils.countTodayByCreateTime(approveProcessMapper, "", "approve_id");
        approveProcess.setApproveId(no);
        approveProcess.setApproveUser(sysUser.getUserId());
        approveProcess.setApproveUserName(sysUser.getNickName());
        approveProcess.setApproveDeptId(sysDept.getDeptId());
        approveProcess.setApproveUserIds(nodeIds.stream().map(String::valueOf).collect(Collectors.joining(",")));
        approveProcess.setApproveDeptName(sysDept.getDeptName());
        approveProcess.setApproveUserNames(sysUsers.stream().map(SysUser::getNickName).collect(Collectors.joining(",")));
        approveProcess.setApproveTime(StringUtils.isEmpty(approveProcessVO.getApproveTime()) ? new Date() : dateFormat.parse(approveProcessVO.getApproveTime()));
        approveProcess.setApproveReason(approveProcessVO.getApproveReason());
        approveProcess.setDeviceRepairId(approveProcessVO.getDeviceRepairId());
        approveProcess.setMaintenancePrice(approveProcessVO.getMaintenancePrice());
        approveProcess.setPrice(approveProcessVO.getPrice());
        approveProcess.setStartDate(approveProcessVO.getStartDate());
        approveProcess.setEndDate(approveProcessVO.getEndDate());
        approveProcess.setApproveStatus(0);
        approveProcess.setApproveDelete(0);
        approveProcess.setApproveType(approveProcessVO.getApproveType());
        approveProcess.setCreateTime(LocalDateTime.now());
        approveProcess.setTenantId(approveProcessVO.getApproveDeptId());
        approveProcess.setApproveUserCurrentId(nodeIds.get(0));
        approveProcess.setApproveUserCurrentName(sysUsers
                .stream()
                .filter(SysUser -> SysUser.getUserId().equals(nodeIds.get(0)))
                .collect(Collectors.toList())
                .get(0)
                .getNickName());
        // è®¾ç½®çŠ¶æ€ä¸ºé‡æ–°æäº¤
        if (approveProcessVO.getId() != null) {
            ApproveProcess approveProcess1 = approveProcessMapper.selectById(approveProcessVO.getId());
            approveProcess1.setApproveStatus(4);
            approveProcessMapper.updateById(approveProcess1);
        }
        // æœ‰å®¡æ ¸äººæ—¶ï¼ŒæŒ‰æ­£å¸¸ååŒå®¡æ‰¹æµç¨‹åˆ›å»ºå®¡æ‰¹ä¸»è¡¨ã€å®¡æ‰¹èŠ‚ç‚¹å¹¶é€šçŸ¥é¦–ä¸ªå®¡æ ¸äººã€‚
        ApproveProcess approveProcess = buildApproveProcess(approveProcessVO, sysUser, sysDept, nodeIds, sysUsers, 0);
        markResubmitted(approveProcessVO);
        save(approveProcess);
        //初始化审批节点
        String nodeIdStr = nodeIds.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
        approveNodeService.initApproveNodes(nodeIdStr, no, approveProcessVO.getApproveDeptId());
        approveNodeService.initApproveNodes(nodeIdStr, approveProcess.getApproveId(), approveProcessVO.getApproveDeptId());
        // é™„件绑定
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.APPROVE_PROCESS, approveProcess.getId(), approveProcessVO.getStorageBlobDTOS());
        /*消息通知*/
@@ -154,24 +117,56 @@
        }
    }
    private void autoPassPurchaseApproveIfNoApprover(ApproveProcessVO approveProcessVO) {
        if (!Objects.equals(approveProcessVO.getApproveType(), 5)
                || !StringUtils.hasText(approveProcessVO.getApproveReason())) {
            throw new RuntimeException("审核用户不存在");
    private ApproveProcess buildApproveProcess(ApproveProcessVO approveProcessVO, SysUser sysUser, SysDept sysDept,
                                               List<Long> nodeIds, List<SysUser> sysUsers, Integer approveStatus) throws Exception {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        ApproveProcess approveProcess = new ApproveProcess();
        String no = OrderUtils.countTodayByCreateTime(approveProcessMapper, "", "approve_id", approveProcess.getCreateTime() != null ? approveProcess.getCreateTime() : LocalDateTime.now());
        approveProcess.setApproveId(no);
        approveProcess.setApproveUser(sysUser.getUserId());
        approveProcess.setApproveUserName(sysUser.getNickName());
        approveProcess.setApproveDeptId(sysDept.getDeptId());
        approveProcess.setApproveDeptName(sysDept.getDeptName());
        approveProcess.setApproveUserIds(nodeIds.stream().map(String::valueOf).collect(Collectors.joining(",")));
        approveProcess.setApproveUserNames(sysUsers.stream().map(SysUser::getNickName).collect(Collectors.joining(",")));
        approveProcess.setApproveTime(StringUtils.isEmpty(approveProcessVO.getApproveTime()) ? new Date() : dateFormat.parse(approveProcessVO.getApproveTime()));
        approveProcess.setApproveReason(approveProcessVO.getApproveReason());
        approveProcess.setDeviceRepairId(approveProcessVO.getDeviceRepairId());
        approveProcess.setMaintenancePrice(approveProcessVO.getMaintenancePrice());
        approveProcess.setPrice(approveProcessVO.getPrice());
        approveProcess.setStartDate(approveProcessVO.getStartDate());
        approveProcess.setEndDate(approveProcessVO.getEndDate());
        approveProcess.setStartDateTime(approveProcessVO.getStartDateTime());
        approveProcess.setEndDateTime(approveProcessVO.getEndDateTime());
        approveProcess.setApproveStatus(approveStatus);
        approveProcess.setApproveDelete(0);
        approveProcess.setApproveType(approveProcessVO.getApproveType());
        approveProcess.setCreateTime(LocalDateTime.now());
        approveProcess.setTenantId(approveProcessVO.getApproveDeptId());
        if (!CollectionUtils.isEmpty(nodeIds)) {
            SysUser currentUser = sysUsers.stream()
                    .filter(user -> user.getUserId().equals(nodeIds.get(0)))
                    .findFirst()
                    .orElseThrow(() -> new RuntimeException("审核用户不存在"));
            approveProcess.setApproveUserCurrentId(currentUser.getUserId());
            approveProcess.setApproveUserCurrentName(currentUser.getNickName());
        }
        if (approveStatus.equals(2) || approveStatus.equals(3) || approveStatus.equals(4)) {
            approveProcess.setApproveOverTime(new Date());
        }
        return approveProcess;
    }
        purchaseLedgerMapper.update(null, new LambdaUpdateWrapper<PurchaseLedger>()
                .eq(PurchaseLedger::getPurchaseContractNumber, approveProcessVO.getApproveReason())
                .set(PurchaseLedger::getApprovalStatus, 3));
        //采购入库
        PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectOne(new LambdaQueryWrapper<PurchaseLedger>()
                .eq(PurchaseLedger::getPurchaseContractNumber, approveProcessVO.getApproveReason())
                .last("limit 1"));
        List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(new QueryWrapper<SalesLedgerProduct>()
                .lambda().eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedger.getId()).eq(SalesLedgerProduct::getType, 2));
        for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) {
            stockUtils.addStockWithBatchNo(salesLedgerProduct.getProductModelId(), salesLedgerProduct.getQuantity(), StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(), purchaseLedger.getId(),purchaseLedger.getPurchaseContractNumber()+"-"+salesLedgerProduct.getId());
    private void markResubmitted(ApproveProcessVO approveProcessVO) {
        if (approveProcessVO.getId() == null) {
            return;
        }
        ApproveProcess approveProcess = approveProcessMapper.selectById(approveProcessVO.getId());
        if (approveProcess == null) {
            return;
        }
        approveProcess.setApproveStatus(4);
        approveProcessMapper.updateById(approveProcess);
    }
    @Override
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementDetailServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.FinReimbursementDetail;
import com.ruoyi.approve.mapper.FinReimbursementDetailMapper;
import com.ruoyi.approve.service.FinReimbursementDetailService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * æŠ¥é”€å•明细表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:38
 */
@Service
public class FinReimbursementDetailServiceImpl extends ServiceImpl<FinReimbursementDetailMapper, FinReimbursementDetail> implements FinReimbursementDetailService {
}
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,544 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.dto.ApprovalInstanceDto;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeApproverDto;
import com.ruoyi.approve.bean.dto.ApprovalTemplateNodeDto;
import com.ruoyi.approve.bean.dto.FinReimbursementDto;
import com.ruoyi.approve.bean.vo.FinReimbursementVo;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.mapper.FinReimbursementDetailMapper;
import com.ruoyi.approve.mapper.FinReimbursementMapper;
import com.ruoyi.approve.mapper.FinReimbursementTravelMapper;
import com.ruoyi.approve.pojo.*;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.approve.service.*;
import com.ruoyi.common.enums.TypeEnums;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.project.system.service.ISysNoticeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
 * <p>
 * æŠ¥é”€å•主表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:15
 */
@Service
@RequiredArgsConstructor
public class FinReimbursementServiceImpl extends ServiceImpl<FinReimbursementMapper, FinReimbursement> implements FinReimbursementService {
    private static final String BILL_STATUS_DRAFT = "DRAFT";
    private static final String BILL_STATUS_IN_APPROVAL = "IN_APPROVAL";
    private static final String NODE_STATUS_WAITING = "WAITING";
    private final ApprovalInstanceMapper approvalInstanceMapper;
    private final ApprovalInstanceService approvalInstanceService;
    private final ApprovalInstanceNodeService approvalInstanceNodeService;
    private final ApprovalTaskService approvalTaskService;
    private final ApprovalRecordService approvalRecordService;
    private final FinReimbursementMapper finReimbursementMapper;
    private final FinReimbursementTravelMapper finReimbursementTravelMapper;
    private final FinReimbursementDetailMapper finReimbursementDetailMapper;
    private final FileUtil fileUtil;
    private final ISysNoticeService sysNoticeService;
    @Override
    public IPage<FinReimbursementVo> listPage(FinReimbursementDto finReimbursementDto, Page<FinReimbursementVo> page) {
        IPage<FinReimbursementVo> finReimbursementVoIPage = finReimbursementMapper.listPage(finReimbursementDto, page);
        finReimbursementVoIPage.getRecords().forEach(vo -> {
            vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.FIN_REIMBURSEMENT,  vo.getId()));
        });
        return finReimbursementVoIPage;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean add(FinReimbursementDto finReimbursementDto) {
        String billStatus = validateAddParam(finReimbursementDto);
        // ç”ŸæˆæŠ¥é”€å•号
        String billNo = OrderUtils.countTodayByCreateTime(finReimbursementMapper, "BXD", "bill_no", finReimbursementDto.getCreateTime() != null ? finReimbursementDto.getCreateTime() : LocalDateTime.now());
        List<FinReimbursementDetail> details = finReimbursementDto.getDetails();
        BigDecimal totalAmount = details.stream()
                .map(FinReimbursementDetail::getAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        FinReimbursement reimbursement = buildReimbursement(finReimbursementDto, billNo, totalAmount, billStatus);
        // ä¿å­˜æŠ¥é”€å•主表
        boolean saved = this.save(reimbursement);
        if (!saved || reimbursement.getId() == null) {
            throw new ServiceException("新增报销单失败");
        }
        Long reimbursementId = reimbursement.getId();
        // ä¿å­˜å·®æ—…报销扩展信息(报销类型为差旅报销时)
        FinReimbursementTravel travel = finReimbursementDto.getTravel();
        if (isTravelReimbursement(finReimbursementDto.getReimbursementType())) {
            travel.setReimbursementId(reimbursementId);
            int travelRows = finReimbursementTravelMapper.insert(travel);
            if (travelRows != 1) {
                throw new ServiceException("新增差旅报销扩展信息失败");
            }
        }
        // ä¿å­˜æŠ¥é”€å•明细
        for (int i = 0; i < details.size(); i++) {
            FinReimbursementDetail detail = details.get(i);
            detail.setId(null);
            detail.setReimbursementId(reimbursementId);
            detail.setRowNo(i + 1);
            int detailRows = finReimbursementDetailMapper.insert(detail);
            if (detailRows != 1) {
                throw new ServiceException("新增报销单明细失败");
            }
        }
        if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
            startApproval(reimbursement, finReimbursementDto);
        }
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, reimbursementId, finReimbursementDto.getStorageBlobDTOs());
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean update(FinReimbursementDto finReimbursementDto) {
        String billStatus = validateUpdateParam(finReimbursementDto);
        Long reimbursementId = finReimbursementDto.getId();
        FinReimbursement existing = finReimbursementMapper.selectById(reimbursementId);
        if (existing == null) {
            throw new ServiceException("报销单不存在");
        }
        // è®¡ç®—明细汇总金额
        List<FinReimbursementDetail> details = finReimbursementDto.getDetails();
        BigDecimal totalAmount = details.stream()
                .map(FinReimbursementDetail::getAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // æ›´æ–°ä¸»è¡¨
        FinReimbursement reimbursement = buildReimbursement(
                finReimbursementDto,
                existing.getBillNo(),
                totalAmount,
                billStatus
        );
        reimbursement.setId(reimbursementId);
        int mainRows = finReimbursementMapper.updateById(reimbursement);
        if (mainRows != 1) {
            throw new ServiceException("更新报销单主表失败");
        }
        // æŸ¥è¯¢æ•°æ®åº“中已有的明细
        List<FinReimbursementDetail> existingDetails = finReimbursementDetailMapper.selectList(
                new LambdaQueryWrapper<FinReimbursementDetail>()
                        .eq(FinReimbursementDetail::getReimbursementId, reimbursementId));
        Set<Long> existingDetailIds = existingDetails.stream()
                .map(FinReimbursementDetail::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        // æ–°æ˜Žç»†ä¸­æœ‰ID的 â†’ æ›´æ–°ï¼›æ— ID的 â†’ æ–°å¢ž
        Set<Long> submittedDetailIds = new HashSet<>();
        for (int i = 0; i < details.size(); i++) {
            FinReimbursementDetail detail = details.get(i);
            detail.setReimbursementId(reimbursementId);
            detail.setRowNo(i + 1);
            if (detail.getId() != null && existingDetailIds.contains(detail.getId())) {
                finReimbursementDetailMapper.updateById(detail);
                submittedDetailIds.add(detail.getId());
            } else {
                detail.setId(null);
                finReimbursementDetailMapper.insert(detail);
            }
        }
        // æ•°æ®åº“中已有但新明细中没有的 â†’ åˆ é™¤
        for (Long existingId : existingDetailIds) {
            if (!submittedDetailIds.contains(existingId)) {
                finReimbursementDetailMapper.deleteById(existingId);
            }
        }
        // å·®æ—…扩展:有则更新,无则新增
        FinReimbursementTravel existingTravel = finReimbursementTravelMapper.selectOne(
                new LambdaQueryWrapper<FinReimbursementTravel>()
                        .eq(FinReimbursementTravel::getReimbursementId, reimbursementId)
                        .last("LIMIT 1"));
        FinReimbursementTravel travel = finReimbursementDto.getTravel();
        if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && travel != null) {
            travel.setReimbursementId(reimbursementId);
            if (existingTravel != null) {
                travel.setId(existingTravel.getId());
                finReimbursementTravelMapper.updateById(travel);
            } else {
                travel.setId(null);
                finReimbursementTravelMapper.insert(travel);
            }
        }
        resetApprovalFlow(existing, reimbursementId);
        if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
            reimbursement.setApprovalInstanceId(null);
            startApproval(reimbursement, finReimbursementDto);
        }
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, reimbursementId, finReimbursementDto.getStorageBlobDTOs());
        return true;
    }
    @Override
    public FinReimbursementVo detail(Long id) {
        if (id == null ) {
            throw new ServiceException("报销单ID不能为空");
        }
        FinReimbursement reimbursement = finReimbursementMapper.selectById(id);
        if (reimbursement == null) {
            throw new ServiceException("报销单不存在");
        }
        FinReimbursementVo vo = new FinReimbursementVo();
        vo.setId(reimbursement.getId());
        vo.setBillNo(reimbursement.getBillNo());
        vo.setReimbursementType(reimbursement.getReimbursementType());
        vo.setExpenseType(reimbursement.getExpenseType());
        vo.setApplicantId(reimbursement.getApplicantId());
        vo.setApplicantCode(reimbursement.getApplicantCode());
        vo.setApplicantName(reimbursement.getApplicantName());
        vo.setApplicantDeptId(reimbursement.getApplicantDeptId());
        vo.setApplicantDeptName(reimbursement.getApplicantDeptName());
        vo.setReason(reimbursement.getReason());
        vo.setApplyAmount(reimbursement.getApplyAmount());
        vo.setDetailTotalAmount(reimbursement.getDetailTotalAmount());
        vo.setPayeeName(reimbursement.getPayeeName());
        vo.setPayeeAccount(reimbursement.getPayeeAccount());
        vo.setPayeeBank(reimbursement.getPayeeBank());
        vo.setApprovalInstanceId(reimbursement.getApprovalInstanceId());
        vo.setApproveProcessId(reimbursement.getApproveProcessId());
        vo.setBillStatus(reimbursement.getBillStatus());
        vo.setApprovedTime(reimbursement.getApprovedTime());
        vo.setPaidTime(reimbursement.getPaidTime());
        vo.setAccountExpenseId(reimbursement.getAccountExpenseId());
        vo.setRemark(reimbursement.getRemark());
        vo.setTenantId(reimbursement.getTenantId());
        vo.setCreateUser(reimbursement.getCreateUser());
        vo.setCreateTime(reimbursement.getCreateTime());
        vo.setUpdateUser(reimbursement.getUpdateUser());
        vo.setUpdateTime(reimbursement.getUpdateTime());
        vo.setDeptId(reimbursement.getDeptId());
        vo.setDeleted(reimbursement.getDeleted());
        vo.setDetails(finReimbursementDetailMapper.selectList(
                new LambdaQueryWrapper<FinReimbursementDetail>()
                        .eq(FinReimbursementDetail::getReimbursementId, reimbursement.getId())
                        .orderByAsc(FinReimbursementDetail::getRowNo)
        ));
        if (isTravelReimbursement(reimbursement.getReimbursementType())) {
            vo.setTravel(finReimbursementTravelMapper.selectOne(
                    new LambdaQueryWrapper<FinReimbursementTravel>()
                            .eq(FinReimbursementTravel::getReimbursementId, reimbursement.getId())
                            .last("LIMIT 1")
            ));
        }
        vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.FIN_REIMBURSEMENT, reimbursement.getId()));
        //审批记录返回
        vo.setTasks(approvalTaskService.list(new LambdaQueryWrapper<ApprovalTask>().eq(ApprovalTask::getInstanceId, reimbursement.getApprovalInstanceId())));
        vo.setRecords(approvalRecordService.list(new LambdaQueryWrapper<ApprovalRecord>().eq(ApprovalRecord::getInstanceId, reimbursement.getApprovalInstanceId())));
        return vo;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean delete(List<Long> ids) {
        fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum.FILE, RecordTypeEnum.FIN_REIMBURSEMENT, ids);
        //先删除明细
        finReimbursementDetailMapper.delete(new LambdaQueryWrapper<FinReimbursementDetail>().in(FinReimbursementDetail::getReimbursementId, ids));
        //删除差旅
        finReimbursementTravelMapper.delete(new LambdaQueryWrapper<FinReimbursementTravel>().in(FinReimbursementTravel::getReimbursementId, ids));
        //删除主表
        int rows = finReimbursementMapper.delete(new LambdaQueryWrapper<FinReimbursement>().in(FinReimbursement::getId, ids));
        return rows == ids.size();
    }
    private String validateUpdateParam(FinReimbursementDto finReimbursementDto) {
        if (finReimbursementDto == null || finReimbursementDto.getId() == null) {
            throw new ServiceException("报销单ID不能为空");
        }
        if (finReimbursementDto.getReimbursementType() == null) {
            throw new ServiceException("报销类型不能为空");
        }
        String billStatus = normalizeBillStatus(finReimbursementDto.getBillStatus());
        if (billStatus == null) {
            throw new ServiceException("单据状态只支持 DRAFT æˆ– IN_APPROVAL");
        }
        if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
            validateApprovalNodes(finReimbursementDto.getNodes());
        }
        List<FinReimbursementDetail> details = finReimbursementDto.getDetails();
        if (details == null || details.isEmpty()) {
            throw new ServiceException("报销单明细不能为空");
        }
        for (FinReimbursementDetail detail : details) {
            if (detail == null) {
                throw new ServiceException("报销单明细不能为空");
            }
            if (detail.getAmount() == null || detail.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
                throw new ServiceException("报销单明细金额必须大于0");
            }
        }
        if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() == null) {
            throw new ServiceException("差旅报销必须填写差旅扩展信息");
        }
        if (!isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() != null) {
            throw new ServiceException("非差旅报销不允许填写差旅扩展信息");
        }
        return billStatus;
    }
    private String validateAddParam(FinReimbursementDto finReimbursementDto) {
        if (finReimbursementDto == null) {
            throw new ServiceException("报销单数据不能为空");
        }
        if (finReimbursementDto.getReimbursementType() == null) {
            throw new ServiceException("报销类型不能为空");
        }
        String billStatus = normalizeBillStatus(finReimbursementDto.getBillStatus());
        if (billStatus == null) {
            throw new ServiceException("单据状态只支持 DRAFT æˆ– IN_APPROVAL");
        }
        if (BILL_STATUS_IN_APPROVAL.equals(billStatus)) {
            validateApprovalNodes(finReimbursementDto.getNodes());
        }
        List<FinReimbursementDetail> details = finReimbursementDto.getDetails();
        if (details == null || details.isEmpty()) {
            throw new ServiceException("报销单明细不能为空");
        }
        for (FinReimbursementDetail detail : details) {
            if (detail == null) {
                throw new ServiceException("报销单明细不能为空");
            }
            if (detail.getAmount() == null || detail.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
                throw new ServiceException("报销单明细金额必须大于0");
            }
        }
        if (isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() == null) {
            throw new ServiceException("差旅报销必须填写差旅扩展信息");
        }
        if (!isTravelReimbursement(finReimbursementDto.getReimbursementType()) && finReimbursementDto.getTravel() != null) {
            throw new ServiceException("非差旅报销不允许填写差旅扩展信息");
        }
        return billStatus;
    }
    private FinReimbursement buildReimbursement(FinReimbursementDto finReimbursementDto, String billNo, BigDecimal totalAmount, String billStatus) {
        FinReimbursement reimbursement = new FinReimbursement();
        reimbursement.setId(null);
        reimbursement.setBillNo(billNo);
        reimbursement.setReimbursementType(finReimbursementDto.getReimbursementType());
        reimbursement.setExpenseType(finReimbursementDto.getExpenseType());
        reimbursement.setApplicantId(finReimbursementDto.getApplicantId());
        reimbursement.setApplicantCode(finReimbursementDto.getApplicantCode());
        reimbursement.setApplicantName(finReimbursementDto.getApplicantName());
        reimbursement.setApplicantDeptId(finReimbursementDto.getApplicantDeptId());
        reimbursement.setApplicantDeptName(finReimbursementDto.getApplicantDeptName());
        reimbursement.setReason(finReimbursementDto.getReason());
        reimbursement.setApplyAmount(finReimbursementDto.getApplyAmount());
        reimbursement.setDetailTotalAmount(totalAmount);
        reimbursement.setPayeeName(finReimbursementDto.getPayeeName());
        reimbursement.setPayeeAccount(finReimbursementDto.getPayeeAccount());
        reimbursement.setPayeeBank(finReimbursementDto.getPayeeBank());
        reimbursement.setRemark(finReimbursementDto.getRemark());
        reimbursement.setTenantId(finReimbursementDto.getTenantId());
        reimbursement.setApproveProcessId(null);
        reimbursement.setBillStatus(billStatus);
        return reimbursement;
    }
    private void startApproval(FinReimbursement reimbursement, FinReimbursementDto finReimbursementDto) {
        Long businessType = resolveBusinessType(finReimbursementDto.getReimbursementType());
        ApprovalInstanceDto approvalInstanceDto = new ApprovalInstanceDto();
        approvalInstanceDto.setInstanceNo(OrderUtils.countTodayByCreateTime(approvalInstanceMapper, "SP", "instance_no", approvalInstanceDto.getCreateTime() != null ? approvalInstanceDto.getCreateTime() : LocalDateTime.now()));
        approvalInstanceDto.setBusinessId(reimbursement.getId());
        approvalInstanceDto.setTemplateId(null);
        approvalInstanceDto.setTemplateName(TypeEnums.getLabelByValue(businessType) + "审批");
        approvalInstanceDto.setBusinessType(businessType);
        approvalInstanceDto.setTitle("报销单号:" + reimbursement.getBillNo());
        approvalInstanceDto.setApplicantId(reimbursement.getApplicantId() != null ? reimbursement.getApplicantId() : SecurityUtils.getUserId());
        approvalInstanceDto.setApplicantName(reimbursement.getApplicantName() != null ? reimbursement.getApplicantName() : SecurityUtils.getLoginUser().getNickName());
        approvalInstanceDto.setApplyTime(LocalDateTime.now());
        approvalInstanceDto.setStatus("PENDING");
        approvalInstanceDto.setCurrentLevel(1);
        boolean approvalSaved = approvalInstanceService.save(approvalInstanceDto);
        if (!approvalSaved || approvalInstanceDto.getId() == null) {
            throw new ServiceException("发起审批失败");
        }
        List<ApprovalTask> firstTasks = createApprovalNodes(approvalInstanceDto, finReimbursementDto.getNodes());
        sendApproveNotice(approvalInstanceDto, firstTasks);
        FinReimbursement update = new FinReimbursement();
        update.setId(reimbursement.getId());
        update.setApprovalInstanceId(approvalInstanceDto.getId());
        update.setBillStatus(BILL_STATUS_IN_APPROVAL);
        int rows = finReimbursementMapper.updateById(update);
        if (rows != 1) {
            throw new ServiceException("回填审批实例失败");
        }
    }
    private List<ApprovalTask> createApprovalNodes(ApprovalInstanceDto approvalInstanceDto, List<ApprovalTemplateNodeDto> nodes) {
        List<ApprovalTask> firstTasks = Collections.emptyList();
        for (int i = 0; i < nodes.size(); i++) {
            ApprovalTemplateNodeDto nodeDto = nodes.get(i);
            ApprovalInstanceNode instanceNode = new ApprovalInstanceNode();
            instanceNode.setInstanceId(approvalInstanceDto.getId());
            instanceNode.setLevelNo(nodeDto.getLevelNo());
            instanceNode.setApproveType(nodeDto.getApproveType());
            instanceNode.setStatus(i == 0 ? "PENDING" : NODE_STATUS_WAITING);
            instanceNode.setStartTime(i == 0 ? LocalDateTime.now() : null);
            instanceNode.setDeleted((byte) 0);
            approvalInstanceNodeService.save(instanceNode);
            List<ApprovalTask> tasks = nodeDto.getApprovers().stream().map(approver -> {
                ApprovalTask task = new ApprovalTask();
                task.setInstanceId(approvalInstanceDto.getId());
                task.setNodeId(instanceNode.getId());
                task.setLevelNo(instanceNode.getLevelNo());
                task.setApproverId(approver.getApproverId());
                task.setApproverName(approver.getApproverName());
                task.setTaskStatus("PENDING");
                task.setIsRead((byte) 0);
                task.setDeleted((byte) 0);
                return task;
            }).collect(Collectors.toList());
            approvalTaskService.saveBatch(tasks);
            if (i == 0) {
                firstTasks = tasks;
                ApprovalRecord record = new ApprovalRecord();
                record.setInstanceId(approvalInstanceDto.getId());
                record.setNodeId(instanceNode.getId());
                record.setOperatorId(approvalInstanceDto.getApplicantId());
                record.setOperatorName(approvalInstanceDto.getApplicantName());
                record.setAction("SUBMIT");
                record.setComment("发起审批");
                record.setDeleted((byte) 0);
                approvalRecordService.save(record);
            }
        }
        return firstTasks;
    }
    private void validateApprovalNodes(List<ApprovalTemplateNodeDto> nodes) {
        if (nodes == null || nodes.isEmpty()) {
            throw new ServiceException("提交审批时审批节点不能为空");
        }
        for (int i = 0; i < nodes.size(); i++) {
            ApprovalTemplateNodeDto node = nodes.get(i);
            if (node == null) {
                throw new ServiceException("审批节点不能为空");
            }
            if (node.getLevelNo() == null) {
                node.setLevelNo(i + 1);
            }
            if (!StringUtils.hasText(node.getApproveType())) {
                throw new ServiceException("审批节点审批方式不能为空");
            }
            List<ApprovalTemplateNodeApproverDto> approvers = node.getApprovers();
            if (approvers == null || approvers.isEmpty()) {
                throw new ServiceException("审批节点审批人不能为空");
            }
            for (ApprovalTemplateNodeApproverDto approver : approvers) {
                if (approver == null || approver.getApproverId() == null) {
                    throw new ServiceException("审批人不能为空");
                }
            }
        }
    }
    private void sendApproveNotice(ApprovalInstanceDto instance, List<ApprovalTask> tasks) {
        if (instance == null || tasks == null || tasks.isEmpty()) {
            return;
        }
        List<Long> approverIds = tasks.stream()
                .map(ApprovalTask::getApproverId)
                .filter(id -> id != null && id > 0)
                .distinct()
                .collect(Collectors.toList());
        if (approverIds.isEmpty()) {
            return;
        }
        String title = "报销审批";
        String message = "审批单号 " + instance.getInstanceNo() + " éœ€è¦æ‚¨å®¡æ‰¹";
        String jumpPath = "/approvalInstance?id=" + instance.getId();
        sysNoticeService.simpleNoticeByUser(title, message, approverIds, jumpPath);
    }
    private void resetApprovalFlow(FinReimbursement existing, Long reimbursementId) {
        if (existing == null || existing.getApprovalInstanceId() == null) {
            return;
        }
        Long approvalInstanceId = existing.getApprovalInstanceId();
        if (!"REJECTED".equals(existing.getBillStatus())) {
            approvalInstanceService.delete(Collections.singletonList(approvalInstanceId));
        }
        clearApprovalBinding(reimbursementId);
    }
    private void clearApprovalBinding(Long reimbursementId) {
        int rows = finReimbursementMapper.update(
                null,
                Wrappers.<FinReimbursement>lambdaUpdate()
                        .eq(FinReimbursement::getId, reimbursementId)
                        .set(FinReimbursement::getApprovalInstanceId, null)
        );
        if (rows != 1) {
            throw new ServiceException("重置审批流程失败");
        }
    }
    private Long resolveBusinessType(Byte reimbursementType) {
        return isTravelReimbursement(reimbursementType)
                ? TypeEnums.TRAVEL_REIMBURSEMENT_APPROVAL.getCode()
                : TypeEnums.EXPENSE_APPROVAL.getCode();
    }
    private String normalizeBillStatus(String billStatus) {
        if (billStatus == null) {
            return BILL_STATUS_DRAFT;
        }
        String normalized = billStatus.trim().toUpperCase();
        if (BILL_STATUS_DRAFT.equals(normalized) || BILL_STATUS_IN_APPROVAL.equals(normalized)) {
            return normalized;
        }
        return null;
    }
    private boolean isTravelReimbursement(Byte reimbursementType) {
        return Byte.valueOf((byte) 1).equals(reimbursementType);
    }
}
src/main/java/com/ruoyi/approve/service/impl/FinReimbursementTravelServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.approve.service.impl;
import com.ruoyi.approve.pojo.FinReimbursementTravel;
import com.ruoyi.approve.mapper.FinReimbursementTravelMapper;
import com.ruoyi.approve.service.FinReimbursementTravelService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * å·®æ—…报销扩展表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-21 09:56:47
 */
@Service
public class FinReimbursementTravelServiceImpl extends ServiceImpl<FinReimbursementTravelMapper, FinReimbursementTravel> implements FinReimbursementTravelService {
}
src/main/java/com/ruoyi/approve/utils/ApproveProcessConfigNodeUtils.java
@@ -1,9 +1,370 @@
package com.ruoyi.approve.utils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalInstanceNode;
import com.ruoyi.approve.pojo.ApprovalRecord;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.approve.pojo.ApprovalTemplateNode;
import com.ruoyi.approve.pojo.ApprovalTemplateNodeApprover;
import com.ruoyi.approve.service.ApprovalInstanceNodeService;
import com.ruoyi.approve.service.ApprovalRecordService;
import com.ruoyi.approve.service.ApprovalTaskService;
import com.ruoyi.approve.service.ApprovalTemplateNodeApproverService;
import com.ruoyi.approve.service.ApprovalTemplateNodeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
 * å®¡æ‰¹æµç¨‹èŠ‚ç‚¹å·¥å…·ç±»
 */
@Component
@RequiredArgsConstructor
public class ApproveProcessConfigNodeUtils {
    private final ApprovalInstanceNodeService instanceNodeService;
    private final ApprovalTaskService approvalTaskService;
    private final ApprovalRecordService approvalRecordService;
    private final ApprovalTemplateNodeService approvalTemplateNodeService;
    private final ApprovalTemplateNodeApproverService approvalTemplateNodeApproverService;
}
    /**
     * æŒ‰å½“前层级创建审批节点和审批任务。
     * è¯¥é‡è½½ä¼šåŒæ—¶å†™å…¥ä¸€æ¡å‘起审批记录。
     */
    @Transactional(rollbackFor = Exception.class)
    public ApprovalInstanceNode createCurrentNodeAndTasks(ApprovalInstance instance) {
        return createCurrentNodeAndTasks(instance, true);
    }
    /**
     * æŒ‰å½“前层级创建审批节点和审批任务。
     *
     * @param instance å®¡æ‰¹å®žä¾‹
     * @param createSubmitRecord æ˜¯å¦åˆ›å»ºå‘起审批记录
     * @return åˆ›å»ºå‡ºçš„当前节点实例
     */
    @Transactional(rollbackFor = Exception.class)
    public ApprovalInstanceNode createCurrentNodeAndTasks(ApprovalInstance instance, boolean createSubmitRecord) {
        if (instance == null || instance.getId() == null) {
            throw new RuntimeException("审批实例不能为空");
        }
        if (instance.getTemplateId() == null) {
            throw new RuntimeException("审批模板不能为空");
        }
        Integer currentLevel = instance.getCurrentLevel() == null ? 1 : instance.getCurrentLevel();
        ApprovalInstanceNode existsNode = instanceNodeService.getOne(
                new LambdaQueryWrapper<ApprovalInstanceNode>()
                        .eq(ApprovalInstanceNode::getInstanceId, instance.getId())
                        .eq(ApprovalInstanceNode::getLevelNo, currentLevel)
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .last("LIMIT 1")
        );
        if (existsNode != null) {
            return existsNode;
        }
        ApprovalTemplateNode templateNode = approvalTemplateNodeService.getOne(
                new LambdaQueryWrapper<ApprovalTemplateNode>()
                        .eq(ApprovalTemplateNode::getTemplateId, instance.getTemplateId())
                        .eq(ApprovalTemplateNode::getLevelNo, currentLevel)
                        .orderByAsc(ApprovalTemplateNode::getId)
                        .last("LIMIT 1")
        );
        if (templateNode == null) {
            throw new RuntimeException("未找到当前层级对应的审批模板节点");
        }
        List<ApprovalTemplateNodeApprover> approvers = approvalTemplateNodeApproverService.list(
                new LambdaQueryWrapper<ApprovalTemplateNodeApprover>()
                        .eq(ApprovalTemplateNodeApprover::getTemplateId, instance.getTemplateId())
                        .eq(ApprovalTemplateNodeApprover::getNodeId, templateNode.getId())
                        .eq(ApprovalTemplateNodeApprover::getDeleted, 0L)
                        .orderByAsc(ApprovalTemplateNodeApprover::getSortNo)
        );
        if (approvers == null || approvers.isEmpty()) {
            throw new RuntimeException("当前审批节点未配置审批人");
        }
        ApprovalInstanceNode instanceNode = new ApprovalInstanceNode();
        instanceNode.setInstanceId(instance.getId());
        instanceNode.setLevelNo(templateNode.getLevelNo());
        instanceNode.setApproveType(templateNode.getApproveType());
        instanceNode.setStatus("PENDING");
        instanceNode.setStartTime(LocalDateTime.now());
        instanceNode.setDeleted((byte) 0);
        instanceNodeService.save(instanceNode);
        List<ApprovalTask> taskList = new ArrayList<>(approvers.size());
        for (ApprovalTemplateNodeApprover approver : approvers) {
            ApprovalTask task = new ApprovalTask();
            task.setInstanceId(instance.getId());
            task.setNodeId(instanceNode.getId());
            task.setLevelNo(instanceNode.getLevelNo());
            task.setApproverId(approver.getApproverId());
            task.setApproverName(approver.getApproverName());
            task.setTaskStatus("PENDING");
            task.setIsRead((byte) 0);
            task.setDeleted((byte) 0);
            taskList.add(task);
        }
        approvalTaskService.saveBatch(taskList);
        if (createSubmitRecord) {
            ApprovalRecord record = new ApprovalRecord();
            record.setInstanceId(instance.getId());
            record.setNodeId(instanceNode.getId());
            record.setOperatorId(instance.getApplicantId());
            record.setOperatorName(instance.getApplicantName());
            record.setAction("SUBMIT");
            record.setComment("发起审批");
            record.setDeleted((byte) 0);
            approvalRecordService.save(record);
        }
        return instanceNode;
    }
    /**
     * æŸ¥è¯¢å½“前待处理节点。
     */
    public ApprovalInstanceNode getCurrentNode(Long instanceId) {
        if (instanceId == null) {
            return null;
        }
        return instanceNodeService.getOne(
                new LambdaQueryWrapper<ApprovalInstanceNode>()
                        .eq(ApprovalInstanceNode::getInstanceId, instanceId)
                        .eq(ApprovalInstanceNode::getStatus, "PENDING")
                        .eq(ApprovalInstanceNode::getDeleted, 0)
                        .orderByAsc(ApprovalInstanceNode::getLevelNo)
                        .last("LIMIT 1")
        );
    }
    /**
     * æŸ¥è¯¢å½“前审批层级。
     */
    public Integer getCurrentLevel(Long instanceId) {
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        return currentNode != null ? currentNode.getLevelNo() : null;
    }
    /**
     * æŸ¥è¯¢å½“前节点下的待审批任务。
     */
    public List<ApprovalTask> getCurrentPendingTasks(Long instanceId) {
        if (instanceId == null) {
            return List.of();
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return List.of();
        }
        return approvalTaskService.list(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getInstanceId, instanceId)
                        .eq(ApprovalTask::getNodeId, currentNode.getId())
                        .eq(ApprovalTask::getTaskStatus, "PENDING")
                        .eq(ApprovalTask::getDeleted, 0)
                        .orderByAsc(ApprovalTask::getLevelNo)
        );
    }
    /**
     * æŸ¥è¯¢å½“前用户在当前节点上的待审批任务。
     */
    public ApprovalTask getCurrentUserTask(Long instanceId, Long userId) {
        if (instanceId == null || userId == null) {
            return null;
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return null;
        }
        return approvalTaskService.getOne(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getInstanceId, instanceId)
                        .eq(ApprovalTask::getNodeId, currentNode.getId())
                        .eq(ApprovalTask::getApproverId, userId)
                        .eq(ApprovalTask::getTaskStatus, "PENDING")
                        .eq(ApprovalTask::getDeleted, 0)
                        .last("LIMIT 1")
        );
    }
    /**
     * åˆ¤æ–­å½“前用户是否是当前审批人。
     */
    public boolean isCurrentApprover(Long instanceId, Long userId) {
        return getCurrentUserTask(instanceId, userId) != null;
    }
    /**
     * æŸ¥è¯¢å½“前节点的审批人 ID åˆ—表。
     */
    public List<Long> getCurrentNodeApproverIds(Long instanceId) {
        return getCurrentPendingTasks(instanceId).stream()
                .map(ApprovalTask::getApproverId)
                .distinct()
                .collect(Collectors.toList());
    }
    /**
     * æŸ¥è¯¢å½“前节点剩余待审批人数。
     */
    public int getRemainingApproverCount(Long instanceId) {
        return getCurrentPendingTasks(instanceId).size();
    }
    /**
     * æŸ¥è¯¢å½“前节点已同意人数。
     */
    public int getApprovedCount(Long instanceId) {
        if (instanceId == null) {
            return 0;
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return 0;
        }
        return Math.toIntExact(approvalTaskService.count(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getInstanceId, instanceId)
                        .eq(ApprovalTask::getNodeId, currentNode.getId())
                        .eq(ApprovalTask::getTaskStatus, "APPROVED")
                        .eq(ApprovalTask::getDeleted, 0)
        ));
    }
    /**
     * æŸ¥è¯¢å½“前节点已拒绝人数。
     */
    public int getRejectedCount(Long instanceId) {
        if (instanceId == null) {
            return 0;
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return 0;
        }
        return Math.toIntExact(approvalTaskService.count(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getInstanceId, instanceId)
                        .eq(ApprovalTask::getNodeId, currentNode.getId())
                        .eq(ApprovalTask::getTaskStatus, "REJECTED")
                        .eq(ApprovalTask::getDeleted, 0)
        ));
    }
    /**
     * åˆ¤æ–­å½“前节点是否可以流转到下一层。
     */
    public boolean canProceedToNextLevel(Long instanceId, String approveType) {
        if (instanceId == null || approveType == null) {
            return false;
        }
        if (getRejectedCount(instanceId) > 0) {
            return false;
        }
        int totalApproverCount = getCurrentPendingTasks(instanceId).size() + getApprovedCount(instanceId);
        int approvedCount = getApprovedCount(instanceId);
        if ("AND".equalsIgnoreCase(approveType)) {
            return approvedCount > 0 && approvedCount == totalApproverCount;
        }
        if ("OR".equalsIgnoreCase(approveType)) {
            return approvedCount > 0;
        }
        return false;
    }
    /**
     * æŸ¥è¯¢å½“前用户在当前节点上的任务状态。
     */
    public String getUserTaskStatus(Long instanceId, Long userId) {
        if (instanceId == null || userId == null) {
            return null;
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return null;
        }
        ApprovalTask task = approvalTaskService.getOne(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getInstanceId, instanceId)
                        .eq(ApprovalTask::getNodeId, currentNode.getId())
                        .eq(ApprovalTask::getApproverId, userId)
                        .eq(ApprovalTask::getDeleted, 0)
                        .last("LIMIT 1")
        );
        return task != null ? task.getTaskStatus() : null;
    }
    /**
     * æŸ¥è¯¢æŒ‡å®šç”¨æˆ·çš„全部待审批任务。
     */
    public List<ApprovalTask> getUserAllPendingTasks(Long userId) {
        if (userId == null) {
            return List.of();
        }
        return approvalTaskService.list(
                new LambdaQueryWrapper<ApprovalTask>()
                        .eq(ApprovalTask::getApproverId, userId)
                        .eq(ApprovalTask::getTaskStatus, "PENDING")
                        .eq(ApprovalTask::getDeleted, 0)
                        .orderByDesc(ApprovalTask::getCreateTime)
        );
    }
    /**
     * æŸ¥è¯¢å®¡æ‰¹å®žä¾‹çš„进度摘要。
     */
    public String getApprovalProgress(Long instanceId) {
        if (instanceId == null) {
            return "无效的审批实例";
        }
        ApprovalInstanceNode currentNode = getCurrentNode(instanceId);
        if (currentNode == null) {
            return "审批已完成或尚未开始";
        }
        int approvedCount = getApprovedCount(instanceId);
        int rejectedCount = getRejectedCount(instanceId);
        int pendingCount = getRemainingApproverCount(instanceId);
        int totalCount = approvedCount + rejectedCount + pendingCount;
        return String.format(
                "第%d级审批:总人数=%d,已同意=%d,已拒绝=%d,待审批=%d",
                currentNode.getLevelNo(),
                totalCount,
                approvedCount,
                rejectedCount,
                pendingCount
        );
    }
}
src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java
@@ -2,6 +2,8 @@
import com.ruoyi.basic.dto.StorageAttachmentDTO;
import com.ruoyi.basic.service.StorageAttachmentService;
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;
@@ -35,6 +37,7 @@
     * @param ids æ–‡ä»¶id列表
     * @return åˆ é™¤ç»“æžœ
     */
    @Log(title = "存储附件", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    @Operation(summary = "删除通用文件上传的附件信息")
    public R batchDelete(@RequestBody List<Long> ids) {
@@ -44,6 +47,7 @@
    /**
     * ä¿å­˜é€šç”¨æ–‡ä»¶ä¸Šä¼ çš„附件信息
     */
    @Log(title = "存储附件", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Operation(summary = "保存通用文件上传的附件信息")
    public R add(@RequestBody StorageAttachmentDTO storageAttachmentDTO) {
src/main/java/com/ruoyi/basic/controller/SupplierManageController.java
@@ -28,6 +28,7 @@
     * @param supplierManage
     * @return
     */
    @Log(title = "供应商管理", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody SupplierManage supplierManage) {
        supplierService.saveSupplier(supplierManage);
@@ -39,6 +40,7 @@
     * @param ids
     * @return
     */
    @Log(title = "供应商管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/del")
    public AjaxResult delSupplier(@RequestBody List<Integer> ids) {
        if(CollectionUtils.isEmpty(ids)){
@@ -63,6 +65,7 @@
     * @param supplierManage
     * @return
     */
    @Log(title = "供应商管理", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    public AjaxResult update(@RequestBody SupplierManage supplierManage) {
        supplierService.supplierUpdate(supplierManage);
@@ -85,6 +88,7 @@
     * @param response
     * @param supplierManageDto
     */
    @Log(title = "供应商管理", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void supplierExport(HttpServletResponse response,SupplierManageDto supplierManageDto) {
        supplierService.supplierExport(response, supplierManageDto);
src/main/java/com/ruoyi/basic/controller/SupplierManageFileController.java
@@ -3,6 +3,8 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.pojo.SupplierManageFile;
import com.ruoyi.basic.service.SupplierManageFileService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
@@ -27,6 +29,7 @@
     * @param supplierManageFile
     * @return
     */
    @Log(title = "新增供应商附件", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody SupplierManageFile supplierManageFile) {
        return AjaxResult.success(supplierManageFileService.save(supplierManageFile));
@@ -37,6 +40,7 @@
     * @param ids
     * @return
     */
    @Log(title = "删除供应商附件", businessType = BusinessType.DELETE)
    @DeleteMapping("/del")
    public AjaxResult delSupplierManageFile(@RequestBody List<Integer> ids) {
        if(CollectionUtils.isEmpty(ids)){
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,13 +192,16 @@
    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"),
    FIN_REIMBURSEMENT("fin_reimbursement"),
    FIN_VOUCHER("fin_voucher"),
    ACCOUNT_FILE("account_file");
    ACCOUNT_FILE("account_file"),
    ENTERPRISE_NEWS("enterprise_news"),
    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
@@ -21,8 +21,14 @@
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.procurementrecord.pojo.ReturnManagement;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesQuotationMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesQuotation;
import com.ruoyi.sales.vo.CustomerTransactionsDetailsVo;
import com.ruoyi.sales.vo.CustomerTransactionsVo;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
@@ -48,7 +54,11 @@
@Slf4j
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements ICustomerService {
    @Autowired
    private  SalesLedgerMapper salesLedgerMapper;
    private SalesLedgerMapper salesLedgerMapper;
    @Autowired
    private SalesQuotationMapper salesQuotationMapper;
    @Autowired
    private ReturnManagementMapper returnManagementMapper;
    @Autowired
    private CustomerMapper customerMapper;
@@ -216,10 +226,24 @@
    @Transactional(rollbackFor = Exception.class)
    public int deleteCustomerByIds(Long[] ids) {
        List<Long> idList = Arrays.asList(ids);
        // æ£€æŸ¥æ˜¯å¦æœ‰é”€å”®å°è´¦å…³è”
        List<SalesLedger> salesLedgers = salesLedgerMapper.selectList(new QueryWrapper<SalesLedger>().lambda().in(SalesLedger::getCustomerId, idList));
        if (!salesLedgers.isEmpty()) {
            throw new RuntimeException("客户档案下有销售合同,请先删除销售合同");
            throw new RuntimeException("客户档案下有销售台账,请先删除销售台账");
        }
        // æ£€æŸ¥æ˜¯å¦æœ‰é”€å”®é€€è´§å…³è”
        List<ReturnManagement> returnManagements = returnManagementMapper.selectList(new QueryWrapper<ReturnManagement>().lambda().in(ReturnManagement::getCustomerId, idList));
        if (!returnManagements.isEmpty()) {
            throw new RuntimeException("客户档案下有销售退货,请先删除销售退货");
        }
        // æ£€æŸ¥æ˜¯å¦æœ‰é”€å”®æŠ¥ä»·å…³è”
        List<SalesQuotation> salesQuotations = salesQuotationMapper.selectList(new QueryWrapper<SalesQuotation>().lambda().in(SalesQuotation::getCustomerId, idList));
        if (!salesQuotations.isEmpty()) {
            throw new RuntimeException("客户档案下有销售报价,请先删除销售报价");
        }
        // æŸ¥è¯¢æ˜¯å¦æœ‰å·²åˆ†é…çš„公海客户
        List<Customer> assignedPools = customerMapper.selectList(
                new QueryWrapper<Customer>().lambda()
@@ -379,6 +403,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/basic/task/ReturnVisitReminderTask.java
@@ -3,9 +3,7 @@
import com.ruoyi.basic.pojo.CustomerReturnVisit;
import com.ruoyi.basic.service.CustomerReturnVisitService;
import com.ruoyi.framework.redis.RedisCache;
import com.ruoyi.project.system.domain.SysUserClient;
import com.ruoyi.project.system.service.SysUserClientService;
import com.ruoyi.project.system.service.impl.UnipushService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
@@ -33,9 +31,7 @@
    private final CustomerReturnVisitService customerReturnVisitService;
    private final UnipushService unipushService;
    private final SysUserClientService userClientService;
    @SuppressWarnings("unchecked")
    @Scheduled(fixedDelay = 60000)
@@ -59,30 +55,22 @@
        }
    }
    @SuppressWarnings("unchecked")
    private void processReminder(Long returnVisitId) {
        CustomerReturnVisit returnVisit = customerReturnVisitService.getById(returnVisitId);
        if (returnVisit == null || returnVisit.getIsEnabled() == 0 || returnVisit.getIsCompleted() == 1) {
            return;
        }
        SysUserClient client = userClientService.getById(returnVisit.getRemindUserId());
        if (client == null || client.getCid() == null) {
            log.warn("用户未绑定CID, æ— æ³•发送Unipush推送: userId={}", returnVisit.getRemindUserId());
            return;
        }
        try {
            unipushService.sendReturnVisitReminder(returnVisitId, client.getCid(), returnVisit.getContent(), returnVisit.getCustomerId());
            // æ ‡è®°å·²å¤„理(推送功能已禁用)
            CustomerReturnVisit updateObj = new CustomerReturnVisit();
            updateObj.setId(returnVisitId);
            updateObj.setIsCompleted(1);
            customerReturnVisitService.updateById(updateObj);
            log.info("回访提醒已通过 Unipush å‘送: ID={}", returnVisitId);
            log.info("回访提醒已处理(推送功能已禁用): ID={}", returnVisitId);
        } catch (Exception e) {
            log.error("发送回访提醒失败,重新加入队列: ID={}", returnVisitId, e);
            long retryTime = System.currentTimeMillis() + 60000;
            redisCache.redisTemplate.opsForZSet().add(REMINDER_QUEUE_KEY, returnVisitId, retryTime);
            log.error("处理回访提醒失败: ID={}", returnVisitId, e);
        }
    }
}
src/main/java/com/ruoyi/collaborativeApproval/controller/DutyPlanController.java
@@ -7,6 +7,8 @@
import com.ruoyi.collaborativeApproval.pojo.RulesRegulationsManagement;
import com.ruoyi.collaborativeApproval.service.DutyPlanService;
import com.ruoyi.common.utils.excel.ExcelUtils;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
@@ -34,18 +36,21 @@
        return AjaxResult.success(dutyPlanService.getNum());
    }
    @Log(title = "新增值班计划", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Operation(summary = "新增")
    public AjaxResult add(@RequestBody DutyPlan dutyPlan){
        return AjaxResult.success(dutyPlanService.save(dutyPlan));
    }
    @Log(title = "修改值班计划", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    @Operation(summary = "修改")
    public AjaxResult update(@RequestBody DutyPlan dutyPlan){
        return AjaxResult.success(dutyPlanService.updateById(dutyPlan));
    }
    @Log(title = "删除值班计划", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    @Operation(summary = "删除")
    public AjaxResult delete(@RequestBody List<Long> ids){
@@ -54,6 +59,7 @@
        }
        return AjaxResult.success(dutyPlanService.removeBatchByIds(ids));
    }
    @Log(title = "导出值班计划", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    @Operation(summary = "导出")
    public void exportData(HttpServletResponse response, DutyPlanDTO dutyPlanDTO){
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.ruoyi.collaborativeApproval.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.collaborativeApproval.dto.EnterpriseNewsDto;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsService;
import com.ruoyi.collaborativeApproval.vo.EnterpriseNewsVo;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * <p>
 * ä¼ä¸šæ–°é—»è¡¨ å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:50:59
 */
@RestController
@RequestMapping("/enterpriseNews")
@Tag(name = "企业新闻表")
@AllArgsConstructor
public class EnterpriseNewsController {
    private final EnterpriseNewsService enterpriseNewsService;
    @Operation(summary = "分页查询")
    @GetMapping("/listPage")
    @Log(title = "企业新闻分页查询", businessType = BusinessType.OTHER)
    public R listPage(Page<EnterpriseNewsVo>  page , EnterpriseNewsDto enterpriseNewsDto) {
        return R.ok(enterpriseNewsService.listPage(page, enterpriseNewsDto));
    }
    @PostMapping("/save")
    @Operation(summary = "保存")
    @Log(title = "保存企业新闻", businessType = BusinessType.INSERT)
    public R save(@RequestBody EnterpriseNewsDto enterpriseNewsDto) {
        return R.ok(enterpriseNewsService.add(enterpriseNewsDto));
    }
    @PutMapping("/update")
    @Operation(summary = "更新")
    @Log(title = "更新企业新闻", businessType = BusinessType.UPDATE)
    public R update(@RequestBody EnterpriseNewsDto enterpriseNewsDto) {
        return R.ok(enterpriseNewsService.updateEnterpriseNewsDto(enterpriseNewsDto));
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除")
    @Log(title = "删除企业新闻", businessType = BusinessType.DELETE)
    public R delete(@RequestBody List<Long> ids) {
        return R.ok(enterpriseNewsService.delete(ids));
    }
}
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeDeptController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.collaborativeApproval.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围部门表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:12
 */
@RestController
@RequestMapping("/enterpriseNewsScopeDept")
public class EnterpriseNewsScopeDeptController {
}
src/main/java/com/ruoyi/collaborativeApproval/controller/EnterpriseNewsScopeUserController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.collaborativeApproval.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围用户表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:23
 */
@RestController
@RequestMapping("/enterpriseNewsScopeUser")
public class EnterpriseNewsScopeUserController {
}
src/main/java/com/ruoyi/collaborativeApproval/controller/MeetingController.java
@@ -13,6 +13,8 @@
import com.ruoyi.collaborativeApproval.vo.SearchMeetingRoomVo;
import com.ruoyi.collaborativeApproval.vo.SearchMeetingUseVo;
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.domain.R;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -42,6 +44,7 @@
        return R.ok(meetingService.getMeetingRoomList(vo));
    }
    @Log(title = "新增会议室", businessType = BusinessType.INSERT)
    @PostMapping("/saveRoom")
    public R saveRoom(@RequestBody MeetingRoom meetingRoom) {
        meetingService.saveMeetRoom(meetingRoom);
@@ -53,6 +56,7 @@
        return R.ok(meetingService.findMeetRoomById(id));
    }
    @Log(title = "删除会议室", businessType = BusinessType.DELETE)
    @DeleteMapping("/delRoom/{id}")
    public R deleteRoom(@PathVariable Long id) {
        meetingService.deleteMeetingRoom(id);
@@ -69,18 +73,21 @@
        return R.ok(meetingService.getMeetingDraftList(vo));
    }
    @Log(title = "保存会议草稿", businessType = BusinessType.INSERT)
    @PostMapping("/saveDraft")
    public R saveMeetingDraft(@RequestBody MeetDraft meetDraft) {
        meetingService.saveMeetDraft(meetDraft);
        return R.ok();
    }
    @Log(title = "删除会议草稿", businessType = BusinessType.DELETE)
    @DeleteMapping("/delDraft/{id}")
    public R deleteMeetingDraft(@PathVariable Long id) {
        meetingService.deleteMeetingDraft(id);
        return R.ok();
    }
    @Log(title = "新增会议申请", businessType = BusinessType.INSERT)
    @PostMapping("/saveMeetingApplication")
    public R saveMeetApplication(@RequestBody MeetApplication meetApplication) {
       return meetingService.saveMeetApplication(meetApplication);
@@ -110,6 +117,7 @@
        return R.ok(meetingService.getMeetingMinutesById(id));
    }
    @Log(title = "保存会议纪要", businessType = BusinessType.INSERT)
    @PostMapping("/saveMeetingMinutes")
    public R saveMeetingMinutes(@RequestBody MeetingMinutes meetingMinutes) {
        meetingService.saveMeetingMinutes(meetingMinutes);
@@ -127,6 +135,7 @@
    }
    @Operation(summary = "会议室设置导出")
    @Log(title = "导出会议室设置", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response) {
        List<MeetingRoom> accountExpenses = meetingService.list();
@@ -137,6 +146,7 @@
    private final MeetDraftMapper meetDraftMapper;
    @Operation(summary = "会议草稿导出")
    @Log(title = "导出会议草稿", businessType = BusinessType.EXPORT)
    @PostMapping("/exportOne")
    public void exportOne(HttpServletResponse response) {
        List<MeetDraft> accountExpenses = meetDraftMapper.selectList(new LambdaQueryWrapper<MeetDraft>());
src/main/java/com/ruoyi/collaborativeApproval/controller/NoticeTypeController.java
@@ -5,6 +5,8 @@
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.collaborativeApproval.pojo.NoticeType;
import com.ruoyi.collaborativeApproval.service.NoticeTypeService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
@@ -33,6 +35,7 @@
     * @param noticeType
     * @return
     */
    @Log(title = "新增公告类型", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody NoticeType noticeType) {
        return AjaxResult.success(noticeTypeService.saveOrUpdate(noticeType));
@@ -43,6 +46,7 @@
     * @param ids
     * @return
     */
    @Log(title = "删除公告类型", businessType = BusinessType.DELETE)
    @DeleteMapping("/del")
    public AjaxResult delNoticeType(@RequestBody List<Integer> ids) {
        if(CollectionUtils.isEmpty(ids)){
src/main/java/com/ruoyi/collaborativeApproval/controller/RulesRegulationsManagementController.java
@@ -8,6 +8,8 @@
import com.ruoyi.collaborativeApproval.pojo.SealApplicationManagement;
import com.ruoyi.collaborativeApproval.service.RulesRegulationsManagementService;
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.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -33,6 +35,7 @@
        return AjaxResult.success(rulesRegulationsManagementService.listPage(page, rulesRegulationsManagement));
    }
    @Log(title = "新增规章制度", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Operation(summary = "新增")
    public AjaxResult add(@RequestBody RulesRegulationsManagement rulesRegulationsManagement){
@@ -40,12 +43,14 @@
        return AjaxResult.success(rulesRegulationsManagement.getId());
    }
    @Log(title = "修改规章制度", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    @Operation(summary = "修改")
    public AjaxResult update(@RequestBody RulesRegulationsManagement rulesRegulationsManagement){
        return AjaxResult.success(rulesRegulationsManagementService.updateById(rulesRegulationsManagement));
    }
    @Log(title = "删除规章制度", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    @Operation(summary = "删除")
    public AjaxResult delete(@PathVariable("ids") List<Long> ids){
@@ -55,11 +60,13 @@
        return AjaxResult.success(rulesRegulationsManagementService.removeBatchByIds(ids));
    }
    //规则查看时新增阅读状态
    @Log(title = "新增阅读状态", businessType = BusinessType.INSERT)
    @PostMapping("/addReadingStatus")
    @Operation(summary = "新增阅读状态")
    public AjaxResult addReadingStatus(@RequestBody ReadingStatus readingStatus){
        return AjaxResult.success(readingStatusMapper.insert(readingStatus));
    }
    @Log(title = "修改阅读状态", businessType = BusinessType.UPDATE)
    @PostMapping("/updateReadingStatus")
    @Operation(summary = "修改阅读状态")
    public AjaxResult updateReadingStatus(@RequestBody ReadingStatus readingStatus){
@@ -77,6 +84,7 @@
    }
    @Operation(summary = "规章制度管理导出")
    @Log(title = "导出规章制度", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response) {
        List<RulesRegulationsManagement> accountExpenses = rulesRegulationsManagementService.list();
src/main/java/com/ruoyi/collaborativeApproval/controller/RulesRegulationsManagementFileController.java
@@ -3,6 +3,8 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.collaborativeApproval.pojo.RulesRegulationsManagementFile;
import com.ruoyi.collaborativeApproval.service.RulesRegulationsManagementFileService;
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.quality.pojo.QualityInspectFile;
import com.ruoyi.quality.service.IQualityInspectFileService;
@@ -33,6 +35,7 @@
     * @param rulesRegulationsManagementFile
     * @return
     */
    @Log(title = "规章制度文件", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody RulesRegulationsManagementFile rulesRegulationsManagementFile) {
        return AjaxResult.success(rulesRegulationsManagementFileService.save(rulesRegulationsManagementFile));
@@ -43,6 +46,7 @@
     * @param ids
     * @return
     */
    @Log(title = "规章制度文件", businessType = BusinessType.DELETE)
    @DeleteMapping("/del")
    public AjaxResult delQualityUnqualified(@RequestBody List<Integer> ids) {
        if(CollectionUtils.isEmpty(ids)){
src/main/java/com/ruoyi/collaborativeApproval/controller/SealApplicationManagementController.java
@@ -10,6 +10,7 @@
import com.ruoyi.collaborativeApproval.service.SealApplicationManagementService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.service.ISysNoticeService;
@@ -39,6 +40,7 @@
        return AjaxResult.success(sealApplicationManagementService.listPage(page,sealApplicationManagement));
    }
    @Log(title = "用章申请", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Operation(summary = "新增")
    public AjaxResult add(@RequestBody SealApplicationManagementDTO sealApplicationManagement){
@@ -57,6 +59,7 @@
        return AjaxResult.success();
    }
    @Log(title = "用章申请", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    @Operation(summary = "修改")
    public AjaxResult update(@RequestBody SealApplicationManagementDTO sealApplicationManagement){
@@ -68,6 +71,7 @@
        return AjaxResult.success(sealApplicationManagementService.updateById(sealApplicationManagement));
    }
    @Log(title = "用章申请", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    @Operation(summary = "删除")
    public AjaxResult delete(@PathVariable("ids") List<Long> ids){
@@ -80,6 +84,7 @@
        return AjaxResult.success(sealApplicationManagementService.removeBatchByIds(ids));
    }
    @Log(title = "用章申请", businessType = BusinessType.EXPORT)
    @Operation(summary = "用印申请管理导出")
    @PostMapping("/export")
    public void export(HttpServletResponse response) {
src/main/java/com/ruoyi/collaborativeApproval/controller/StaffContactsPersonalController.java
@@ -4,6 +4,8 @@
import com.ruoyi.collaborativeApproval.dto.StaffContactsPersonalDTO;
import com.ruoyi.collaborativeApproval.pojo.StaffContactsPersonal;
import com.ruoyi.collaborativeApproval.service.StaffContactsPersonalService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
@@ -21,12 +23,14 @@
        return AjaxResult.success(staffContactsPersonalService.listPage(page, staffContactsPersonalDTO));
    }
    @Log(title = "新增员工通讯录", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Operation(summary = "新增")
    public AjaxResult add(@RequestBody StaffContactsPersonal staffContactsPersonal) {
        return AjaxResult.success(staffContactsPersonalService.save(staffContactsPersonal));
    }
    @Log(title = "删除员工通讯录", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete/{id}")
    @Operation(summary = "删除")
    public AjaxResult delete(@PathVariable("id") Long id) {
src/main/java/com/ruoyi/collaborativeApproval/dto/EnterpriseNewsDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.ruoyi.collaborativeApproval.dto;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import lombok.Data;
import java.util.List;
@Data
public class EnterpriseNewsDto extends EnterpriseNews {
    private String createUserName;
    private List<Long> deptIds;
    private List<Long> userIds;
    private Long templateId;
    private String templateName;
    private String createTimeStart;
    private String createTimeEnd;
    private List<StorageBlobDTO> storageBlobDTOs;
}
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.collaborativeApproval.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.collaborativeApproval.dto.EnterpriseNewsDto;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import com.ruoyi.collaborativeApproval.vo.EnterpriseNewsVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
 * ä¼ä¸šæ–°é—»è¡¨ Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:50:59
 */
@Mapper
public interface EnterpriseNewsMapper extends BaseMapper<EnterpriseNews> {
    IPage<EnterpriseNewsVo> listPage(Page<EnterpriseNewsVo> page,@Param("enterpriseNewsDto") EnterpriseNewsDto enterpriseNewsDto);
}
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeDeptMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.collaborativeApproval.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围部门表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:12
 */
@Mapper
public interface EnterpriseNewsScopeDeptMapper extends BaseMapper<EnterpriseNewsScopeDept> {
}
src/main/java/com/ruoyi/collaborativeApproval/mapper/EnterpriseNewsScopeUserMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.ruoyi.collaborativeApproval.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围用户表 Mapper æŽ¥å£
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:23
 */
@Mapper
public interface EnterpriseNewsScopeUserMapper extends BaseMapper<EnterpriseNewsScopeUser> {
}
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNews.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
package com.ruoyi.collaborativeApproval.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * ä¼ä¸šæ–°é—»è¡¨
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:50:59
 */
@Getter
@Setter
@ToString
@TableName("enterprise_news")
@ApiModel(value = "EnterpriseNews对象", description = "企业新闻表")
public class EnterpriseNews implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "编号 ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "标题 Title")
    private String title;
    @Schema(description = "摘要 Summary")
    private String summary;
    @Schema(description = "正文 Content")
    private String content;
    @Schema(description = "分类 Category")
    private String category;
    @Schema(description = "阅读范围 Read scope: all å…¨å‘˜, dept éƒ¨é—¨, custom è‡ªå®šä¹‰")
    private String readScope;
    @Schema(description = "是否必读 Required flag: 0 å¦, 1 æ˜¯")
    private Byte isRequired;
    @Schema(description = "状态 Status: DRAFT è‰ç¨¿, PENDING å¾…审批, PUBLISHED å·²å‘布, REJECTED é©³å›ž, OFFLINE å·²ä¸‹çº¿")
    private String status;
    @Schema(description = "应读人数 Required read count")
    private Integer requiredReadCount;
    @Schema(description = "已读人数 Read count")
    private Integer readCount;
    @Schema(description = "创建人 Create user")
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @Schema(description = "创建时间 Create time")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss", iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime createTime;
    @Schema(description = "更新人 Update user")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    @Schema(description = "更新时间 Update time")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss", iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime updateTime;
    @Schema(description = "部门ID Dept ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeDept.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package com.ruoyi.collaborativeApproval.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围部门表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:12
 */
@Getter
@Setter
@ToString
@TableName("enterprise_news_scope_dept")
@ApiModel(value = "EnterpriseNewsScopeDept对象", description = "企业新闻阅读范围部门表")
public class EnterpriseNewsScopeDept implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ç¼–号
     */
    @ApiModelProperty("编号")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * ä¼ä¸šæ–°é—»ID
     */
    @ApiModelProperty("企业新闻ID")
    private Long newsId;
    /**
     * éƒ¨é—¨ID
     */
    @ApiModelProperty("部门ID")
    private Long deptId;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}
src/main/java/com/ruoyi/collaborativeApproval/pojo/EnterpriseNewsScopeUser.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package com.ruoyi.collaborativeApproval.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围用户表
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:23
 */
@Getter
@Setter
@ToString
@TableName("enterprise_news_scope_user")
@ApiModel(value = "EnterpriseNewsScopeUser对象", description = "企业新闻阅读范围用户表")
public class EnterpriseNewsScopeUser implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * ç¼–号
     */
    @ApiModelProperty("编号")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * ä¼ä¸šæ–°é—»ID
     */
    @ApiModelProperty("企业新闻ID")
    private Long newsId;
    /**
     * ç”¨æˆ·ID
     */
    @ApiModelProperty("用户ID")
    private Long userId;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @ApiModelProperty("创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeDeptService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.collaborativeApproval.service;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围部门表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:12
 */
public interface EnterpriseNewsScopeDeptService extends IService<EnterpriseNewsScopeDept> {
}
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsScopeUserService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.collaborativeApproval.service;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围用户表 æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:23
 */
public interface EnterpriseNewsScopeUserService extends IService<EnterpriseNewsScopeUser> {
}
src/main/java/com/ruoyi/collaborativeApproval/service/EnterpriseNewsService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package com.ruoyi.collaborativeApproval.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.collaborativeApproval.dto.EnterpriseNewsDto;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.collaborativeApproval.vo.EnterpriseNewsVo;
import java.util.List;
/**
 * <p>
 * ä¼ä¸šæ–°é—»è¡¨ æœåŠ¡ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:50:59
 */
public interface EnterpriseNewsService extends IService<EnterpriseNews> {
    IPage<EnterpriseNewsVo> listPage(Page<EnterpriseNewsVo> page, EnterpriseNewsDto enterpriseNewsDto);
    Boolean add(EnterpriseNewsDto enterpriseNewsDto);
    Boolean updateEnterpriseNewsDto(EnterpriseNewsDto enterpriseNewsDto);
    Boolean delete(List<Long> ids);
}
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeDeptServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.collaborativeApproval.service.impl;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeDeptMapper;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsScopeDeptService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围部门表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:12
 */
@Service
public class EnterpriseNewsScopeDeptServiceImpl extends ServiceImpl<EnterpriseNewsScopeDeptMapper, EnterpriseNewsScopeDept> implements EnterpriseNewsScopeDeptService {
}
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsScopeUserServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.collaborativeApproval.service.impl;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsScopeUserMapper;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsScopeUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
 * <p>
 * ä¼ä¸šæ–°é—»é˜…读范围用户表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:51:23
 */
@Service
public class EnterpriseNewsScopeUserServiceImpl extends ServiceImpl<EnterpriseNewsScopeUserMapper, EnterpriseNewsScopeUser> implements EnterpriseNewsScopeUserService {
}
src/main/java/com/ruoyi/collaborativeApproval/service/impl/EnterpriseNewsServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,409 @@
package com.ruoyi.collaborativeApproval.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.mapper.ApprovalInstanceMapper;
import com.ruoyi.approve.mapper.ApprovalTemplateMapper;
import com.ruoyi.approve.pojo.ApprovalInstance;
import com.ruoyi.approve.pojo.ApprovalTask;
import com.ruoyi.approve.pojo.ApprovalTemplate;
import com.ruoyi.approve.service.ApprovalInstanceService;
import com.ruoyi.approve.utils.ApproveProcessConfigNodeUtils;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.collaborativeApproval.dto.EnterpriseNewsDto;
import com.ruoyi.collaborativeApproval.mapper.EnterpriseNewsMapper;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeDept;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNewsScopeUser;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsScopeDeptService;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsScopeUserService;
import com.ruoyi.collaborativeApproval.service.EnterpriseNewsService;
import com.ruoyi.collaborativeApproval.vo.EnterpriseNewsVo;
import com.ruoyi.common.enums.TypeEnums;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.project.system.domain.SysDept;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysDeptMapper;
import com.ruoyi.project.system.mapper.SysUserDeptMapper;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.project.system.service.ISysNoticeService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
 * ä¼ä¸šæ–°é—»è¡¨æœåŠ¡å®žçŽ°ç±»
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-05-20 11:50:59
 */
@Service
@RequiredArgsConstructor
public class EnterpriseNewsServiceImpl extends ServiceImpl<EnterpriseNewsMapper, EnterpriseNews> implements EnterpriseNewsService {
    private static final String READ_SCOPE_ALL = "all";
    private static final String READ_SCOPE_DEPT = "dept";
    private static final String READ_SCOPE_CUSTOM = "custom";
    private static final String STATUS_DRAFT = "DRAFT";
    private static final String STATUS_PENDING = "PENDING";
    private static final String STATUS_PUBLISHED = "PUBLISHED";
    private static final String STATUS_REJECTED = "REJECTED";
    private static final String STATUS_OFFLINE = "OFFLINE";
    private final EnterpriseNewsMapper enterpriseNewsMapper;
    private final EnterpriseNewsScopeDeptService enterpriseNewsScopeDeptService;
    private final EnterpriseNewsScopeUserService enterpriseNewsScopeUserService;
    private final SysUserMapper sysUserMapper;
    private final SysDeptMapper sysDeptMapper;
    private final SysUserDeptMapper sysUserDeptMapper;
    private final ApprovalInstanceMapper approvalInstanceMapper;
    private final ApprovalInstanceService approvalInstanceService;
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final ApproveProcessConfigNodeUtils approveProcessConfigNodeUtils;
    private final ISysNoticeService sysNoticeService;
    private final FileUtil fileUtil;
    @Override
    public IPage<EnterpriseNewsVo> listPage(Page<EnterpriseNewsVo> page, EnterpriseNewsDto enterpriseNewsDto) {
        IPage<EnterpriseNewsVo> enterpriseNewsVoIPage = enterpriseNewsMapper.listPage(page, enterpriseNewsDto);
        enterpriseNewsVoIPage.getRecords().forEach(enterpriseNewsVo -> {
            enterpriseNewsVo.setStorageBlobDTOs(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.ENTERPRISE_NEWS, enterpriseNewsVo.getId()));
        });
        return enterpriseNewsVoIPage;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean add(EnterpriseNewsDto enterpriseNewsDto) {
        validateForSave(enterpriseNewsDto);
        String readScope = normalizeReadScope(enterpriseNewsDto.getReadScope());
        List<Long> deptIds = distinctIds(enterpriseNewsDto.getDeptIds());
        List<Long> userIds = distinctIds(enterpriseNewsDto.getUserIds());
        EnterpriseNews enterpriseNews = new EnterpriseNews();
        BeanUtils.copyProperties(enterpriseNewsDto, enterpriseNews);
        enterpriseNews.setReadScope(readScope);
        enterpriseNews.setIsRequired(enterpriseNewsDto.getIsRequired() == null ? (byte) 0 : enterpriseNewsDto.getIsRequired());
        enterpriseNews.setStatus(normalizeStatus(enterpriseNewsDto.getStatus(), STATUS_DRAFT));
        enterpriseNews.setReadCount(0);
        enterpriseNews.setRequiredReadCount(calculateRequiredReadCount(readScope, deptIds, userIds));
        Long[] loginDeptIds = SecurityUtils.getDeptId();
        if (StringUtils.isNotEmpty(loginDeptIds)) {
            enterpriseNews.setDeptId(loginDeptIds[0]);
        }
        if (!save(enterpriseNews) || enterpriseNews.getId() == null) {
            throw new ServiceException("新增企业新闻失败");
        }
        saveReadScopeRelations(enterpriseNews.getId(), readScope, deptIds, userIds);
        if (STATUS_PENDING.equals(enterpriseNews.getStatus())) {
            startEnterpriseNewsApproval(enterpriseNews, enterpriseNewsDto);
        }
        //添加附件
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.ENTERPRISE_NEWS, enterpriseNews.getId(), enterpriseNewsDto.getStorageBlobDTOs());
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean updateEnterpriseNewsDto(EnterpriseNewsDto enterpriseNewsDto) {
        if (enterpriseNewsDto == null || enterpriseNewsDto.getId() == null) {
            throw new ServiceException("企业新闻ID不能为空");
        }
        EnterpriseNews oldEnterpriseNews = getById(enterpriseNewsDto.getId());
        if (oldEnterpriseNews == null) {
            throw new ServiceException("企业新闻不存在");
        }
        if (!STATUS_DRAFT.equals(oldEnterpriseNews.getStatus())
                && !STATUS_REJECTED.equals(oldEnterpriseNews.getStatus())) {
            throw new ServiceException("待审批或已发布的企业新闻不允许修改");
        }
        validateForSave(enterpriseNewsDto);
        String readScope = normalizeReadScope(enterpriseNewsDto.getReadScope());
        List<Long> deptIds = distinctIds(enterpriseNewsDto.getDeptIds());
        List<Long> userIds = distinctIds(enterpriseNewsDto.getUserIds());
        EnterpriseNews enterpriseNews = new EnterpriseNews();
        BeanUtils.copyProperties(enterpriseNewsDto, enterpriseNews);
        enterpriseNews.setReadScope(readScope);
        enterpriseNews.setIsRequired(enterpriseNewsDto.getIsRequired() == null ? oldEnterpriseNews.getIsRequired() : enterpriseNewsDto.getIsRequired());
        enterpriseNews.setStatus(normalizeStatus(enterpriseNewsDto.getStatus(), oldEnterpriseNews.getStatus()));
        enterpriseNews.setReadCount(oldEnterpriseNews.getReadCount());
        enterpriseNews.setRequiredReadCount(calculateRequiredReadCount(readScope, deptIds, userIds));
        enterpriseNews.setCreateUser(oldEnterpriseNews.getCreateUser());
        enterpriseNews.setCreateTime(oldEnterpriseNews.getCreateTime());
        enterpriseNews.setDeptId(oldEnterpriseNews.getDeptId());
        if (!updateById(enterpriseNews)) {
            throw new ServiceException("修改企业新闻失败");
        }
        clearReadScopeRelations(enterpriseNews.getId());
        saveReadScopeRelations(enterpriseNews.getId(), readScope, deptIds, userIds);
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.ENTERPRISE_NEWS, enterpriseNews.getId(), enterpriseNewsDto.getStorageBlobDTOs());
        if (STATUS_PENDING.equals(enterpriseNews.getStatus())) {
            resetEnterpriseNewsApprovalFlow(oldEnterpriseNews);
            startEnterpriseNewsApproval(enterpriseNews, enterpriseNewsDto);
        }
        return true;
    }
    @Override
    public Boolean delete(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return false;
        }
        if (!removeByIds(ids)) {
            throw new ServiceException("删除企业新闻失败");
        }
        ids.forEach(this::clearReadScopeRelations);
        return true;
    }
    private void validateForSave(EnterpriseNewsDto enterpriseNewsDto) {
        if (enterpriseNewsDto == null) {
            throw new ServiceException("企业新闻数据不能为空");
        }
        if (StringUtils.isEmpty(enterpriseNewsDto.getTitle())) {
            throw new ServiceException("标题不能为空");
        }
        if (StringUtils.isEmpty(enterpriseNewsDto.getContent())) {
            throw new ServiceException("正文不能为空");
        }
        normalizeStatus(enterpriseNewsDto.getStatus(), STATUS_DRAFT);
        String readScope = normalizeReadScope(enterpriseNewsDto.getReadScope());
        List<Long> deptIds = distinctIds(enterpriseNewsDto.getDeptIds());
        List<Long> userIds = distinctIds(enterpriseNewsDto.getUserIds());
        if (READ_SCOPE_DEPT.equals(readScope) && StringUtils.isEmpty(deptIds)) {
            throw new ServiceException("请选择阅读范围部门");
        }
        if (READ_SCOPE_CUSTOM.equals(readScope) && StringUtils.isEmpty(userIds)) {
            throw new ServiceException("请选择自定义阅读人员");
        }
        validateDeptIds(deptIds);
        validateUserIds(userIds);
    }
    private String normalizeReadScope(String readScope) {
        String normalized = StringUtils.isEmpty(readScope) ? READ_SCOPE_ALL : readScope.trim();
        if (!READ_SCOPE_ALL.equals(normalized)
                && !READ_SCOPE_DEPT.equals(normalized)
                && !READ_SCOPE_CUSTOM.equals(normalized)) {
            throw new ServiceException("阅读范围不合法");
        }
        return normalized;
    }
    private String normalizeStatus(String status, String defaultStatus) {
        String normalized = StringUtils.isEmpty(status) ? defaultStatus : status.trim().toUpperCase();
        if (!STATUS_DRAFT.equals(normalized)
                && !STATUS_PENDING.equals(normalized)
                && !STATUS_PUBLISHED.equals(normalized)
                && !STATUS_REJECTED.equals(normalized)
                && !STATUS_OFFLINE.equals(normalized)) {
            throw new ServiceException("企业新闻状态不合法");
        }
        return normalized;
    }
    private void validateDeptIds(List<Long> deptIds) {
        if (StringUtils.isEmpty(deptIds)) {
            return;
        }
        for (Long deptId : deptIds) {
            SysDept sysDept = sysDeptMapper.selectDeptById(deptId);
            if (deptId == null || sysDept == null) {
                throw new ServiceException("阅读范围部门不存在");
            }
        }
    }
    private void validateUserIds(List<Long> userIds) {
        if (StringUtils.isEmpty(userIds)) {
            return;
        }
        List<SysUser> users = sysUserMapper.selectUsersByIds(userIds);
        if (users.size() != userIds.size()) {
            throw new ServiceException("自定义阅读人员包含无效用户");
        }
    }
    private Integer calculateRequiredReadCount(String readScope, List<Long> deptIds, List<Long> userIds) {
        if (READ_SCOPE_ALL.equals(readScope)) {
            Long count = sysUserMapper.selectCount(new LambdaQueryWrapper<SysUser>()
                    .eq(SysUser::getDelFlag, "0"));
            return count == null ? 0 : count.intValue();
        }
        if (READ_SCOPE_DEPT.equals(readScope)) {
            List<Long> allDeptIds = collectDeptIdsWithChildren(deptIds);
            if (StringUtils.isEmpty(allDeptIds)) {
                return 0;
            }
            Long count = sysUserDeptMapper.countDistinctUserIdsByDeptIds(allDeptIds);
            return count == null ? 0 : count.intValue();
        }
        return userIds.size();
    }
    private List<Long> collectDeptIdsWithChildren(List<Long> deptIds) {
        Set<Long> allDeptIds = new LinkedHashSet<>();
        for (Long deptId : deptIds) {
            if (deptId == null) {
                continue;
            }
            allDeptIds.add(deptId);
            List<SysDept> children = sysDeptMapper.selectChildrenDeptById(deptId);
            if (StringUtils.isNotEmpty(children)) {
                for (SysDept child : children) {
                    allDeptIds.add(child.getDeptId());
                }
            }
        }
        return new ArrayList<>(allDeptIds);
    }
    private void saveReadScopeRelations(Long newsId, String readScope, List<Long> deptIds, List<Long> userIds) {
        if (READ_SCOPE_DEPT.equals(readScope)) {
            List<EnterpriseNewsScopeDept> scopeDeptList = new ArrayList<>();
            for (Long deptId : deptIds) {
                EnterpriseNewsScopeDept scopeDept = new EnterpriseNewsScopeDept();
                scopeDept.setNewsId(newsId);
                scopeDept.setDeptId(deptId);
                scopeDeptList.add(scopeDept);
            }
            if (StringUtils.isNotEmpty(scopeDeptList)) {
                enterpriseNewsScopeDeptService.saveBatch(scopeDeptList);
            }
            return;
        }
        if (READ_SCOPE_CUSTOM.equals(readScope)) {
            List<EnterpriseNewsScopeUser> scopeUserList = new ArrayList<>();
            for (Long userId : userIds) {
                EnterpriseNewsScopeUser scopeUser = new EnterpriseNewsScopeUser();
                scopeUser.setNewsId(newsId);
                scopeUser.setUserId(userId);
                scopeUserList.add(scopeUser);
            }
            if (StringUtils.isNotEmpty(scopeUserList)) {
                enterpriseNewsScopeUserService.saveBatch(scopeUserList);
            }
        }
    }
    private void clearReadScopeRelations(Long newsId) {
        enterpriseNewsScopeDeptService.remove(new LambdaQueryWrapper<EnterpriseNewsScopeDept>()
                .eq(EnterpriseNewsScopeDept::getNewsId, newsId));
        enterpriseNewsScopeUserService.remove(new LambdaQueryWrapper<EnterpriseNewsScopeUser>()
                .eq(EnterpriseNewsScopeUser::getNewsId, newsId));
    }
    private void resetEnterpriseNewsApprovalFlow(EnterpriseNews oldEnterpriseNews) {
        if (oldEnterpriseNews == null || !STATUS_DRAFT.equals(oldEnterpriseNews.getStatus())) {
            return;
        }
        List<Long> approvalInstanceIds = approvalInstanceMapper.selectList(new LambdaQueryWrapper<ApprovalInstance>()
                        .eq(ApprovalInstance::getBusinessId, oldEnterpriseNews.getId())
                        .eq(ApprovalInstance::getBusinessType, TypeEnums.ENTERPRISE_NEWS_APPROVAL.getCode())
                        .eq(ApprovalInstance::getDeleted, (byte) 0))
                .stream()
                .map(ApprovalInstance::getId)
                .filter(id -> id != null && id > 0)
                .collect(Collectors.toList());
        if (StringUtils.isEmpty(approvalInstanceIds)) {
            return;
        }
        approvalInstanceService.delete(approvalInstanceIds);
    }
    private List<Long> distinctIds(List<Long> ids) {
        if (StringUtils.isEmpty(ids)) {
            return new ArrayList<>();
        }
        Set<Long> distinctSet = new LinkedHashSet<>();
        for (Long id : ids) {
            if (id != null) {
                distinctSet.add(id);
            }
        }
        return new ArrayList<>(distinctSet);
    }
    private void startEnterpriseNewsApproval(EnterpriseNews enterpriseNews, EnterpriseNewsDto enterpriseNewsDto) {
        if (enterpriseNewsDto.getTemplateId() == null) {
            throw new ServiceException("审批模板不能为空");
        }
        String templateName = enterpriseNewsDto.getTemplateName();
        if (StringUtils.isEmpty(templateName)) {
            ApprovalTemplate approvalTemplate = approvalTemplateMapper.selectById(enterpriseNewsDto.getTemplateId());
            if (approvalTemplate == null) {
                throw new ServiceException("审批模板不存在");
            }
            templateName = approvalTemplate.getTemplateName();
        }
        ApprovalInstance approvalInstance = new ApprovalInstance();
        approvalInstance.setInstanceNo(OrderUtils.countTodayByCreateTime(approvalInstanceMapper, "SP", "instance_no", enterpriseNews.getCreateTime() != null ? enterpriseNews.getCreateTime() : LocalDateTime.now()));
        approvalInstance.setTemplateId(enterpriseNewsDto.getTemplateId());
        approvalInstance.setTemplateName(templateName);
        approvalInstance.setBusinessId(enterpriseNews.getId());
        approvalInstance.setBusinessType(TypeEnums.ENTERPRISE_NEWS_APPROVAL.getCode());
        approvalInstance.setTitle(enterpriseNews.getTitle());
        approvalInstance.setStatus("PENDING");
        approvalInstance.setCurrentLevel(1);
        approvalInstance.setApplicantId(SecurityUtils.getUserId());
        approvalInstance.setApplicantName(SecurityUtils.getLoginUser().getNickName());
        approvalInstance.setApplyTime(LocalDateTime.now());
        approvalInstance.setDeleted((byte) 0);
        approvalInstance.setCreateUser(SecurityUtils.getUserId());
        approvalInstance.setUpdateUser(SecurityUtils.getUserId());
        approvalInstance.setDeptId(enterpriseNews.getDeptId());
        approvalInstanceMapper.insert(approvalInstance);
        approveProcessConfigNodeUtils.createCurrentNodeAndTasks(approvalInstance);
        sendApproveNotice(approvalInstance, approveProcessConfigNodeUtils.getCurrentPendingTasks(approvalInstance.getId()));
    }
    private void sendApproveNotice(ApprovalInstance instance, List<ApprovalTask> tasks) {
        if (instance == null || tasks == null || tasks.isEmpty()) {
            return;
        }
        List<Long> approverIds = tasks.stream()
                .map(ApprovalTask::getApproverId)
                .filter(id -> id != null && id > 0)
                .distinct()
                .collect(Collectors.toList());
        if (approverIds.isEmpty()) {
            return;
        }
        String title = StringUtils.isNotEmpty(instance.getTemplateName()) ? instance.getTemplateName() : "审批提醒";
        String message = "审批单号 " + instance.getInstanceNo() + " éœ€è¦æ‚¨å®¡æ‰¹";
        String jumpPath = "/officeProcessAutomation/ApproveManage/approve-list/?id=" + instance.getId();
        sysNoticeService.simpleNoticeByUser(title, message, approverIds, jumpPath);
    }
}
src/main/java/com/ruoyi/collaborativeApproval/vo/EnterpriseNewsVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package com.ruoyi.collaborativeApproval.vo;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.collaborativeApproval.pojo.EnterpriseNews;
import lombok.Data;
import java.util.List;
@Data
public class EnterpriseNewsVo extends EnterpriseNews {
    private String createUserName;
    private List<StorageBlobVO> storageBlobDTOs;
}
src/main/java/com/ruoyi/common/enums/ApprovalStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package com.ruoyi.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å®¡æ‰¹çŠ¶æ€æžšä¸¾
 */
@Getter
@AllArgsConstructor
public enum ApprovalStatusEnum implements BaseEnum<Integer> {
    DRAFT(0, "草稿"),
    PENDING(1, "待审批"),
    IN_PROGRESS(2, "审批中"),
    APPROVED(3, "已通过"),
    REJECTED(4, "已驳回");
    private final Integer value;
    private final String label;
    @Override
    public Integer getCode() {
        return value;
    }
    @Override
    public String getValue() {
        return label;
    }
    public static ApprovalStatusEnum fromValue(Integer value) {
        if (value == null) {
            return null;
        }
        for (ApprovalStatusEnum status : values()) {
            if (status.getCode().equals(value)) {
                return status;
            }
        }
        return null;
    }
    public static String getLabelByValue(Integer value) {
        ApprovalStatusEnum statusEnum = fromValue(value);
        return statusEnum != null ? statusEnum.getValue() : "未知状态";
    }
}
src/main/java/com/ruoyi/common/enums/EnterpriseNewsStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,143 @@
package com.ruoyi.common.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
 * ä¼ä¸šæ–°é—»çŠ¶æ€æžšä¸¾ç±»
 *
 * @author ruoyi
 * @date 2026-05-20
 */
@Schema(description = "企业新闻状态枚举")
public enum EnterpriseNewsStatusEnum implements BaseEnum<String> {
    /**
     * è‰ç¨¿
     */
    @Schema(description = "草稿")
    DRAFT("DRAFT", "草稿"),
    /**
     * å¾…审批
     */
    @Schema(description = "待审批")
    PENDING("PENDING", "待审批"),
    /**
     * å·²å‘布
     */
    @Schema(description = "已发布")
    PUBLISHED("PUBLISHED", "已发布"),
    /**
     * é©³å›ž
     */
    @Schema(description = "驳回")
    REJECTED("REJECTED", "驳回"),
    /**
     * å·²ä¸‹çº¿
     */
    @Schema(description = "已下线")
    OFFLINE("OFFLINE", "已下线");
    /**
     * çŠ¶æ€ç 
     */
    private final String code;
    /**
     * çŠ¶æ€æè¿°
     * -- GETTER --
     *  èŽ·å–çŠ¶æ€æè¿°
     *
     * @return çŠ¶æ€æè¿°
     */
    @Getter
    private final String description;
    EnterpriseNewsStatusEnum(String code, String description) {
        this.code = code;
        this.description = description;
    }
    /**
     * èŽ·å–çŠ¶æ€ç 
     *
     * @return çŠ¶æ€ç 
     */
    @JsonValue
    public String getCode() {
        return code;
    }
    @Override
    public String getValue() {
        return "";
    }
    /**
     * æ ¹æ®çŠ¶æ€ç èŽ·å–æžšä¸¾
     *
     * @param code çŠ¶æ€ç 
     * @return æžšä¸¾å€¼
     */
    @JsonCreator
    public static EnterpriseNewsStatusEnum getByCode(String code) {
        for (EnterpriseNewsStatusEnum status : values()) {
            if (status.code.equals(code)) {
                return status;
            }
        }
        throw new IllegalArgumentException("Invalid enterprise news status code: " + code);
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºè‰ç¨¿çŠ¶æ€
     *
     * @return æ˜¯å¦ä¸ºè‰ç¨¿çŠ¶æ€
     */
    public boolean isDraft() {
        return DRAFT.equals(this);
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºå¾…审批状态
     *
     * @return æ˜¯å¦ä¸ºå¾…审批状态
     */
    public boolean isPending() {
        return PENDING.equals(this);
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºå·²å‘布状态
     *
     * @return æ˜¯å¦ä¸ºå·²å‘布状态
     */
    public boolean isPublished() {
        return PUBLISHED.equals(this);
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºé©³å›žçŠ¶æ€
     *
     * @return æ˜¯å¦ä¸ºé©³å›žçŠ¶æ€
     */
    public boolean isRejected() {
        return REJECTED.equals(this);
    }
    /**
     * åˆ¤æ–­æ˜¯å¦ä¸ºå·²ä¸‹çº¿çŠ¶æ€
     *
     * @return æ˜¯å¦ä¸ºå·²ä¸‹çº¿çŠ¶æ€
     */
    public boolean isOffline() {
        return OFFLINE.equals(this);
    }
}
src/main/java/com/ruoyi/common/enums/SalesQuotationStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package com.ruoyi.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * é”€å”®æŠ¥ä»·çŠ¶æ€æžšä¸¾
 */
@Getter
@AllArgsConstructor
public enum SalesQuotationStatusEnum implements BaseEnum<String> {
    DRAFT("草稿", "草稿"),
    PENDING("待审批", "待审批"),
    IN_PROGRESS("审核中", "审核中"),
    APPROVED("通过", "通过"),
    REJECTED("拒绝", "拒绝");
    private final String value;
    private final String label;
    @Override
    public String getCode() {
        return value;
    }
    @Override
    public String getValue() {
        return label;
    }
    public static SalesQuotationStatusEnum fromValue(String value) {
        if (value == null) {
            return null;
        }
        for (SalesQuotationStatusEnum status : values()) {
            if (status.getCode().equals(value)) {
                return status;
            }
        }
        return null;
    }
}
src/main/java/com/ruoyi/common/enums/ShippingStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package com.ruoyi.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * å‘货审批状态枚举
 */
@Getter
@AllArgsConstructor
public enum ShippingStatusEnum implements BaseEnum<String> {
    PENDING("待确认", "待确认"),
    IN_PROGRESS("审核中", "审核中"),
    APPROVED("审核通过", "审核通过"),
    REJECTED("审核拒绝", "审核拒绝");
    private final String value;
    private final String label;
    @Override
    public String getCode() {
        return value;
    }
    @Override
    public String getValue() {
        return label;
    }
    public static ShippingStatusEnum fromValue(String value) {
        if (value == null) {
            return null;
        }
        for (ShippingStatusEnum status : values()) {
            if (status.getCode().equals(value)) {
                return status;
            }
        }
        return null;
    }
}
src/main/java/com/ruoyi/common/enums/TypeEnums.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
package com.ruoyi.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum TypeEnums implements BaseEnum<Long> {
    PUBLIC_OUT(1L, "公出管理"),
    LEAVE(2L, "请假管理"),
    BUSINESS_TRIP(3L, "出差管理"),
    REIMBURSEMENT(4L, "报销管理"),
    PURCHASE_APPROVAL(5L, "采购审批"),
    QUOTATION_APPROVAL(6L, "报价审批"),
    SHIPPING_APPROVAL(7L, "发货审批"),
    DANGEROUS_OPERATION(8L, "危险作业审批"),
    OFFICE_SUPPLIES(9L, "办公用品审批"),
    REGULARIZATION_APPROVAL(10L, "转正审批"),
    TRANSFER_APPROVAL(11L, "调动审批"),
    RESIGNATION_APPROVAL(12L, "离职审批"),
    WORK_HANDOVER_APPROVAL(13L, "工作交接审批"),
    LEAVE_APPROVAL(14L, "请假审批"),
    OVERTIME_APPROVAL(15L, "加班审批"),
    TRAVEL_REIMBURSEMENT_APPROVAL(16L, "出差报销审批"),
    EXPENSE_APPROVAL(17L, "费用审批"),
    ENTERPRISE_NEWS_APPROVAL(18L, "企业新闻审批");
    private final Long value;
    private final String label;
    @Override
    public Long getCode() {
        return value;
    }
    @Override
    public String getValue() {
        return label;
    }
    /**
     * æ ¹æ®å€¼èŽ·å–å¯¹åº”çš„æžšä¸¾
     * @param value ä¸šåŠ¡ç±»åž‹å€¼
     * @return å¯¹åº”的枚举,未匹配返回null
     */
    public static TypeEnums fromValue(Long value) {
        if (value == null) {
            return null;
        }
        for (TypeEnums type : values()) {
            if (type.getCode().equals(value)) {
                return type;
            }
        }
        return null;
    }
    /**
     * æ ¹æ®å€¼èŽ·å–æè¿°
     * @param value ä¸šåŠ¡ç±»åž‹å€¼
     * @return ä¸šåŠ¡ç±»åž‹æè¿°ï¼ŒæœªåŒ¹é…è¿”å›ž"自定义审批"
     */
    public static String getLabelByValue(Long value) {
        TypeEnums typeEnum = fromValue(value);
        return typeEnum != null ? typeEnum.getValue() : "自定义审批";
    }
}
src/main/java/com/ruoyi/common/utils/OrderUtils.java
@@ -61,8 +61,11 @@
     * @param <T> å®žä½“类型
     * @return è®¢å•编号
     */
    public static <T> String countTodayByCreateTime(BaseMapper<T> mapper,String preFix,String code) {
        LocalDate today = LocalDate.now();
    public static <T> String countTodayByCreateTime(BaseMapper<T> mapper,String preFix,String code, LocalDateTime createTime) {
        if (createTime == null) {
            createTime = LocalDateTime.now();
        }
        LocalDate today = createTime.toLocalDate();
        LocalDateTime todayStart = today.atStartOfDay();
        LocalDateTime tomorrowStart = today.plusDays(1).atStartOfDay();
        String dateStr = today.format(DateTimeFormatter.BASIC_ISO_DATE);
@@ -109,13 +112,17 @@
     * @param <T> å®žä½“类泛型
     * @return å½“天记录数量
     */
    public static <T> String countAfterServiceTodayByCreateTime(BaseMapper<T> mapper,String preFix) {
    public static <T> String countAfterServiceTodayByCreateTime(BaseMapper<T> mapper,String preFix, LocalDateTime createTime) {
        if (createTime == null) {
            createTime = LocalDateTime.now();
        }
        LocalDate localDate = createTime.toLocalDate();
        LocalDateTime todayStart = LocalDateTime.of(
                LocalDateTime.now().toLocalDate(),
                localDate,
                LocalTime.MIN
        );
        LocalDateTime todayEnd = LocalDateTime.of(
                LocalDateTime.now().toLocalDate(),
                localDate,
                LocalTime.MAX
        );
@@ -127,6 +134,6 @@
                .lt("create_time", endDate);
        Long aLong = mapper.selectCount(queryWrapper);
        return preFix + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE).replaceAll("-", "") + String.format("%03d", (aLong + 1));
        return preFix + localDate.format(DateTimeFormatter.ISO_LOCAL_DATE).replaceAll("-", "") + String.format("%03d", (aLong + 1));
    }
}
src/main/java/com/ruoyi/customervisits/service/impl/CustomerVisitsServiceImpl.java
@@ -37,7 +37,7 @@
                wrapper.like(CustomerVisits::getVisitingPeople, customerVisits.getVisitingPeople());
            }
        }
        wrapper.orderByDesc(CustomerVisits::getId);
        return customerVisitsMapper.selectPage(page, wrapper);
    }
src/main/java/com/ruoyi/device/controller/DeviceDefectRecordController.java
@@ -4,6 +4,8 @@
import com.ruoyi.device.dto.DeviceDefectRecordDto;
import com.ruoyi.device.pojo.DeviceDefectRecord;
import com.ruoyi.device.service.DeviceDefectRecordService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -29,16 +31,19 @@
        return AjaxResult.success(deviceDefectRecordService.listPage(new Page<>(1,-1),deviceDefectRecordDto));
    }
    @Log(title = "新增设备缺陷记录", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Operation(summary = "添加设备缺陷记录")
    public AjaxResult add(@RequestBody DeviceDefectRecord deviceDefectRecord) {
        return AjaxResult.success(deviceDefectRecordService.add(deviceDefectRecord));
    }
    @Log(title = "修改设备缺陷记录", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    @Operation(summary = "修改设备缺陷记录")
    public AjaxResult update(@RequestBody DeviceDefectRecord deviceDefectRecord) {
        return AjaxResult.success(deviceDefectRecordService.updateByDDR(deviceDefectRecord));
    }
    @Log(title = "删除设备缺陷记录", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    @Operation(summary = "删除设备缺陷记录")
    public AjaxResult delete(@PathVariable Long id) {
src/main/java/com/ruoyi/device/controller/DeviceLedgerController.java
@@ -12,6 +12,8 @@
import com.ruoyi.device.pojo.DeviceMaintenance;
import com.ruoyi.device.service.IDeviceLedgerService;
import com.ruoyi.framework.aspectj.lang.annotation.Anonymous;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -35,32 +37,34 @@
    private DeviceMaintenanceMapper deviceMaintenanceMapper;
    @Operation(summary = "设备台账列表")
    @GetMapping("/page")
    public AjaxResult page(Page page , DeviceLedgerDto deviceLedger) {
        return AjaxResult.success(deviceLedgerService.queryPage(page,deviceLedger));
    public AjaxResult page(Page page, DeviceLedgerDto deviceLedger) {
        return AjaxResult.success(deviceLedgerService.queryPage(page, deviceLedger));
    }
    @Log(title = "新增设备台账", businessType = BusinessType.INSERT)
    @PostMapping()
    @Operation(summary = "添加设备台账")
    public AjaxResult add(@RequestBody DeviceLedger deviceLedger) {
        return deviceLedgerService.saveDeviceLedger(deviceLedger);
    public AjaxResult add(@RequestBody DeviceLedgerDto deviceLedgerDto) {
        return deviceLedgerService.saveDeviceLedger(deviceLedgerDto);
    }
    @Operation(summary = "根据id查询设备台账")
    @GetMapping("/{id}")
    public AjaxResult detail(@PathVariable Long id) {
        return AjaxResult.success(deviceLedgerService.getById(id));
        DeviceLedgerDto deviceLedgerDto = deviceLedgerService.getDeviceLedgerDetail(id);
        return AjaxResult.success(deviceLedgerDto);
    }
    @PutMapping ()
    @Log(title = "修改设备台账", businessType = BusinessType.UPDATE)
    @PutMapping()
    @Operation(summary = "修改设备台账")
    public AjaxResult update(@RequestBody DeviceLedger deviceLedger) {
        return deviceLedgerService.updateDeviceLedger(deviceLedger);
    public AjaxResult update(@RequestBody DeviceLedgerDto deviceLedgerDto) {
        return deviceLedgerService.updateDeviceLedger(deviceLedgerDto);
    }
    @Log(title = "删除设备台账", businessType = BusinessType.DELETE)
    @DeleteMapping("/{ids}")
    @Operation(summary = "删除设备台账")
    public AjaxResult delete(@PathVariable("ids") ArrayList<Long> ids) {
@@ -71,12 +75,14 @@
        return AjaxResult.success();
    }
    @Log(title = "导出设备台账", businessType = BusinessType.EXPORT)
    @PostMapping("export")
    @Operation(summary = "导出设备台账")
    public void export(HttpServletResponse response, Long[] ids) {
         deviceLedgerService.export(response, ids);
        deviceLedgerService.export(response, ids);
    }
    @Log(title = "下载设备台账模板", businessType = BusinessType.EXPORT)
    @Operation(summary = "下载模板")
    @PostMapping("/downloadTemplate")
    public void downloadTemplate(HttpServletResponse response) {
@@ -84,6 +90,7 @@
        util.importTemplateExcel(response, "设备导入模板");
    }
    @Log(title = "导入设备台账", businessType = BusinessType.IMPORT)
    @PostMapping("/import")
    @Operation(summary = "导入设备台账")
    public AjaxResult importData(MultipartFile file) throws IOException {
@@ -97,9 +104,9 @@
    @GetMapping("getDeviceLedger")
    @Operation(summary = "获取设备台账")
    public AjaxResult getDeviceLedger( ) {
    public AjaxResult getDeviceLedger() {
        return AjaxResult.success(deviceLedgerService.list(new QueryWrapper<DeviceLedger>().lambda()
                .select(DeviceLedger::getId, DeviceLedger::getDeviceName,DeviceLedger::getDeviceModel)));
                .select(DeviceLedger::getId, DeviceLedger::getDeviceName, DeviceLedger::getDeviceModel)));
    }
    @GetMapping("scanDevice")
@@ -108,8 +115,8 @@
    public AjaxResult scanDevice(Long id) {
        List<DeviceMaintenance> list = deviceMaintenanceMapper.list1(id);
        DeviceLedger deviceLedger = deviceLedgerMapper.selectById1(id);
        if (list.size()>0){
            deviceLedger.setUpdateTime(list.get(0).getMaintenanceActuallyTime());//最后维护时间
        if (!list.isEmpty()) {
            deviceLedger.setUpdateTime(list.getFirst().getMaintenanceActuallyTime());//最后维护时间
        }
        deviceLedger.setCreateTime(deviceLedger.getUpdateTime().plusMonths(1));//下次维护时间
        return AjaxResult.success(deviceLedger);
src/main/java/com/ruoyi/device/controller/DeviceMaintenanceController.java
@@ -7,6 +7,8 @@
import com.ruoyi.device.pojo.DeviceMaintenance;
import com.ruoyi.device.service.IDeviceLedgerService;
import com.ruoyi.device.service.IDeviceMaintenanceService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -32,6 +34,7 @@
        return AjaxResult.success(deviceMaintenanceService.queryPage(page,deviceMaintenanceDto));
    }
    @Log(title = "新增设备保养", businessType = BusinessType.INSERT)
    @PostMapping()
    @Operation(summary = "添加设备保养")
    public AjaxResult add(@RequestBody DeviceMaintenanceDto deviceMaintenance) {
@@ -47,6 +50,7 @@
        return AjaxResult.success(deviceMaintenanceService.detailById(id));
    }
    @Log(title = "修改设备保养", businessType = BusinessType.UPDATE)
    @PutMapping ()
    @Operation(summary = "修改设备保养")
    public AjaxResult update(@RequestBody DeviceMaintenanceDto deviceMaintenance) {
@@ -56,6 +60,7 @@
        return deviceMaintenanceService.updateDeviceDeviceMaintenance(deviceMaintenance);
    }
    @Log(title = "执行设备保养", businessType = BusinessType.UPDATE)
    @PostMapping ("maintenance")
    @Operation(summary = "修改设备保养")
    public AjaxResult maintenance(@RequestBody DeviceMaintenanceDto deviceMaintenance) {
@@ -63,6 +68,7 @@
    }
    @Log(title = "删除设备保养", businessType = BusinessType.DELETE)
    @DeleteMapping("/{ids}")
    @Operation(summary = "删除设备保养")
    public AjaxResult delete(@PathVariable("ids") Long[] ids) {
@@ -73,6 +79,7 @@
        return AjaxResult.success();
    }
    @Log(title = "导出设备保养", businessType = BusinessType.EXPORT)
    @PostMapping("export")
    @Operation(summary = "导出设备保养")
    public void export(HttpServletResponse response, Long[] ids) {
src/main/java/com/ruoyi/device/controller/DeviceMaintenanceFileController.java
@@ -4,6 +4,8 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.device.pojo.DeviceMaintenanceFile;
import com.ruoyi.device.service.DeviceMaintenanceFileService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.util.CollectionUtils;
@@ -34,6 +36,7 @@
     * @param deviceMaintenanceFile
     * @return
     */
    @Log(title = "新增设备保养附件", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody DeviceMaintenanceFile deviceMaintenanceFile) {
        return AjaxResult.success(deviceMaintenanceFileService.save(deviceMaintenanceFile));
@@ -44,6 +47,7 @@
     * @param ids
     * @return
     */
    @Log(title = "删除设备保养附件", businessType = BusinessType.DELETE)
    @DeleteMapping("/del")
    public AjaxResult delQualityUnqualified(@RequestBody List<Integer> ids) {
        if(CollectionUtils.isEmpty(ids)){
src/main/java/com/ruoyi/device/controller/DeviceRepairController.java
@@ -5,6 +5,8 @@
import com.ruoyi.device.dto.DeviceRepairDto;
import com.ruoyi.device.pojo.DeviceRepair;
import com.ruoyi.device.service.IDeviceRepairService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -28,6 +30,7 @@
        return AjaxResult.success(deviceRepairService.queryPage(page,deviceRepairDto));
    }
    @Log(title = "新增设备报修", businessType = BusinessType.INSERT)
    @PostMapping()
    @Operation(summary = "添加设备报修")
    public AjaxResult add( @RequestBody DeviceRepairDto deviceRepairDto) {
@@ -40,24 +43,28 @@
        return AjaxResult.success(deviceRepairService.detailById(id));
    }
    @Log(title = "修改设备报修", businessType = BusinessType.UPDATE)
    @PutMapping ()
    @Operation(summary = "修改设备报修")
    public AjaxResult update( @RequestBody DeviceRepairDto deviceRepairDto) {
        return deviceRepairService.updateDeviceRepair(deviceRepairDto);
    }
    @Log(title = "设备维修", businessType = BusinessType.UPDATE)
    @PostMapping ("/repair")
    @Operation(summary = "设备维修")
    public AjaxResult repair( @RequestBody DeviceRepairDto deviceRepairDto) {
        return deviceRepairService.confirmRepair(deviceRepairDto);
    }
    @Log(title = "设备报修验收", businessType = BusinessType.UPDATE)
    @PostMapping ("/acceptance")
    @Operation(summary = "设备报修验收审批")
    public AjaxResult acceptance(@RequestBody DeviceRepairDto deviceRepairDto) {
        return deviceRepairService.approveRepairAcceptance(deviceRepairDto);
    }
    @Log(title = "删除设备报修", businessType = BusinessType.DELETE)
    @DeleteMapping("/{ids}")
    @Operation(summary = "删除设备报修")
    public AjaxResult delete(@PathVariable("ids") Long[] ids) {
@@ -68,6 +75,7 @@
        return AjaxResult.success();
    }
    @Log(title = "导出设备报修", businessType = BusinessType.EXPORT)
    @PostMapping("export")
    @Operation(summary = "导出设备报修")
    public void export(HttpServletResponse response, Long[] ids) {
src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java
@@ -12,6 +12,9 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.basic.dto.StorageBlobVO;
/**
 * è®¾å¤‡å°è´¦å®žä½“ç±»
@@ -55,6 +58,20 @@
    private String supplierName;
    /**
     * è®¾å¤‡é™„ä»¶(用于接收)
     */
    @TableField(exist = false)
    @Schema(description = "设备附件接收列表")
    private List<StorageBlobDTO> storageBlobDTOs;
    /**
     * è®¾å¤‡é™„ä»¶(用于返回)
     */
    @TableField(exist = false)
    @Schema(description = "设备附件展示列表")
    private List<StorageBlobVO> storageBlobVOs;
    /**
     * å•位
     */
    private String unit;
src/main/java/com/ruoyi/device/pojo/MaintenanceTask.java
@@ -62,8 +62,8 @@
    @Schema(description = "最后执行时间")
    private LocalDateTime lastExecutionTime;
    @Schema(description = "是否激活")
    private boolean isActive;
    @Schema(description = "是否激活, 1=启用, 0=停用")
    private Integer isActive;
    @Schema(description = "备注")
    @Excel(name = "备注")
src/main/java/com/ruoyi/device/service/IDeviceLedgerService.java
@@ -14,9 +14,11 @@
public interface IDeviceLedgerService  extends IService<DeviceLedger> {
    IPage<DeviceLedgerDto> queryPage(Page page, DeviceLedgerDto deviceLedger);
    AjaxResult saveDeviceLedger(DeviceLedger deviceLedger);
    AjaxResult saveDeviceLedger(DeviceLedgerDto deviceLedgerDto);
    AjaxResult updateDeviceLedger(DeviceLedger deviceLedger);
    AjaxResult updateDeviceLedger(DeviceLedgerDto deviceLedgerDto);
    DeviceLedgerDto getDeviceLedgerDetail(Long id);
    void export(HttpServletResponse response, Long[] ids);
src/main/java/com/ruoyi/device/service/impl/DeviceLedgerServiceImpl.java
@@ -12,6 +12,9 @@
import com.ruoyi.device.execl.DeviceLedgerExeclDto;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.basic.dto.StorageAttachmentDTO;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.service.StorageAttachmentService;
import com.ruoyi.device.service.IDeviceLedgerService;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.domain.SysUser;
@@ -36,6 +39,7 @@
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final SysUserMapper sysUserMapper;
    private final StorageAttachmentService storageAttachmentService;
    @Override
    public IPage<DeviceLedgerDto> queryPage(Page page, DeviceLedgerDto deviceLedger) {
@@ -44,25 +48,61 @@
    }
    @Override
    public AjaxResult saveDeviceLedger(DeviceLedger deviceLedger) {
    public AjaxResult saveDeviceLedger(DeviceLedgerDto deviceLedgerDto) {
        LambdaQueryWrapper<DeviceLedger> deviceLedgerLambdaQueryWrapper = new LambdaQueryWrapper<>();
        deviceLedgerLambdaQueryWrapper.eq(DeviceLedger::getDeviceName,deviceLedger.getDeviceName());
        deviceLedgerLambdaQueryWrapper.eq(DeviceLedger::getDeviceName,deviceLedgerDto.getDeviceName());
        if (this.count(deviceLedgerLambdaQueryWrapper) > 0) {
            return AjaxResult.error("设备名称已存在");
        }
        DeviceLedger deviceLedger = new DeviceLedger();
        BeanUtils.copyProperties(deviceLedgerDto, deviceLedger);
        boolean save = this.save(deviceLedger);
        if (save){
            if (deviceLedgerDto.getStorageBlobDTOs() != null) {
                StorageAttachmentDTO attachmentDTO = new StorageAttachmentDTO();
                attachmentDTO.setApplication("image");
                attachmentDTO.setRecordType(RecordTypeEnum.DEVICE_LEDGER.getType());
                attachmentDTO.setRecordId(deviceLedger.getId());
                attachmentDTO.setStorageBlobDTOs(deviceLedgerDto.getStorageBlobDTOs());
                storageAttachmentService.saveStorageAttachment(attachmentDTO);
            }
            return AjaxResult.success();
        }
        return AjaxResult.error();
    }
    @Override
    public AjaxResult updateDeviceLedger(DeviceLedger deviceLedger) {
    public AjaxResult updateDeviceLedger(DeviceLedgerDto deviceLedgerDto) {
        DeviceLedger deviceLedger = new DeviceLedger();
        BeanUtils.copyProperties(deviceLedgerDto, deviceLedger);
        if (this.updateById(deviceLedger)) {
            if (deviceLedgerDto.getStorageBlobDTOs() != null) {
                StorageAttachmentDTO attachmentDTO = new StorageAttachmentDTO();
                attachmentDTO.setApplication("image");
                attachmentDTO.setRecordType(RecordTypeEnum.DEVICE_LEDGER.getType());
                attachmentDTO.setRecordId(deviceLedger.getId());
                attachmentDTO.setStorageBlobDTOs(deviceLedgerDto.getStorageBlobDTOs());
                storageAttachmentService.saveStorageAttachment(attachmentDTO);
            }
            return AjaxResult.success();
        }
        return AjaxResult.error();
    }
    @Override
    public DeviceLedgerDto getDeviceLedgerDetail(Long id) {
        DeviceLedger deviceLedger = this.getById(id);
        if (deviceLedger != null) {
            DeviceLedgerDto deviceLedgerDto = new DeviceLedgerDto();
            BeanUtils.copyProperties(deviceLedger, deviceLedgerDto);
            StorageAttachmentDTO dto = new StorageAttachmentDTO();
            dto.setRecordType(RecordTypeEnum.DEVICE_LEDGER.getType());
            dto.setRecordId(id);
            dto.setApplication("image");
            deviceLedgerDto.setStorageBlobVOs(storageAttachmentService.list(dto));
            return deviceLedgerDto;
        }
        return null;
    }
    @Override
@@ -82,9 +122,7 @@
            util.exportExcel(response, deviceLedgerExeclDtos, "设备台账导出");
        }else  {
            ArrayList<Long> arrayList = new ArrayList<>();
            Arrays.stream(ids).map(id -> {
                return arrayList.add( id);
            });
            Arrays.stream(ids).map(arrayList::add);
            List<DeviceLedger> supplierManageList = deviceLedgerMapper.selectBatchIds(arrayList);
            ArrayList<DeviceLedgerExeclDto> deviceLedgerExeclDtos = new ArrayList<>();
            supplierManageList.stream().forEach(deviceLedger -> {
@@ -116,7 +154,10 @@
            deviceLedger.setTaxIncludingPriceTotal(c.getTaxIncludingPriceUnit());
            deviceLedger.setNumber(BigDecimal.ONE);
            deviceLedger.setPlanRuntimeTime(DateUtils.toLocalDate(c.getPlanRuntimeTime()));
            deviceLedger.setUnTaxIncludingPriceTotal(deviceLedger.getTaxIncludingPriceTotal().divide(BigDecimal.ONE.add(c.getTaxRate()),2, RoundingMode.HALF_UP));
            // è®¡ç®—不含税总价,处理空值情况
            if (deviceLedger.getTaxIncludingPriceTotal() != null && c.getTaxRate() != null) {
                deviceLedger.setUnTaxIncludingPriceTotal(deviceLedger.getTaxIncludingPriceTotal().divide(BigDecimal.ONE.add(c.getTaxRate()), 2, RoundingMode.HALF_UP));
            }
            deviceLedgerMapper.insert(deviceLedger);
        });
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java
@@ -25,13 +25,25 @@
    private final Scheduler scheduler;
    /**
     * æ·»åŠ æ–°ä»»åŠ¡åˆ°è°ƒåº¦å™¨
     * æ·»åŠ æˆ–æ›´æ–°ä»»åŠ¡åˆ°è°ƒåº¦å™¨
     */
    public void scheduleMaintenanceTask(MaintenanceTask task){
        try {
            JobDetail jobDetail = buildJobDetail(task);
            Trigger trigger = buildJobTrigger(task, jobDetail);
            scheduler.scheduleJob(jobDetail, trigger);
            // æ£€æŸ¥è§¦å‘器是否已存在
            TriggerKey triggerKey = trigger.getKey();
            Trigger existingTrigger = scheduler.getTrigger(triggerKey);
            if (existingTrigger != null) {
                // è§¦å‘器已存在,更新它
                scheduler.rescheduleJob(triggerKey, trigger);
            } else {
                // è§¦å‘器不存在,先确保 Job å­˜åœ¨ï¼Œç„¶åŽè°ƒåº¦è§¦å‘器
                scheduler.addJob(jobDetail, true);
                scheduler.scheduleJob(trigger);
            }
        }catch (SchedulerException e){
            log.error("SchedulerException scheduleMaintenanceTask ERROR",e);
            throw new RuntimeException(e);
@@ -45,26 +57,41 @@
       try{
           TriggerKey triggerKey = new TriggerKey("triggerMaintenanceTask_" + task.getId());
           // èŽ·å–çŽ°æœ‰è§¦å‘å™¨å¹¶è½¬æ¢ä¸º CronTrigger
           // èŽ·å–çŽ°æœ‰è§¦å‘å™¨
           Trigger oldTrigger = scheduler.getTrigger(triggerKey);
           // æž„建新的 JobDetail å’Œ Trigger
           JobDetail jobDetail = buildJobDetail(task);
           Trigger newTrigger = buildJobTrigger(task, jobDetail);
           if (oldTrigger == null) {
               // è§¦å‘器不存在,说明任务之前被删除过
               // å…ˆç¡®ä¿ Job å­˜åœ¨ï¼Œç„¶åŽè°ƒåº¦è§¦å‘器
               scheduler.addJob(jobDetail, true);
               scheduler.scheduleJob(newTrigger);
               return;
           }
           // è§¦å‘器存在,直接更新
           if (!(oldTrigger instanceof CronTrigger)) {
               throw new SchedulerException("Existing trigger is not a CronTrigger");
           }
           // 3. æž„建CronTrigger,确保持久化配置
           CronTrigger newTrigger = TriggerBuilder.newTrigger()
                   .withIdentity(triggerKey)                // å”¯ä¸€æ ‡è¯†ï¼Œç”¨äºŽæŒä¹…化存储
                   .withDescription(task.getTaskName() + "_TRIGGER") // è§¦å‘器描述
                   .forJob(oldTrigger.getJobKey())                       // å…³è”对应的Job
           // æž„建新的 CronTrigger
           CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                   .withIdentity(triggerKey)
                   .withDescription(task.getTaskName() + "_TRIGGER")
                   .forJob(oldTrigger.getJobKey())
                   .withSchedule(CronScheduleBuilder
                           .cronSchedule(convertToCronExpression(task)) // é”™è¿‡æ‰§è¡Œæ—¶çš„策略(根据业务调整)
                           .cronSchedule(convertToCronExpression(task))
                           .withMisfireHandlingInstructionDoNothing()
                   )
                   // 4. è®¾ç½®å¼€å§‹æ—¶é—´ï¼ˆè‹¥ä¸ºnull则立即生效)
                   .startAt(task.getNextExecutionTime() != null
                           ? Date.from(task.getNextExecutionTime().atZone(ZoneId.systemDefault()).toInstant())
                           : new Date())
                   .build();
           scheduler.rescheduleJob(triggerKey, newTrigger);
           scheduler.rescheduleJob(triggerKey, cronTrigger);
       }catch (SchedulerException e){
           log.error("SchedulerException rescheduleMaintenanceTask ERROR",e);
           throw new RuntimeException(e);
@@ -93,6 +120,7 @@
    public void unscheduleMaintenanceTask(Long taskId){
        try {
            JobKey jobKey = new JobKey("MaintenanceTask_" + taskId);
            // åˆ é™¤ Job ä¼šè‡ªåŠ¨åˆ é™¤å…³è”çš„ Trigger
            scheduler.deleteJob(jobKey);
        }catch (SchedulerException e){
            log.error("SchedulerException unscheduleMaintenanceTask ERROR",e);
src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskServiceImpl.java
@@ -1,5 +1,6 @@
package com.ruoyi.device.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.bean.BeanUtils;
@@ -34,7 +35,7 @@
    @Override
    public AjaxResult listPage(Page page, MaintenanceTask maintenanceTask) {
        Page<MaintenanceTask> taskPage = maintenanceTaskMapper.selectPage(page, null);
        Page<MaintenanceTask> taskPage = maintenanceTaskMapper.selectPage(page, new QueryWrapper<MaintenanceTask>().orderByDesc("create_time"));
        // 2. å¦‚果没有数据,直接返回空分页
        if (taskPage.getRecords().isEmpty()) {
            return AjaxResult.success(taskPage);
@@ -67,7 +68,6 @@
    @Override
    public AjaxResult add(MaintenanceTask maintenanceTask) {
        maintenanceTask.setActive(true);
        // è®¡ç®—首次执行时间
        TimingTask task = new TimingTask();
        task.setFrequencyType(maintenanceTask.getFrequencyType());
@@ -76,7 +76,10 @@
        maintenanceTask.setNextExecutionTime(firstExecutionTime);
        int insert = maintenanceTaskMapper.insert(maintenanceTask);
        if (insert > 0) {
            maintenanceTaskScheduler.scheduleMaintenanceTask(maintenanceTask);
            // åªæœ‰å½“ isActive ä¸º 1 æ—¶æ‰æ·»åŠ åˆ°å®šæ—¶ä»»åŠ¡è°ƒåº¦å™¨
            if (maintenanceTask.getIsActive() != null && maintenanceTask.getIsActive() == 1) {
                maintenanceTaskScheduler.scheduleMaintenanceTask(maintenanceTask);
            }
        }
        return AjaxResult.success("添加成功");
    }
@@ -87,22 +90,50 @@
        if (maintenanceTask1 == null) {
            return AjaxResult.warn("没有此数据");
        }
        // ä¿å­˜æ—§çš„ isActive çŠ¶æ€
        Integer oldIsActive = maintenanceTask1.getIsActive();
        Integer newIsActive = maintenanceTask.getIsActive();
        BeanUtils.copyProperties(maintenanceTask, maintenanceTask1);
        int update = maintenanceTaskMapper.updateById(maintenanceTask1);
        if (update > 0) {
            maintenanceTaskScheduler.rescheduleMaintenanceTask(maintenanceTask1);
            // å¤„理 isActive çŠ¶æ€å˜åŒ–
            if (newIsActive != null && newIsActive == 1) {
                // æ–°çŠ¶æ€ä¸ºå¯ç”¨ï¼šæ·»åŠ åˆ°å®šæ—¶ä»»åŠ¡è°ƒåº¦å™¨
                if (oldIsActive == null || oldIsActive != 1) {
                    // ä»Žæœªå¯ç”¨å˜ä¸ºå¯ç”¨ï¼Œæ·»åŠ åˆ°è°ƒåº¦å™¨
                    maintenanceTaskScheduler.scheduleMaintenanceTask(maintenanceTask1);
                } else {
                    // å·²ç»å¯ç”¨ï¼Œæ›´æ–°è°ƒåº¦å™¨ä¸­çš„任务
                    maintenanceTaskScheduler.rescheduleMaintenanceTask(maintenanceTask1);
                }
            } else {
                // æ–°çŠ¶æ€ä¸ºåœç”¨ï¼šä»Žå®šæ—¶ä»»åŠ¡è°ƒåº¦å™¨ä¸­ç§»é™¤
                if (oldIsActive != null && oldIsActive == 1) {
                    maintenanceTaskScheduler.unscheduleMaintenanceTask(maintenanceTask1.getId());
                }
            }
        }
        return AjaxResult.success("更新成功");
    }
    @Override
    public AjaxResult delete(List<Long> ids) {
        // å…ˆä»Žå®šæ—¶ä»»åŠ¡è°ƒåº¦å™¨ä¸­ç§»é™¤æ‰€æœ‰å¾…åˆ é™¤çš„ä»»åŠ¡
        ids.forEach(id -> {
            try {
                maintenanceTaskScheduler.unscheduleMaintenanceTask(id);
            } catch (Exception e) {
                log.error("删除定时任务调度失败, id: {}", id, e);
            }
        });
        // å†ä»Žæ•°æ®åº“中删除记录
        int delete = maintenanceTaskMapper.deleteBatchIds(ids);
        if (delete > 0) {
            ids.forEach(id -> {
                maintenanceTaskScheduler.unscheduleMaintenanceTask(id);
            });
            return AjaxResult.success("删除成功");
        }
        return AjaxResult.success("删除成功");
        return AjaxResult.error("删除失败");
    }
}
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/dto/StatisticsReceivablePayableDto.java
@@ -19,10 +19,10 @@
    @Schema(description = "应付金额")
    private BigDecimal payableMoney;
    @Schema(description = "预收金额")
    @Schema(description = "收款金额")
    private BigDecimal advanceMoney;
    @Schema(description = "预付金额")
    @Schema(description = "付款金额")
    private BigDecimal prepayMoney;
}
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java
@@ -3,9 +3,20 @@
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.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.bean.dto.purchase.PurchaseInboundDto;
import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto;
import com.ruoyi.account.bean.dto.sales.SalesOutboundDto;
import com.ruoyi.account.bean.dto.sales.SalesReturnDto;
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.AccountStatementMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.approve.mapper.ApproveProcessMapper;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.basic.mapper.CustomerMapper;
@@ -26,27 +37,27 @@
import com.ruoyi.home.dto.*;
import com.ruoyi.home.mapper.HomeMapper;
import com.ruoyi.home.service.HomeService;
import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper;
import com.ruoyi.production.bean.dto.ProductionProductOutputDto;
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.mapper.PurchaseReturnOrdersMapper;
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;
import com.ruoyi.staff.pojo.StaffOnJob;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -72,6 +83,10 @@
public class HomeServiceImpl implements HomeService {
    private final SalesLedgerMapper salesLedgerMapper;
    private final StockOutRecordMapper stockOutRecordMapper;
    private final ReturnManagementMapper returnManagementMapper;
    private final StockInRecordMapper stockInRecordMapper;
    private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
@@ -82,10 +97,6 @@
    private final QualityInspectMapper qualityStatisticsMapper;
    private final ApproveProcessMapper approveProcessMapper;
    private final ReceiptPaymentMapper receiptPaymentMapper;
    private final PaymentRegistrationMapper paymentRegistrationMapper;
    private final SysDeptMapper sysDeptMapper;
@@ -111,9 +122,9 @@
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final AccountExpenseMapper accountExpenseMapper;
    private final AccountIncomeMapper accountIncomeMapper;
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final AccountStatementMapper accountStatementMapper;
    private final ProductionAccountMapper productionAccountMapper;
@@ -141,12 +152,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 +176,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();
@@ -312,19 +313,35 @@
            queryWrapper.ge(QualityInspect::getCheckTime, monthStart.toString())
                    .le(QualityInspect::getCheckTime, monthEnd.toString());
            List<QualityInspect> monthInspects = qualityStatisticsMapper.selectList(queryWrapper);
            // ç»Ÿè®¡æ€»æ•°é‡ï¼ˆåˆæ ¼æ•°é‡ + ä¸åˆæ ¼æ•°é‡ï¼‰
            BigDecimal reduce = monthInspects.stream()
                    .filter(inspect -> inspect.getInspectType().equals(0))
                    .map(QualityInspect::getQuantity)
                    .map(inspect -> {
                        BigDecimal qualified = inspect.getQualifiedQuantity() != null ? inspect.getQualifiedQuantity() : BigDecimal.ZERO;
                        BigDecimal unqualified = inspect.getUnqualifiedQuantity() != null ? inspect.getUnqualifiedQuantity() : BigDecimal.ZERO;
                        return qualified.add(unqualified);
                    })
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            supplierNum = supplierNum.add(reduce);
            BigDecimal reduce1 = monthInspects.stream()
                    .filter(inspect -> inspect.getInspectType().equals(1))
                    .map(QualityInspect::getQuantity)
                    .map(inspect -> {
                        BigDecimal qualified = inspect.getQualifiedQuantity() != null ? inspect.getQualifiedQuantity() : BigDecimal.ZERO;
                        BigDecimal unqualified = inspect.getUnqualifiedQuantity() != null ? inspect.getUnqualifiedQuantity() : BigDecimal.ZERO;
                        return qualified.add(unqualified);
                    })
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            processNum = processNum.add(reduce1);
            BigDecimal reduce2 = monthInspects.stream()
                    .filter(inspect -> inspect.getInspectType().equals(2))
                    .map(QualityInspect::getQuantity)
                    .map(inspect -> {
                        BigDecimal qualified = inspect.getQualifiedQuantity() != null ? inspect.getQualifiedQuantity() : BigDecimal.ZERO;
                        BigDecimal unqualified = inspect.getUnqualifiedQuantity() != null ? inspect.getUnqualifiedQuantity() : BigDecimal.ZERO;
                        return qualified.add(unqualified);
                    })
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            factoryNum = factoryNum.add(reduce2);
@@ -334,25 +351,22 @@
            // 1. ä¾›åº”商检验(类型0)- åˆæ ¼æ•°é‡
            BigDecimal supplierQualified = monthInspects.stream()
                    .filter(inspect -> inspect.getInspectType().equals(0)
                            && "合格".equals(inspect.getCheckResult()))
                    .map(QualityInspect::getQuantity)
                    .filter(inspect -> inspect.getInspectType().equals(0))
                    .map(inspect -> inspect.getQualifiedQuantity() != null ? inspect.getQualifiedQuantity() : BigDecimal.ZERO)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            item.setSupplierNum(supplierQualified);
            // 2. å·¥åºæ£€éªŒï¼ˆç±»åž‹1)- åˆæ ¼æ•°é‡
            BigDecimal processQualified = monthInspects.stream()
                    .filter(inspect -> inspect.getInspectType().equals(1)
                            && "合格".equals(inspect.getCheckResult()))
                    .map(QualityInspect::getQuantity)
                    .filter(inspect -> inspect.getInspectType().equals(1))
                    .map(inspect -> inspect.getQualifiedQuantity() != null ? inspect.getQualifiedQuantity() : BigDecimal.ZERO)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            item.setProcessNum(processQualified);
            // 3. å·¥åŽ‚æ£€éªŒï¼ˆç±»åž‹2)- åˆæ ¼æ•°é‡
            BigDecimal factoryQualified = monthInspects.stream()
                    .filter(inspect -> inspect.getInspectType().equals(2)
                            && "合格".equals(inspect.getCheckResult()))
                    .map(QualityInspect::getQuantity)
                    .filter(inspect -> inspect.getInspectType().equals(2))
                    .map(inspect -> inspect.getQualifiedQuantity() != null ? inspect.getQualifiedQuantity() : BigDecimal.ZERO)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            item.setFactoryNum(factoryQualified);
@@ -414,76 +428,33 @@
     */
    @Override
    public StatisticsReceivablePayableDto statisticsReceivablePayable(Integer type) {
        LocalDate today = LocalDate.now();
        LocalDate startDate = null;
        LocalDate endDate = null;
        switch (type) {
            case 1:
                // èŽ·å–æœ¬å‘¨å‘¨ä¸€
                startDate = today.with(DayOfWeek.MONDAY);
                // èŽ·å–æœ¬å‘¨å‘¨æ—¥
                endDate = today.with(DayOfWeek.SUNDAY);
                break;
            case 2:
                startDate = today.with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.with(TemporalAdjusters.lastDayOfMonth());
                break;
            case 3:
                Month currentMonth = today.getMonth();
                Month firstMonthOfQuarter = currentMonth.firstMonthOfQuarter();
                Month lastMonthOfQuarter = Month.of(firstMonthOfQuarter.getValue() + 2);
        StatisticsReceivablePayableDto dto = new StatisticsReceivablePayableDto();
        LocalDate[] range = resolveFinanceRange(type);
        LocalDate startDate = range[0];
        LocalDate endDate = range[1];
                startDate = today.withMonth(firstMonthOfQuarter.getValue())
                        .with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.withMonth(lastMonthOfQuarter.getValue())
                        .with(TemporalAdjusters.lastDayOfMonth());
                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);
        //销售出库
        BigDecimal receivableBase = sumSalesContractAmount(startDate, endDate);
        //销售退货
        BigDecimal salesReturnAmount = sumSalesReturnAmount(startDate, endDate);
        //采购入库
        BigDecimal payableBase = sumPurchaseContractAmount(startDate, endDate);
        //采购退货
        BigDecimal purchaseReturnAmount = sumPurchaseReturnAmount(startDate, endDate);
        //收款
        BigDecimal advanceMoney = sumCollectionAmount(startDate, endDate);
        //付款
        BigDecimal prepayMoney = sumPaymentAmount(startDate, endDate);
        return statisticsReceivablePayableDto;
        //应收金额=销售出库-销售退货
        dto.setReceivableMoney(scaleMoney(maxZero(receivableBase.subtract(salesReturnAmount))));
        //应付金额=采购入库-采购退货
        dto.setPayableMoney(scaleMoney(maxZero(payableBase.subtract(purchaseReturnAmount))));
        //收款金额=收款单
        dto.setAdvanceMoney(scaleMoney(advanceMoney));
        //付款金额=付款单
        dto.setPrepayMoney(scaleMoney(prepayMoney));
        return dto;
    }
    public static <T> BigDecimal sumAmount(List<T> list, java.util.function.Function<T, BigDecimal> amountExtractor) {
@@ -1164,13 +1135,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 +1146,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 +1160,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 +1192,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 +1229,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;
@@ -1311,161 +1263,55 @@
    @Override
    public MonthlyIncomeDto monthlyIncome() {
        MonthlyIncomeDto dto = new MonthlyIncomeDto();
        LocalDate now = LocalDate.now();
        YearMonth currentMonth = YearMonth.from(now);
        LocalDateTime startOfMonth = currentMonth.atDay(1).atStartOfDay();
        LocalDateTime endOfMonth = currentMonth.atEndOfMonth().atTime(23, 59, 59);
        LambdaQueryWrapper<SalesLedgerProduct> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SalesLedgerProduct::getType, 1);
        wrapper.ge(SalesLedgerProduct::getRegisterDate, startOfMonth);
        wrapper.le(SalesLedgerProduct::getRegisterDate, endOfMonth);
        List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(wrapper);
        if (CollectionUtils.isEmpty(products)) {
            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);
        }
        LocalDate today = LocalDate.now();
        YearMonth currentMonth = YearMonth.from(today);
        LocalDate startDate = currentMonth.atDay(1);
        LocalDate endDate = currentMonth.atEndOfMonth();
        //收款
        BigDecimal monthlyIncome = sumCollectionAmount(startDate, endDate);
        //销售出库
        BigDecimal receivableBase = sumSalesContractAmount(startDate, endDate);
        //销售退货
        BigDecimal salesReturnAmount = sumSalesReturnAmount(startDate, endDate);
        //回款率=收款/(销售出库-销售退货)应收
        String collectionRate = toRateString(monthlyIncome, receivableBase.subtract(salesReturnAmount));
        //逾期数=(销售出库金额-销售退货金额)应收金额-收款金额
        BigDecimal overdueAmount = receivableBase.subtract(salesReturnAmount).subtract(monthlyIncome);
        //逾期率=逾期数/应收金额
        String overdueRate = toRateString(overdueAmount, receivableBase);
        dto.setMonthlyIncome(scaleMoney(monthlyIncome));
        dto.setCollectionRate(collectionRate);
        dto.setOverdueNum(overdueAmount);
        dto.setOverdueRate(overdueRate);
        return dto;
    }
    @Override
    public MonthlyExpenditureDto monthlyExpenditure() {
        MonthlyExpenditureDto dto = new MonthlyExpenditureDto();
        LocalDate today = LocalDate.now();
        YearMonth currentMonth = YearMonth.from(today);
        LocalDate startDate = currentMonth.atDay(1);
        LocalDate endDate = currentMonth.atEndOfMonth();
        //支出
        BigDecimal monthlyExpenditure = sumPaymentAmount(startDate, endDate);
        //采购入库
        BigDecimal payableBase = sumPurchaseContractAmount(startDate, endDate);
        //采购退货
        BigDecimal purchaseReturnAmount = sumPurchaseReturnAmount(startDate, endDate);
        //付款率=付款/(采购入库-采购退货)应付
        String paymentRate = toRateString(monthlyExpenditure, payableBase.subtract(purchaseReturnAmount));
        //收款
        BigDecimal monthlyIncome = sumCollectionAmount(startDate, endDate);
        //毛利润= æ”¶æ¬¾-支出
        BigDecimal grossProfit = monthlyIncome.subtract(monthlyExpenditure);
        //利润率=毛利润/收款
        String profitMarginRate = toRateString(grossProfit, monthlyIncome);
        // å½“月时间范围
        LocalDate now = LocalDate.now();
        YearMonth currentMonth = YearMonth.from(now);
        LocalDateTime startOfMonth = currentMonth.atDay(1).atStartOfDay();
        LocalDateTime endOfMonth = currentMonth.atEndOfMonth().atTime(23, 59, 59);
        // é‡‡è´­å°è´¦ï¼ˆtype = 2)
        LambdaQueryWrapper<SalesLedgerProduct> purchaseWrapper = new LambdaQueryWrapper<>();
        purchaseWrapper.eq(SalesLedgerProduct::getType, 2);
        purchaseWrapper.ge(SalesLedgerProduct::getRegisterDate, startOfMonth);
        purchaseWrapper.le(SalesLedgerProduct::getRegisterDate, endOfMonth);
        List<SalesLedgerProduct> purchaseProducts = salesLedgerProductMapper.selectList(purchaseWrapper);
        BigDecimal rawMaterialCost = BigDecimal.ZERO; // åŽŸææ–™æˆæœ¬
        BigDecimal paidAmount = BigDecimal.ZERO; // å·²ä»˜æ¬¾é‡‘额
        BigDecimal pendingAmount = BigDecimal.ZERO; // å¾…付款金额
        if (!CollectionUtils.isEmpty(purchaseProducts)) {
            for (SalesLedgerProduct p : purchaseProducts) {
                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;
        dto.setMonthlyExpenditure(monthlyExpenditure);
        // å·²ä»˜æ¬¾ Ã·ï¼ˆå·²ä»˜æ¬¾ + å¾…付款)
        BigDecimal totalPayable = paidAmount.add(pendingAmount);
        if (totalPayable.compareTo(BigDecimal.ZERO) > 0) {
            String paymentRate = paidAmount
                    .divide(totalPayable, 4, RoundingMode.HALF_UP)
                    .multiply(BigDecimal.valueOf(100))
                    .setScale(2, RoundingMode.HALF_UP)
                    .toString();
            dto.setPaymentRate(paymentRate);
        }
        // å”®å°è´¦ï¼ˆtype = 1)
        LambdaQueryWrapper<SalesLedgerProduct> salesWrapper = new LambdaQueryWrapper<>();
        salesWrapper.eq(SalesLedgerProduct::getType, 1);
        salesWrapper.ge(SalesLedgerProduct::getRegisterDate, startOfMonth);
        salesWrapper.le(SalesLedgerProduct::getRegisterDate, endOfMonth);
        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) {
            // æ¯›åˆ©æ¶¦ = é”€å”®æ”¶å…¥ - åŽŸææ–™æˆæœ¬
            BigDecimal grossProfit = revenue.subtract(rawMaterialCost);
            dto.setGrossProfit(grossProfit);
            // åˆ©æ¶¦çއ = (销售收入 - æœˆåº¦æ€»æ”¯å‡º) / é”€å”®æ”¶å…¥
            BigDecimal profit = revenue.subtract(monthlyExpenditure);
            String profitMarginRate = profit
                    .divide(revenue, 4, RoundingMode.HALF_UP)
                    .multiply(BigDecimal.valueOf(100))
                    .setScale(2, RoundingMode.HALF_UP)
                    .toString();
            dto.setProfitMarginRate(profitMarginRate);
        }
        dto.setMonthlyExpenditure(scaleMoney(monthlyExpenditure));
        dto.setPaymentRate(paymentRate);
        dto.setGrossProfit(scaleMoney(grossProfit));
        dto.setProfitMarginRate(profitMarginRate);
        return dto;
    }
@@ -1904,11 +1750,8 @@
        BigDecimal unqualifiedCount = BigDecimal.ZERO;
        for (QualityInspect item : list) {
            if ("合格".equals(item.getCheckResult())) {
                qualifiedCount = qualifiedCount.add(item.getQuantity());
            } else {
                unqualifiedCount = unqualifiedCount.add(item.getQuantity());
            }
            qualifiedCount = qualifiedCount.add(item.getQualifiedQuantity() != null ? item.getQualifiedQuantity() : BigDecimal.ZERO);
            unqualifiedCount = unqualifiedCount.add(item.getUnqualifiedQuantity() != null ? item.getUnqualifiedQuantity() : BigDecimal.ZERO);
        }
        BigDecimal totalCount = qualifiedCount.add(unqualifiedCount);
@@ -2177,13 +2020,11 @@
                continue;
            }
            BigDecimal quantity = item.getQuantity();
            BigDecimal qualifiedQty = item.getQualifiedQuantity() != null ? item.getQualifiedQuantity() : BigDecimal.ZERO;
            BigDecimal unqualifiedQty = item.getUnqualifiedQuantity() != null ? item.getUnqualifiedQuantity() : BigDecimal.ZERO;
            if ("合格".equals(item.getCheckResult())) {
                dto.setQualifiedCount(dto.getQualifiedCount().add(quantity));
            } else {
                dto.setUnqualifiedCount(dto.getUnqualifiedCount().add(quantity));
            }
            dto.setQualifiedCount(dto.getQualifiedCount().add(qualifiedQty));
            dto.setUnqualifiedCount(dto.getUnqualifiedCount().add(unqualifiedQty));
        }
        // è®¡ç®—合格率
@@ -2224,14 +2065,12 @@
            BigDecimal unqualifiedCount = BigDecimal.ZERO;
            for (QualityInspect item : items) {
                BigDecimal qty = item.getQuantity();
                totalCount = totalCount.add(qty);
                BigDecimal qualifiedQty = item.getQualifiedQuantity() != null ? item.getQualifiedQuantity() : BigDecimal.ZERO;
                BigDecimal unqualifiedQty = item.getUnqualifiedQuantity() != null ? item.getUnqualifiedQuantity() : BigDecimal.ZERO;
                if ("合格".equals(item.getCheckResult())) {
                    qualifiedCount = qualifiedCount.add(qty);
                } else {
                    unqualifiedCount = unqualifiedCount.add(qty);
                }
                totalCount = totalCount.add(qualifiedQty.add(unqualifiedQty));
                qualifiedCount = qualifiedCount.add(qualifiedQty);
                unqualifiedCount = unqualifiedCount.add(unqualifiedQty);
            }
            if (totalCount.compareTo(BigDecimal.ZERO) == 0) {
@@ -2360,13 +2199,17 @@
        dto.setProcessNum(sumQuantity(qualityInspectList, 1)); // è¿‡ç¨‹
        dto.setFactoryNum(sumQuantity(qualityInspectList, 2)); // å‡ºåŽ‚
        // å‡è®¾ qualityInspectList æ˜¯ä¸€ä¸ª List<QualityInspect> ç±»åž‹çš„集合
        Map<String, List<QualityInspect>> groupedByCheckResult = qualityInspectList.stream()
                .collect(Collectors.groupingBy(QualityInspect::getCheckResult));
        List<QualityInspect> qualityInspects = groupedByCheckResult.get("不合格");
        if (ObjectUtils.isNull(qualityInspects) || qualityInspects.size() == 0) {
            return null;
        // æ ¹æ® unqualifiedQuantity > 0 ç­›é€‰ä¸åˆæ ¼è®°å½•
        List<QualityInspect> qualityInspects = qualityInspectList.stream()
                .filter(i -> i.getUnqualifiedQuantity() != null && i.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) > 0)
                .collect(Collectors.toList());
        if (ObjectUtils.isEmpty(qualityInspects)) {
            // å³ä½¿æ²¡æœ‰ä¸åˆæ ¼è®°å½•,也应该返回统计数据,只是图表项为空
            dto.setItem(new ArrayList<>());
            return dto;
        }
        // 4. å¤„理图表项 (Item)
        List<QualityStatisticsItem> itemList = new ArrayList<>();
@@ -2409,8 +2252,11 @@
    private BigDecimal sumQuantity(List<QualityInspect> list, Integer type) {
        return list.stream()
                .filter(i -> i.getInspectType().equals(type))
                .map(QualityInspect::getQuantity)
                .filter(Objects::nonNull)
                .map(i -> {
                    BigDecimal qualified = i.getQualifiedQuantity() != null ? i.getQualifiedQuantity() : BigDecimal.ZERO;
                    BigDecimal unqualified = i.getUnqualifiedQuantity() != null ? i.getUnqualifiedQuantity() : BigDecimal.ZERO;
                    return qualified.add(unqualified);
                })
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
@@ -2418,11 +2264,18 @@
        QualityStatisticsItem item = new QualityStatisticsItem();
        item.setDate(dateLabel);
        item.setSupplierNum(list.stream().filter(i -> i.getInspectType() == 0).map(QualityInspect::getQuantity)
        // ç»Ÿè®¡æ¯ç§æ£€éªŒç±»åž‹çš„不合格数量
        item.setSupplierNum(list.stream()
                .filter(i -> i.getInspectType() == 0)
                .map(i -> i.getUnqualifiedQuantity() != null ? i.getUnqualifiedQuantity() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add));
        item.setProcessNum(list.stream().filter(i -> i.getInspectType() == 1).map(QualityInspect::getQuantity)
        item.setProcessNum(list.stream()
                .filter(i -> i.getInspectType() == 1)
                .map(i -> i.getUnqualifiedQuantity() != null ? i.getUnqualifiedQuantity() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add));
        item.setFactoryNum(list.stream().filter(i -> i.getInspectType() == 2).map(QualityInspect::getQuantity)
        item.setFactoryNum(list.stream()
                .filter(i -> i.getInspectType() == 2)
                .map(i -> i.getUnqualifiedQuantity() != null ? i.getUnqualifiedQuantity() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add));
        return item;
@@ -2462,6 +2315,121 @@
        return productionOperationTaskMapper.calculateProductionStatistics(startDateTime, endDateTime, userId, processIds);
    }
    private LocalDate[] resolveFinanceRange(Integer type) {
        LocalDate today = LocalDate.now();
        int safeType = type == null ? 1 : type;
        LocalDate startDate;
        LocalDate endDate;
        switch (safeType) {
            case 1:
                startDate = today.with(DayOfWeek.MONDAY);
                endDate = today.with(DayOfWeek.SUNDAY);
                break;
            case 2:
                startDate = today.with(TemporalAdjusters.firstDayOfMonth());
                endDate = today.with(TemporalAdjusters.lastDayOfMonth());
                break;
            case 3:
                Month firstMonthOfQuarter = today.getMonth().firstMonthOfQuarter();
                startDate = LocalDate.of(today.getYear(), firstMonthOfQuarter, 1);
                endDate = startDate.plusMonths(2).with(TemporalAdjusters.lastDayOfMonth());
                break;
            default:
                startDate = today.with(DayOfWeek.MONDAY);
                endDate = today.with(DayOfWeek.SUNDAY);
                break;
        }
        return new LocalDate[]{startDate, endDate};
    }
    //计算日期内的销售出库金额
    private BigDecimal sumSalesContractAmount(LocalDate startDate, LocalDate endDate) {
        SalesOutboundDto salesOutboundDto = new SalesOutboundDto();
        salesOutboundDto.setStartDate(startDate);
        salesOutboundDto.setEndDate(endDate);
        List<SalesOutboundVo> salesOutboundVos = stockOutRecordMapper.listPageAccountSales(new Page(1, -1), salesOutboundDto).getRecords();
        return sumAmount(salesOutboundVos, SalesOutboundVo::getOutboundAmount);
    }
    //计算日期内的销售退货金额
    private BigDecimal sumSalesReturnAmount(LocalDate startDate, LocalDate endDate) {
        SalesReturnDto salesReturnDto = new SalesReturnDto();
        salesReturnDto.setStartDate(startDate);
        salesReturnDto.setEndDate(endDate);
        List<SalesReturnVo> salesReturnVos = returnManagementMapper.listPageAccountSalesReturn(new Page(1, -1), salesReturnDto).getRecords();
        return sumAmount(salesReturnVos, SalesReturnVo::getRefundAmount);
    }
    //计算日期内的采购入库金额
    private BigDecimal sumPurchaseContractAmount(LocalDate startDate, LocalDate endDate) {
        PurchaseInboundDto purchaseInboundDto = new PurchaseInboundDto();
        purchaseInboundDto.setStartDate(startDate);
        purchaseInboundDto.setEndDate(endDate);
        List<PurchaseInboundVo> purchaseInboundVos = stockInRecordMapper.listPageAccountPurchase(new Page(1, -1), purchaseInboundDto).getRecords();
        return sumAmount(purchaseInboundVos, PurchaseInboundVo::getInboundAmount);
    }
    //计算日期内的采购退货金额
    private BigDecimal sumPurchaseReturnAmount(LocalDate startDate, LocalDate endDate) {
        PurchaseReturnDto purchaseReturnDto = new PurchaseReturnDto();
        purchaseReturnDto.setStartDate(startDate);
        purchaseReturnDto.setEndDate(endDate);
        List<PurchaseReturnVo> purchaseReturnVos = purchaseReturnOrdersMapper.listPageAccountPurchaseReturn(new Page(1, -1), purchaseReturnDto).getRecords();
        return sumAmount(purchaseReturnVos, PurchaseReturnVo::getTotalAmount);
    }
    //计算日期内的总收款金额
    private BigDecimal sumCollectionAmount(LocalDate startDate, LocalDate endDate) {
        List<AccountSalesCollection> collections = defaultList(accountSalesCollectionMapper.selectList(
                new LambdaQueryWrapper<AccountSalesCollection>()
                        .ge(AccountSalesCollection::getCollectionDate, startDate)
                        .le(AccountSalesCollection::getCollectionDate, endDate)));
        return sumAmount(collections, AccountSalesCollection::getCollectionAmount);
    }
    //计算日期内的总付款金额
    private BigDecimal sumPaymentAmount(LocalDate startDate, LocalDate endDate) {
        List<AccountPurchasePayment> payments = defaultList(accountPurchasePaymentMapper.selectList(
                new LambdaQueryWrapper<AccountPurchasePayment>()
                        .ge(AccountPurchasePayment::getPaymentDate, startDate)
                        .le(AccountPurchasePayment::getPaymentDate, endDate)));
        return sumAmount(payments, AccountPurchasePayment::getPaymentAmount);
    }
    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 String toRateString(BigDecimal numerator, BigDecimal denominator) {
        if (denominator == null || denominator.compareTo(BigDecimal.ZERO) <= 0) {
            return "0.00";
        }
        return defaultDecimal(numerator)
                .divide(denominator, 4, RoundingMode.HALF_UP)
                .multiply(BigDecimal.valueOf(100))
                .setScale(2, RoundingMode.HALF_UP)
                .toString();
    }
    private BigDecimal maxZero(BigDecimal value) {
        if (value == null) {
            return BigDecimal.ZERO;
        }
        return value.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : value;
    }
    private BigDecimal scaleMoney(BigDecimal value) {
        return defaultDecimal(value).setScale(2, RoundingMode.HALF_UP);
    }
    private <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
src/main/java/com/ruoyi/inspectiontask/controller/InspectionTaskController.java
@@ -3,6 +3,8 @@
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;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.inspectiontask.dto.InspectionTaskDto;
@@ -43,8 +45,9 @@
    /**
     * å¯¼å‡º
     */
    @PostMapping("/export")
    @Log(title = "检验任务", businessType = BusinessType.EXPORT)
    @Operation(summary = "导出定时任务记录")
    @PostMapping("/export")
    public void export(HttpServletResponse response) {
        Page page = new Page<>(-1, -1);
        InspectionTaskDto timingTask = new InspectionTaskDto();
@@ -56,8 +59,9 @@
    /**
     * å·¡æ£€ä»»åŠ¡è¡¨æ–°å¢žä¿®æ”¹
     */
    @PostMapping("/addOrEditInspectionTask")
    @Log(title = "检验任务", businessType = BusinessType.INSERT)
    @Operation(summary = "巡检任务表新增修改")
    @PostMapping("/addOrEditInspectionTask")
    @Transactional(rollbackFor = Exception.class)
    public R addOrEditInspectionTask(@RequestBody InspectionTaskDto inspectionTaskDto) {
        return R.ok(inspectionTaskService.addOrEditInspectionTask(inspectionTaskDto));
@@ -66,8 +70,9 @@
    /**
     * å·¡æ£€ä»»åŠ¡è¡¨åˆ é™¤
     */
    @DeleteMapping("/delInspectionTask")
    @Log(title = "检验任务", businessType = BusinessType.DELETE)
    @Operation(summary = "巡检任务表删除")
    @DeleteMapping("/delInspectionTask")
    @Transactional(rollbackFor = Exception.class)
    public R remove(@RequestBody Long[] ids) {
        return R.ok(inspectionTaskService.delByIds(ids));
src/main/java/com/ruoyi/inspectiontask/controller/QrCodeController.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.inspectiontask.dto.QrCodeDto;
@@ -37,8 +39,9 @@
    /**
     * äºŒç»´ç ç®¡ç†è¡¨æ–°å¢žä¿®æ”¹
     */
    @PostMapping("/addOrEditQrCode")
    @Log(title = "二维码", businessType = BusinessType.INSERT)
    @Operation(summary = "二维码管理表新增修改")
    @PostMapping("/addOrEditQrCode")
    public R<Long> addOrEditQrCode(@RequestBody QrCodeDto qrCodeDto) {
        return R.ok(qrCodeService.addOrEditQrCode(qrCodeDto));
    }
@@ -46,8 +49,9 @@
    /**
     * äºŒç»´ç ç®¡ç†è¡¨åˆ é™¤
     */
    @DeleteMapping("/delQrCode")
    @Log(title = "二维码", businessType = BusinessType.DELETE)
    @Operation(summary = "二维码管理表删除")
    @DeleteMapping("/delQrCode")
    public R remove(@RequestBody Long[] ids) {
        return R.ok(qrCodeService.delByIds(ids));
    }
src/main/java/com/ruoyi/inspectiontask/controller/QrCodeScanRecordController.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.inspectiontask.dto.QrCodeScanRecordDto;
@@ -38,8 +40,9 @@
    /**
     * äºŒç»´ç æ‰«ç è®°å½•表新增修改
     */
    @PostMapping("/addOrEditQrCodeRecord")
    @Log(title = "二维码扫描记录", businessType = BusinessType.INSERT)
    @Operation(summary = "二维码扫码记录表新增修改")
    @PostMapping("/addOrEditQrCodeRecord")
    public R addOrEditQrCodeRecord(@RequestBody QrCodeScanRecordDto qrCodeScanRecordDto) {
        return R.ok(qrCodeScanRecordService.addOrEditQrCodeRecord(qrCodeScanRecordDto));
    }
@@ -47,8 +50,9 @@
    /**
     * äºŒç»´ç æ‰«ç è®°å½•表删除
     */
    @DeleteMapping("/delSalesRecord")
    @Log(title = "二维码扫描记录", businessType = BusinessType.DELETE)
    @Operation(summary = "二维码扫码记录表删除")
    @DeleteMapping("/delSalesRecord")
    public R remove(@RequestBody Long[] ids) {
        return R.ok(qrCodeScanRecordService.delByIds(ids));
    }
src/main/java/com/ruoyi/inspectiontask/controller/TimingTaskController.java
@@ -42,6 +42,7 @@
    /**
     * å¯¼å‡º
     */
    @Log(title = "定时任务", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    @Operation(summary = "导出定时任务")
    public void export(HttpServletResponse response) {
@@ -55,9 +56,9 @@
    /**
     * å®šæ—¶å·¡æ£€ä»»åŠ¡è¡¨æ–°å¢žä¿®æ”¹
     */
    @Log(title = "定时任务", businessType = BusinessType.INSERT)
    @PostMapping("/addOrEditTimingTask")
    @Operation(summary = "新增修改定时任务")
    @Log(title = "定时任务", businessType = BusinessType.INSERT)
    public R addOrEditTimingTask(@RequestBody TimingTaskDto timingTaskDto) throws SchedulerException {
        return R.ok(timingTaskService.addOrEditTimingTask(timingTaskDto));
    }
@@ -65,9 +66,9 @@
    /**
     * å®šæ—¶å·¡æ£€ä»»åŠ¡è¡¨åˆ é™¤
     */
    @Log(title = "定时任务", businessType = BusinessType.DELETE)
    @DeleteMapping("/delTimingTask")
    @Operation(summary = "删除定时任务")
    @Log(title = "定时任务", businessType = BusinessType.DELETE)
    public R remove(@RequestBody Long[] ids) {
        return R.ok(timingTaskService.delByIds(ids));
    }
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java
@@ -67,6 +67,7 @@
        if (StringUtils.isNotBlank(inspectionTaskDto.getInspectionProject())) {
            queryWrapper.like(InspectionTask::getInspectionProject, inspectionTaskDto.getInspectionProject());
        }
        queryWrapper.orderByDesc(InspectionTask::getCreateTime);
        IPage<InspectionTask> entityPage = inspectionTaskMapper.selectPage(page, queryWrapper);
        //  æ— æ•°æ®æå‰è¿”回
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskScheduler.java
@@ -35,6 +35,17 @@
        // èŽ·å–çŽ°æœ‰è§¦å‘å™¨å¹¶è½¬æ¢ä¸º CronTrigger
        Trigger oldTrigger = scheduler.getTrigger(triggerKey);
        if (oldTrigger == null) {
            JobKey jobKey = new JobKey("timingTask_" + task.getId());
            JobDetail jobDetail = scheduler.getJobDetail(jobKey);
            if (jobDetail != null) {
                Trigger trigger = buildJobTrigger(task, jobDetail);
                scheduler.scheduleJob(trigger);
            } else {
                scheduleTimingTask(task);
            }
            return;
        }
        if (!(oldTrigger instanceof CronTrigger)) {
            throw new SchedulerException("Existing trigger is not a CronTrigger");
        }
@@ -144,18 +155,13 @@
        // ä½¿ç”¨switch确保条件互斥
        String frequencyType = task.getFrequencyType().toUpperCase(); // ç»Ÿä¸€è½¬ä¸ºå¤§å†™æ¯”较
        switch (frequencyType) {
            case "DAILY":
                return convertDailyToCron(task.getFrequencyDetail());
            case "WEEKLY":
                return convertWeeklyToCron(task.getFrequencyDetail());
            case "MONTHLY":
                return convertMonthlyToCron(task.getFrequencyDetail());
            case "QUARTERLY":
                return convertQuarterlyToCron(task.getFrequencyDetail());
            default:
                throw new IllegalArgumentException("不支持的频率类型: " + task.getFrequencyType());
        }
        return switch (frequencyType) {
            case "DAILY" -> convertDailyToCron(task.getFrequencyDetail());
            case "WEEKLY" -> convertWeeklyToCron(task.getFrequencyDetail());
            case "MONTHLY" -> convertMonthlyToCron(task.getFrequencyDetail());
            case "QUARTERLY" -> convertQuarterlyToCron(task.getFrequencyDetail());
            default -> throw new IllegalArgumentException("不支持的频率类型: " + task.getFrequencyType());
        };
    }
    // æ¯æ—¥ä»»åŠ¡è½¬æ¢
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
@@ -53,6 +53,7 @@
        if (timingTask.getIsEnabled() != null) {
            queryWrapper.eq(TimingTask::getIsEnabled, timingTask.getIsEnabled());
        }
        queryWrapper.orderByDesc(TimingTask::getCreateTime);
        IPage<TimingTask> taskPage = timingTaskMapper.selectPage(page, queryWrapper);
        // 2. å¦‚果没有数据,直接返回空分页
src/main/java/com/ruoyi/measuringinstrumentledger/controller/SparePartsRequisitionRecordController.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.measuringinstrumentledger.dto.SparePartsRequisitionRecordDto;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRequisitionRecordService;
@@ -29,6 +31,7 @@
    @GetMapping("/listPage")
    @Operation(summary = "备件分类-分页查询")
    @Log(title = "备件领用记录-分页查询", businessType = BusinessType.OTHER)
    public AjaxResult listPage(Page page, SparePartsRequisitionRecordDto sparePartsRequisitionRecordDto){
        IPage<SparePartsRequisitionRecordDto> listPage = sparePartsRequisitionRecordService.listPage(page, sparePartsRequisitionRecordDto);
        return AjaxResult.success(listPage);
src/main/java/com/ruoyi/officesupplies/controller/OfficeSuppliesController.java
@@ -44,6 +44,7 @@
    @PostMapping("/add")
    @Operation(summary = "办公物资-添加")
    @Log(title = "办公物资-添加", businessType = BusinessType.INSERT)
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult add(@RequestBody OfficeSupplies officeSupplies) {
        // æŒ‰ç…§å½“前时间yyyyMMdd + å½“天新增数量 + 1生成编号
@@ -60,6 +61,7 @@
    @PostMapping("/update")
    @Operation(summary = "办公物资-修改")
    @Log(title = "办公物资-修改", businessType = BusinessType.UPDATE)
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult update(@RequestBody OfficeSupplies officeSupplies) {
        return officeSuppliesService.updateById(officeSupplies) ? success() : error();
@@ -67,6 +69,7 @@
    @DeleteMapping("/delete")
    @Operation(summary = "办公物资-删除")
    @Log(title = "办公物资-删除", businessType = BusinessType.DELETE)
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if(CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
src/main/java/com/ruoyi/other/controller/PdaVersionController.java
@@ -1,6 +1,8 @@
package com.ruoyi.other.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.other.dto.PdaVersionDTO;
import com.ruoyi.other.pojo.PdaVersion;
@@ -23,6 +25,7 @@
    }
    @Operation(summary = "添加版本")
    @Log(title = "PDA版本-添加", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public R add(@RequestBody PdaVersionDTO pdaVersion) {
        return R.ok(pdaVersionService.add(pdaVersion));
src/main/java/com/ruoyi/procurementrecord/controller/GasTankWarningController.java
@@ -3,6 +3,8 @@
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.pojo.GasTankWarning;
import com.ruoyi.procurementrecord.service.GasTankWarningService;
@@ -23,23 +25,26 @@
        return AjaxResult.success(gasTankWarningService.listPage(page, gasTankWarning));
    }
    @Log(title = "新增气瓶预警", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody GasTankWarning gasTankWarning) {
        return AjaxResult.success(gasTankWarningService.save(gasTankWarning));
    }
    @Log(title = "修改气瓶预警", businessType = BusinessType.UPDATE)
    @PostMapping("update")
    public AjaxResult update(@RequestBody GasTankWarning gasTankWarning) {
        return AjaxResult.success(gasTankWarningService.updateById(gasTankWarning));
    }
    @Log(title = "删除气瓶预警", businessType = BusinessType.DELETE)
    @DeleteMapping("delete")
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
        return AjaxResult.success(gasTankWarningService.removeByIds(ids));
    }
    //导出
    @Log(title = "导出气瓶预警", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, @RequestParam(name = "ids", required = false) List<Long> ids) {
        gasTankWarningService.export(response, ids);
src/main/java/com/ruoyi/procurementrecord/controller/InboundManagementController.java
@@ -3,6 +3,8 @@
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.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.pojo.InboundManagement;
@@ -35,6 +37,7 @@
        return AjaxResult.success(result);
    }
    @Log(title = "新增到货管理", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Operation(summary = "到货管理-添加")
    @Transactional(rollbackFor = Exception.class)
@@ -44,6 +47,7 @@
        return result ? AjaxResult.success() : AjaxResult.error();
    }
    @Log(title = "修改到货管理", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    @Operation(summary = "到货管理-修改")
    @Transactional(rollbackFor = Exception.class)
@@ -52,6 +56,7 @@
        return result ? AjaxResult.success() : AjaxResult.error();
    }
    @Log(title = "删除到货管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/del")
    @Operation(summary = "到货管理-删除")
    @Transactional(rollbackFor = Exception.class)
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementExceptionRecordController.java
@@ -1,5 +1,7 @@
package com.ruoyi.procurementrecord.controller;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.mapper.ProcurementExceptionRecordMapper;
@@ -21,12 +23,14 @@
    private ProcurementExceptionRecordMapper procurementExceptionRecordMapper;
    @Log(title = "新增采购异常记录", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Transactional
    public AjaxResult add(@RequestBody ProcurementExceptionRecord procurementExceptionRecord) {
        return AjaxResult.success(procurementExceptionRecordMapper.insert(procurementExceptionRecord));
    }
    @Log(title = "修改采购异常记录", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    @Transactional
    public AjaxResult updatePro(@RequestBody ProcurementExceptionRecord procurementExceptionRecord) {
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementPlanController.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.pojo.ProcurementPlan;
@@ -33,6 +35,7 @@
        return AjaxResult.success(result);
    }
    @Log(title = "新增采购计划", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Operation(summary = "采购计划-添加")
    public AjaxResult add(@RequestBody ProcurementPlan procurementPlan){
@@ -40,6 +43,7 @@
        return result ? AjaxResult.success() : AjaxResult.error();
    }
    @Log(title = "修改采购计划", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    @Operation(summary = "采购计划-修改")
    public AjaxResult update(@RequestBody ProcurementPlan procurementPlan){
@@ -47,6 +51,7 @@
        return result ? AjaxResult.success() : AjaxResult.error();
    }
    @Log(title = "删除采购计划", businessType = BusinessType.DELETE)
    @DeleteMapping("/del")
    @Operation(summary = "采购计划-删除")
    public AjaxResult del(@RequestBody List<Long> ids){
@@ -54,10 +59,7 @@
        return result ? AjaxResult.success() : AjaxResult.error();
    }
    /**
     * å¯¼å‡º
     * @param response
     */
    @Log(title = "导出采购计划", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response) {
        procurementPlanService.export(response);
src/main/java/com/ruoyi/procurementrecord/controller/ProcurementPriceManagementController.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.pojo.ProcurementPriceManagement;
@@ -34,6 +36,7 @@
        return AjaxResult.success(result);
    }
    @Log(title = "新增采购价格管理", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Operation(summary = "采购价格管理-添加")
    @Transactional(rollbackFor = Exception.class)
@@ -42,6 +45,7 @@
        return result ? AjaxResult.success() : AjaxResult.error();
    }
    @Log(title = "修改采购价格管理", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    @Operation(summary = "采购价格管理-修改")
    @Transactional(rollbackFor = Exception.class)
@@ -50,6 +54,7 @@
        return result ? AjaxResult.success() : AjaxResult.error();
    }
    @Log(title = "删除采购价格管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/del")
    @Operation(summary = "采购价格管理-删除")
    @Transactional(rollbackFor = Exception.class)
@@ -61,10 +66,7 @@
        return result ? AjaxResult.success() : AjaxResult.error();
    }
    /**
     * å¯¼å‡º
     * @param response
     */
    @Log(title = "导出采购价格管理", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response) {
        procurementPriceManagementService.export(response);
src/main/java/com/ruoyi/procurementrecord/controller/ReturnManagementController.java
@@ -3,11 +3,18 @@
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.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.procurementrecord.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 +38,7 @@
    private ReturnManagementService returnManagementService;
    private ReturnSaleProductService returnSaleProductService;
    private final AccountStatementDetailsService accountStatementDetailsService;
    @GetMapping("/listPage")
    @Operation(summary = "销售退货-查询")
@@ -39,6 +47,7 @@
        return AjaxResult.success(result);
    }
    @Log(title = "新增销售退货", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    @Operation(summary = "销售退货-添加")
    @Transactional(rollbackFor = Exception.class)
@@ -46,6 +55,7 @@
        return returnManagementService.addReturnManagementDto(returnManagementDto) ? success() : error();
    }
    @Log(title = "修改销售退货", businessType = BusinessType.UPDATE)
    @PostMapping("/update")
    @Operation(summary = "销售退货-修改")
    @Transactional(rollbackFor = Exception.class)
@@ -62,11 +72,20 @@
    }
    @Log(title = "删除销售退货", businessType = BusinessType.DELETE)
    @DeleteMapping("/del")
    @Operation(summary = "销售退货-删除")
    @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/controller/ReturnSaleProductController.java
@@ -1,5 +1,7 @@
package com.ruoyi.procurementrecord.controller;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
src/main/java/com/ruoyi/procurementrecord/pojo/CustomStorage.java
@@ -97,7 +97,6 @@
    /**
     * å…¥åº“æ—¶é—´
     */
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDateTime createTime;
src/main/java/com/ruoyi/procurementrecord/pojo/ReturnManagement.java
@@ -63,7 +63,6 @@
    @Schema(description = "创建时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @Schema(description = "更新时间")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
src/main/java/com/ruoyi/procurementrecord/service/impl/ProcurementRecordServiceImpl.java
@@ -535,7 +535,7 @@
            Long aLong = customStorageMapper.selectCount(null);
            item.setInboundBatches(aLong.equals(0L) ? "第1批次(自定义入库)" : "第"+ (aLong + 1) + "批次(自定义入库)" );
            item.setCreateBy(loginUser.getNickName());
            item.setCode(OrderUtils.countTodayByCreateTime(customStorageMapper, "", "code"));
            item.setCode(OrderUtils.countTodayByCreateTime(customStorageMapper, "", "code", item.getCreateTime() != null ? item.getCreateTime() : LocalDateTime.now()));
            customStorageMapper.insert(item);
        });
        return AjaxResult.success("自定义入库成功");
src/main/java/com/ruoyi/procurementrecord/service/impl/ReturnManagementServiceImpl.java
@@ -4,12 +4,12 @@
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;
import com.ruoyi.common.utils.SecurityUtils;
import java.time.LocalDateTime;
import com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto;
import com.ruoyi.procurementrecord.bean.dto.ReturnSaleProductDto;
import com.ruoyi.procurementrecord.bean.vo.ShippingInfoVo;
@@ -31,7 +31,6 @@
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
@@ -49,7 +48,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) {
@@ -60,7 +58,7 @@
    @Override
    public boolean addReturnManagementDto(ReturnManagementDto returnManagementDto) {
        if (ObjectUtils.isEmpty(returnManagementDto.getReturnNo())){
            String rt = OrderUtils.countTodayByCreateTime(returnManagementMapper, "RT","return_no");
            String rt = OrderUtils.countTodayByCreateTime(returnManagementMapper, "RT","return_no", returnManagementDto.getCreateTime() != null ? returnManagementDto.getCreateTime() : LocalDateTime.now());
            returnManagementDto.setReturnNo(rt);
        }
        save(returnManagementDto);
@@ -127,19 +125,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/procurementrecord/utils/StockUtils.java
@@ -18,6 +18,7 @@
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collections;
@Component
@@ -88,11 +89,21 @@
     * @param recordId
     */
    public void addStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId) {
        addStock(productModelId, quantity, recordType, recordId, null);
    }
    /**
     * åˆæ ¼å…¥åº“
     * @param recordType
     * @param recordId
     */
    public void addStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId, LocalDateTime createTime) {
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setRecordId(recordId);
        stockInventoryDto.setRecordType(String.valueOf(recordType));
        stockInventoryDto.setQualitity(quantity);
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setCreateTime(createTime);
        stockInventoryService.addStockInRecordOnly(stockInventoryDto);
    }
@@ -104,12 +115,22 @@
     * @param recordId
     */
    public void addStockWithBatchNo(Long productModelId, BigDecimal quantity, String recordType, Long recordId, String batchNo) {
        addStockWithBatchNo(productModelId, quantity, recordType, recordId, batchNo, null);
    }
    /**
     * åˆæ ¼å…¥åº“带批次号
     * @param recordType
     * @param recordId
     */
    public void addStockWithBatchNo(Long productModelId, BigDecimal quantity, String recordType, Long recordId, String batchNo, LocalDateTime createTime) {
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setRecordId(recordId);
        stockInventoryDto.setRecordType(String.valueOf(recordType));
        stockInventoryDto.setQualitity(quantity);
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setBatchNo(batchNo);
        stockInventoryDto.setCreateTime(createTime);
        stockInventoryService.addStockInRecordOnly(stockInventoryDto);
    }
src/main/java/com/ruoyi/production/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/controller/ProductionBomStructureController.java
@@ -1,5 +1,7 @@
package com.ruoyi.production.controller;
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.production.bean.dto.ProductionBomStructureDto;
import com.ruoyi.production.bean.vo.ProductionBomStructureVo;
@@ -31,6 +33,7 @@
        return R.ok(productionBomStructureService.listByBomId(bomId));
    }
    @Log(title = "新增或修改BOM结构", businessType = BusinessType.UPDATE)
    @PostMapping("/addOrUpdateBomStructs")
    @Operation(summary = "新增或修改BOM结构")
    public R addProductionBomStructure(@RequestBody ProductionBomStructureDto productionBomStructureDto) {
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java
@@ -1,5 +1,7 @@
package com.ruoyi.production.controller;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
@@ -40,36 +42,42 @@
        return R.ok(productionOperationTaskService.getProductionOperationTaskInfo(id));
    }
    @Log(title = "新增工单", businessType = BusinessType.INSERT)
    @PostMapping
    @Operation(summary = "新增工单")
    public R<Boolean> add(@RequestBody ProductionOperationTask productionOperationTask) {
        return R.ok(productionOperationTaskService.saveProductionOperationTask(productionOperationTask));
    }
    @Log(title = "修改工单", businessType = BusinessType.UPDATE)
    @PutMapping
    @Operation(summary = "修改工单")
    public R<Boolean> edit(@RequestBody ProductionOperationTask productionOperationTask) {
        return R.ok(productionOperationTaskService.saveProductionOperationTask(productionOperationTask));
    }
    @Log(title = "删除工单", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    @Operation(summary = "删除工单")
    public R<Boolean> remove(@RequestBody List<Long> ids) {
        return R.ok(productionOperationTaskService.removeProductionOperationTask(ids));
    }
    @Log(title = "产品工单更新", businessType = BusinessType.UPDATE)
    @Operation(summary = "产品工单更新")
    @PostMapping("/updateProductWorkOrder")
    public R updateProductWorkOrder(@RequestBody ProductionOperationTaskDto dto) {
        return R.ok(productionOperationTaskService.updateProductWorkOrder(dto));
    }
    @Log(title = "指派报工人", businessType = BusinessType.UPDATE)
    @Operation(summary = "指派报工人")
    @PostMapping("/assign")
    public R<Boolean> assign(@RequestBody ProductionOperationTaskDto dto) {
        return R.ok(productionOperationTaskService.assign(dto));
    }
    @Log(title = "工单流转卡下载", businessType = BusinessType.EXPORT)
    @PostMapping("/down")
    @Operation(summary = "工单流转卡下载")
    public void down(HttpServletResponse response, @RequestBody ProductionOperationTaskDto dto) {
src/main/java/com/ruoyi/production/controller/ProductionOrderController.java
@@ -53,6 +53,7 @@
        return R.ok(productionOrderService.getProductionOrderInfo(id));
    }
    @Log(title = "生产工单", businessType = BusinessType.INSERT)
    @PostMapping("/addOrder")
    @Operation(summary = "新增生产订单", description = "新增下单只支持1种方式:生产计划生成,传 productionPlanIds,系统自动汇总计划得到产品规格和数量;"
                    + "technologyRoutingId ä¸ºç©ºæ—¶ä¼šè‡ªåŠ¨åŒ¹é…è¯¥äº§å“è§„æ ¼æœ€æ–°å·¥è‰ºè·¯çº¿ï¼Œquantity æœ€ç»ˆå¿…须大于 0。")
@@ -62,18 +63,21 @@
        return R.ok(productionOrderService.saveProductionOrder(productionOrder));
    }
    @Log(title = "生产工单", businessType = BusinessType.UPDATE)
    @Operation(summary = "绑定工艺路线")
    @PostMapping("/bindingRoute")
    public R bindingRoute(@RequestBody ProductionOrderDto productionOrderDto) {
        return R.ok(productionOrderService.bindingRoute(productionOrderDto));
    }
    @Log(title = "生产工单", businessType = BusinessType.UPDATE)
    @PostMapping("/syncSnapshot/{id}")
    @Operation(summary = "同步生产订单工艺/BOM快照")
    public R<Integer> syncSnapshot(@PathVariable Long id) {
        return R.ok(productionOrderService.syncProductionOrderSnapshot(id));
    }
    @Log(title = "生产工单", businessType = BusinessType.DELETE)
    @DeleteMapping("/delete")
    @Operation(summary = "删除生产订单")
    public R<Boolean> remove(@RequestBody List<Long> ids) {
@@ -98,6 +102,7 @@
        return R.ok(productionOrderService.getWorkOrderReportInspectDetail(productionOrderDto));
    }
    @Log(title = "生产工单", businessType = BusinessType.UPDATE)
    @Operation(summary = "更新订单状态")
    @PostMapping("/updateOrder")
    public R updateOrder(@RequestBody ProductionOrderDto productionOrderDto) {
src/main/java/com/ruoyi/production/controller/ProductionOrderPickController.java
@@ -1,5 +1,7 @@
package com.ruoyi.production.controller;
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.production.bean.dto.ProductionOrderPickDto;
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
@@ -27,12 +29,14 @@
    private final ProductionOrderPickService productionOrderPickService;
    @Log(title = "领料保存到线边仓", businessType = BusinessType.INSERT)
    @PostMapping("/savePick")
    @Operation(summary = "领料保存到线边仓")
    public R<Boolean> savePick(@RequestBody ProductionOrderPickDto dto) {
        return R.ok(productionOrderPickService.savePick(dto));
    }
    @Log(title = "变更领料", businessType = BusinessType.UPDATE)
    @PostMapping("/updatePick")
    @Operation(summary = "变更领料")
    public R<Boolean> updatePick(@RequestBody ProductionOrderPickDto dto) {
src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java
@@ -1,5 +1,7 @@
package com.ruoyi.production.controller;
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.production.pojo.ProductionOrderRoutingOperation;
import com.ruoyi.production.service.ProductionOrderRoutingOperationService;
@@ -30,24 +32,28 @@
        return R.ok(productionOrderRoutingService.listMain(orderId));
    }
    @Log(title = "生产工序路由", businessType = BusinessType.INSERT)
    @PostMapping("/addRouteItem")
    @Operation(summary = "新增生产订单的工艺路线详情")
    public R addRouteItem(@RequestBody ProductionOrderRoutingOperation productionOrderRoutingOperation) {
        return productionOrderRoutingOperationService.addRouteItem(productionOrderRoutingOperation);
    }
    @Log(title = "生产工序路由", businessType = BusinessType.UPDATE)
    @PostMapping("/updateRouteItem")
    @Operation(summary = "修改生产订单的工艺路线详情")
    public R updateRouteItem(@RequestBody ProductionOrderRoutingOperation productionOrderRoutingOperation) {
        return R.ok(productionOrderRoutingOperationService.updateById(productionOrderRoutingOperation));
        return productionOrderRoutingOperationService.updateRouteItem(productionOrderRoutingOperation);
    }
    @Log(title = "生产工序路由", businessType = BusinessType.DELETE)
    @DeleteMapping("/deleteRouteItem/{id}")
    @Operation(summary = "删除生产工艺路线")
    public R deleteRouteItem(@PathVariable("id") Long id) {
        return productionOrderRoutingOperationService.deleteRouteItem(id);
    }
    @Log(title = "生产工序路由", businessType = BusinessType.UPDATE)
    @PostMapping("/sortRouteItem")
    @Operation(summary = "排序")
    public R sortRouteItem(@RequestBody ProductionOrderRoutingOperation productionOrderRoutingOperation) {
src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingOperationParamController.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.bean.dto.ProductionOrderRoutingOperationParamDto;
import com.ruoyi.production.bean.vo.ProductionOrderRoutingOperationParamVo;
@@ -41,18 +43,21 @@
        return R.ok(productionOrderRoutingOperationParamService.getProductionOrderRoutingOperationParamInfo(id));
    }
    @Log(title = "生产工序参数", businessType = BusinessType.INSERT)
    @PostMapping
    @Operation(summary = "新增生产订单工艺路线工序参数")
    public R<Boolean> add(@RequestBody ProductionOrderRoutingOperationParam item) {
        return R.ok(productionOrderRoutingOperationParamService.saveProductionOrderRoutingOperationParam(item));
    }
    @Log(title = "生产工序参数", businessType = BusinessType.UPDATE)
    @PutMapping
    @Operation(summary = "修改生产订单工艺路线工序参数")
    public R<Boolean> edit(@RequestBody ProductionOrderRoutingOperationParam item) {
        return R.ok(productionOrderRoutingOperationParamService.updateById(item));
    }
    @Log(title = "生产工序参数", businessType = BusinessType.DELETE)
    @DeleteMapping("/{id}")
    @Operation(summary = "删除生产订单工艺路线工序参数")
    public R<Boolean> remove(@PathVariable("id") Long id) {
src/main/java/com/ruoyi/production/controller/ProductionProductMainController.java
@@ -2,6 +2,8 @@
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;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.service.ProductionProductMainService;
@@ -50,30 +52,35 @@
     * @param productionProductMainDto
     * @return
     */
    @Log(title = "生产产品", businessType = BusinessType.INSERT)
    @PostMapping("/addProductMain")
    @PreAuthorize("@ss.hasPermi('productionProductMain:add')")
    public R addProductMain(@RequestBody ProductionProductMainDto productionProductMainDto) {
        return R.ok(productionProductMainService.addProductMain(productionProductMainDto));
    }
    @Log(title = "生产产品", businessType = BusinessType.INSERT)
    @PostMapping
    @Operation(summary = "新增生产报工")
    public R add(@RequestBody ProductionProductMainDto productionProductMainDto) {
        return R.ok(productionProductMainService.saveProductionProductMain(productionProductMainDto));
    }
    @Log(title = "生产产品", businessType = BusinessType.UPDATE)
    @PutMapping
    @Operation(summary = "修改生产报工")
    public R edit(@RequestBody ProductionProductMainDto productionProductMainDto) {
        return R.ok(productionProductMainService.saveProductionProductMain(productionProductMainDto));
    }
    @Log(title = "生产产品", businessType = BusinessType.DELETE)
    @Operation(summary = "删除报工")
    @DeleteMapping("/delete")
    public R delete(@RequestBody ProductionProductMainDto productionProductMainDto) {
        return R.ok(productionProductMainService.removeProductMain(productionProductMainDto.getId()));
    }
    @Log(title = "生产产品", businessType = BusinessType.DELETE)
    @DeleteMapping("/{id}")
    @Operation(summary = "删除生产报工")
    public R remove(@PathVariable("id") Long id) {
@@ -84,6 +91,7 @@
    /**
     * å¯¼å‡º
     */
    @Log(title = "生产产品", businessType = BusinessType.EXPORT)
    @PostMapping("/export")
    public void export(HttpServletResponse response, ProductionProductMainDto productionProductMainDto) {
        List<ProductionProductMainDto> list;
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/pojo/ProductionOrder.java
@@ -41,7 +41,6 @@
    private String npsNo;
    @Schema(description = "录入时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @Schema(description = "更新时间")
src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java
@@ -8,7 +8,10 @@
    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;
@@ -310,7 +311,7 @@
            if (matchedOperation == null) {
                matchedOperation = insertRoutingOperationSnapshot(orderRouting.getId(), productionOrderId, desiredOperation);
            } else {
                updateRoutingOperationSnapshotIfNecessary(desiredOperation, orderRouting.getId(), productionOrderId, matchedOperation);
                updateRoutingOperationSnapshotIfNecessary(matchedOperation, orderRouting.getId(), productionOrderId, desiredOperation);
            }
            finalOperationList.add(matchedOperation);
        }
@@ -381,17 +382,32 @@
        Map<Long, ProductionBomStructure> structureById = structureList.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructure::getId, item -> item, (left, right) -> left));
        Map<String, ProductionBomStructure> uniqueOperationMap = new LinkedHashMap<>();
        for (ProductionBomStructure bomStructure : structureList) {
            if (bomStructure == null || bomStructure.getTechnologyOperationId() == null) {
                continue;
        // æž„建父-子映射关系
        Map<Long, List<ProductionBomStructure>> treeMap = buildParentChildMap(structureList);
        // ä½¿ç”¨åŽåºéåŽ†æž„å»ºæ“ä½œåˆ—è¡¨ï¼ˆå…ˆå­åŽçˆ¶ï¼Œç¡®ä¿å·¥è‰ºè·¯çº¿é¡ºåºæ­£ç¡®ï¼‰
        // ä½¿ç”¨æ·±åº¦ä½œä¸ºæŽ’序依据的辅助结构
        Map<String, ProductionBomStructure> operationMap = new LinkedHashMap<>();
        Map<String, Integer> depthMap = new HashMap<>();
        buildOperationListPostOrderWithDepth(null, treeMap, operationMap, depthMap, structureById, rootProductModelId, 1);
        // æŒ‰æ·±åº¦æŽ’序,深度大的排前面
        List<Map.Entry<String, ProductionBomStructure>> sortedEntries = new ArrayList<>(operationMap.entrySet());
        sortedEntries.sort((a, b) -> {
            int depthCompare = Integer.compare(
                    depthMap.getOrDefault(b.getKey(), 0),
                    depthMap.getOrDefault(a.getKey(), 0));
            if (depthCompare != 0) {
                return depthCompare;
            }
            Long outputProductModelId = resolveOutputProductModelId(resolveOperationOutputNode(bomStructure, structureById), rootProductModelId);
            uniqueOperationMap.putIfAbsent(buildBomOperationDedupKey(bomStructure, outputProductModelId), bomStructure);
        }
            return 0;
        });
        List<ProductionOrderRoutingOperation> desiredOperationList = new ArrayList<>();
        int dragSort = 1;
        for (ProductionBomStructure bomStructure : uniqueOperationMap.values()) {
        for (Map.Entry<String, ProductionBomStructure> entry : sortedEntries) {
            ProductionBomStructure bomStructure = entry.getValue();
            Long outputProductModelId = resolveOutputProductModelId(resolveOperationOutputNode(bomStructure, structureById), rootProductModelId);
            TechnologyOperation technologyOperation = getTechnologyOperation(bomStructure.getTechnologyOperationId());
            ProductionOrderRoutingOperation routingOperation = new ProductionOrderRoutingOperation();
@@ -405,6 +421,127 @@
            desiredOperationList.add(routingOperation);
        }
        return desiredOperationList;
    }
    private void buildOperationListPostOrderWithDepth(Long parentId,
                                                      Map<Long, List<ProductionBomStructure>> treeMap,
                                                      Map<String, ProductionBomStructure> operationMap,
                                                      Map<String, Integer> depthMap,
                                                      Map<Long, ProductionBomStructure> structureById,
                                                      Long rootProductModelId,
                                                      int currentDepth) {
        List<ProductionBomStructure> children = treeMap.get(parentId);
        if (children == null || children.isEmpty()) {
            return;
        }
        for (ProductionBomStructure child : children) {
            // å…ˆé€’归处理子节点
            buildOperationListPostOrderWithDepth(child.getId(), treeMap, operationMap, depthMap, structureById, rootProductModelId, currentDepth + 1);
            // å†å¤„理当前节点
            if (child.getTechnologyOperationId() != null) {
                Long outputProductModelId = resolveOutputProductModelId(resolveOperationOutputNode(child, structureById), rootProductModelId);
                String key = buildBomOperationDedupKey(child, outputProductModelId);
                // ä¿ç•™æ·±åº¦æœ€å¤§çš„æ“ä½œ
                Integer existingDepth = depthMap.get(key);
                if (existingDepth == null || currentDepth > existingDepth) {
                    operationMap.put(key, child);
                    depthMap.put(key, currentDepth);
                }
            }
        }
    }
    private Map<Long, List<ProductionBomStructure>> buildParentChildMap(List<ProductionBomStructure> structureList) {
        Map<Long, List<ProductionBomStructure>> treeMap = new LinkedHashMap<>();
        Map<Long, ProductionBomStructure> structureById = new HashMap<>();
        // æž„建父-子映射和ID映射
        for (ProductionBomStructure structure : structureList) {
            if (structure == null) continue;
            Long parentId = structure.getParentId();
            treeMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(structure);
            if (structure.getId() != null) {
                structureById.put(structure.getId(), structure);
            }
        }
        // è®¡ç®—每个节点的深度(从根节点到当前节点的距离,根节点深度为1)
        Map<Long, Integer> depthMap = new HashMap<>();
        for (ProductionBomStructure structure : structureList) {
            if (structure == null || structure.getId() == null) continue;
            computeDepthFromRoot(structure.getId(), structureById, depthMap);
        }
        // å¯¹æ¯ä¸ªçˆ¶èŠ‚ç‚¹ä¸‹çš„å­èŠ‚ç‚¹æŒ‰æ·±åº¦å€’åºæŽ’åºï¼ˆæœ€æ·±å±‚çš„ä¼˜å…ˆï¼‰
        for (Map.Entry<Long, List<ProductionBomStructure>> entry : treeMap.entrySet()) {
            List<ProductionBomStructure> children = entry.getValue();
            children.sort((a, b) -> {
                // ä¼˜å…ˆæŒ‰æ·±åº¦æŽ’序,深度大的排前面(最深层优先)
                int depthCompare = Integer.compare(
                        depthMap.getOrDefault(b.getId(), 0),
                        depthMap.getOrDefault(a.getId(), 0));
                if (depthCompare != 0) {
                    return depthCompare;
                }
                // æ·±åº¦ç›¸åŒæ—¶æŒ‰ID排序保证稳定性
                return Long.compare(a.getId(), b.getId());
            });
        }
        return treeMap;
    }
    /**
     * è®¡ç®—节点深度(从根节点到当前节点的距离)
     * æ ¹èŠ‚ç‚¹æ·±åº¦ä¸º1,每向下一层深度加1
     */
    private int computeDepthFromRoot(Long nodeId, Map<Long, ProductionBomStructure> structureById, Map<Long, Integer> depthMap) {
        if (depthMap.containsKey(nodeId)) {
            return depthMap.get(nodeId);
        }
        ProductionBomStructure structure = structureById.get(nodeId);
        if (structure == null) {
            depthMap.put(nodeId, 1);
            return 1;
        }
        Long parentId = structure.getParentId();
        if (parentId == null || parentId == 0L) {
            // æ ¹èŠ‚ç‚¹æ·±åº¦ä¸º1
            depthMap.put(nodeId, 1);
            return 1;
        }
        // å­èŠ‚ç‚¹æ·±åº¦ = çˆ¶èŠ‚ç‚¹æ·±åº¦ + 1
        int parentDepth = computeDepthFromRoot(parentId, structureById, depthMap);
        int depth = parentDepth + 1;
        depthMap.put(nodeId, depth);
        return depth;
    }
    private void buildOperationListPostOrder(Long parentId,
                                             Map<Long, List<ProductionBomStructure>> treeMap,
                                             Map<String, ProductionBomStructure> uniqueOperationMap,
                                             Map<Long, ProductionBomStructure> structureById,
                                             Long rootProductModelId) {
        List<ProductionBomStructure> children = treeMap.get(parentId);
        if (children == null || children.isEmpty()) {
            return;
        }
        for (ProductionBomStructure child : children) {
            // å…ˆé€’归处理子节点
            buildOperationListPostOrder(child.getId(), treeMap, uniqueOperationMap, structureById, rootProductModelId);
            // å†å¤„理当前节点
            if (child.getTechnologyOperationId() != null) {
                Long outputProductModelId = resolveOutputProductModelId(resolveOperationOutputNode(child, structureById), rootProductModelId);
                String key = buildBomOperationDedupKey(child, outputProductModelId);
                // åŽ»é‡æ—¶ä¿ç•™æ·±åº¦æœ€å¤§çš„æ“ä½œï¼ˆåŽåºéåŽ†å…ˆé‡åˆ°æ·±å±‚èŠ‚ç‚¹ï¼Œæ‰€ä»¥ç›´æŽ¥è¦†ç›–å³å¯ï¼‰
                uniqueOperationMap.put(key, child);
            }
        }
    }
    private Map<String, Deque<ProductionOrderRoutingOperation>> buildExistingRoutingOperationBucketMap(List<ProductionOrderRoutingOperation> existingOperationList) {
@@ -571,7 +708,7 @@
            return;
        }
        if (defaultDecimal(task.getCompleteQuantity()).compareTo(BigDecimal.ZERO) > 0) {
            throw new ServiceException("工序已产生报工记录,无法根据 BOM å˜æ›´åˆ é™¤å¯¹åº”工序快照");
            throw new ServiceException("工序已产生报工记录,无法根据 BOM å˜æ›´åˆ é™¤å¯¹åº”工序快照" + task.getWorkOrderNo());
        }
        long reportCount = productionProductMainMapper.selectCount(
                Wrappers.<ProductionProductMain>lambdaQuery()
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
@@ -182,4 +283,24 @@
        }
        return 0;
    }
    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/service/impl/ProductionOrderServiceImpl.java
@@ -44,6 +44,7 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@@ -59,6 +60,7 @@
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final ProductionProductOutputMapper productionProductOutputMapper;
    private final ProductionOrderPickMapper productionOrderPickMapper;
@@ -114,7 +116,7 @@
        // ä¸‹å•入口统一补齐来源单据、计划和工艺信息,避免前端分别传多套字段。
        validateAndFillOrder(productionOrder, oldOrder);
        if (productionOrder.getNpsNo() == null || productionOrder.getNpsNo().trim().isEmpty()) {
            productionOrder.setNpsNo(generateNextOrderNo());
            productionOrder.setNpsNo(generateNextOrderNo(productionOrder.getCreateTime() != null ? productionOrder.getCreateTime() : LocalDateTime.now()));
        }
        if (productionOrder.getCompleteQuantity() == null) {
            productionOrder.setCompleteQuantity(BigDecimal.ZERO);
@@ -507,9 +509,10 @@
                .orderByDesc(ProductionOrder::getId);
    }
    private String generateNextOrderNo() {
    private String generateNextOrderNo(LocalDateTime createTime) {
        // ç”Ÿæˆä¸‹ä¸€ä¸ªç”Ÿäº§è®¢å•号
        String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        LocalDate localDate = createTime.toLocalDate();
        String datePrefix = localDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String prefix = "SC" + datePrefix;
        ProductionOrder latestOrder = this.getOne(Wrappers.<ProductionOrder>lambdaQuery()
                .likeRight(ProductionOrder::getNpsNo, prefix)
@@ -1029,13 +1032,46 @@
            return Collections.emptyList();
        }
        List<ProductionBomStructureVo> bomStructureList = productionBomStructureMapper.pickByBomId(orderBom.getId());
        // æŸ¥è¯¢å®Œæ•´çš„BOM结构(包括根节点),用于计算层级需求数量
        List<ProductionBomStructureVo> bomStructureList = productionBomStructureMapper.listByBomId(orderBom.getId());
        if (bomStructureList == null || bomStructureList.isEmpty()) {
            return Collections.emptyList();
        }
        // æŸ¥è¯¢ç”Ÿäº§è®¢å•获取订单数量
        ProductionOrder productionOrder = productionOrderMapper.selectById(productionOrderId);
        BigDecimal orderQuantity = productionOrder != null ? defaultDecimal(productionOrder.getQuantity()) : BigDecimal.ZERO;
        // æž„建树形结构并计算层级需求数量
        Map<Long, ProductionBomStructureVo> structureByIdMap = bomStructureList.stream()
                .filter(s -> s != null && s.getId() != null)
                .collect(Collectors.toMap(ProductionBomStructureVo::getId, s -> s));
        // æŒ‰å±‚级计算需求数量:子级需求数量 = çˆ¶çº§éœ€æ±‚数量 Ã— å­çº§å•位产出所需数量
        for (ProductionBomStructureVo structure : bomStructureList) {
            if (structure == null) continue;
            if (structure.getParentId() == null || structure.getParentId() == 0) {
                // æ ¹èŠ‚ç‚¹ï¼šéœ€æ±‚æ•°é‡ = è®¢å•数量
                structure.setDemandedQuantity(orderQuantity);
            } else {
                // å­èŠ‚ç‚¹ï¼šéœ€æ±‚æ•°é‡ = çˆ¶çº§éœ€æ±‚数量 Ã— å­çº§å•位产出所需数量
                ProductionBomStructureVo parent = structureByIdMap.get(structure.getParentId());
                if (parent != null) {
                    BigDecimal parentDemandedQty = defaultDecimal(parent.getDemandedQuantity());
                    BigDecimal unitQuantity = defaultDecimal(structure.getUnitQuantity());
                    structure.setDemandedQuantity(parentDemandedQty.multiply(unitQuantity));
                }
            }
        }
        // è¿‡æ»¤å‡ºéžæ ¹èŠ‚ç‚¹ï¼ˆå®žé™…é¢†æ–™é¡¹ï¼‰
        List<ProductionBomStructureVo> childStructureList = bomStructureList.stream()
                .filter(s -> s != null && s.getParentId() != null && s.getParentId() != 0)
                .collect(Collectors.toList());
        // éåŽ†å¤„ç†æ•°æ®å¹¶ç»„è£…ç»“æžœ
        List<Long> productModelIds = bomStructureList.stream()
        List<Long> productModelIds = childStructureList.stream()
                .map(ProductionBomStructureVo::getProductModelId)
                .filter(Objects::nonNull)
                .distinct()
@@ -1060,7 +1096,7 @@
        }
        Map<String, ProductionOrderPickVo> mergedPickMap = new LinkedHashMap<>();
        for (ProductionBomStructureVo structure : bomStructureList) {
        for (ProductionBomStructureVo structure : childStructureList) {
            if (structure == null || structure.getProductModelId() == null) {
                continue;
            }
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -401,7 +401,7 @@
            productionAccount.setSchedulingUserId(user == null ? null : user.getUserId());
            productionAccount.setSchedulingUserName(user == null ? dto.getUserName() : user.getNickName());
            productionAccount.setFinishedNum(productQty);
            productionAccount.setWorkHours(workHours);
            productionAccount.setWorkHours(technologyOperation != null ? technologyOperation.getSalaryQuota() : null);
            productionAccount.setTechnologyOperationName(technologyOperation == null ? null : technologyOperation.getName());
            productionAccount.setSchedulingDate(LocalDateTime.now());
            productionAccountMapper.insert(productionAccount);
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/common/CommonController.java
@@ -3,6 +3,8 @@
import com.ruoyi.basic.service.StorageBlobService;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Anonymous;
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;
@@ -36,6 +38,7 @@
    private final StorageBlobService storageBlobService;
    private final FileUtil fileUtil;
    @Log(title = "通用文件上传", businessType = BusinessType.INSERT)
    @PostMapping({"/upload"})
    @Operation(summary = "文件上传")
    public R upload(@RequestParam("files") List<MultipartFile> files) {
@@ -46,6 +49,7 @@
     * å…¬å…±æ–‡ä»¶ä¸Šä¼ 
     * æ­¤æŽ¥å£ä¸Šä¼ çš„æ–‡ä»¶æ°¸ä¹…有效,慎用
     */
    @Log(title = "公共文件上传", businessType = BusinessType.INSERT)
    @PostMapping({"/public/upload"})
    @Operation(summary = "文件上传")
    public R publicUpload(@RequestParam("files") List<MultipartFile> files) {
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 src/main/java/com/ruoyi/project/monitor/controller/SysJobController.java src/main/java/com/ruoyi/project/monitor/controller/SysJobLogController.java src/main/java/com/ruoyi/project/monitor/controller/SysLogininforController.java src/main/java/com/ruoyi/project/monitor/controller/SysOperlogController.java src/main/java/com/ruoyi/project/monitor/controller/SysUserOnlineController.java src/main/java/com/ruoyi/project/system/controller/SysConfigController.java src/main/java/com/ruoyi/project/system/controller/SysDeptController.java src/main/java/com/ruoyi/project/system/controller/SysDictDataController.java src/main/java/com/ruoyi/project/system/controller/SysDictTypeController.java src/main/java/com/ruoyi/project/system/controller/SysLoginController.java src/main/java/com/ruoyi/project/system/controller/SysMenuController.java src/main/java/com/ruoyi/project/system/controller/SysNoticeController.java src/main/java/com/ruoyi/project/system/controller/SysPostController.java src/main/java/com/ruoyi/project/system/controller/SysProfileController.java src/main/java/com/ruoyi/project/system/controller/SysRegisterController.java src/main/java/com/ruoyi/project/system/controller/SysRoleController.java src/main/java/com/ruoyi/project/system/controller/SysUserClientController.java (已删除) src/main/java/com/ruoyi/project/system/controller/SysUserController.java src/main/java/com/ruoyi/project/system/domain/GetuiConfig.java (已删除) src/main/java/com/ruoyi/project/system/domain/SysUserClient.java (已删除) src/main/java/com/ruoyi/project/system/mapper/SysUserClientMapper.java (已删除) src/main/java/com/ruoyi/project/system/mapper/SysUserDeptMapper.java src/main/java/com/ruoyi/project/system/service/ISysUserService.java src/main/java/com/ruoyi/project/system/service/SysUserClientService.java (已删除) src/main/java/com/ruoyi/project/system/service/impl/SysNoticeServiceImpl.java src/main/java/com/ruoyi/project/system/service/impl/SysUserClientServiceImpl.java (已删除) src/main/java/com/ruoyi/project/system/service/impl/SysUserServiceImpl.java src/main/java/com/ruoyi/project/system/service/impl/UnipushService.java (已删除) src/main/java/com/ruoyi/project/tool/gen/controller/GenController.java src/main/java/com/ruoyi/project/tool/swagger/TestController.java src/main/java/com/ruoyi/projectManagement/controller/InfoController.java src/main/java/com/ruoyi/projectManagement/controller/PlanController.java src/main/java/com/ruoyi/projectManagement/controller/RolesController.java src/main/java/com/ruoyi/projectManagement/pojo/Roles.java src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java src/main/java/com/ruoyi/purchase/controller/InvoicePurchaseController.java (已删除) src/main/java/com/ruoyi/purchase/controller/PaymentRegistrationController.java (已删除) src/main/java/com/ruoyi/purchase/controller/PurchaseLedgerController.java src/main/java/com/ruoyi/purchase/controller/PurchaseReturnOrdersController.java src/main/java/com/ruoyi/purchase/controller/TicketRegistrationController.java (已删除) src/main/java/com/ruoyi/purchase/dto/InvoicePurchaseDto.java (已删除) src/main/java/com/ruoyi/purchase/dto/InvoicePurchaseReportDto.java (已删除) src/main/java/com/ruoyi/purchase/dto/PaymentRegistrationDto.java (已删除) src/main/java/com/ruoyi/purchase/dto/ProductRecordDto.java (已删除) src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java src/main/java/com/ruoyi/purchase/dto/TicketRegistrationDto.java (已删除) src/main/java/com/ruoyi/purchase/dto/VatDto.java src/main/java/com/ruoyi/purchase/mapper/InvoicePurchaseMapper.java (已删除) src/main/java/com/ruoyi/purchase/mapper/PaymentRegistrationMapper.java (已删除) src/main/java/com/ruoyi/purchase/mapper/ProductRecordMapper.java (已删除) src/main/java/com/ruoyi/purchase/mapper/PurchaseLedgerMapper.java src/main/java/com/ruoyi/purchase/mapper/TicketRegistrationMapper.java (已删除) src/main/java/com/ruoyi/purchase/pojo/InvoicePurchase.java (已删除) src/main/java/com/ruoyi/purchase/pojo/PaymentRegistration.java (已删除) src/main/java/com/ruoyi/purchase/pojo/ProductRecord.java (已删除) src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java src/main/java/com/ruoyi/purchase/pojo/PurchaseReturnOrders.java src/main/java/com/ruoyi/purchase/pojo/TicketRegistration.java (已删除) src/main/java/com/ruoyi/purchase/service/IInvoicePurchaseService.java (已删除) src/main/java/com/ruoyi/purchase/service/IPaymentRegistrationService.java (已删除) src/main/java/com/ruoyi/purchase/service/IProductRecordService.java (已删除) src/main/java/com/ruoyi/purchase/service/IPurchaseLedgerService.java src/main/java/com/ruoyi/purchase/service/ITicketRegistrationService.java (已删除) src/main/java/com/ruoyi/purchase/service/PurchaseReportService.java src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java (已删除) src/main/java/com/ruoyi/purchase/service/impl/PaymentRegistrationServiceImpl.java (已删除) src/main/java/com/ruoyi/purchase/service/impl/ProductRecordServiceImpl.java (已删除) src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java src/main/java/com/ruoyi/purchase/service/impl/PurchaseReportServiceImpl.java src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java (已删除) src/main/java/com/ruoyi/purchase/vo/PurchaseReportVo.java src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsDetailsVo.java src/main/java/com/ruoyi/purchase/vo/SupplierTransactionsVo.java src/main/java/com/ruoyi/quality/controller/QualityInspectController.java src/main/java/com/ruoyi/quality/controller/QualityInspectFileController.java src/main/java/com/ruoyi/quality/controller/QualityInspectParamController.java src/main/java/com/ruoyi/quality/controller/QualityReportController.java src/main/java/com/ruoyi/quality/controller/QualityTestStandardBindingController.java src/main/java/com/ruoyi/quality/controller/QualityTestStandardController.java src/main/java/com/ruoyi/quality/controller/QualityTestStandardParamController.java src/main/java/com/ruoyi/quality/controller/QualityUnqualifiedController.java src/main/java/com/ruoyi/quality/pojo/QualityInspect.java src/main/java/com/ruoyi/quality/service/IQualityInspectService.java src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java src/main/java/com/ruoyi/quality/utils/QualityInspectHelper.java src/main/java/com/ruoyi/safe/controller/SafeAccidentController.java src/main/java/com/ruoyi/safe/controller/SafeCertificationController.java src/main/java/com/ruoyi/safe/controller/SafeCertificationFileController.java src/main/java/com/ruoyi/safe/controller/SafeContingencyPlanController.java src/main/java/com/ruoyi/safe/controller/SafeHazardController.java src/main/java/com/ruoyi/safe/controller/SafeHazardRecordController.java src/main/java/com/ruoyi/safe/controller/SafeHiddenController.java src/main/java/com/ruoyi/safe/controller/SafeHiddenFileController.java src/main/java/com/ruoyi/safe/controller/SafeTrainingController.java src/main/java/com/ruoyi/safe/controller/SafeTrainingDetailsController.java src/main/java/com/ruoyi/safe/controller/SafeTrainingFileController.java src/main/java/com/ruoyi/sales/controller/InvoiceLedgerController.java (已删除) src/main/java/com/ruoyi/sales/controller/InvoiceRegistrationController.java (已删除) src/main/java/com/ruoyi/sales/controller/MetricStatisticsController.java src/main/java/com/ruoyi/sales/controller/PaymentShippingController.java src/main/java/com/ruoyi/sales/controller/ReceiptPaymentController.java (已删除) src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java src/main/java/com/ruoyi/sales/controller/SalesLedgerProductController.java src/main/java/com/ruoyi/sales/controller/SalesQuotationController.java src/main/java/com/ruoyi/sales/controller/SalespersonManagementController.java src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java src/main/java/com/ruoyi/sales/dto/InvoiceLedgerDto.java (已删除) src/main/java/com/ruoyi/sales/dto/InvoiceRegistrationDto.java (已删除) src/main/java/com/ruoyi/sales/dto/InvoiceRegistrationProductDto.java (已删除) src/main/java/com/ruoyi/sales/dto/ReceiptPaymentDto.java (已删除) src/main/java/com/ruoyi/sales/dto/ReceiptPaymentExeclDto.java (已删除) src/main/java/com/ruoyi/sales/dto/ReceiptPaymentRecordDto.java (已删除) src/main/java/com/ruoyi/sales/dto/SalesQuotationDto.java src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java src/main/java/com/ruoyi/sales/excel/InvoiceLedgerExcelDto.java src/main/java/com/ruoyi/sales/excel/InvoiceRegisAndProductExcelDto.java (已删除) src/main/java/com/ruoyi/sales/mapper/InvoiceLedgerFileMapper.java (已删除) src/main/java/com/ruoyi/sales/mapper/InvoiceLedgerMapper.java (已删除) src/main/java/com/ruoyi/sales/mapper/InvoiceRegistrationMapper.java (已删除) src/main/java/com/ruoyi/sales/mapper/InvoiceRegistrationProductMapper.java (已删除) src/main/java/com/ruoyi/sales/mapper/ReceiptPaymentMapper.java (已删除) src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java src/main/java/com/ruoyi/sales/mapper/SalesLedgerProductMapper.java src/main/java/com/ruoyi/sales/pojo/InvoiceLedger.java (已删除) src/main/java/com/ruoyi/sales/pojo/InvoiceLedgerFile.java (已删除) src/main/java/com/ruoyi/sales/pojo/InvoiceRegistration.java (已删除) src/main/java/com/ruoyi/sales/pojo/InvoiceRegistrationProduct.java (已删除) src/main/java/com/ruoyi/sales/pojo/PaymentShipping.java src/main/java/com/ruoyi/sales/pojo/ReceiptPayment.java (已删除) src/main/java/com/ruoyi/sales/pojo/SalesLedger.java src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java src/main/java/com/ruoyi/sales/pojo/SalesQuotation.java src/main/java/com/ruoyi/sales/pojo/ShippingInfo.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/ShippingInfoService.java src/main/java/com/ruoyi/sales/service/impl/InvoiceLedgerServiceImpl.java (已删除) src/main/java/com/ruoyi/sales/service/impl/InvoiceRegistrationServiceImpl.java (已删除) src/main/java/com/ruoyi/sales/service/impl/MetricStatisticsServiceImpl.java src/main/java/com/ruoyi/sales/service/impl/ReceiptPaymentServiceImpl.java (已删除) src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java src/main/java/com/ruoyi/sales/service/impl/SalesQuotationServiceImpl.java src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java src/main/java/com/ruoyi/sales/vo/CustomerTransactionsVo.java src/main/java/com/ruoyi/staff/controller/HolidayApplicationController.java src/main/java/com/ruoyi/staff/controller/PersonalAttendanceLocationConfigController.java src/main/java/com/ruoyi/staff/controller/PersonalAttendanceRecordsController.java src/main/java/com/ruoyi/staff/controller/PersonalShiftController.java src/main/java/com/ruoyi/staff/controller/StaffLeaveController.java src/main/java/com/ruoyi/staff/controller/StaffOnJobController.java src/main/java/com/ruoyi/staff/controller/StaffSchedulingController.java src/main/java/com/ruoyi/staff/dto/StaffOnJobDto.java src/main/java/com/ruoyi/staff/mapper/StaffOnJobMapper.java src/main/java/com/ruoyi/staff/service/IStaffOnJobService.java src/main/java/com/ruoyi/staff/service/impl/SchemeApplicableStaffServiceImpl.java src/main/java/com/ruoyi/staff/service/impl/StaffLeaveServiceImpl.java src/main/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java src/main/java/com/ruoyi/staff/service/impl/StaffSalaryMainServiceImpl.java src/main/java/com/ruoyi/stock/controller/StockInRecordController.java src/main/java/com/ruoyi/stock/controller/StockUninventoryController.java src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java src/main/java/com/ruoyi/stock/pojo/StockInRecord.java src/main/java/com/ruoyi/stock/pojo/StockOutRecord.java src/main/java/com/ruoyi/stock/service/StockInRecordService.java src/main/java/com/ruoyi/stock/service/impl/StockInRecordServiceImpl.java src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java src/main/java/com/ruoyi/stock/service/impl/StockOutRecordServiceImpl.java src/main/java/com/ruoyi/stock/service/impl/StockUninventoryServiceImpl.java src/main/java/com/ruoyi/technology/controller/TechnologyBomStructureController.java src/main/java/com/ruoyi/technology/controller/TechnologyOperationController.java src/main/java/com/ruoyi/technology/controller/TechnologyOperationParamController.java src/main/java/com/ruoyi/technology/controller/TechnologyRoutingController.java src/main/java/com/ruoyi/technology/controller/TechnologyRoutingOperationController.java src/main/java/com/ruoyi/technology/controller/TechnologyRoutingOperationParamController.java src/main/java/com/ruoyi/technology/pojo/TechnologyParam.java src/main/java/com/ruoyi/technology/pojo/TechnologyRouting.java src/main/java/com/ruoyi/technology/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-ckgm.yml src/main/resources/application-hqjc.yml src/main/resources/application-jhy.yml src/main/resources/application.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/purchase/AccountPaymentApplicationMapper.xml src/main/resources/mapper/account/purchase/AccountPurchaseInvoiceMapper.xml src/main/resources/mapper/account/purchase/AccountPurchasePaymentMapper.xml src/main/resources/mapper/account/sales/AccountInvoiceApplicationMapper.xml src/main/resources/mapper/account/sales/AccountSalesCollectionMapper.xml src/main/resources/mapper/account/sales/AccountSalesInvoiceMapper.xml src/main/resources/mapper/aftersalesservice/AfterSalesNearExpiryMapper.xml src/main/resources/mapper/aftersalesservice/AfterSalesServiceMapper.xml src/main/resources/mapper/approve/ApprovalInstanceMapper.xml src/main/resources/mapper/approve/ApprovalInstanceNodeMapper.xml src/main/resources/mapper/approve/ApprovalRecordMapper.xml src/main/resources/mapper/approve/ApprovalTaskMapper.xml src/main/resources/mapper/approve/ApprovalTemplateMapper.xml src/main/resources/mapper/approve/ApprovalTemplateNodeApproverMapper.xml src/main/resources/mapper/approve/ApprovalTemplateNodeMapper.xml src/main/resources/mapper/approve/ApproveProcessMapper.xml src/main/resources/mapper/approve/FinReimbursementDetailMapper.xml src/main/resources/mapper/approve/FinReimbursementMapper.xml src/main/resources/mapper/approve/FinReimbursementTravelMapper.xml src/main/resources/mapper/approve/KnowledgeBaseMapper.xml src/main/resources/mapper/basic/CustomerMapper.xml src/main/resources/mapper/basic/ProductModelMapper.xml src/main/resources/mapper/basic/SupplierManageMapper.xml src/main/resources/mapper/collaborativeApproval/EnterpriseNewsMapper.xml src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeDeptMapper.xml src/main/resources/mapper/collaborativeApproval/EnterpriseNewsScopeUserMapper.xml src/main/resources/mapper/collaborativeApproval/NoticeMapper.xml src/main/resources/mapper/collaborativeApproval/RulesRegulationsManagementMapper.xml src/main/resources/mapper/collaborativeApproval/SealApplicationManagementMapper.xml src/main/resources/mapper/device/DeviceMaintenanceMapper.xml src/main/resources/mapper/device/DeviceRepairMapper.xml src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerRecordMapper.xml src/main/resources/mapper/measuringinstrumentledger/SparePartsMapper.xml src/main/resources/mapper/measuringinstrumentledger/SparePartsRequisitionRecordMapper.xml src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml src/main/resources/mapper/production/ProductionOperationTaskMapper.xml src/main/resources/mapper/production/ProductionOrderMapper.xml src/main/resources/mapper/production/ProductionPlanMapper.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/QualityInspectMapper.xml src/main/resources/mapper/quality/QualityTestStandardMapper.xml src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml src/main/resources/mapper/sales/InvoiceLedgerMapper.xml (已删除) src/main/resources/mapper/sales/InvoiceRegistrationMapper.xml (已删除) src/main/resources/mapper/sales/InvoiceRegistrationProductMapper.xml (已删除) src/main/resources/mapper/sales/ReceiptPaymentMapper.xml (已删除) src/main/resources/mapper/sales/SalesLedgerMapper.xml src/main/resources/mapper/sales/SalesLedgerProductMapper.xml src/main/resources/mapper/sales/SalesQuotationMapper.xml src/main/resources/mapper/staff/PersonalAttendanceRecordsMapper.xml src/main/resources/mapper/staff/PersonalShiftMapper.xml src/main/resources/mapper/staff/StaffLeaveMapper.xml src/main/resources/mapper/staff/StaffOnJobMapper.xml src/main/resources/mapper/stock/StockInRecordMapper.xml src/main/resources/mapper/stock/StockInventoryMapper.xml src/main/resources/mapper/stock/StockOutRecordMapper.xml src/main/resources/mapper/system/SysDeptMapper.xml src/main/resources/mapper/system/SysDictTypeMapper.xml src/main/resources/mapper/system/SysNoticeMapper.xml src/main/resources/mapper/system/SysPostMapper.xml src/main/resources/mapper/system/SysRoleMapper.xml src/main/resources/mapper/system/SysUserMapper.xml src/main/resources/mapper/technology/TechnologyOperationMapper.xml src/main/resources/mapper/technology/TechnologyOperationParamMapper.xml src/main/resources/mapper/warehouse/DocumentationBorrowManagementMapper.xml src/main/resources/mapper/warehouse/DocumentationMapper.xml src/main/resources/mapper/warehouse/DocumentationReturnManagementMapper.xml src/main/resources/mapper/warehouse/WarehouseGoodsShelvesRowcolMapper.xml src/main/resources/purchase-agent-prompt.txt src/main/resources/sales-agent-prompt.txt src/test/java/com/ruoyi/stock/service/impl/StockOutRecordBatchUpdateTest.java