hsy
4 天以前 bb0b80bd7ca7c1f979ab0125ae01f3faa1d14f4b
Merge branch 'refs/heads/dev_business' into fix/hsy/20260811
已修改64个文件
已添加51个文件
已删除1个文件
4979 ■■■■■ 文件已修改
.playwright-mcp/page-2026-08-14T05-18-36-135Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-14T05-18-50-100Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-14T05-19-08-995Z.yml 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
approval-detail-before-full.png 补丁 | 查看 | 原始文档 | blame | 历史
approval-detail-before.png 补丁 | 查看 | 原始文档 | blame | 历史
approval-detail-v2.jpeg 补丁 | 查看 | 原始文档 | blame | 历史
contract-approve-detail.png 补丁 | 查看 | 原始文档 | blame | 历史
docs/batch_trace_extended_frontend.md 305 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/contract_generate_order_frontend.md 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/erp_purchase_invoice_remove_audit.md 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_dv_metering_frontend_integration.md 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_item_status_control.md 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/pd_project_bpm_approval_frontend.md 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/projectSql/ruoyi-vue-pro.sql 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/purchase_invoice_payment_link.md 49 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/erp_purchase_invoice_remove_audit.sql 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/srm_certificate_attachment_integration.md 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/srm_supplier_tender_integration.md 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_dv_metering_init.sql 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/attach/BpmBusinessAttachController.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/attach/vo/attach/BpmBusinessAttachPageReqVO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/attach/vo/attach/BpmBusinessAttachRespVO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/attach/vo/attach/BpmBusinessAttachSaveReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmBusinessAttachDO.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/attach/BpmBusinessAttachMapper.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/attach/BpmBusinessAttachService.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/attach/BpmBusinessAttachServiceImpl.java 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordSaveReqVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/CrmSaleQuotationController.java 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationRespVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/followup/CrmFollowUpRecordServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseInvoiceController.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoicePageReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceRespVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInvoiceDO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInvoiceMapper.java 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseInvoiceAuditStatusEnum.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceService.java 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java 67 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemSyncReqDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/MesDvMeteringController.java 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringPageReqVO.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringRespVO.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringSaveReqVO.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/MesDvMeteringCheckController.java 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckPageReqVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckRespVO.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckSaveReqVO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/MesMdItemController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/project/MesPdProjectController.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/project/vo/MesPdProjectRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/MesWmBatchController.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchProcessTraceRespVO.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchPurchaseSourceRespVO.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchSalesTargetRespVO.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/metering/MesDvMeteringDO.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/meteringcheck/MesDvMeteringCheckDO.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pd/project/MesPdProjectDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/meteringcheck/MesDvMeteringCheckMapper.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/dv/metering/MesDvMeteringRemindRedisDAO.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesPdStatusConstants.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringCheckResultEnum.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringExpireStatusEnum.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringStatusEnum.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pd/MesPdProjectStatusEnum.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindServiceImpl.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringService.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringServiceImpl.java 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckService.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckServiceImpl.java 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/project/MesPdProjectService.java 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/project/MesPdProjectServiceImpl.java 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/project/listener/MesPdProjectStatusListener.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchService.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml 65 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/apply/vo/SrmSupplierApplyRespVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/apply/vo/SrmSupplierApplySaveReqVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/SrmSupplierCertificateController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/vo/SrmSupplierCertificateRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/vo/SrmSupplierCertificateSaveReqVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/supplier/SrmSupplierController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmTenderProjectController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectPartialUpdateReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectSaveReqVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/dataobject/SrmSupplierApplyDO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/TenderStatusEnum.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyServiceImpl.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/certificate/SrmSupplierCertificateServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierServiceImpl.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderProjectServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/resources/sql/srm_init.sql 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/application-yzfx.yaml 324 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-14T05-18-36-135Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-08-14T05-18-50-100Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-08-14T05-19-08-995Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
- generic [ref=f1e7]:
  - complementary [ref=f1e8]:
    - generic [ref=f1e9]:
      - generic [ref=f1e10] [cursor=pointer]
      - link "工业AI智造平台 å·¥ä¸šAI智造平台" [ref=f1e15] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工业AI智造平台" [ref=f1e18]
        - generic [ref=f1e19]: å·¥ä¸šAI智造平台
      - menu [ref=f1e22]:
        - menuitem "首页" [ref=f1e23] [cursor=pointer]
        - listitem [ref=f1e30]:
          - generic [ref=f1e31] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=f1e39]:
          - generic [ref=f1e40] [cursor=pointer]: é‡‡è´­ç®¡ç†
        - listitem [ref=f1e46]:
          - generic [ref=f1e47] [cursor=pointer]: è¥é”€ç®¡ç†
        - listitem [ref=f1e53]:
          - generic [ref=f1e54] [cursor=pointer]: è®¾å¤‡ç®¡ç†
        - listitem [ref=f1e61]:
          - generic [ref=f1e62] [cursor=pointer]: ä»“储物流
        - listitem [ref=f1e70]:
          - generic [ref=f1e71] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
        - listitem [ref=f1e78]:
          - generic [ref=f1e79] [cursor=pointer]: è´¨é‡ç®¡ç†
        - listitem [ref=f1e85]:
          - generic [ref=f1e86] [cursor=pointer]: å†³ç­–BI大屏
        - listitem [ref=f1e92]:
          - generic [ref=f1e93] [cursor=pointer]: ååŒåŠžå…¬
          - list [ref=f1e99]:
            - listitem [ref=f1e100]:
              - generic [ref=f1e101] [cursor=pointer]: æµç¨‹ç®¡ç†
              - list [ref=f1e107]:
                - menuitem "流程模型" [ref=f1e108] [cursor=pointer]
                - menuitem "流程表单" [ref=f1e113] [cursor=pointer]
                - menuitem "流程分类" [ref=f1e118] [cursor=pointer]
                - menuitem "用户分组" [ref=f1e123] [cursor=pointer]
                - menuitem "流程监听器" [ref=f1e128] [cursor=pointer]
                - menuitem "流程表达式" [ref=f1e133] [cursor=pointer]
                - menuitem "流程实例" [ref=f1e138] [cursor=pointer]
                - menuitem "流程任务" [ref=f1e143] [cursor=pointer]
            - listitem [ref=f1e148]:
              - generic [ref=f1e149] [cursor=pointer]: å®¡æ‰¹ä¸­å¿ƒ
            - menuitem "API密钥管理" [ref=f1e155] [cursor=pointer]
            - menuitem "模型管理" [ref=f1e161] [cursor=pointer]
            - menuitem "知识库管理" [ref=f1e166] [cursor=pointer]
            - menuitem "通知公告" [ref=f1e172] [cursor=pointer]
        - listitem [ref=f1e177]:
          - generic [ref=f1e178] [cursor=pointer]: äººåŠ›èµ„æº
        - listitem [ref=f1e184]:
          - generic [ref=f1e185] [cursor=pointer]: å”®åŽç®¡ç†
        - listitem [ref=f1e191]:
          - generic [ref=f1e192] [cursor=pointer]: åŸºç¡€è®¾æ–½
        - listitem [ref=f1e198]:
          - generic [ref=f1e199] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
        - listitem [ref=f1e205]:
          - generic [ref=f1e206] [cursor=pointer]: IM å³æ—¶é€šè®¯
      - generic [ref=f1e214] [cursor=pointer]
  - generic [ref=f1e219]:
    - generic [ref=f1e220]:
      - banner [ref=f1e221]:
        - button [ref=f1e222] [cursor=pointer]
        - button [ref=f1e225] [cursor=pointer]
        - navigation "breadcrumb" [ref=f1e230]:
          - list [ref=f1e231]:
            - listitem [ref=f1e232]:
              - link "协同办公" [ref=f1e233] [cursor=pointer]:
                - /url: javascript:void 0
              - listitem [ref=f1e237]
            - listitem [ref=f1e240]:
              - link "流程管理" [ref=f1e241] [cursor=pointer]:
                - /url: javascript:void 0
              - listitem [ref=f1e245]
            - listitem [ref=f1e248]:
              - link "流程模型" [disabled] [ref=f1e249]
        - generic [ref=f1e254]:
          - generic [ref=f1e256] [cursor=pointer]:
            - generic [ref=f1e260]: æœç´¢
            - generic [ref=f1e261]: Ctrl K
          - button [ref=f1e263] [cursor=pointer]
          - button "dark" [ref=f1e268] [cursor=pointer]
          - button [ref=f1e277] [cursor=pointer]
          - button [ref=f1e283] [cursor=pointer]
          - button [ref=f1e286] [cursor=pointer]
          - button [ref=f1e292] [cursor=pointer]:
            - button [ref=f1e294]
          - button "工作台" [ref=f1e299] [cursor=pointer]
          - button [ref=f1e303] [cursor=pointer]:
            - img "超级管理员" [ref=f1e308]
      - generic [ref=f1e310]:
        - generic [ref=f1e315]:
          - generic [ref=f1e316] [cursor=pointer]: é¦–页
          - generic [ref=f1e334] [cursor=pointer]: æµç¨‹æ¨¡åž‹
        - generic [ref=f1e351]:
          - button [ref=f1e352] [cursor=pointer]
          - generic [ref=f1e359] [cursor=pointer]
          - generic [ref=f1e363] [cursor=pointer]
    - main [ref=f1e370]:
      - generic [ref=f1e376]:
        - generic [ref=f1e377]: æµç¨‹æ¨¡åž‹
        - generic [ref=f1e379]:
          - textbox "搜索流程" [ref=f1e381]
          - button "新建模型" [ref=f1e383] [cursor=pointer]
          - button [ref=f1e387] [cursor=pointer]
approval-detail-before-full.png
approval-detail-before.png
approval-detail-v2.jpeg
contract-approve-detail.png
docs/batch_trace_extended_frontend.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,305 @@
# æ‰¹æ¬¡è¿½æº¯æ‰©å±• - å‰ç«¯è”调方案
## æ¦‚è¿°
在现有批次追溯(向前/向后)基础上,扩展 4 ä¸ªèƒ½åŠ›ï¼Œè¡¥é½æ‰¹æ¬¡å…¨é“¾è·¯è¿½æº¯ï¼š
1. **采购来源**:从批次反查采购入库单 + IQC è´¨æ£€ç»“论(原料侧断链打通)
2. **销售去向**:从批次反查销售出库单 + OQC è´¨æ£€ç»“论(成品侧断链打通)
3. **工序流转**:从批次查看产出/投入的工序级事件(无需建表,动态聚合)
4. **SN åæŸ¥**:输入 SN åºåˆ—号直接定位批次并跳转详情
后端 4 ä¸ªæ–°æŽ¥å£å‡å·²å®Œæˆå¹¶ç¼–译通过,本文档指导前端对接。
---
## ä¸€ã€æ¶‰åŠé¡µé¢
| é¡µé¢ | è·¯å¾„ | æ”¹åŠ¨è¯´æ˜Ž |
|------|------|----------|
| æ‰¹æ¬¡è¿½æº¯é¡µ | `src/views/mes/qc/batchtrace/index.vue` | é¡¶éƒ¨æ–°å¢ž SN åæŸ¥è¾“入框 |
| è¿½æº¯è¯¦æƒ…弹窗 | `src/views/mes/qc/batchtrace/modules/trace-detail.vue` | å­é¡µç­¾æ‰©å±•为 5 ä¸ªï¼ˆç”Ÿäº§/采购来源/销售去向/工序流转/质检) |
| é‡‡è´­æ¥æºå­åˆ—表 | `src/views/mes/qc/batchtrace/modules/purchase-source-list.vue` | æ–°å¢žå­ç»„ä»¶ |
| é”€å”®åŽ»å‘å­åˆ—è¡¨ | `src/views/mes/qc/batchtrace/modules/sales-target-list.vue` | æ–°å¢žå­ç»„ä»¶ |
| å·¥åºæµè½¬å­åˆ—表 | `src/views/mes/qc/batchtrace/modules/process-trace-list.vue` | æ–°å¢žå­ç»„ä»¶ |
| æ‰¹æ¬¡ API | `src/api/mes/wm/batch/index.ts` | æ–°å¢ž 4 ä¸ªæŽ¥å£å‡½æ•° |
| åˆ—定义 | `src/views/mes/qc/batchtrace/data.ts` | æ–°å¢ž 3 ç»„列配置 |
---
## äºŒã€API æŽ¥å£
### 2.1 æ‰¹æ¬¡é‡‡è´­æ¥æº
**请求方式**:GET
**请求路径**:`/mes/wm/batch/purchase-sources`
**请求参数**:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| code | String | æ˜¯ | æ‰¹æ¬¡ç¼–码 |
**响应**:
```json
{
  "code": 0,
  "data": [
    {
      "receiptCode": "IR20250713001",
      "receiptDate": "2026-08-01 10:00:00",
      "purchaseOrderCode": "PO20250701001",
      "vendorId": 20,
      "vendorCode": "VENDOR001",
      "vendorName": "供应商A",
      "iqcCode": "IQC20250713001",
      "checkResult": 1,
      "itemId": 100,
      "itemCode": "ITEM001",
      "itemName": "原料A",
      "batchCode": "BT20260731001",
      "receivedQuantity": 100
    }
  ]
}
```
**字段说明**:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| receiptCode | String | é‡‡è´­å…¥åº“单号 |
| receiptDate | Date | å…¥åº“日期 |
| purchaseOrderCode | String | é‡‡è´­è®¢å•号 |
| vendorCode / vendorName | String | ä¾›åº”商编码/名称(后端富化) |
| iqcCode | String | IQC è´¨æ£€å•号(可能为空) |
| checkResult | Integer | IQC è´¨æ£€ç»“论,字典 `mes_qc_check_result` |
| itemCode / itemName | String | ç‰©æ–™ç¼–码/名称(后端富化) |
| batchCode | String | æ‰¹æ¬¡ç¼–码 |
| receivedQuantity | BigDecimal | å…¥åº“数量 |
### 2.2 æ‰¹æ¬¡é”€å”®å޻向
**请求方式**:GET
**请求路径**:`/mes/wm/batch/sales-targets`
**请求参数**:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| code | String | æ˜¯ | æ‰¹æ¬¡ç¼–码 |
**响应**:
```json
{
  "code": 0,
  "data": [
    {
      "salesCode": "SS20250713001",
      "salesDate": "2026-08-05 14:00:00",
      "salesOrderCode": "SO20250701001",
      "clientId": 30,
      "clientName": "客户A",
      "oqcCode": "OQC20250713001",
      "checkResult": 1,
      "itemId": 100,
      "itemCode": "ITEM001",
      "itemName": "成品A",
      "batchCode": "BT20260804001",
      "quantity": 50,
      "qualityStatus": 1
    }
  ]
}
```
**字段说明**:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| salesCode | String | é”€å”®å‡ºåº“单号 |
| salesDate | Date | å‡ºåº“日期 |
| salesOrderCode | String | é”€å”®è®¢å•号 |
| clientName | String | å®¢æˆ·åç§°ï¼ˆåŽç«¯å¯ŒåŒ–,注意:客户无编码字段) |
| oqcCode | String | OQC è´¨æ£€å•号(可能为空) |
| checkResult | Integer | OQC è´¨æ£€ç»“论,字典 `mes_qc_check_result` |
| itemCode / itemName | String | ç‰©æ–™ç¼–码/名称(后端富化) |
| batchCode | String | æ‰¹æ¬¡ç¼–码 |
| quantity | BigDecimal | å‡ºåº“数量 |
| qualityStatus | Integer | è´¨é‡çŠ¶æ€ï¼Œå­—å…¸ `mes_wm_quality_status` |
### 2.3 æ‰¹æ¬¡å·¥åºæµè½¬
**请求方式**:GET
**请求路径**:`/mes/wm/batch/process-trace`
**请求参数**:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| code | String | æ˜¯ | æ‰¹æ¬¡ç¼–码 |
**响应**:
```json
{
  "code": 0,
  "data": [
    {
      "eventType": "IN",
      "workOrderId": 50,
      "workOrderCode": "WO20260730001",
      "processId": 10,
      "processCode": "P001",
      "processName": "下料",
      "batchCode": "BT20260804001",
      "quantity": 100,
      "qualityStatus": null,
      "eventTime": "2026-08-04 09:00:00",
      "processSort": 1
    },
    {
      "eventType": "OUT",
      "workOrderId": 50,
      "workOrderCode": "WO20260730001",
      "processId": 10,
      "processCode": "P001",
      "processName": "下料",
      "batchCode": "BT20260804001",
      "quantity": 90,
      "qualityStatus": 1,
      "eventTime": "2026-08-04 10:00:00",
      "processSort": 1
    }
  ]
}
```
**字段说明**:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| eventType | String | äº‹ä»¶ç±»åž‹ï¼š`OUT` = äº§å‡ºã€`IN` = æŠ•å…¥ |
| workOrderCode | String | å·¥å•号(后端富化) |
| processCode / processName | String | å·¥åºç¼–码/名称(后端富化) |
| batchCode | String | æ‰¹æ¬¡ç¼–码 |
| quantity | BigDecimal | æ•°é‡ï¼ˆOUT=产出数量,IN=投入数量) |
| qualityStatus | Integer | è´¨é‡çŠ¶æ€ï¼ˆä»… OUT ä¾§æœ‰å€¼ï¼ŒIN ä¾§ä¸º null) |
| eventTime | Date | äº‹ä»¶æ—¶é—´ï¼ˆOUT=产出日期,IN=消耗日期) |
| processSort | Integer | å·¥åºåºå·ï¼ˆç”¨äºŽå±•示工序先后) |
**排序规则**:后端已按 `workOrderId asc â†’ processSort asc(空值置后)→ eventTime asc` æŽ’序,前端直接按返回顺序展示即可。
### 2.4 SN åæŸ¥æ‰¹æ¬¡
**请求方式**:GET
**请求路径**:`/mes/wm/batch/get-by-sn`
**请求参数**:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| sn | String | æ˜¯ | SN åºåˆ—号 |
**响应**:与 `GET /mes/wm/batch/get` å®Œå…¨ä¸€è‡´ï¼ˆ`MesWmBatchRespVO`),未命中返回 `data: null`:
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "code": "BT20260804001",
    "itemId": 100,
    "itemCode": "ITEM001",
    "itemName": "成品A",
    "vendorCode": "VENDOR001",
    "vendorName": "供应商A",
    "clientName": "客户A",
    "workOrderCode": "WO20260730001",
    "taskCode": "TASK001",
    "workstationCode": "WS001",
    "toolCode": "TOOL001",
    "lotNumber": "LOT20260801"
  }
}
```
**说明**:SN åœ¨ `mes_wm_sn` è¡¨é€šè¿‡ `code` å”¯ä¸€åŒ¹é…ï¼Œå‘½ä¸­åŽå–å…¶ `batch_code` åæŸ¥æ‰¹æ¬¡ã€‚前端根据 `data` æ˜¯å¦ä¸ºç©ºåˆ¤æ–­æ˜¯å¦æç¤º"未找到对应批次"。
---
## ä¸‰ã€å­—段展示规则
| åŠŸèƒ½ | å±•示位置 | å­—段说明 |
|------|----------|----------|
| é‡‡è´­æ¥æº | è¯¦æƒ…弹窗子页签"采购来源" | å…¥åº“单号、入库日期、采购订单、供应商、IQC单号、IQC结论、物料、数量 |
| é”€å”®å޻向 | è¯¦æƒ…弹窗子页签"销售去向" | å‡ºåº“单号、出库日期、销售订单、客户、OQC单号、OQC结论、物料、数量、质量状态 |
| å·¥åºæµè½¬ | è¯¦æƒ…弹窗子页签"工序流转" | äº‹ä»¶ç±»åž‹ã€å·¥å•号、工序编码/名称、数量、时间、质量状态 |
| SN åæŸ¥ | è¿½æº¯é¡µé¡¶éƒ¨è¾“入框 | è¾“å…¥ SN â†’ å‘½ä¸­ç›´æŽ¥æ‰“开批次详情弹窗 |
**字典说明**:
| å­—å…¸ | å–值 |
|------|------|
| `mes_qc_check_result` | 1=合格、2=特采、3=退货、4=报废 |
| `mes_wm_quality_status` | 0=待检、1=合格、2=不合格 |
---
## å››ã€æ•°æ®å¸¦å…¥è¯´æ˜Ž
| åœºæ™¯ | å¸¦å…¥è¯´æ˜Ž |
|------|----------|
| è¿½æº¯é¡µ â†’ é‡‡è´­æ¥æº | ç‚¹å‡»æ‰¹æ¬¡è¡Œï¼Œæºå¸¦è¯¥è¡Œçš„ `code` è°ƒç”¨ `getPurchaseSourceList(code)` |
| è¿½æº¯é¡µ â†’ é”€å”®å޻向 | ç‚¹å‡»æ‰¹æ¬¡è¡Œï¼Œæºå¸¦è¯¥è¡Œçš„ `code` è°ƒç”¨ `getSalesTargetList(code)` |
| è¿½æº¯é¡µ â†’ å·¥åºæµè½¬ | ç‚¹å‡»æ‰¹æ¬¡è¡Œï¼Œæºå¸¦è¯¥è¡Œçš„ `code` è°ƒç”¨ `getProcessTraceList(code)` |
| SN è¾“入框 â†’ è¯¦æƒ… | è¾“å…¥ SN è°ƒç”¨ `getBatchBySn(sn)`,返回的 `code` ä½œä¸ºè¯¦æƒ…弹窗查询依据(或直接使用返回数据) |
---
## äº”、接口调用时序
1. è¿½æº¯é¡µåŠ è½½æ‰¹æ¬¡åˆ—è¡¨ï¼ˆ`/mes/wm/batch/page`)
2. ç‚¹å‡»æŸæ‰¹æ¬¡è¡Œ â†’ æ‰“开详情弹窗,弹窗内默认展示"生产"页签(向前/向后追溯)
3. åˆ‡æ¢"采购来源/销售去向/工序流转"页签时,按需调用对应接口(建议懒加载:首次切换才请求)
4. é¡¶éƒ¨ SN è¾“入框 â†’ æŒ‰å›žè½¦æˆ–点击搜索 â†’ `getBatchBySn` â†’ å‘½ä¸­æ‰“开详情弹窗,未命中提示无记录
---
## å…­ã€ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| åŽŸæ–™æ‰¹æ¬¡ | æœ‰é‡‡è´­æ¥æºï¼ˆå…¥åº“+IQC),无销售去向;工序流转以 IN æŠ•入为主 |
| æˆå“æ‰¹æ¬¡ | æœ‰é”€å”®åŽ»å‘ï¼ˆå‡ºåº“+OQC),无采购来源;工序流转以 OUT äº§å‡ºä¸ºä¸» |
| ä¸­é—´æ‰¹æ¬¡ | åŒæ—¶å­˜åœ¨ IN ä¸Ž OUT äº‹ä»¶ï¼Œä½“现工序级流转 |
| è´¨æ£€å•缺失 | `iqcCode`/`oqcCode` å¯èƒ½ä¸ºç©ºï¼ˆå•据未关联),结论列展示"-" |
| SN æœªå‘½ä¸­ | `get-by-sn` è¿”回 `data: null`,前端提示"未找到该 SN å¯¹åº”的批次" |
| æ•°æ®ä¸ºç©º | ä¸‰ä¸ªå­åˆ—表接口均可能返回空数组,页面展示空态提示 |
---
## ä¸ƒã€æ³¨æ„äº‹é¡¹
- ä¸‰ä¸ªåˆ—表接口与既有 forward/backward ç›¸åŒï¼Œå‡éœ€è¦ `mes:wm-batch:query` æƒé™
- å­é¡µç­¾å»ºè®®æ‡’加载 + ç¼“存,避免每次切换重复请求
- å·¥åºæµè½¬çš„ `processSort` ç”±åŽç«¯æŽ’好序,前端勿再排序
- é”€å”®ä¾§ `qualityStatus` ä¸ºå­—典值,展示用 `mes_wm_quality_status` å­—典翻译
- æµ‹è¯•环境可能存在无 `batchCode` çš„销售出库行数据,属历史脏数据,前端对空字段做兜底展示即可
---
## å…«ã€ç›¸å…³æŽ¥å£æ±‡æ€»
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/wm/batch/purchase-sources | æ‰¹æ¬¡é‡‡è´­æ¥æºï¼ˆå…¥åº“ + IQC) |
| GET | /mes/wm/batch/sales-targets | æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼ˆå‡ºåº“ + OQC) |
| GET | /mes/wm/batch/process-trace | æ‰¹æ¬¡å·¥åºæµè½¬ï¼ˆäº§å‡º/投入) |
| GET | /mes/wm/batch/get-by-sn | SN åæŸ¥æ‰¹æ¬¡ |
docs/contract_generate_order_frontend.md
@@ -228,6 +228,9 @@
ALTER TABLE `erp_sale_order`
ADD COLUMN `contract_id` bigint DEFAULT NULL COMMENT '关联 CRM åˆåŒç¼–号' AFTER `customer_id`,
ADD COLUMN `contract_no` varchar(32) DEFAULT NULL COMMENT 'CRM åˆåŒå•号' AFTER `contract_id`;
ALTER TABLE `erp_sale_order`
MODIFY COLUMN `deposit_price` decimal(24,6) NOT NULL DEFAULT 0 COMMENT '定金金额,单位:元';
```
---
docs/erp_purchase_invoice_remove_audit.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
# ERP é‡‡è´­æ¥ç¥¨åŽ»æŽ‰å®¡æ ¸ - å‰ç«¯è”调方案
## å˜æ›´èƒŒæ™¯
采购来票(`erp_purchase_invoice`)不再需要审核流程,来票创建后直接生效、可直接用于付款。后端已删除审核状态字段(`auditStatus`)与工作流残留字段(`processInstanceId`),并移除了审核相关接口;付款创建时不再校验"来票必须审核通过"。
## æ¶‰åŠé¡µé¢
- é‡‡è´­æ¥ç¥¨ç®¡ç†é¡µï¼ˆ`views/erp/purchase/invoice/`)
- ä»˜æ¬¾å•管理页(`views/erp/finance/payment/`,仅行为变化,无需改代码)
## æŽ¥å£å˜æ›´
### å·²åˆ é™¤æŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| PUT | /erp/purchase-invoice/update-status | å®¡æ ¸/反审核,前端需删除对应按钮与 API å‡½æ•° |
| GET | /erp/purchase-invoice/audit-count | å¾…审核来票数量,前端需删除对应 API å‡½æ•° |
### å“åº”字段变化
| æŽ¥å£ | å˜åŒ– |
|------|------|
| /erp/purchase-invoice/page | å“åº”移除 `auditStatus`、`processInstanceId` å­—段 |
| /erp/purchase-invoice/get | å“åº”移除 `auditStatus`、`processInstanceId` å­—段 |
| /erp/purchase-invoice/simple-list | å“åº”移除 `auditStatus` å­—段;不再只返回"审核通过"来票,改为返回该供应商/订单下"已上传附件且剩余可付 > 0"的全部来票 |
| /erp/finance-payment/get、/page | åµŒå¥— `invoice` å¯¹è±¡ä¸­ä¸å†åŒ…含 `auditStatus` å­—段 |
### æ ¡éªŒè§„则变化(后端,前端无改动)
| åœºæ™¯ | å˜åŒ– |
|------|------|
| ä»˜æ¬¾åˆ›å»º/更新 | åŽ»æŽ‰"关联来票必须审核通过"校验,其余校验(来票存在、已传附件、供应商一致、金额不超剩余可付)保留 |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. API å®šä¹‰ï¼ˆ`src/api/erp/purchase/invoice/index.ts`)
- `PurchaseInvoice` ç±»åž‹ï¼šåˆ é™¤ `auditStatus`、`processInstanceId` å­—段
- `PurchaseInvoiceSimple` ç±»åž‹ï¼šåˆ é™¤ `auditStatus` å­—段
- åˆ é™¤å‡½æ•°ï¼š`updatePurchaseInvoiceStatus`、`getAuditPurchaseInvoiceCount`
- æ›´æ–° `getPurchaseInvoiceSimpleList` æ³¨é‡Šè¯´æ˜Ž
### 2. æ¥ç¥¨åˆ—表页(`src/views/erp/purchase/invoice/index.vue`)
- åˆ é™¤"审核""反审核"操作按钮及对应处理函数
- åˆ é™¤"审批状态"列的状态 Tag æ¨¡æ¿
- ç¼–辑按钮不再按审核状态控制显隐(任意状态可编辑)
- åˆ é™¤æŒ‰é’®ä¸å†æŒ‰å®¡æ ¸çŠ¶æ€æŽ§åˆ¶æ˜¾éšï¼ˆæ˜¯å¦å¯åˆ ç”±åŽç«¯æ ¡éªŒ"已有有效付款单"决定)
### 3. æ¥ç¥¨åˆ—表配置(`src/views/erp/purchase/invoice/data.ts`)
- æœç´¢è¡¨å•:删除"审核状态"筛选项
- åˆ—表列:删除"审批状态"列
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ¥ç¥¨åˆ›å»º | åˆ›å»ºå³ç”Ÿæ•ˆï¼Œæ— å®¡æ ¸çŽ¯èŠ‚ |
| æ¥ç¥¨ç¼–辑 | ä»»æ„çŠ¶æ€å¯ç¼–è¾‘ |
| æ¥ç¥¨åˆ é™¤ | å·²æœ‰æœ‰æ•ˆä»˜æ¬¾å•(付款金额 > 0)关联的来票不可删除,后端会返回错误提示 |
| ä»˜æ¬¾é€‰æ‹©æ¥ç¥¨ | ä¸‹æ‹‰åªå±•示"已上传附件且剩余可付 > 0"的来票,不再有"待审核"来票 |
## æ³¨æ„äº‹é¡¹
- ä»˜æ¬¾æ¨¡å—(`views/erp/finance/payment/`)未引用 `auditStatus`,无需修改;其"关联来票"下拉可选范围随后端 simple-list é€»è¾‘自动变化。
- åŽ†å²æ¥ç¥¨æ•°æ®ï¼šæ•°æ®åº“å·²åˆ é™¤ `audit_status`、`process_instance_id` ä¸¤åˆ—,历史来票全部视为有效,可直接用于付款。
- å‰ç«¯æŽ¥å£åˆ é™¤åŽï¼Œè¯·åŒæ­¥æ£€æŸ¥æ˜¯å¦æœ‰å…¶å®ƒé¡µé¢ï¼ˆå¦‚工作台/待办)调用已删除的接口,当前未发现。
docs/mes_dv_metering_frontend_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,116 @@
# MES è®¡é‡å™¨å…·ä¸Žè®¡é‡æ£€æµ‹è®°å½• - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
| é¡µé¢ | èœå•路径 | è·¯ç”±è·¯å¾„ | component_name |
|------|----------|----------|----------------|
| è®¡é‡å™¨å…· | è®¾å¤‡ç®¡ç† -> è®¡é‡å™¨å…· | `mes/dv/metering/index` | MesDvMetering |
| è®¡é‡æ£€æµ‹è®°å½• | è®¾å¤‡ç®¡ç† -> è®¡é‡æ£€æµ‹è®°å½• | `mes/dv/meteringcheck/index` | MesDvMeteringCheck |
> ä¸¤ä¸ªé¡µé¢ä¸ºå¹³çº§èœå•。检测记录页可通过「计量器具」下拉筛选,仅查看某台器具的检测历史。
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. è®¡é‡å™¨å…·åˆ—表 -> æ–°å¢ž/编辑表单:录入器具编码、名称、类别、校准周期、使用部门、责任人等。
2. è®¡é‡æ£€æµ‹è®°å½• -> æ–°å¢ž/编辑表单:选择「计量器具」(从 `/mes/dv/metering/simple-list` å¸¦å…¥ `meteringId`、`code`、`name`)。
3. æ£€æµ‹è®°å½•保存后,后端自动计算 `nextCheckDate` å¹¶**回写**到计量器具的 `lastCheckDate`、`nextCheckDate`:
   - `nextCheckDate` ä¼ ç©ºæ—¶ = `checkDate + æ ¡å‡†å‘¨æœŸ(月)`(自动计算);
   - `nextCheckDate` ä¼ å€¼æ—¶ = ä»¥ä¼ å…¥å€¼ä¸ºå‡†ï¼ˆæ‰‹åŠ¨æŒ‡å®šï¼‰ã€‚
4. è®¡é‡å™¨å…·åˆ—表展示 `expireStatus` åˆ°æœŸçŠ¶æ€æ ‡è®°ï¼Œç”±åŽç«¯æŒ‰ `nextCheckDate` ä¸Žå…¨å±€é…ç½®çš„æé†’天数计算。
## API
### è®¡é‡å™¨å…·
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž | æƒé™ |
|------|------|------|------|
| POST | /mes/dv/metering/create | åˆ›å»ºè®¡é‡å™¨å…· | mes:dv-metering:create |
| PUT | /mes/dv/metering/update | æ›´æ–°è®¡é‡å™¨å…· | mes:dv-metering:update |
| DELETE | /mes/dv/metering/delete?id= | åˆ é™¤è®¡é‡å™¨å…·ï¼ˆæœ‰å…³è”检测记录时拒绝) | mes:dv-metering:delete |
| GET | /mes/dv/metering/get?id= | èŽ·å¾—è®¡é‡å™¨å…·è¯¦æƒ… | mes:dv-metering:query |
| GET | /mes/dv/metering/page | è®¡é‡å™¨å…·åˆ†é¡µ | mes:dv-metering:query |
| GET | /mes/dv/metering/export-excel | å¯¼å‡º Excel | mes:dv-metering:export |
| GET | /mes/dv/metering/simple-list | ç²¾ç®€åˆ—表(下拉选项) | mes:dv-metering:query |
**分页筛选参数(page æŽ¥å£ï¼‰ï¼š**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| code | String | å¦ | å™¨å…·ç¼–码模糊 |
| name | String | å¦ | å™¨å…·åç§°æ¨¡ç³Š |
| brand | String | å¦ | å“ç‰Œ/生产厂家模糊 |
| category | Integer | å¦ | å™¨å…·ç±»åˆ«ï¼ˆå­—典) |
| deptId | Long | å¦ | ä½¿ç”¨éƒ¨é—¨ |
| chargeUserId | Long | å¦ | è´£ä»»äºº |
| status | Integer | å¦ | çŠ¶æ€ï¼ˆå­—å…¸ï¼‰ |
| expireStatus | Integer | å¦ | åˆ°æœŸçŠ¶æ€ï¼š1 å·²è¿‡æœŸ / 2 å³å°†åˆ°æœŸ / 3 æ­£å¸¸ |
### è®¡é‡æ£€æµ‹è®°å½•
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž | æƒé™ |
|------|------|------|------|
| POST | /mes/dv/metering-check/create | åˆ›å»ºæ£€æµ‹è®°å½• | mes:dv-metering-check:create |
| PUT | /mes/dv/metering-check/update | æ›´æ–°æ£€æµ‹è®°å½• | mes:dv-metering-check:update |
| DELETE | /mes/dv/metering-check/delete?id= | åˆ é™¤æ£€æµ‹è®°å½• | mes:dv-metering-check:delete |
| GET | /mes/dv/metering-check/get?id= | èŽ·å¾—æ£€æµ‹è®°å½•è¯¦æƒ… | mes:dv-metering-check:query |
| GET | /mes/dv/metering-check/page | æ£€æµ‹è®°å½•分页 | mes:dv-metering-check:query |
| GET | /mes/dv/metering-check/export-excel | å¯¼å‡º Excel | mes:dv-metering-check:export |
**分页筛选参数(page æŽ¥å£ï¼‰ï¼š**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| meteringId | Long | å¦ | è®¡é‡å™¨å…·ï¼ˆä»Žå™¨å…·åˆ—表带入) |
| checkCode | String | å¦ | æ£€æµ‹ç¼–号模糊 |
| checkResult | Integer | å¦ | æ£€æµ‹ç»“论(字典) |
| checkDate | LocalDate[] | å¦ | æ£€æµ‹æ—¥æœŸåŒºé—´ |
### å“åº”字段新增说明
计量器具列表/详情响应额外返回:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| deptName | String | ä½¿ç”¨éƒ¨é—¨åç§° |
| chargeUserNickname | String | è´£ä»»äººæ˜µç§° |
| expireStatus | Integer | åˆ°æœŸçŠ¶æ€ï¼š1 å·²è¿‡æœŸ / 2 å³å°†åˆ°æœŸ / 3 æ­£å¸¸ |
| expireDays | Long | è·åˆ°æœŸå¤©æ•°ï¼ˆæ­£æ•°æœªåˆ°ã€è´Ÿæ•°å·²è¿‡æœŸï¼‰ |
计量检测记录列表/详情响应额外返回:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| meteringCode | String | è®¡é‡å™¨å…·ç¼–码 |
| meteringName | String | è®¡é‡å™¨å…·åç§° |
| checkUserNickname | String | æ£€æµ‹äººæ˜µç§° |
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| expireStatus | è®¡é‡å™¨å…·åˆ—表 | 1 å·²è¿‡æœŸ=危险(danger);2 å³å°†åˆ°æœŸ=警告(warning);3 æ­£å¸¸=成功(success) |
| status | è®¡é‡å™¨å…·åˆ—表、表单 | å­—å…¸ mes_dv_metering_status(在用=success/停用=info/报废=danger) |
| category | è®¡é‡å™¨å…·åˆ—表、表单 | å­—å…¸ mes_dv_metering_category |
| nextCheckDate | ä¸¤ä¸ªé¡µé¢ | è®¡é‡å™¨å…·å±•示为「下次检测日期」列;检测记录保存时回写 |
| checkResult | æ£€æµ‹è®°å½•列表、表单 | å­—å…¸ mes_dv_metering_check_result(合格=success/不合格=danger) |
| meteringCode/meteringName | æ£€æµ‹è®°å½•列表 | ä»Žå™¨å…·å¸¦å…¥å±•示 |
| deptName/chargeUserNickname | è®¡é‡å™¨å…·åˆ—表 | éƒ¨é—¨ã€è´£ä»»äººåç§° |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ£€æµ‹è®°å½•新增 | æœªå¡« nextCheckDate æ—¶æŒ‰ `checkDate + æ ¡å‡†å‘¨æœŸ` è‡ªåŠ¨è®¡ç®—å¹¶ä¿å­˜ |
| æ£€æµ‹è®°å½•æ›´æ–° | é‡æ–°è®¡ç®— nextCheckDate,并回写器具 |
| æ£€æµ‹è®°å½•删除 | å™¨å…·æŒ‰å‰©ä½™ã€Œæœ€æ–°ä¸€æ¡è®°å½•」整体重算回写(无记录则 lastCheckDate/nextCheckDate ç½®ç©ºï¼‰ |
| è®¡é‡å™¨å…·åˆ é™¤ | å·²å…³è”检测记录时拒绝删除,提示「计量器具已关联检测记录,无法删除」 |
| åˆ°æœŸæé†’ | åˆ°æœŸå‰ N å¤©ï¼ˆå…¨å±€é…ç½® `mes.dv-metering.remind-days`,默认 30)发送站内信给责任人;同一器具同一到期日 60 å¤©å†…只提醒一次 |
| ç¼–码/编号唯一 | å™¨å…· code、检测 checkCode å‡å”¯ä¸€ï¼Œé‡å¤æ—¶åŽç«¯æŠ¥é”™ |
## æ³¨æ„äº‹é¡¹
- expireStatus ç”±åŽç«¯è®¡ç®—并返回,前端只需按三态渲染 tag é¢œè‰²ï¼Œæ— éœ€è‡ªè¡Œè®¡ç®—天数。
- åˆ é™¤å™¨å…·å‰éœ€ç¡®è®¤æ— å…³è”检测记录;删除检测记录后器具的到期日会随之变化,前端应刷新器具列表。
- æ£€æµ‹è®°å½•页依赖计量器具数据,应先存在器具再登记检测记录。
- æé†’天数可在「基础设施 -> é…ç½®ç®¡ç†ã€ä¸­ä¿®æ”¹ `mes.dv-metering.remind-days`,改后立即生效。
- ä¸‰ä¸ªæ–°å­—典可在「系统管理 -> å­—典管理」中维护:mes_dv_metering_category、mes_dv_metering_status、mes_dv_metering_check_result。
docs/mes_item_status_control.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
# MES ç‰©æ–™çŠ¶æ€æŽ§åˆ¶ - å‰ç«¯è”调方案
## å˜æ›´æ¦‚è¿°
- MES æ¨¡å—**不再允许控制物料状态**,状态只能由基础数据(MDM)物料管理控制。
- ç§»é™¤ MES æŽ¥å£ `PUT /mes/md/item/update-status`。
- MDM ç‰©æ–™åŒæ­¥åˆ° MES æ—¶ä¼ é€’状态(启用/禁用),MES ç‰©æ–™çŠ¶æ€è·Ÿéš MDM ä¿æŒåŒæ­¥ã€‚
## æ¶‰åŠé¡µé¢
- MES ç‰©æ–™åˆ—表页:`src/views/mes/md/item/index.vue`(移除状态开关,改为只读展示)
- MES ç‰©æ–™æ–°å¢ž/编辑表单:`src/views/mes/md/item/modules/form.vue`(状态字段改为禁用/只读)
- åŸºç¡€æ•°æ®ç‰©æ–™ç®¡ç†é¡µï¼š`src/views/basicData/mdm/index.vue`(状态控制入口,保持不变)
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| PUT(已移除) | `/mes/md/item/update-status` | MES ä¾§çŠ¶æ€æŽ§åˆ¶æŽ¥å£ï¼Œå·²åºŸå¼ƒï¼Œå‰ç«¯**不得再调用** |
MDM çŠ¶æ€æŽ§åˆ¶æŽ¥å£ï¼ˆä¿æŒä¸å˜ï¼ŒçŠ¶æ€ä¼šåŒæ­¥åˆ° MES):
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/mdm/item/create` | åˆ›å»ºç‰©æ–™ï¼Œstatus éšåŒæ­¥å¸¦å…¥ MES |
| PUT | `/mdm/item/update` | æ›´æ–°ç‰©æ–™ï¼Œstatus éšåŒæ­¥å¸¦å…¥ MES |
| PUT | `/mdm/item/update-status` | æ›´æ–°ç‰©æ–™çŠ¶æ€ï¼Œå®žæ—¶åŒæ­¥åˆ° MES |
## æ•°æ®å¸¦å…¥è¯´æ˜Ž
1. MDM åˆ›å»º/编辑物料时,`status`(0=启用,1=禁用)随同步链路带入 MES,MES ç‰©æ–™æ–°å»ºæ²¿ç”¨ MDM çŠ¶æ€ã€‚
2. MDM ç‰©æ–™å¯ç”¨/禁用(`update-status`),状态实时同步到 MES。
3. MES æœ¬åœ°æ–°å¢žç‰©æ–™ï¼ˆä¸ç»è¿‡ MDM åŒæ­¥ï¼‰é»˜è®¤çŠ¶æ€ä¸º**禁用**,且 MES ä¾§æ— æ³•自行启用,须在 MDM ä¸­å¯ç”¨åŽåŒæ­¥å›ž MES。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| çŠ¶æ€ | MES ç‰©æ–™åˆ—表 | åªè¯»å±•示,来源为 MDM åŒæ­¥çŠ¶æ€ï¼Œä¸å†æä¾›å¼€å…³åˆ‡æ¢ |
| çŠ¶æ€ | MES æ–°å¢ž/编辑表单 | ç¦ç”¨/隐藏,不允许修改 |
| çŠ¶æ€ | MDM ç‰©æ–™åˆ—表/表单 | å¯ç¼–辑,为状态唯一控制入口 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| MDM åˆ›å»º/编辑物料 | çŠ¶æ€éšåŒæ­¥åˆ° MES,MES ç‰©æ–™çŠ¶æ€ä¿æŒä¸€è‡´ |
| MDM å¯ç”¨/禁用物料 | çŠ¶æ€å®žæ—¶åŒæ­¥åˆ° MES |
| MES æœ¬åœ°æ–°å¢žç‰©æ–™ | é»˜è®¤ç¦ç”¨ï¼ŒMES ä¾§æ— æ³•启用,须在 MDM ä¸­å¯ç”¨ |
| MES ç¼–辑物料 | ä¸å…è®¸ä¿®æ”¹çŠ¶æ€å­—æ®µ |
| MES çŠ¶æ€æŽ§åˆ¶ | ä¸å†æä¾›å¯ç”¨/禁用入口 |
## æ³¨æ„äº‹é¡¹
- MES åˆ—表页需移除状态开关(`updateItemStatus` ç›¸å…³è°ƒç”¨ï¼‰ï¼ŒçŠ¶æ€åˆ—æ”¹ä¸ºåªè¯»æ–‡æœ¬/标签展示,或保留展示列但去掉可切换能力。
- MES æ–°å¢ž/编辑表单中的状态字段需禁用或隐藏,避免误传导致状态被修改。
- å‰ç«¯ `src/api/mes/md/item/index.ts` ä¸­çš„ `updateItemStatus` å‡½æ•°å¯ä¸€å¹¶æ¸…理(接口已移除)。
- MDM å¯ç”¨ç‰©æ–™æ—¶ï¼Œè‹¥è¯¥ç‰©æ–™åœ¨ MES ä¾§å¯ç”¨äº†æ‰¹æ¬¡ç®¡ç†ä½†æœªé…ç½®æ‰¹æ¬¡å±žæ€§ã€æˆ–为产品但未配置 BOM,MES åŒæ­¥æ ¡éªŒå¯èƒ½æ‹¦æˆªå¯ç”¨æ“ä½œï¼Œéœ€å…ˆåœ¨ MES ä¾§è¡¥é½å¯¹åº”配置。
docs/pd_project_bpm_approval_frontend.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
# MES äº§å“é¡¹ç›®æŽ¥å…¥ BPM å®¡æ‰¹ - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- ç”Ÿäº§ç®¡æŽ§-产品设计-产品项目 åˆ—表页(`src/views/mes/pd/project/index.vue`)
- äº§å“é¡¹ç›® è¯¦æƒ…页(`src/views/mes/pd/project/detail/index.vue`)
- äº§å“é¡¹ç›® æµç¨‹è¿›åº¦ç»„件(`src/views/mes/pd/project/modules/workflow-timeline.vue`)
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. å¾…审核(2) â†’ åˆ—表/详情点「提交审批」:先调 `GET /mes/pd/project/approve-process-list` èŽ·å–å¯ç”¨ BPM æµç¨‹ â†’ æ‰“开流程选择弹窗 â†’ é€‰ä¸­æµç¨‹åŽè°ƒ `PUT /mes/pd/project/submit-approval?id={id}&processDefinitionKey={key}` â†’ é¡¹ç›®çŠ¶æ€å˜ä¸º **审批中(5)**
2. BPM åŽå°å®¡æ‰¹é€šè¿‡ â†’ åŽç«¯ç›‘听器回调 â†’ é¡¹ç›®çŠ¶æ€å˜ä¸º **待归档(3)**;审批驳回/取消 â†’ çŠ¶æ€å›žåˆ° **待审核(2)**,可重新提交
3. å®¡æ‰¹ä¸­(5) â†’ åˆ—表/详情显示「查看审批」按钮 â†’ è·³è½¬ BPM æµç¨‹è¯¦æƒ…页查看审批进度
4. å¾…å½’æ¡£(3) â†’ ç‚¹ã€Œå½’档」→ `PUT /mes/pd/project/archive?id={id}` â†’ çŠ¶æ€å˜ä¸º **已归档(4)**
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/pd/project/approve-process-list | èŽ·å¾—å¯ç”¨ BPM å®¡æ‰¹æµç¨‹åˆ—表 |
| PUT | /mes/pd/project/submit-approval | æäº¤å®¡æ‰¹ï¼ˆå¾…审核→审批中) |
| PUT | /mes/pd/project/archive | å½’档(待归档→已归档) |
**请求参数(submit-approval):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | é¡¹ç›®ç¼–号 |
| processDefinitionKey | String | æ˜¯ | é€‰ä¸­çš„ BPM æµç¨‹å®šä¹‰ Key |
**响应字段变更(/get、/page):**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| processInstanceId | String | BPM æµç¨‹å®žä¾‹ç¼–号,审批中及以后有值 |
**响应示例(approve-process-list):**
```json
{
  "code": 0,
  "data": [
    { "id": "xxx", "name": "产品项目审批流程", "key": "mes-pd-project-audit" }
  ]
}
```
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| çŠ¶æ€ | åˆ—表、详情 | æ–°å¢žå­—典值「审批中」(value=5),列表 `CellDict` è‡ªåŠ¨æ¸²æŸ“ |
| processInstanceId | åˆ—表/详情「查看审批」 | æœ‰å€¼æ‰æ˜¾ç¤ºã€ŒæŸ¥çœ‹å®¡æ‰¹ã€æŒ‰é’® |
## é¡µé¢æ”¹åЍ
### 1. åˆ—表页 index.vue
- ã€Œå®¡æ ¸ã€æŒ‰é’®æ”¹ä¸ºã€Œæäº¤å®¡æ‰¹ã€ï¼š`ifShow` ä»ä¸º `status === PENDING_AUDIT`,点击先查流程列表(空则提示「暂无可用审批流程,请先配置 BPM æµç¨‹ã€ï¼‰ï¼Œæœ‰åˆ™æ‰“开流程选择弹窗
- æ–°å¢žã€ŒæŸ¥çœ‹å®¡æ‰¹ã€æŒ‰é’®ï¼š`ifShow` ä¸º `status === APPROVING` ä¸” `processInstanceId` æœ‰å€¼ï¼Œç‚¹å‡»è·³ BPM æµç¨‹è¯¦æƒ…
- æ–°å¢žæµç¨‹é€‰æ‹©å¼¹çª—组件 `process-select-modal.vue`(Select ä¸‹æ‹‰é€‰æµç¨‹ï¼Œé»˜è®¤é€‰ç¬¬ä¸€ä¸ªï¼Œç¡®è®¤åŽè°ƒ submit-approval)
### 2. è¯¦æƒ…页 detail/index.vue
- ã€Œå®¡æ ¸é€šè¿‡ã€æŒ‰é’®æ”¹ä¸ºã€Œæäº¤å®¡æ‰¹ã€ï¼Œé€»è¾‘同上,成功后刷新详情
- æ–°å¢žã€ŒæŸ¥çœ‹å®¡æ‰¹ã€æŒ‰é’®ï¼š`ifShow` ä¸ºå®¡æ‰¹ä¸­çŠ¶æ€ï¼Œç‚¹å‡»è·³ BPM æµç¨‹è¯¦æƒ…
- é¡¶éƒ¨æŒ‰é’®åŒºæ–°å¢žå®¡æ‰¹ä¸­åˆ¤æ–­
### 3. æµç¨‹è¿›åº¦ç»„ä»¶ workflow-timeline.vue
- æ­¥éª¤ç”± 5 æ­¥æ‰©ä¸º 6 æ­¥ï¼šå¾…发布 â†’ å¾…上传 â†’ å¾…审核 â†’ **审批中** â†’ å¾…å½’æ¡£ â†’ å·²å½’æ¡£
- çŠ¶æ€å€¼åˆ°æ­¥éª¤ç´¢å¼•çš„æ˜ å°„éœ€æ˜¾å¼å¤„ç†ï¼ˆå®¡æ‰¹ä¸­ value=5 æ’在待审核之后)
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æäº¤å®¡æ‰¹ | ä»…待审核(2)状态可提交;若项目下存在未审核的设计资料,后端返回错误「该项目下存在未审核的资料」 |
| å®¡æ‰¹é€šè¿‡ | çŠ¶æ€ â†’ å¾…å½’æ¡£(3),可执行归档 |
| å®¡æ‰¹é©³å›ž/取消 | çŠ¶æ€ â†’ å¾…审核(2),可修改后重新提交 |
| å½’æ¡£ | ä»…å¾…å½’æ¡£(3)状态可归档,归档后生成台账记录、项目不可再修改 |
## æ³¨æ„äº‹é¡¹
- éœ€å…ˆåœ¨ BPM åŽå°æ‰‹å·¥éƒ¨ç½²å®¡æ‰¹æµç¨‹ï¼ˆæµç¨‹ key=`mes-pd-project-audit`,分类=`mes_pd_project_approve`),否则「提交审批」提示暂无可用流程
- æœªæäº¤å®¡æ‰¹ï¼ˆprocessInstanceId ä¸ºç©ºï¼‰æ—¶ç‚¹ã€ŒæŸ¥çœ‹å®¡æ‰¹ã€åº”提示「该项目尚未提交审批」
- åŽ†å²æ•°æ® status 0-4 ä¸å—影响;时间线组件需兼容状态值到步骤的非连续映射
docs/projectSql/ruoyi-vue-pro.sql
@@ -3449,7 +3449,7 @@
  `total_tax_price` decimal(24, 6) NOT NULL COMMENT '合计税额,单位:元',
  `discount_percent` decimal(24, 6) NOT NULL COMMENT '优惠率,百分比',
  `discount_price` decimal(24, 6) NOT NULL COMMENT '优惠金额,单位:元',
  `deposit_price` decimal(24, 6) NOT NULL COMMENT '定金金额,单位:元',
  `deposit_price` decimal(24, 6) NOT NULL DEFAULT 0 COMMENT '定金金额,单位:元',
  `file_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '附件地址',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
  `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'BPM æµç¨‹å®žä¾‹ç¼–号',
docs/purchase_invoice_payment_link.md
@@ -2,7 +2,7 @@
## æ¶‰åŠé¡µé¢
- é‡‡è´­æ¥ç¥¨ç®¡ç†é¡µï¼ˆ**新建页面**,参考采购申请/采购订单页面结构)
- é‡‡è´­æ¥ç¥¨ç®¡ç†é¡µï¼ˆæ¥ç¥¨åˆ—表、新增/编辑/详情弹窗)
- é‡‡è´­è®¢å•详情页(展示该订单下已关联的来票信息)
- ä»˜æ¬¾å•管理页(新建/编辑弹窗、列表、详情)
- ä»˜æ¬¾å•详情页(展示关联的来票信息)
@@ -10,15 +10,14 @@
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. é‡‡è´­è®¢å•(审核通过)-> åˆ›å»ºæ¥ç¥¨ï¼š`purchaseOrderId` å¿…选,供应商自动带出(以采购订单的供应商为准,前端无需传 `supplierId`,由后端按采购订单查询设置)。
2. æ¥ç¥¨åˆ›å»ºåŽ -> ä¸Šä¼ å‘票附件(走 system æ¨¡å— Storage API)。
3. æ¥ç¥¨æäº¤å®¡æ‰¹ -> å®¡æ‰¹é€šè¿‡ -> æ‰å¯ä½œä¸ºä»˜æ¬¾å…³è”对象。
4. æ–°å»ºä»˜æ¬¾å• -> é€‰æ‹©å…³è”来票(`invoiceId` å¿…填):从 `GET /erp/purchase-invoice/simple-list?supplierId=&purchaseOrderId=` é€‰æ‹©ï¼Œä»…显示"审批通过 + å·²ä¸Šä¼ é™„ä»¶ + å‰©ä½™å¯ä»˜é‡‘额 > 0"的来票;选择后 `remainingInvoicePrice` å¯ç”¨äºŽé¢„填付款金额上限。
2. æ¥ç¥¨åˆ›å»ºåŽ -> ä¸Šä¼ å‘票附件(走 system æ¨¡å— Storage API)。**来票创建即生效,无审核环节**。
3. æ–°å»ºä»˜æ¬¾å• -> é€‰æ‹©å…³è”来票(`invoiceId` å¿…填):从 `GET /erp/purchase-invoice/simple-list?supplierId=&purchaseOrderId=` é€‰æ‹©ï¼Œä»…显示"已上传附件 + å‰©ä½™å¯ä»˜é‡‘额 > 0"的来票;选择后 `remainingInvoicePrice` å¯ç”¨äºŽé¢„填付款金额上限。
对应关系:**采购订单(1) -> æ¥ç¥¨(N) -> ä»˜æ¬¾(M)**。
## API
### 1. æ¥ç¥¨ç®¡ç†ï¼ˆæ–°ï¼‰
### 1. æ¥ç¥¨ç®¡ç†
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
@@ -28,9 +27,9 @@
| GET | /erp/purchase-invoice/get?id= | èŽ·å¾—æ¥ç¥¨è¯¦æƒ… |
| GET | /erp/purchase-invoice/page | æ¥ç¥¨åˆ†é¡µ |
| GET | /erp/purchase-invoice/export-excel | å¯¼å‡ºæ¥ç¥¨ Excel |
| PUT | /erp/purchase-invoice/submit?id= | æäº¤æ¥ç¥¨å®¡æ‰¹ |
| GET | /erp/purchase-invoice/audit-count | å¾…审核来票数量 |
| GET | /erp/purchase-invoice/simple-list?supplierId=&purchaseOrderId= | ç²¾ç®€åˆ—表(付款表单选来票) |
> å·²ç§»é™¤æŽ¥å£ï¼š`/erp/purchase-invoice/update-status`(审核/反审核)、`/erp/purchase-invoice/audit-count`(待审核数量)。前端请同步删除对应按钮与接口调用。
**创建/更新来票请求参数:**
@@ -46,17 +45,18 @@
| remark | String | å¦ | å¤‡æ³¨ |
| blobIds | List\<Long\> | å¦ | å‘票附件 blobId åˆ—表 |
**来票分页/详情响应新增字段:**
**来票分页/详情响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| no | String | æ¥ç¥¨ç¼–号(后端生成,前缀 FPD) |
| supplierName | String | ä¾›åº”商名称 |
| order | Object | é‡‡è´­è®¢å•信息(嵌套) |
| auditStatus | Integer | å®¡æ‰¹çŠ¶æ€ï¼ˆ0未提交/10审批中/20审核通过/30审核不通过/40已取消) |
| attachmentList | List | å‘票附件列表 |
| hasAttachment | Boolean | æ˜¯å¦å·²ä¸Šä¼ å‘票附件 |
| remainingInvoicePrice | BigDecimal | å‰©ä½™å¯ä»˜é‡‘额 = æ¥ç¥¨é‡‘额 - è¯¥æ¥ç¥¨å·²ä»˜æ¬¾é‡‘额 |
> å·²ç§»é™¤å“åº”字段:`auditStatus`(审批状态)、`processInstanceId`(工作流编号)。
**simple-list å“åº”:**
@@ -69,18 +69,17 @@
| price | BigDecimal | æ¥ç¥¨é‡‘额 |
| supplierId | Long | ä¾›åº”商编号 |
| purchaseOrderId | Long | é‡‡è´­è®¢å•编号 |
| auditStatus | Integer | å®¡æ‰¹çŠ¶æ€ |
| hasAttachment | Boolean | æ˜¯å¦å·²ä¸Šä¼ é™„件(恒为 true) |
| remainingInvoicePrice | BigDecimal | å‰©ä½™å¯ä»˜é‡‘额 |
### 2. ä»˜æ¬¾å•(接口变更)
### 2. ä»˜æ¬¾å•
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /erp/finance-payment/create | åˆ›å»ºä»˜æ¬¾å•,请求体新增 `invoiceId`(必填) |
| PUT | /erp/finance-payment/update | æ›´æ–°ä»˜æ¬¾å•,请求体新增 `invoiceId`(必填) |
| GET | /erp/finance-payment/get | å“åº”新增 `invoiceId`、`invoice` åµŒå¥—对象 |
| GET | /erp/finance-payment/page | å“åº”新增 `invoiceId`、`invoice` åµŒå¥—对象 |
| POST | /erp/finance-payment/create | åˆ›å»ºä»˜æ¬¾å•,请求体含 `invoiceId`(必填) |
| PUT | /erp/finance-payment/update | æ›´æ–°ä»˜æ¬¾å•,请求体含 `invoiceId`(必填) |
| GET | /erp/finance-payment/get | å“åº”含 `invoiceId`、`invoice` åµŒå¥—对象 |
| GET | /erp/finance-payment/page | å“åº”含 `invoiceId`、`invoice` åµŒå¥—对象 |
**请求参数新增:**
@@ -93,7 +92,7 @@
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| invoiceId | Long | å…³è”的来票编号 |
| invoice | Object | å…³è”的来票信息(含 no、invoiceNo、invoiceTitle、price、auditStatus ç­‰ï¼‰ |
| invoice | Object | å…³è”的来票信息(含 no、invoiceNo、invoiceTitle、price ç­‰ï¼‰ |
**响应示例:**
@@ -109,8 +108,7 @@
      "no": "FPD20260810000001",
      "invoiceNo": "02564178",
      "invoiceTitle": "XX科技有限公司",
      "price": 9000.00,
      "auditStatus": 20
      "price": 9000.00
    },
    "paymentPrice": 5000.00
  }
@@ -127,20 +125,20 @@
| å‘票号码/抬头 | æ¥ç¥¨åˆ—表、详情 | å½•入字段 |
| æ¥ç¥¨é‡‘额 | æ¥ç¥¨åˆ—表、详情、付款表单 | å…ƒ |
| æ¥ç¥¨æ—¥æœŸ | æ¥ç¥¨åˆ—表、详情 | |
| å®¡æ‰¹çŠ¶æ€ | æ¥ç¥¨åˆ—表、详情 | å­—典展示 |
| å‘票附件 | æ¥ç¥¨è¯¦æƒ…、来票列表 | ä¸Šä¼ /预览/下载 |
| å‘票附件 | æ¥ç¥¨è¯¦æƒ…、来票列表 | ä¸Šä¼ /预览/下载,有附件显示"已上传"Tag |
| å‰©ä½™å¯ä»˜é‡‘额 | æ¥ç¥¨åˆ—表、付款表单 | æ¥ç¥¨é‡‘额 - å·²ä»˜æ¬¾é‡‘额 |
| å…³è”来票 | ä»˜æ¬¾åˆ—表、付款详情 | å±•示 invoice.no / invoice.invoiceNo / invoice.price |
> å·²ç§»é™¤å±•示字段:来票列表的"审批状态"列、搜索条件"审核状态"、"审核/反审核"操作按钮。
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ¥ç¥¨åˆ›å»º | é‡‡è´­è®¢å•必须审核通过;来票供应商与采购订单一致;来票金额不超过订单剩余可开票金额 |
| æ¥ç¥¨ç¼–辑 | ä»…"未提交"状态可编辑 |
| æ¥ç¥¨åˆ é™¤ | å®¡æ‰¹é€šè¿‡åŽä¸å¯åˆ é™¤ |
| æ¥ç¥¨æäº¤å®¡æ‰¹ | ä»…"未提交"状态可提交,提交后进入审批中 |
| ä»˜æ¬¾é€‰æ‹©æ¥ç¥¨ | ä»…审批通过 + å·²ä¸Šä¼ å‘票附件的来票可选 |
| æ¥ç¥¨åˆ›å»º | é‡‡è´­è®¢å•必须审核通过;来票供应商与采购订单一致;来票金额不超过订单剩余可开票金额;**创建即生效,无需审核** |
| æ¥ç¥¨ç¼–辑 | ä»»æ„çŠ¶æ€å¯ç¼–è¾‘ï¼Œæ— å®¡æ ¸çŠ¶æ€é™åˆ¶ |
| æ¥ç¥¨åˆ é™¤ | å·²æœ‰æœ‰æ•ˆä»˜æ¬¾å•(未作废且付款金额 > 0)关联的来票不可删除,其余可删除 |
| ä»˜æ¬¾é€‰æ‹©æ¥ç¥¨ | å·²ä¸Šä¼ å‘票附件且剩余可付金额 > 0 çš„æ¥ç¥¨å¯é€‰ï¼ˆ**不再要求审核通过**) |
| ä»˜æ¬¾é‡‘额上限 | ä»˜æ¬¾é‡‘额 <= è¯¥æ¥ç¥¨é‡‘额 - è¯¥æ¥ç¥¨å·²ä»˜æ¬¾é‡‘额(未审核+审批中+已审核付款,排除作废) |
| ä¾›åº”商一致性 | ä»˜æ¬¾çš„供应商必须与关联来票的供应商一致 |
| å­˜é‡ä»˜æ¬¾å• | invoiceId ä¸ºç©ºçš„存量付款单,编辑时需重新选择合格来票 |
@@ -151,5 +149,4 @@
- ä»˜æ¬¾è¡¨å•选择来票时,先按供应商过滤(`supplierId`),可选订单时传 `purchaseOrderId` è¿›ä¸€æ­¥è¿‡æ»¤ã€‚
- `remainingInvoicePrice` ä¸ºè´Ÿæ•°æˆ– 0 çš„æ¥ç¥¨ä¸ä¼šå‡ºçŽ°åœ¨ simple-list ä¸­ï¼Œå‰ç«¯å¯ç›´æŽ¥æŒ‰è¿”回列表渲染。
- ä»˜æ¬¾é‡‘额校验在后端,前端可将来票的 `remainingInvoicePrice` ä½œä¸ºä»˜æ¬¾é‡‘额输入上限提示。
- æ¥ç¥¨å®¡æ‰¹çŠ¶æ€å˜åŒ–é€šè¿‡ BPM å›žè°ƒæ›´æ–°ï¼Œå‰ç«¯æ— éœ€å¤„理,分页接口直接返回最新状态。
- æ¥ç¥¨å‘票附件上传使用 system æ¨¡å— Storage API(upload -> bind),不要使用旧版 /infra/file æŽ¥å£ã€‚
docs/sql/erp_purchase_invoice_remove_audit.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
-- ERP é‡‡è´­æ¥ç¥¨åŽ»æŽ‰å®¡æ ¸ï¼šåˆ é™¤å®¡æ ¸çŠ¶æ€å­—æ®µä¸Žå·¥ä½œæµæ®‹ç•™å­—æ®µ
-- èƒŒæ™¯ï¼šæ¥ç¥¨åˆ›å»ºå³ç”Ÿæ•ˆï¼Œæ— éœ€å®¡æ ¸ï¼Œä»˜æ¬¾ä¸å†æ ¡éªŒ"来票审核通过"
-- æ‰§è¡Œæ—¶æœºï¼šæ–°åŽç«¯éƒ¨ç½²åŽçš„停服窗口执行(旧代码读写这两列,先删列会导致运行时报错)
ALTER TABLE erp_purchase_invoice DROP COLUMN audit_status;
ALTER TABLE erp_purchase_invoice DROP COLUMN process_instance_id;
-- æ¸…理孤儿菜单权限(对应已删除的 /update-status å®¡æ ¸æŽ¥å£ï¼‰
DELETE FROM system_menu WHERE permission = 'erp:purchase-invoice:update-status';
docs/srm_certificate_attachment_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
# SRM èµ„质附件上传 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- é‡‡è´­ç®¡ç†-SRM-供应商管理-资质证书列表页
- é‡‡è´­ç®¡ç†-SRM-供应商管理-资质证书新增/编辑弹窗
- é‡‡è´­ç®¡ç†-SRM-招标项目-新增/编辑弹窗(含发布中/投标中的部分更新)
- é‡‡è´­ç®¡ç†-SRM-招标项目-详情页
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. ç”¨æˆ·åœ¨è¡¨å•中通过上传组件选择文件
2. ä¸Šä¼ ç»„件调用 `/system/storage-blob/upload` èŽ·å¾— `blobId`
3. ä¿å­˜ä¸šåŠ¡è®°å½•ï¼ˆèµ„è´¨è¯ä¹¦ / æ‹›æ ‡é¡¹ç›®ï¼‰æ—¶ï¼Œå°†æ–‡ä»¶ ID åˆ—表 `blobIds` éšä¸šåŠ¡æ•°æ®ä¸€å¹¶æäº¤
4. åŽç«¯åœ¨ä¸šåŠ¡è®°å½•ä¿å­˜åŽï¼Œé€šè¿‡ `recordType + recordId` å°†æ–‡ä»¶ç»‘定到 `system_storage_attachment` ä¸­é—´è¡¨
5. æŸ¥è¯¢ä¸šåŠ¡è®°å½•ï¼ˆè¯¦æƒ…ã€åˆ†é¡µï¼‰æ—¶ï¼ŒåŽç«¯è¿”å›ž `attachmentList`(含文件 URL、名称),前端展示下载链接
## API
### 1. èµ„质证书附件
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /system/storage-blob/upload | ä¸Šä¼ æ–‡ä»¶ï¼Œå…¥å‚ multipart çš„ `files`,返回 Blob åˆ—表(含 `id`) |
| POST | /srm/supplier-certificate/create | åˆ›å»ºèµ„质证书,新增入参 `blobIds` |
| PUT | /srm/supplier-certificate/update | æ›´æ–°èµ„质证书,新增入参 `blobIds`(增量更新附件) |
| DELETE | /srm/supplier-certificate/delete | åˆ é™¤èµ„质证书,同步删除附件 |
| GET | /srm/supplier-certificate/page | åˆ†é¡µæŸ¥è¯¢ï¼Œå“åº”新增 `attachmentList` |
| GET | /srm/supplier-certificate/get | è¯¦æƒ…查询,响应新增 `attachmentList` |
| GET | /srm/supplier-certificate/list-by-supplier | æŒ‰ä¾›åº”商查询,响应新增 `attachmentList` |
**请求参数(create/update æ–°å¢žå­—段):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| blobIds | Array\<Long\> | å¦ | èµ„质证书附件文件 ID åˆ—表 |
**响应字段新增(attachmentList æ•°ç»„元素):**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | æ–‡ä»¶ ID |
| name | String | åŽŸæ–‡ä»¶å |
| url | String | æ–‡ä»¶è®¿é—®åœ°å€ |
| previewURL | String | é¢„览地址 |
| downloadURL | String | ä¸‹è½½åœ°å€ |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "supplierId": 10,
    "certificateName": "ISO9001质量管理体系",
    "attachmentList": [
      {
        "id": 1001,
        "name": "iso9001_cert.pdf",
        "url": "https://example.com/files/xxx.pdf"
      }
    ]
  }
}
```
### 2. æ‹›æ ‡é¡¹ç›®èµ„质要求附件
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /system/storage-blob/upload | ä¸Šä¼ æ–‡ä»¶ï¼Œå…¥å‚ multipart çš„ `files`,返回 Blob åˆ—表(含 `id`) |
| POST | /srm/tender-project/create | åˆ›å»ºæ‹›æ ‡é¡¹ç›®ï¼Œæ–°å¢žå…¥å‚ `blobIds` |
| PUT | /srm/tender-project/update | æ›´æ–°æ‹›æ ‡é¡¹ç›®ï¼Œæ–°å¢žå…¥å‚ `blobIds`(草稿/发布中/投标中均增量更新附件) |
| PUT | /srm/tender-project/update-info | éƒ¨åˆ†æ›´æ–°ï¼ˆå‘布中/投标中),新增入参 `blobIds` |
| DELETE | /srm/tender-project/delete | åˆ é™¤æ‹›æ ‡é¡¹ç›®ï¼ŒåŒæ­¥åˆ é™¤é™„ä»¶ |
| GET | /srm/tender-project/get | è¯¦æƒ…查询,响应新增 `attachmentList` |
| GET | /srm/tender-project/page | åˆ†é¡µæŸ¥è¯¢ï¼Œå“åº”新增 `attachmentList` |
**请求参数(create/update/update-info æ–°å¢žå­—段):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| blobIds | Array\<Long\> | å¦ | èµ„质要求附件文件 ID åˆ—表 |
**响应字段新增:** åŒèµ„质证书,`attachmentList` æ•°ç»„元素含 `id`、`name`、`url`、`previewURL`、`downloadURL`。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| è¯ä¹¦é™„ä»¶ | èµ„质证书列表 | æ–°å¢žã€Œé™„件」列,显示附件名称链接,点击新窗口打开;无附件显示 `--` |
| è¯ä¹¦é™„ä»¶ | èµ„质证书新增/编辑弹窗 | ã€Œè¯ä¹¦é™„件」上传项,支持多文件,编辑时回显已上传文件 |
| èµ„质要求附件 | æ‹›æ ‡é¡¹ç›®æ–°å¢ž/编辑弹窗 | ã€Œèµ„质要求附件」上传项,支持多文件,编辑时回显已上传文件 |
| èµ„质要求附件 | æ‹›æ ‡é¡¹ç›®è¯¦æƒ…页 | ã€Œèµ„质要求」折叠面板内展示附件名称链接,点击新窗口打开 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| ç¼–辑回显 | ç¼–辑时后端返回 `attachmentList`,前端将其映射为上传组件文件对象(`uid`、`name`、`url`、`status='done'`、`id`)并赋给 `blobIds`,提交时按 `valueKey='id'` æå–文件 ID æäº¤ |
| é™„ä»¶æ›´æ–° | åŽç«¯æŒ‰ã€Œå¢žé‡æ›´æ–°ã€å¤„理:以本次提交的 `blobIds` ä¸ºå‡†ï¼Œè‡ªåŠ¨åˆ é™¤å·²ç§»é™¤çš„æ–‡ä»¶ç»‘å®šã€æ–°å¢žæ–°ä¸Šä¼ çš„æ–‡ä»¶ç»‘å®š |
| åˆ é™¤ä¸šåŠ¡è®°å½• | åˆ é™¤èµ„质证书/招标项目时,后端同步删除其附件 |
| å‘布中/投标中编辑 | æ‹›æ ‡é¡¹ç›®åœ¨å‘布中/投标中阶段通过 `update-info` æŽ¥å£ä»…能修改招标说明、资质要求及资质要求附件 |
| ä¸Šä¼ æ ¼å¼ | æ”¯æŒ jpg、jpeg、png、pdf、doc、docx,单个文件不超过 2MB |
## æ³¨æ„äº‹é¡¹
- ä¸Šä¼ å¿…须走 `/system/storage-blob/upload`,禁止使用旧版 `/infra/file/*` æŽ¥å£ã€‚
- èµ„质证书与招标项目使用不同的 `recordType`:资质证书为 `srm_supplier_certificate`,招标项目为 `srm_tender_project`,前端无需关心,由后端处理。
- ç¼–辑回显时若用户未改动附件,提交的 `blobIds` ä»åº”包含原有文件 ID(由上传组件回显对象携带),避免附件被误删。
- æ‹›æ ‡é¡¹ç›®éƒ¨åˆ†æ›´æ–°ï¼ˆ`update-info`)请求体同样需携带 `blobIds`。
- åˆ—表页附件链接建议 `target="_blank"` æ–°çª—口打开,便于预览 PDF/图片。
docs/srm_supplier_tender_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
# SRM ä¾›åº”商投标选人 & ä¸­æ ‡è‡ªåЍ准入 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- æ‹›æ ‡é¡¹ç›®è¯¦æƒ…页(投标管理 Tab、评标 Tab çš„供应商选择)
- SRM ä¾›åº”商管理页面(仅新增 `getSupplierList` API,页面本身无改动)
## èƒŒæ™¯ä¸Žéœ€æ±‚
1. æ‹›æ ‡é¡¹ç›®è¯¦æƒ…页中「新增投标」「评标」时选择供应商,**不再过滤准入状态**,直接查询供应商档案列表。
2. ä¸­æ ‡å¹¶ç”Ÿæˆé‡‡è´­è®¢å•时,如果供应商尚未准入,后端会自动创建准入申请并置为已通过,前端无需额外处理。
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /srm/supplier/list | èŽ·å–ä¾›åº”å•†æ¡£æ¡ˆåˆ—è¡¨ï¼ˆä¸è¿‡æ»¤å‡†å…¥çŠ¶æ€ï¼‰ |
**请求参数:** æ— 
**响应:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | number | ä¾›åº”商 ID |
| name | string | ä¾›åº”商名称 |
| code | string | ä¾›åº”商编码 |
| status | number | çŠ¶æ€ï¼ˆ0 å¯ç”¨ï¼Œ1 ç¦ç”¨ï¼‰ |
**响应示例:**
```json
{
  "code": 0,
  "data": [
    { "id": 1, "code": "SUP001", "name": "某某公司", "status": 0 }
  ]
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æ–°å¢ž API å‡½æ•°
在 `src/api/srm/supplier/index.ts` ä¸­æ–°å¢ž `getSupplierList()`,请求 `/srm/supplier/list`。
> åŽŸæœ‰çš„ `getSupplierSimpleList()`(请求 `/srm/supplier/simple-list`,仅返回已准入供应商)**保留不动**,其它页面(采购订单、采购入库、IQC ç­‰ï¼‰ä»ä½¿ç”¨å®ƒã€‚
### 2. æ‹›æ ‡é¡¹ç›®è¯¦æƒ…页改用档案列表
`src/views/srm/tender/detail.vue` ä¸­ï¼š
- å°† `getSupplierSimpleList` çš„ import æ›¿æ¢ä¸º `getSupplierList`
- å°† `onMounted` ä¸­åŠ è½½ä¾›åº”å•†æ•°æ®çš„è°ƒç”¨æ”¹ä¸º `getSupplierList`
该页面的「投标管理」供应商选择下拉、「评标」供应商选择下拉,均使用同一个 `supplierList`,替换后两个下拉都会展示全部供应商档案(不过滤准入)。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| id | ä¸‹æ‹‰ value | ä¾›åº”商 ID,提交给后端 |
| name | ä¸‹æ‹‰ label | ä¾›åº”商名称 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æŠ•标时选择供应商 | å±•示全部供应商档案,不再过滤准入状态 |
| è¯„标时选择供应商 | åŒæŠ•标,展示全部供应商档案 |
| ä¸­æ ‡ç”Ÿæˆé‡‡è´­è®¢å• | è‹¥ä¾›åº”商未准入,后端自动创建准入申请并直接置为已通过,再生成采购订单;前端无需调用准入相关接口 |
| æœªå‡†å…¥ä¾›åº”商能否投标 | å¯ä»¥ï¼ŒæŠ•标不受准入状态限制 |
## æ³¨æ„äº‹é¡¹
- `getSupplierList` è¿”回的数组元素含 `id`、`name`,可直接用于下拉选项(`{ id, name }`)。
- ä¾›åº”商被禁用(status = 1)时是否允许投标:本接口不过滤准入状态,`status` å­—段由前端决定是否过滤展示;目前招标详情页下拉不过滤。
- ä¸­æ ‡è‡ªåŠ¨å‡†å…¥æ˜¯åŽç«¯è¡Œä¸ºï¼Œå‰ç«¯ã€Œç¡®è®¤ä¸­æ ‡ã€ã€Œç”Ÿæˆé‡‡è´­è®¢å•ã€æŒ‰é’®çš„è°ƒç”¨æ–¹å¼ä¸å˜ã€‚
sql/mysql/mes_dv_metering_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,119 @@
-- ============================================
-- MES è®¾å¤‡ç®¡ç†-计量器具 & è®¡é‡æ£€æµ‹è®°å½• æ¨¡å—初始化脚本
-- 1. å»ºè¡¨  2. èœå•  3. å­—å…¸  4. ç«™å†…信模板  5. å…¨å±€é…ç½®
-- ============================================
-- 1. å»ºè¡¨
DROP TABLE IF EXISTS `mes_dv_metering_check`;
DROP TABLE IF EXISTS `mes_dv_metering`;
CREATE TABLE `mes_dv_metering` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '计量器具编码',
    `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '计量器具名称',
    `specification` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '规格型号',
    `brand` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '品牌/生产厂家',
    `category` int NULL DEFAULT NULL COMMENT '器具类别(卡尺/千分尺/压力表等),字典 mes_dv_metering_category',
    `precision` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '精度',
    `range` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '量程',
    `dept_id` bigint NULL DEFAULT NULL COMMENT '使用部门编号',
    `charge_user_id` bigint NULL DEFAULT NULL COMMENT '责任人编号(站内信发送对象)',
    `check_cycle_count` int NOT NULL COMMENT '校准周期(月)',
    `use_date` date NULL DEFAULT NULL COMMENT '投入使用日期',
    `last_check_date` date NULL DEFAULT NULL COMMENT '上次检测日期',
    `next_check_date` date NULL DEFAULT NULL COMMENT '下次检测日期(到期日)',
    `status` int NOT NULL COMMENT '状态(在用/停用/报废),字典 mes_dv_metering_status',
    `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
    -- æ ‡å‡†å­—段
    `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE INDEX `uk_code` (`code`),
    INDEX `idx_dept_id` (`dept_id`),
    INDEX `idx_charge_user_id` (`charge_user_id`),
    INDEX `idx_next_check_date` (`next_check_date`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MES è®¡é‡å™¨å…·' ROW_FORMAT = Dynamic;
CREATE TABLE `mes_dv_metering_check` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `metering_id` bigint NOT NULL COMMENT '计量器具ID',
    `check_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '检测编号',
    `check_date` date NULL DEFAULT NULL COMMENT '检测日期',
    `check_org` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '检测机构',
    `check_result` int NULL DEFAULT NULL COMMENT '检测结论(合格/不合格),字典 mes_dv_metering_check_result',
    `cert_no` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '证书编号',
    `check_user_id` bigint NULL DEFAULT NULL COMMENT '检测人编号',
    `next_check_date` date NULL DEFAULT NULL COMMENT '下次检测日期(自动计算或手动指定)',
    `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
    -- æ ‡å‡†å­—段
    `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE INDEX `uk_check_code` (`check_code`),
    INDEX `idx_metering_id` (`metering_id`),
    INDEX `idx_check_date` (`check_date`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MES è®¡é‡æ£€æµ‹è®°å½•' ROW_FORMAT = Dynamic;
-- 2. èœå•(父菜单 5300 è®¾å¤‡ç®¡ç†ï¼‰
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES (5380, '计量器具', '', 2, 8, 5300, 'metering', 'ep:scale-to-original', 'mes/dv/metering/index', 'MesDvMetering', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
(5381, '计量器具查询', 'mes:dv-metering:query', 3, 1, 5380, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5382, '计量器具创建', 'mes:dv-metering:create', 3, 2, 5380, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5383, '计量器具更新', 'mes:dv-metering:update', 3, 3, 5380, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5384, '计量器具删除', 'mes:dv-metering:delete', 3, 4, 5380, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5385, '计量器具导出', 'mes:dv-metering:export', 3, 5, 5380, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES (5386, '计量检测记录', '', 2, 9, 5300, 'metering-check', 'ep:checked', 'mes/dv/meteringcheck/index', 'MesDvMeteringCheck', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
(5387, '检测记录查询', 'mes:dv-metering-check:query', 3, 1, 5386, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5388, '检测记录创建', 'mes:dv-metering-check:create', 3, 2, 5386, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5389, '检测记录更新', 'mes:dv-metering-check:update', 3, 3, 5386, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5390, '检测记录删除', 'mes:dv-metering-check:delete', 3, 4, 5386, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(5391, '检测记录导出', 'mes:dv-metering-check:export', 3, 5, 5386, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- 3. å­—典类型
INSERT INTO `system_dict_type` (`name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
('MES è®¡é‡å™¨å…·ç±»åˆ«', 'mes_dv_metering_category', 0, '计量器具类别', '1', NOW(), '1', NOW(), b'0'),
('MES è®¡é‡å™¨å…·çŠ¶æ€', 'mes_dv_metering_status', 0, '计量器具状态', '1', NOW(), '1', NOW(), b'0'),
('MES è®¡é‡æ£€æµ‹ç»“论', 'mes_dv_metering_check_result', 0, '计量检测结论', '1', NOW(), '1', NOW(), b'0');
-- 4. å­—典数据
INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
-- å™¨å…·ç±»åˆ«
(1, '卡尺', '1', 'mes_dv_metering_category', 0, '', '', '卡尺', '1', NOW(), '1', NOW(), b'0'),
(2, '千分尺', '2', 'mes_dv_metering_category', 0, '', '', '千分尺', '1', NOW(), '1', NOW(), b'0'),
(3, '压力表', '3', 'mes_dv_metering_category', 0, '', '', '压力表', '1', NOW(), '1', NOW(), b'0'),
(4, '温度计', '4', 'mes_dv_metering_category', 0, '', '', '温度计', '1', NOW(), '1', NOW(), b'0'),
(5, '万用表', '5', 'mes_dv_metering_category', 0, '', '', '万用表', '1', NOW(), '1', NOW(), b'0'),
(6, '天平砝码', '6', 'mes_dv_metering_category', 0, '', '', '天平砝码', '1', NOW(), '1', NOW(), b'0'),
(7, '其他', '7', 'mes_dv_metering_category', 0, '', '', '其他', '1', NOW(), '1', NOW(), b'0'),
-- å™¨å…·çŠ¶æ€
(1, '在用', '1', 'mes_dv_metering_status', 0, 'success', '', '在用', '1', NOW(), '1', NOW(), b'0'),
(2, '停用', '2', 'mes_dv_metering_status', 0, 'info', '', '停用', '1', NOW(), '1', NOW(), b'0'),
(3, '报废', '3', 'mes_dv_metering_status', 0, 'danger', '', '报废', '1', NOW(), '1', NOW(), b'0'),
-- æ£€æµ‹ç»“论
(1, '合格', '1', 'mes_dv_metering_check_result', 0, 'success', '', '合格', '1', NOW(), '1', NOW(), b'0'),
(2, '不合格', '2', 'mes_dv_metering_check_result', 0, 'danger', '', '不合格', '1', NOW(), '1', NOW(), b'0');
-- 5. ç«™å†…信模板
INSERT INTO `system_notify_template` (`name`, `code`, `nickname`, `content`, `type`, `params`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES ('计量器具到期提醒', 'mes_metering_expire_remind', 'MES ç³»ç»Ÿ', '计量器具【{meteringCode}】{meteringName} å°†äºŽ {nextCheckDate} åˆ°æœŸï¼Œè¯·åŠæ—¶å®‰æŽ’校准/检定。', 2, '["meteringCode","meteringName","nextCheckDate"]', 0, '1', NOW(), '1', NOW(), b'0');
-- 6. å…¨å±€é…ç½®ï¼ˆæé†’天数,默认 30 å¤©ï¼‰
INSERT INTO `infra_config` (`category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES ('biz', 2, 'MES è®¡é‡å™¨å…·åˆ°æœŸæé†’天数', 'mes.dv-metering.remind-days', '30', 0, '计量器具下次检测日期到期前 N å¤©æé†’(站内信发送给责任人)', '1', NOW(), '1', NOW(), b'0');
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/attach/BpmBusinessAttachController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.bpm.controller.admin.attach;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.attach.vo.attach.BpmBusinessAttachPageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.attach.vo.attach.BpmBusinessAttachRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.attach.vo.attach.BpmBusinessAttachSaveReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmBusinessAttachDO;
import cn.iocoder.yudao.module.bpm.service.attach.BpmBusinessAttachService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - BPM ä¸šåŠ¡é™„ä»¶é…ç½®")
@RestController
@RequestMapping("/bpm/business-attach")
@Validated
public class BpmBusinessAttachController {
    @Resource
    private BpmBusinessAttachService businessAttachService;
    @PostMapping("/create")
    @Operation(summary = "创建业务附件配置")
    @PreAuthorize("@ss.hasPermission('bpm:business-attach:create')")
    public CommonResult<Long> createBusinessAttach(@Valid @RequestBody BpmBusinessAttachSaveReqVO createReqVO) {
        return success(businessAttachService.createBusinessAttach(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新业务附件配置")
    @PreAuthorize("@ss.hasPermission('bpm:business-attach:update')")
    public CommonResult<Boolean> updateBusinessAttach(@Valid @RequestBody BpmBusinessAttachSaveReqVO updateReqVO) {
        businessAttachService.updateBusinessAttach(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除业务附件配置")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('bpm:business-attach:delete')")
    public CommonResult<Boolean> deleteBusinessAttach(@RequestParam("id") Long id) {
        businessAttachService.deleteBusinessAttach(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得业务附件配置")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('bpm:business-attach:query')")
    public CommonResult<BpmBusinessAttachRespVO> getBusinessAttach(@RequestParam("id") Long id) {
        BpmBusinessAttachDO attach = businessAttachService.getBusinessAttach(id);
        return success(BeanUtils.toBean(attach, BpmBusinessAttachRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获得业务附件配置分页")
    @PreAuthorize("@ss.hasPermission('bpm:business-attach:query')")
    public CommonResult<PageResult<BpmBusinessAttachRespVO>> getBusinessAttachPage(@Valid BpmBusinessAttachPageReqVO pageReqVO) {
        PageResult<BpmBusinessAttachDO> pageResult = businessAttachService.getBusinessAttachPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, BpmBusinessAttachRespVO.class));
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/attach/vo/attach/BpmBusinessAttachPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.bpm.controller.admin.attach.vo.attach;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - BPM ä¸šåŠ¡é™„ä»¶é…ç½®åˆ†é¡µ Request VO")
@Data
public class BpmBusinessAttachPageReqVO extends PageParam {
    @Schema(description = "流程定义的 Key", example = "purchase_approve")
    private String processDefinitionKey;
    @Schema(description = "业务类型,例如 erp_purchase_order", example = "erp_purchase_order")
    private String businessType;
    @Schema(description = "业务详情组件路径,例如 /erp/purchase/order/detail/index.vue")
    private String componentPath;
    @Schema(description = "状态,参见 CommonStatusEnum æžšä¸¾", example = "1")
    @InEnum(CommonStatusEnum.class)
    private Integer status;
    @Schema(description = "创建时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] createTime;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/attach/vo/attach/BpmBusinessAttachRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.bpm.controller.admin.attach.vo.attach;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - BPM ä¸šåŠ¡é™„ä»¶é…ç½® Response VO")
@Data
public class BpmBusinessAttachRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "流程定义的 Key", requiredMode = Schema.RequiredMode.REQUIRED, example = "purchase_approve")
    private String processDefinitionKey;
    @Schema(description = "业务类型,例如 erp_purchase_order", example = "erp_purchase_order")
    private String businessType;
    @Schema(description = "业务详情组件路径,例如 /erp/purchase/order/detail/index.vue", requiredMode = Schema.RequiredMode.REQUIRED)
    private String componentPath;
    @Schema(description = "状态,参见 CommonStatusEnum æžšä¸¾", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer status;
    @Schema(description = "备注", example = "采购订单审批展示采购订单详情")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/attach/vo/attach/BpmBusinessAttachSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.bpm.controller.admin.attach.vo.attach;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - BPM ä¸šåŠ¡é™„ä»¶é…ç½®æ–°å¢ž/修改 Request VO")
@Data
public class BpmBusinessAttachSaveReqVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "流程定义的 Key", requiredMode = Schema.RequiredMode.REQUIRED, example = "purchase_approve")
    @NotEmpty(message = "流程定义的 Key ä¸èƒ½ä¸ºç©º")
    private String processDefinitionKey;
    @Schema(description = "业务类型,例如 erp_purchase_order", example = "erp_purchase_order")
    private String businessType;
    @Schema(description = "业务详情组件路径,例如 /erp/purchase/order/detail/index.vue", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotEmpty(message = "业务详情组件路径不能为空")
    private String componentPath;
    @Schema(description = "状态,参见 CommonStatusEnum æžšä¸¾", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "状态不能为空")
    @InEnum(CommonStatusEnum.class)
    private Integer status;
    @Schema(description = "备注", example = "采购订单审批展示采购订单详情")
    private String remark;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java
@@ -11,6 +11,7 @@
import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.service.attach.BpmBusinessAttachService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
@@ -56,6 +57,8 @@
    private BpmProcessDefinitionService processDefinitionService;
    @Resource
    private BpmCategoryService categoryService;
    @Resource
    private BpmBusinessAttachService businessAttachService;
    @Resource
    private AdminUserApi adminUserApi;
@@ -217,10 +220,16 @@
        List<HistoricTaskInstance> tasks = taskService.getFinishedTaskListByProcessInstanceIdWithoutCancel(processInstanceId);
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(tasks, item -> Long.valueOf(item.getAssignee())));
        return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePrintData(historicProcessInstance,
        BpmProcessPrintDataRespVO printData = BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePrintData(
                historicProcessInstance,
                processDefinitionService.getProcessDefinitionInfo(historicProcessInstance.getProcessDefinitionId()),
                tasks, userMap,
                new UserSimpleBaseVO().setNickname(startUser.getNickname()).setDeptName(dept.getName())));
                new UserSimpleBaseVO().setNickname(startUser.getNickname()).setDeptName(dept.getName()));
        // å¡«å……业务附件配置,实现打印自动展示外部业务信息
        businessAttachService.fillBusinessForm(printData.getProcessInstance().getProcessDefinition(),
                processDefinitionService.getProcessDefinition(historicProcessInstance.getProcessDefinitionId()).getKey(),
                historicProcessInstance.getBusinessKey());
        return success(printData);
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
@@ -58,8 +58,9 @@
            List<DeptRespDTO> startDepts = metaInfo != null ? convertList(metaInfo.getStartDeptIds(), deptMap::get) : null;
            return buildModel0(model, metaInfo, form, category, deployment, processDefinition, startUsers, startDepts);
        });
        // æŽ’序
        result.sort(Comparator.comparing(BpmModelMetaInfoVO::getSort));
        // æŽ’序:手工部署(DB ç›´æ’)的模型可能没有 metaInfo/sort,兜底排到最后
        result.sort(Comparator.comparing(BpmModelMetaInfoVO::getSort,
                Comparator.nullsLast(Comparator.naturalOrder())));
        return result;
    }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmBusinessAttachDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * BPM ä¸šåŠ¡é™„ä»¶é…ç½® DO
 *
 * ç”¨äºŽå»ºç«‹ã€Œæµç¨‹å®šä¹‰ Key -> ä¸šåŠ¡è¯¦æƒ…ç»„ä»¶ã€çš„æ˜ å°„ï¼Œå®žçŽ°å®¡æ‰¹è¯¦æƒ…è‡ªåŠ¨å±•ç¤ºå¤–éƒ¨ä¸šåŠ¡ä¿¡æ¯
 *
 * @author è¶…级管理员
 */
@TableName("bpm_business_attach")
@KeySequence("bpm_business_attach_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BpmBusinessAttachDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * æµç¨‹å®šä¹‰çš„ Key
     */
    private String processDefinitionKey;
    /**
     * ä¸šåŠ¡ç±»åž‹ï¼Œä¾‹å¦‚ erp_purchase_order
     */
    private String businessType;
    /**
     * ä¸šåŠ¡è¯¦æƒ…ç»„ä»¶è·¯å¾„ï¼Œä¾‹å¦‚ /erp/purchase/order/detail/index.vue
     */
    private String componentPath;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/attach/BpmBusinessAttachMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.bpm.dal.mysql.attach;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.bpm.controller.admin.attach.vo.attach.BpmBusinessAttachPageReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmBusinessAttachDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * BPM ä¸šåŠ¡é™„ä»¶é…ç½® Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface BpmBusinessAttachMapper extends BaseMapperX<BpmBusinessAttachDO> {
    default PageResult<BpmBusinessAttachDO> selectPage(BpmBusinessAttachPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<BpmBusinessAttachDO>()
                .likeIfPresent(BpmBusinessAttachDO::getProcessDefinitionKey, reqVO.getProcessDefinitionKey())
                .likeIfPresent(BpmBusinessAttachDO::getBusinessType, reqVO.getBusinessType())
                .likeIfPresent(BpmBusinessAttachDO::getComponentPath, reqVO.getComponentPath())
                .eqIfPresent(BpmBusinessAttachDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(BpmBusinessAttachDO::getCreateTime, reqVO.getCreateTime())
                .orderByDesc(BpmBusinessAttachDO::getId));
    }
    default BpmBusinessAttachDO selectByProcessDefinitionKey(String processDefinitionKey) {
        return selectOne(BpmBusinessAttachDO::getProcessDefinitionKey, processDefinitionKey);
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
@@ -89,4 +89,8 @@
    // ========== BPM æµç¨‹è¡¨è¾¾å¼ 1-009-014-000 ==========
    ErrorCode PROCESS_EXPRESSION_NOT_EXISTS = new ErrorCode(1_009_014_000, "流程表达式不存在");
    // ========== BPM ä¸šåŠ¡é™„ä»¶é…ç½® 1-009-015-000 ==========
    ErrorCode BUSINESS_ATTACH_NOT_EXISTS = new ErrorCode(1_009_015_000, "业务附件配置不存在");
    ErrorCode BUSINESS_ATTACH_PROCESS_DEFINITION_KEY_DUPLICATE = new ErrorCode(1_009_015_001, "流程定义的 Key({})已存在");
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/attach/BpmBusinessAttachService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.bpm.service.attach;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.bpm.controller.admin.attach.vo.attach.BpmBusinessAttachPageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.attach.vo.attach.BpmBusinessAttachSaveReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmBusinessAttachDO;
import jakarta.validation.Valid;
/**
 * BPM ä¸šåŠ¡é™„ä»¶é…ç½® Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface BpmBusinessAttachService {
    /**
     * åˆ›å»ºä¸šåŠ¡é™„ä»¶é…ç½®
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createBusinessAttach(@Valid BpmBusinessAttachSaveReqVO createReqVO);
    /**
     * æ›´æ–°ä¸šåŠ¡é™„ä»¶é…ç½®
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateBusinessAttach(@Valid BpmBusinessAttachSaveReqVO updateReqVO);
    /**
     * åˆ é™¤ä¸šåŠ¡é™„ä»¶é…ç½®
     *
     * @param id ç¼–号
     */
    void deleteBusinessAttach(Long id);
    /**
     * èŽ·å¾—ä¸šåŠ¡é™„ä»¶é…ç½®
     *
     * @param id ç¼–号
     * @return ä¸šåŠ¡é™„ä»¶é…ç½®
     */
    BpmBusinessAttachDO getBusinessAttach(Long id);
    /**
     * èŽ·å¾—ä¸šåŠ¡é™„ä»¶é…ç½®åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return ä¸šåŠ¡é™„ä»¶é…ç½®åˆ†é¡µ
     */
    PageResult<BpmBusinessAttachDO> getBusinessAttachPage(BpmBusinessAttachPageReqVO pageReqVO);
    /**
     * èŽ·å¾—ä¸šåŠ¡é™„ä»¶é…ç½®ï¼ŒåŸºäºŽæµç¨‹å®šä¹‰ Key
     *
     * @param processDefinitionKey æµç¨‹å®šä¹‰ Key
     * @return ä¸šåŠ¡é™„ä»¶é…ç½®
     */
    BpmBusinessAttachDO getBusinessAttachByProcessDefinitionKey(String processDefinitionKey);
    /**
     * å¡«å……业务表单信息到流程定义 Response
     *
     * è‹¥è¯¥æµç¨‹å®šä¹‰é…ç½®äº†ä¸šåŠ¡é™„ä»¶ï¼ˆä¸”å·²å¯ç”¨ã€å½“å‰æ˜¯ NORMAL æµç¨‹è¡¨å•、存在业务主键),
     * åˆ™å°† formType è¦†ç›–为 CUSTOM å¹¶æ³¨å…¥ä¸šåŠ¡è¯¦æƒ…ç»„ä»¶è·¯å¾„ï¼Œå®žçŽ°å®¡æ‰¹è¯¦æƒ…è‡ªåŠ¨å±•ç¤ºä¸šåŠ¡ä¿¡æ¯
     *
     * @param definitionResp        æµç¨‹å®šä¹‰ Response
     * @param processDefinitionKey  æµç¨‹å®šä¹‰ Key
     * @param businessKey           ä¸šåŠ¡çš„å”¯ä¸€æ ‡è¯†ï¼ˆæœªå‘èµ·æ—¶ä¸º null)
     */
    void fillBusinessForm(BpmProcessDefinitionRespVO definitionResp, String processDefinitionKey, String businessKey);
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/attach/BpmBusinessAttachServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,119 @@
package cn.iocoder.yudao.module.bpm.service.attach;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.attach.vo.attach.BpmBusinessAttachPageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.attach.vo.attach.BpmBusinessAttachSaveReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmBusinessAttachDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.attach.BpmBusinessAttachMapper;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.BUSINESS_ATTACH_NOT_EXISTS;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.BUSINESS_ATTACH_PROCESS_DEFINITION_KEY_DUPLICATE;
/**
 * BPM ä¸šåŠ¡é™„ä»¶é…ç½® Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class BpmBusinessAttachServiceImpl implements BpmBusinessAttachService {
    @Resource
    private BpmBusinessAttachMapper businessAttachMapper;
    @Override
    public Long createBusinessAttach(BpmBusinessAttachSaveReqVO createReqVO) {
        // æ ¡éªŒæµç¨‹å®šä¹‰ Key å”¯ä¸€
        validateProcessDefinitionKeyUnique(createReqVO);
        // æ’å…¥
        BpmBusinessAttachDO attach = BeanUtils.toBean(createReqVO, BpmBusinessAttachDO.class);
        businessAttachMapper.insert(attach);
        return attach.getId();
    }
    @Override
    public void updateBusinessAttach(BpmBusinessAttachSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateBusinessAttachExists(updateReqVO.getId());
        // æ ¡éªŒæµç¨‹å®šä¹‰ Key å”¯ä¸€
        validateProcessDefinitionKeyUnique(updateReqVO);
        // æ›´æ–°
        BpmBusinessAttachDO updateObj = BeanUtils.toBean(updateReqVO, BpmBusinessAttachDO.class);
        businessAttachMapper.updateById(updateObj);
    }
    private void validateProcessDefinitionKeyUnique(BpmBusinessAttachSaveReqVO reqVO) {
        BpmBusinessAttachDO attach = businessAttachMapper.selectByProcessDefinitionKey(reqVO.getProcessDefinitionKey());
        if (attach == null || ObjUtil.equal(attach.getId(), reqVO.getId())) {
            return;
        }
        throw exception(BUSINESS_ATTACH_PROCESS_DEFINITION_KEY_DUPLICATE, reqVO.getProcessDefinitionKey());
    }
    @Override
    public void deleteBusinessAttach(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateBusinessAttachExists(id);
        // åˆ é™¤
        businessAttachMapper.deleteById(id);
    }
    private BpmBusinessAttachDO validateBusinessAttachExists(Long id) {
        BpmBusinessAttachDO attach = businessAttachMapper.selectById(id);
        if (attach == null) {
            throw exception(BUSINESS_ATTACH_NOT_EXISTS);
        }
        return attach;
    }
    @Override
    public BpmBusinessAttachDO getBusinessAttach(Long id) {
        return businessAttachMapper.selectById(id);
    }
    @Override
    public PageResult<BpmBusinessAttachDO> getBusinessAttachPage(BpmBusinessAttachPageReqVO pageReqVO) {
        return businessAttachMapper.selectPage(pageReqVO);
    }
    @Override
    public BpmBusinessAttachDO getBusinessAttachByProcessDefinitionKey(String processDefinitionKey) {
        return businessAttachMapper.selectByProcessDefinitionKey(processDefinitionKey);
    }
    @Override
    public void fillBusinessForm(BpmProcessDefinitionRespVO definitionResp, String processDefinitionKey, String businessKey) {
        // æœªå‘èµ· / æ— ä¸šåŠ¡ä¸»é”®ï¼šä¸æ³¨å…¥
        if (definitionResp == null || StrUtil.isBlank(businessKey)) {
            return;
        }
        // éž NORMAL æµç¨‹è¡¨å•(已手动配置 CUSTOM):不覆盖
        if (!Objects.equals(definitionResp.getFormType(), BpmModelFormTypeEnum.NORMAL.getType())) {
            return;
        }
        // æœªå‘½ä¸­é…ç½® / é…ç½®è¢«ç¦ç”¨ï¼šä¿æŒåŽŸæ ·
        BpmBusinessAttachDO attach = getBusinessAttachByProcessDefinitionKey(processDefinitionKey);
        if (attach == null || !Objects.equals(attach.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
            return;
        }
        // è¦†ç›–为 CUSTOM,注入业务详情组件路径
        definitionResp.setFormType(BpmModelFormTypeEnum.CUSTOM.getType());
        definitionResp.setFormCustomViewPath(attach.getComponentPath());
        // æ¸…空普通表单字段:详情页 CUSTOM åˆ†æ”¯ä¸è¯»ï¼›æ‰“印页按 formFields.length == 0 åˆ¤æ–­æ¸²æŸ“业务组件
        definitionResp.setFormFields(null);
        definitionResp.setFormConf(null);
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
@@ -37,6 +37,7 @@
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
import cn.iocoder.yudao.module.bpm.service.attach.BpmBusinessAttachService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -123,6 +124,9 @@
    @Resource
    private BpmProcessIdRedisDAO processIdRedisDAO;
    @Resource
    private BpmBusinessAttachService businessAttachService;
    // ========== Query æŸ¥è¯¢ç›¸å…³æ–¹æ³• ==========
@@ -389,9 +393,14 @@
        Map<String, String> formFieldsPermission = getFormFieldsPermission(bpmnModel, reqVO.getActivityId(), taskId);
        // 3. æ‹¼æŽ¥æ•°æ®
        return BpmProcessInstanceConvert.INSTANCE.buildApprovalDetail(bpmnModel, processDefinition,
                processDefinitionInfo, processInstance,
        BpmApprovalDetailRespVO detailRespVO = BpmProcessInstanceConvert.INSTANCE.buildApprovalDetail(bpmnModel,
                processDefinition, processDefinitionInfo, processInstance,
                processInstanceStatus, approveNodes, todoTask, formFieldsPermission, userMap, deptMap);
        // 4. å¡«å……业务附件配置,实现审批详情自动展示外部业务信息
        businessAttachService.fillBusinessForm(detailRespVO.getProcessDefinition(), processDefinition.getKey(),
                processInstance != null ? processInstance.getBusinessKey() : null);
        return detailRespVO;
    }
    /**
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/CrmBusinessController.java
@@ -36,9 +36,7 @@
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
@@ -123,18 +121,39 @@
        List<CrmBusinessProductDO> businessProducts = businessService.getBusinessProductListByBusinessId(businessVO.getId());
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(businessProducts, CrmBusinessProductDO::getItemId));
        Map<Long, MdmUnitMeasureRespDTO> unitMap = mdmUnitMeasureApi.getUnitMeasureMap(
                convertSet(businessProducts, CrmBusinessProductDO::getItemUnitId));
        // æ”¶é›†æ‰€æœ‰éœ€è¦æŸ¥è¯¢çš„单位ID(主单位 + è¾…单位1 + è¾…单位2)
        Set<Long> unitIds = new HashSet<>();
        unitIds.addAll(convertSet(businessProducts, CrmBusinessProductDO::getItemUnitId));
        itemMap.values().forEach(item -> {
            if (item.getUnitMeasureId2() != null) {
                unitIds.add(item.getUnitMeasureId2());
            }
            if (item.getUnitMeasureId3() != null) {
                unitIds.add(item.getUnitMeasureId3());
            }
        });
        Map<Long, MdmUnitMeasureRespDTO> unitMap = mdmUnitMeasureApi.getUnitMeasureMap(unitIds);
        businessVO.setProducts(BeanUtils.toBean(businessProducts, CrmBusinessRespVO.Product.class, businessProductVO ->
                MapUtils.findAndThen(itemMap, businessProductVO.getItemId(),
                        item -> businessProductVO.setItemName(item.getName())
                                .setItemCode(item.getCode())
                                .setBarCode(item.getBarCode())
                                .setItemSpecification(item.getSpecification()))));
        // å¡«å……单位名称
        // å¡«å……单位名称(主单位 + è¾…单位1 + è¾…单位2)
        businessVO.getProducts().forEach(product -> {
            MapUtils.findAndThen(unitMap, product.getItemUnitId(),
                    unit -> product.setItemUnitName(unit.getName()));
            // å¡«å……辅单位名称
            MapUtils.findAndThen(itemMap, product.getItemId(), item -> {
                if (item.getUnitMeasureId2() != null) {
                    MapUtils.findAndThen(unitMap, item.getUnitMeasureId2(),
                            unit -> product.setItemUnitName2(unit.getName()));
                }
                if (item.getUnitMeasureId3() != null) {
                    MapUtils.findAndThen(unitMap, item.getUnitMeasureId3(),
                            unit -> product.setItemUnitName3(unit.getName()));
                }
            });
        });
        return businessVO;
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java
@@ -134,6 +134,10 @@
        private Long itemUnitId;
        @Schema(description = "计量单位名称", example = "个")
        private String itemUnitName;
        @Schema(description = "辅单位1名称")
        private String itemUnitName2;
        @Schema(description = "辅单位2名称")
        private String itemUnitName3;
        @Schema(description = "物料条码", example = "12345678")
        private String barCode;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordSaveReqVO.java
@@ -44,5 +44,25 @@
    private List<Long> contactIds;
    @Schema(description = "图片 URL åˆ—表")
    private List<String> picUrls;
    private Object picUrls; // æŽ¥å—字符串或数组
    /**
     * èŽ·å–å›¾ç‰‡ URL åˆ—表
     */
    public List<String> getPicUrlList() {
        if (picUrls == null) {
            return null;
        }
        if (picUrls instanceof List) {
            return (List<String>) picUrls;
        }
        if (picUrls instanceof String) {
            String str = (String) picUrls;
            if (str.isEmpty()) {
                return null;
            }
            return java.util.Arrays.asList(str.split(","));
        }
        return null;
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/CrmSaleQuotationController.java
@@ -161,9 +161,14 @@
        // 1. æž„建物料明细
        if (CollUtil.isNotEmpty(productList)) {
            Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(convertSet(productList, CrmSaleQuotationProductDO::getItemId));
            java.util.Set<Long> unitIds = new java.util.HashSet<>(convertSet(productList, CrmSaleQuotationProductDO::getItemUnitId));
            itemMap.values().forEach(item -> {
                if (item.getUnitMeasureId2() != null) {
                    unitIds.add(item.getUnitMeasureId2());
                }
            });
            Map<Long, MdmUnitMeasureRespDTO> unitMap = convertMap(
                    mdmUnitMeasureApi.getUnitMeasureList(
                            convertSet(productList, CrmSaleQuotationProductDO::getItemUnitId)).getCheckedData(),
                    mdmUnitMeasureApi.getUnitMeasureList(unitIds).getCheckedData(),
                    MdmUnitMeasureRespDTO::getId);
            respVO.setItems(BeanUtils.toBean(productList, CrmSaleQuotationRespVO.Item.class, item -> {
                MdmItemRespDTO mdmItem = itemMap.get(item.getItemId());
@@ -171,8 +176,13 @@
                    item.setItemCode(mdmItem.getCode());
                    item.setItemName(mdmItem.getName());
                    item.setItemBarCode(mdmItem.getBarCode());
                    item.setItemSpecification(mdmItem.getSpecification());
                    item.setItemUnitId2(mdmItem.getUnitMeasureId2());
                    item.setItemUnitId3(mdmItem.getUnitMeasureId3());
                }
                MapUtils.findAndThen(unitMap, item.getItemUnitId(), unit -> item.setItemUnitName(unit.getName()));
                MapUtils.findAndThen(unitMap, item.getItemUnitId2(), unit -> item.setItemUnitName2(unit.getName()));
                MapUtils.findAndThen(unitMap, item.getItemUnitId3(), unit -> item.setItemUnitName3(unit.getName()));
            }));
        }
        // 2. å¡«å……客户名称
@@ -206,6 +216,15 @@
        Map<Long, List<CrmSaleQuotationProductDO>> productMap = convertMultiMap(productList, CrmSaleQuotationProductDO::getQuotationId);
        // 1.2 ç‰©æ–™ä¿¡æ¯
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(convertSet(productList, CrmSaleQuotationProductDO::getItemId));
        java.util.Set<Long> unitIds = new java.util.HashSet<>(convertSet(productList, CrmSaleQuotationProductDO::getItemUnitId));
        itemMap.values().forEach(item -> {
            if (item.getUnitMeasureId2() != null) {
                unitIds.add(item.getUnitMeasureId2());
            }
        });
        Map<Long, MdmUnitMeasureRespDTO> unitMap = convertMap(
                mdmUnitMeasureApi.getUnitMeasureList(unitIds).getCheckedData(),
                MdmUnitMeasureRespDTO::getId);
        // 1.3 å®¢æˆ·ä¿¡æ¯
        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
                convertSet(pageResult.getList(), CrmSaleQuotationDO::getCustomerId));
@@ -222,8 +241,18 @@
        // 2. å¼€å§‹æ‹¼æŽ¥
        return BeanUtils.toBean(pageResult, CrmSaleQuotationRespVO.class, vo -> {
            vo.setItems(BeanUtils.toBean(productMap.get(vo.getId()), CrmSaleQuotationRespVO.Item.class,
                    item -> MapUtils.findAndThen(itemMap, item.getItemId(), mdmItem -> item.setItemCode(mdmItem.getCode())
                            .setItemName(mdmItem.getName()).setItemBarCode(mdmItem.getBarCode()))));
                    item -> {
                        MapUtils.findAndThen(itemMap, item.getItemId(), mdmItem -> item
                                .setItemCode(mdmItem.getCode())
                                .setItemName(mdmItem.getName())
                                .setItemBarCode(mdmItem.getBarCode())
                                .setItemSpecification(mdmItem.getSpecification())
                                .setItemUnitId2(mdmItem.getUnitMeasureId2())
                                .setItemUnitId3(mdmItem.getUnitMeasureId3()));
                        MapUtils.findAndThen(unitMap, item.getItemUnitId(), unit -> item.setItemUnitName(unit.getName()));
                        MapUtils.findAndThen(unitMap, item.getItemUnitId2(), unit -> item.setItemUnitName2(unit.getName()));
                        MapUtils.findAndThen(unitMap, item.getItemUnitId3(), unit -> item.setItemUnitName3(unit.getName()));
                    }));
            MapUtils.findAndThen(customerMap, vo.getCustomerId(), customer -> vo.setCustomerName(customer.getName()));
            MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user -> vo.setOwnerUserName(user.getNickname()));
            MapUtils.findAndThen(deptMap, vo.getOwnerUserId(), dept -> vo.setOwnerUserDeptName(dept.getName()));
@@ -258,4 +287,4 @@
        }
    }
}
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationRespVO.java
@@ -141,10 +141,20 @@
        private String itemName;
        @Schema(description = "物料条码", example = "BAR001")
        private String itemBarCode;
        @Schema(description = "规格型号", example = "M8*20")
        private String itemSpecification;
        @Schema(description = "计量单位编号", example = "1")
        private Long itemUnitId;
        @Schema(description = "计量单位名称", example = "个")
        private String itemUnitName;
        @Schema(description = "辅单位1编号", example = "2")
        private Long itemUnitId2;
        @Schema(description = "辅单位1名称", example = "ç®±")
        private String itemUnitName2;
        @Schema(description = "辅单位2编号", example = "3")
        private Long itemUnitId3;
        @Schema(description = "辅单位2名称", example = "托")
        private String itemUnitName3;
        @Schema(description = "物料原价", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
        private BigDecimal itemPrice;
@@ -169,4 +179,4 @@
    }
}
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/followup/CrmFollowUpRecordServiceImpl.java
@@ -67,8 +67,10 @@
    @Override
    @CrmPermission(bizTypeValue = "#createReqVO.bizType", bizId = "#createReqVO.bizId", level = CrmPermissionLevelEnum.WRITE)
    public Long createFollowUpRecord(CrmFollowUpRecordSaveReqVO createReqVO) {
        // 1. åˆ›å»ºæ›´è¿›è®°å½•
        // 1. åˆ›å»ºè·Ÿè¿›è®°å½•
        CrmFollowUpRecordDO record = BeanUtils.toBean(createReqVO, CrmFollowUpRecordDO.class);
        // æ‰‹åŠ¨å¤„ç† picUrls å­—段
        record.setPicUrls(createReqVO.getPicUrlList());
        crmFollowUpRecordMapper.insert(record);
        storageAttachmentApi.bindAttachments("file", "crm_follow_up_record",
                record.getId(), createReqVO.getBlobIds());
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseInvoiceController.java
@@ -15,7 +15,6 @@
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import cn.iocoder.yudao.module.erp.service.finance.ErpFinancePaymentService;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceService;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceServiceImpl;
@@ -169,24 +168,8 @@
        });
    }
    @PutMapping("/update-status")
    @Operation(summary = "更新来票的状态(审核/反审核)")
    @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:update-status')")
    public CommonResult<Boolean> updatePurchaseInvoiceStatus(@RequestParam("id") Long id,
                                                             @RequestParam("status") Integer status) {
        invoiceService.updatePurchaseInvoiceStatus(id, status);
        return success(true);
    }
    @GetMapping("/audit-count")
    @Operation(summary = "获得待审核来票数量")
    @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:query')")
    public CommonResult<Long> getAuditInvoiceCount() {
        return success(invoiceService.getAuditInvoiceCount());
    }
    @GetMapping("/simple-list")
    @Operation(summary = "获得来票精简列表", description = "只包含审核通过且已上传发票附件的来票,主要用于付款创建时选择关联来票")
    @Operation(summary = "获得来票精简列表", description = "只包含已上传发票附件的来票,主要用于付款创建时选择关联来票")
    @Parameter(name = "supplierId", description = "供应商编号", required = true)
    @Parameter(name = "purchaseOrderId", description = "采购订单编号(可选,用于按订单过滤)")
    @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:query')")
@@ -195,11 +178,9 @@
            @RequestParam(value = "purchaseOrderId", required = false) Long purchaseOrderId) {
        List<ErpPurchaseInvoiceDO> invoices;
        if (purchaseOrderId != null) {
            invoices = invoiceService.getPurchaseInvoiceListByOrderIdAndStatus(purchaseOrderId,
                    List.of(ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus()));
            invoices = invoiceService.getPurchaseInvoiceListByOrderId(purchaseOrderId);
        } else {
            invoices = invoiceService.getPurchaseInvoiceListBySupplierIdAndStatus(supplierId,
                    List.of(ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus()));
            invoices = invoiceService.getPurchaseInvoiceListBySupplierId(supplierId);
        }
        if (CollUtil.isEmpty(invoices)) {
            return success(Collections.emptyList());
@@ -225,7 +206,7 @@
                    .setId(invoice.getId()).setNo(invoice.getNo()).setInvoiceNo(invoice.getInvoiceNo())
                    .setInvoiceTitle(invoice.getInvoiceTitle()).setPrice(invoice.getPrice())
                    .setSupplierId(invoice.getSupplierId()).setPurchaseOrderId(invoice.getPurchaseOrderId())
                    .setAuditStatus(invoice.getAuditStatus()).setHasAttachment(true)
                    .setHasAttachment(true)
                    .setRemainingInvoicePrice(remainingPrice));
        }
        return success(result);
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoicePageReqVO.java
@@ -1,8 +1,6 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -22,9 +20,5 @@
    @Schema(description = "采购订单编号", example = "17386")
    private Long purchaseOrderId;
    @Schema(description = "审批状态", example = "20")
    @InEnum(ErpPurchaseInvoiceAuditStatusEnum.class)
    private Integer auditStatus;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceRespVO.java
@@ -1,9 +1,6 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderRespVO;
import cn.iocoder.yudao.module.erp.enums.DictTypeConstants;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
@@ -54,15 +51,6 @@
    @Schema(description = "来票日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
    @ExcelProperty("来票日期")
    private LocalDateTime invoiceTime;
    @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @ExcelProperty(value = "审批状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.AUDIT_STATUS)
    private Integer auditStatus;
    @Schema(description = "工作流编号", example = "1043")
    @ExcelProperty("工作流编号")
    private String processInstanceId;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInvoiceDO.java
@@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -66,17 +65,5 @@
     * å¤‡æ³¨
     */
    private String remark;
    /**
     * å·¥ä½œæµç¼–号
     *
     * å…³è” ProcessInstance çš„ id å±žæ€§
     */
    private String processInstanceId;
    /**
     * å®¡æ‰¹çŠ¶æ€
     *
     * æžšä¸¾ {@link ErpPurchaseInvoiceAuditStatusEnum}
     */
    private Integer auditStatus;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInvoiceMapper.java
@@ -5,11 +5,8 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoicePageReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
@@ -24,34 +21,22 @@
        return selectOne(ErpPurchaseInvoiceDO::getNo, no);
    }
    default int updateByIdAndStatus(Long id, Integer status, ErpPurchaseInvoiceDO updateObj) {
        return update(updateObj, new LambdaUpdateWrapper<ErpPurchaseInvoiceDO>()
                .eq(ErpPurchaseInvoiceDO::getId, id).eq(ErpPurchaseInvoiceDO::getAuditStatus, status));
    }
    default PageResult<ErpPurchaseInvoiceDO> selectPage(ErpPurchaseInvoicePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<ErpPurchaseInvoiceDO>()
                .likeIfPresent(ErpPurchaseInvoiceDO::getNo, reqVO.getNo())
                .eqIfPresent(ErpPurchaseInvoiceDO::getSupplierId, reqVO.getSupplierId())
                .eqIfPresent(ErpPurchaseInvoiceDO::getPurchaseOrderId, reqVO.getPurchaseOrderId())
                .eqIfPresent(ErpPurchaseInvoiceDO::getAuditStatus, reqVO.getAuditStatus())
                .orderByDesc(ErpPurchaseInvoiceDO::getId));
    }
    default List<ErpPurchaseInvoiceDO> selectListByOrderIdAndStatus(Long orderId, Collection<Integer> auditStatuses) {
    default List<ErpPurchaseInvoiceDO> selectListByOrderId(Long orderId) {
        return selectList(new LambdaQueryWrapperX<ErpPurchaseInvoiceDO>()
                .eq(ErpPurchaseInvoiceDO::getPurchaseOrderId, orderId)
                .in(ErpPurchaseInvoiceDO::getAuditStatus, auditStatuses));
                .eq(ErpPurchaseInvoiceDO::getPurchaseOrderId, orderId));
    }
    default List<ErpPurchaseInvoiceDO> selectListBySupplierIdAndStatus(Long supplierId, Collection<Integer> auditStatuses) {
    default List<ErpPurchaseInvoiceDO> selectListBySupplierId(Long supplierId) {
        return selectList(new LambdaQueryWrapperX<ErpPurchaseInvoiceDO>()
                .eq(ErpPurchaseInvoiceDO::getSupplierId, supplierId)
                .in(ErpPurchaseInvoiceDO::getAuditStatus, auditStatuses));
    }
    default Long selectCountByAudit() {
        return selectCount(ErpPurchaseInvoiceDO::getAuditStatus, ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus());
                .eq(ErpPurchaseInvoiceDO::getSupplierId, supplierId));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseInvoiceAuditStatusEnum.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
@@ -57,14 +57,10 @@
    // ========== ERP é‡‡è´­æ¥ç¥¨ï¼ˆ1-030-105-000) ==========
    ErrorCode PURCHASE_INVOICE_NOT_EXISTS = new ErrorCode(1_030_105_000, "来票不存在");
    ErrorCode PURCHASE_INVOICE_NO_EXISTS = new ErrorCode(1_030_105_001, "生成来票编号失败,请重新提交");
    ErrorCode PURCHASE_INVOICE_UPDATE_FAIL_EDITING_PROHIBITED = new ErrorCode(1_030_105_002, "来票({})不是未审核状态,无法修改");
    ErrorCode PURCHASE_INVOICE_DELETE_FAIL_IS_APPROVE = new ErrorCode(1_030_105_003, "删除来票失败,原因:来票审批已通过");
    ErrorCode PURCHASE_INVOICE_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_030_105_005, "更新来票审核状态失败,原因:来票不是审核中状态");
    ErrorCode PURCHASE_INVOICE_DELETE_FAIL_HAS_PAYMENT = new ErrorCode(1_030_105_004, "删除来票失败,原因:来票已存在有效付款单");
    ErrorCode PURCHASE_INVOICE_CREATE_FAIL_ORDER_NOT_APPROVE = new ErrorCode(1_030_105_006, "创建来票失败,原因:采购订单不是审核通过状态");
    ErrorCode PURCHASE_INVOICE_CREATE_FAIL_SUPPLIER_MISMATCH = new ErrorCode(1_030_105_007, "创建来票失败,原因:来票供应商与采购订单不一致");
    ErrorCode PURCHASE_INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT = new ErrorCode(1_030_105_008, "创建来票失败,原因:来票金额超出采购订单金额,目前可开票:{} å…ƒ");
    ErrorCode PURCHASE_INVOICE_APPROVE_FAIL = new ErrorCode(1_030_105_009, "审核失败,只有未审核的来票才能审核");
    ErrorCode PURCHASE_INVOICE_PROCESS_FAIL = new ErrorCode(1_030_105_010, "反审核失败,只有已审核的来票才能反审核");
    // ========== ERP é‡‡è´­é€€è´§ï¼ˆ1-030-103-000) ==========
    ErrorCode PURCHASE_RETURN_NOT_EXISTS = new ErrorCode(1_030_103_000, "采购退货单不存在");
@@ -193,7 +189,6 @@
    ErrorCode FINANCE_PAYMENT_NO_EXISTS = new ErrorCode(1_030_601_004, "生成付款单号失败,请重新提交");
    ErrorCode FINANCE_PAYMENT_UPDATE_FAIL_APPROVE = new ErrorCode(1_030_601_005, "付款单({})已审核,无法修改");
    ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_EXISTS = new ErrorCode(1_030_601_006, "创建付款单失败,原因:关联的来票不存在");
    ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_APPROVE = new ErrorCode(1_030_601_007, "创建付款单失败,原因:关联的来票不是审批通过状态");
    ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_ATTACHMENT = new ErrorCode(1_030_601_008, "创建付款单失败,原因:关联的来票未上传发票附件");
    ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_SUPPLIER_MISMATCH = new ErrorCode(1_030_601_009, "创建付款单失败,原因:关联的来票与所选供应商不匹配");
    ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_PRICE_EXCEEDS = new ErrorCode(1_030_601_010, "创建付款单失败,原因:付款金额超出该来票剩余可付金额,目前可付:{} å…ƒ");
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java
@@ -20,7 +20,6 @@
import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinancePaymentMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceService;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceServiceImpl;
import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
@@ -354,7 +353,7 @@
    }
    /**
     * æ ¡éªŒä»˜æ¬¾å•关联的来票:存在、审批通过、已上传发票附件、供应商与来票一致
     * æ ¡éªŒä»˜æ¬¾å•关联的来票:存在、已上传发票附件、供应商与来票一致
     *
     * @param invoiceId æ¥ç¥¨ç¼–号
     * @param supplierId ä¾›åº”商编号
@@ -365,16 +364,12 @@
        if (invoice == null) {
            throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_EXISTS);
        }
        // 2. æ ¡éªŒæ¥ç¥¨å®¡æ‰¹é€šè¿‡
        if (ObjectUtil.notEqual(invoice.getAuditStatus(), ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus())) {
            throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_APPROVE);
        }
        // 3. æ ¡éªŒå·²ä¸Šä¼ å‘票附件
        // 2. æ ¡éªŒå·²ä¸Šä¼ å‘票附件
        if (CollUtil.isEmpty(storageAttachmentApi.listAttachments(
                ErpPurchaseInvoiceServiceImpl.PURCHASE_INVOICE_RECORD_TYPE, invoiceId))) {
            throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_ATTACHMENT);
        }
        // 4. æ ¡éªŒä¾›åº”商与来票一致(防脏数据)
        // 3. æ ¡éªŒä¾›åº”商与来票一致(防脏数据)
        if (ObjectUtil.notEqual(invoice.getSupplierId(), supplierId)) {
            throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_SUPPLIER_MISMATCH);
        }
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceService.java
@@ -42,14 +42,6 @@
    void deletePurchaseInvoice(Long id);
    /**
     * æ›´æ–°æ¥ç¥¨çš„状态(审核/反审核)
     *
     * @param id ç¼–号
     * @param status çŠ¶æ€
     */
    void updatePurchaseInvoiceStatus(Long id, Integer status);
    /**
     * èŽ·å¾—æ¥ç¥¨
     *
     * @param id ç¼–号
@@ -84,28 +76,19 @@
    PageResult<ErpPurchaseInvoiceDO> getPurchaseInvoicePage(ErpPurchaseInvoicePageReqVO pageReqVO);
    /**
     * æ ¹æ®é‡‡è´­è®¢å•编号和审批状态获得来票列表
     * æ ¹æ®é‡‡è´­è®¢å•编号获得来票列表
     *
     * @param orderId é‡‡è´­è®¢å•编号
     * @param auditStatuses å®¡æ‰¹çŠ¶æ€é›†åˆ
     * @return æ¥ç¥¨åˆ—表
     */
    List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderIdAndStatus(Long orderId, Collection<Integer> auditStatuses);
    List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderId(Long orderId);
    /**
     * æ ¹æ®ä¾›åº”商编号和审批状态获得来票列表
     * æ ¹æ®ä¾›åº”商编号获得来票列表
     *
     * @param supplierId ä¾›åº”商编号
     * @param auditStatuses å®¡æ‰¹çŠ¶æ€é›†åˆ
     * @return æ¥ç¥¨åˆ—表
     */
    List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierIdAndStatus(Long supplierId, Collection<Integer> auditStatuses);
    /**
     * èŽ·å¾—å¾…å®¡æ ¸æ¥ç¥¨æ•°é‡
     *
     * @return æ•°é‡
     */
    Long getAuditInvoiceCount();
    List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierId(Long supplierId);
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java
@@ -9,9 +9,9 @@
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinancePaymentMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInvoiceMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -21,6 +21,7 @@
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue;
@@ -44,6 +45,9 @@
    private ErpPurchaseInvoiceMapper invoiceMapper;
    @Resource
    private ErpFinancePaymentMapper financePaymentMapper;
    @Resource
    private ErpNoRedisDAO noRedisDAO;
    @Resource
@@ -65,9 +69,8 @@
            throw exception(PURCHASE_INVOICE_NO_EXISTS);
        }
        // 2.1 æ’入来票(创建后即为未审核状态)
        ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class)
                .setNo(no).setAuditStatus(ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus());
        // 2.1 æ’入来票(创建后即为有效状态,无需审核)
        ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class).setNo(no);
        invoiceMapper.insert(invoice);
        // 2.2 ç»‘定发票附件
@@ -88,12 +91,9 @@
    }
    private void validatePurchaseInvoicePriceExceedsLimit(ErpPurchaseInvoiceSaveReqVO reqVO) {
        // 1. è®¡ç®—剩余可开票金额,不包括 reqVO è‡ªèº«ï¼ˆå®¡æ‰¹ä¸­ä¹Ÿå ç”¨é¢åº¦ï¼Œé˜²æ­¢ç´¯è®¡æ¥ç¥¨è¶…过采购订单金额)
        // 1. è®¡ç®—剩余可开票金额,不包括 reqVO è‡ªèº«ï¼ˆæ‰€æœ‰æœ‰æ•ˆæ¥ç¥¨éƒ½å ç”¨é¢åº¦ï¼Œé˜²æ­¢ç´¯è®¡æ¥ç¥¨è¶…过采购订单金额)
        ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.getPurchaseOrder(reqVO.getPurchaseOrderId());
        List<ErpPurchaseInvoiceDO> invoices = invoiceMapper.selectListByOrderIdAndStatus(reqVO.getPurchaseOrderId(),
                List.of(ErpPurchaseInvoiceAuditStatusEnum.DRAFT.getStatus(),
                        ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus(),
                        ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus()));
        List<ErpPurchaseInvoiceDO> invoices = invoiceMapper.selectListByOrderId(reqVO.getPurchaseOrderId());
        if (reqVO.getId() != null) {
            invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), reqVO.getId()));
        }
@@ -110,12 +110,8 @@
    public void updatePurchaseInvoice(ErpPurchaseInvoiceSaveReqVO updateReqVO) {
        Assert.notNull(updateReqVO.getId(), "来票编号不能为空");
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpPurchaseInvoiceDO oldInvoice = validatePurchaseInvoiceExists(updateReqVO.getId());
        // 1.2 åªæœ‰æœªå®¡æ ¸å¯ä»¥ç¼–辑
        if (ObjectUtil.notEqual(oldInvoice.getAuditStatus(), ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus())) {
            throw exception(PURCHASE_INVOICE_UPDATE_FAIL_EDITING_PROHIBITED, oldInvoice.getNo());
        }
        // 1.3 æ ¡éªŒå…³è”数据 + å¯å¼€ç¥¨é‡‘额
        validatePurchaseInvoiceExists(updateReqVO.getId());
        // 1.2 æ ¡éªŒå…³è”数据 + å¯å¼€ç¥¨é‡‘额
        validateRelationDataExists(updateReqVO);
        validatePurchaseInvoicePriceExceedsLimit(updateReqVO);
@@ -131,35 +127,17 @@
    @Transactional(rollbackFor = Exception.class)
    public void deletePurchaseInvoice(Long id) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpPurchaseInvoiceDO invoice = validatePurchaseInvoiceExists(id);
        // 1.2 å®¡æ‰¹é€šè¿‡æ—¶ï¼Œä¸å…è®¸åˆ é™¤
        if (ObjectUtil.equal(invoice.getAuditStatus(), ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus())) {
            throw exception(PURCHASE_INVOICE_DELETE_FAIL_IS_APPROVE);
        validatePurchaseInvoiceExists(id);
        // 1.2 å·²æœ‰æœ‰æ•ˆä»˜æ¬¾å•关联的来票,不允许删除(保持来票与付款的关联完整性)
        Map<Long, BigDecimal> paidMap = financePaymentMapper.selectPaymentPriceMapByInvoiceId(List.of(id));
        if (paidMap.getOrDefault(id, BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0) {
            throw exception(PURCHASE_INVOICE_DELETE_FAIL_HAS_PAYMENT);
        }
        // 2.1 åˆ é™¤å‘票附件
        storageAttachmentApi.deleteAttachmentsByRecord(PURCHASE_INVOICE_RECORD_TYPE, id);
        // 2.2 åˆ é™¤æ¥ç¥¨
        invoiceMapper.deleteById(id);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updatePurchaseInvoiceStatus(Long id, Integer status) {
        boolean approve = ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus().equals(status);
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpPurchaseInvoiceDO invoice = validatePurchaseInvoiceExists(id);
        // 1.2 æ ¡éªŒçŠ¶æ€
        if (invoice.getAuditStatus().equals(status)) {
            throw exception(approve ? PURCHASE_INVOICE_APPROVE_FAIL : PURCHASE_INVOICE_PROCESS_FAIL);
        }
        // 2. æ›´æ–°çŠ¶æ€
        int updateCount = invoiceMapper.updateByIdAndStatus(id, invoice.getAuditStatus(),
                new ErpPurchaseInvoiceDO().setAuditStatus(status));
        if (updateCount == 0) {
            throw exception(approve ? PURCHASE_INVOICE_APPROVE_FAIL : PURCHASE_INVOICE_PROCESS_FAIL);
        }
    }
    private ErpPurchaseInvoiceDO validatePurchaseInvoiceExists(Long id) {
@@ -189,18 +167,13 @@
    }
    @Override
    public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderIdAndStatus(Long orderId, Collection<Integer> auditStatuses) {
        return invoiceMapper.selectListByOrderIdAndStatus(orderId, auditStatuses);
    public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderId(Long orderId) {
        return invoiceMapper.selectListByOrderId(orderId);
    }
    @Override
    public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierIdAndStatus(Long supplierId, Collection<Integer> auditStatuses) {
        return invoiceMapper.selectListBySupplierIdAndStatus(supplierId, auditStatuses);
    }
    @Override
    public Long getAuditInvoiceCount() {
        return invoiceMapper.selectCountByAudit();
    public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierId(Long supplierId) {
        return invoiceMapper.selectListBySupplierId(supplierId);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -101,6 +101,7 @@
        // 2.1 æ’入订单
        ErpSaleOrderDO saleOrder = BeanUtils.toBean(createReqVO, ErpSaleOrderDO.class, in -> in
                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
                .setDepositPrice(ObjectUtil.defaultIfNull(createReqVO.getDepositPrice(), BigDecimal.ZERO))
                .setOutCount(BigDecimal.ZERO).setOutStatus(ErpSaleOrderOutStatusEnum.NOT_OUT.getStatus()));
        calculateTotalPrice(saleOrder, saleOrderItems);
        saleOrderMapper.insert(saleOrder);
@@ -136,6 +137,7 @@
        // 2.1 æ›´æ–°è®¢å•
        ErpSaleOrderDO updateObj = BeanUtils.toBean(updateReqVO, ErpSaleOrderDO.class);
        updateObj.setDepositPrice(ObjectUtil.defaultIfNull(updateReqVO.getDepositPrice(), BigDecimal.ZERO));
        calculateTotalPrice(updateObj, saleOrderItems);
        saleOrderMapper.updateById(updateObj);
        // 2.2 æ›´æ–°è®¢å•项
@@ -478,4 +480,4 @@
        }
    }
}
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java
@@ -143,6 +143,7 @@
        syncReqDTO.setMaxStock(reqVO.getMaxStock());
        syncReqDTO.setHighValue(reqVO.getIsHighValue());
        syncReqDTO.setBatchFlag(reqVO.getIsBatchManaged());
        syncReqDTO.setStatus(reqVO.getStatus());
        syncReqDTO.setRemark(reqVO.getRemark());
        // æ‰¹æ¬¡é…ç½®å­—段
        syncReqDTO.setProduceDateFlag(reqVO.getProduceDateFlag());
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/dto/MesMdItemSyncReqDTO.java
@@ -42,6 +42,9 @@
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "状态", example = "1")
    private Integer status;
    // ========== æ‰¹æ¬¡å±žæ€§é…ç½® ==========
    @Schema(description = "生产日期必填", example = "true")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/MesDvMeteringController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,145 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.metering;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
import cn.iocoder.yudao.module.mes.enums.dv.MesDvMeteringExpireStatusEnum;
import cn.iocoder.yudao.module.mes.service.dv.metering.MesDvMeteringService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - MES è®¡é‡å™¨å…·")
@RestController
@RequestMapping("/mes/dv/metering")
@Validated
public class MesDvMeteringController {
    @Resource
    private MesDvMeteringService meteringService;
    @Resource
    private DeptApi deptApi;
    @Resource
    private AdminUserApi adminUserApi;
    @PostMapping("/create")
    @Operation(summary = "创建计量器具")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering:create')")
    public CommonResult<Long> createMetering(@Valid @RequestBody MesDvMeteringSaveReqVO createReqVO) {
        return success(meteringService.createMetering(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新计量器具")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering:update')")
    public CommonResult<Boolean> updateMetering(@Valid @RequestBody MesDvMeteringSaveReqVO updateReqVO) {
        meteringService.updateMetering(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除计量器具")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:dv-metering:delete')")
    public CommonResult<Boolean> deleteMetering(@RequestParam("id") Long id) {
        meteringService.deleteMetering(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得计量器具")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering:query')")
    public CommonResult<MesDvMeteringRespVO> getMetering(@RequestParam("id") Long id) {
        MesDvMeteringDO metering = meteringService.getMetering(id);
        if (metering == null) {
            return success(null);
        }
        return success(buildMeteringRespVOList(Collections.singletonList(metering)).get(0));
    }
    @GetMapping("/page")
    @Operation(summary = "获得计量器具分页")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering:query')")
    public CommonResult<PageResult<MesDvMeteringRespVO>> getMeteringPage(@Valid MesDvMeteringPageReqVO pageReqVO) {
        PageResult<MesDvMeteringDO> pageResult = meteringService.getMeteringPage(pageReqVO);
        return success(new PageResult<>(buildMeteringRespVOList(pageResult.getList()), pageResult.getTotal()));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出计量器具 Excel")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportMeteringExcel(@Valid MesDvMeteringPageReqVO pageReqVO,
                                    HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<MesDvMeteringDO> list = meteringService.getMeteringPage(pageReqVO).getList();
        ExcelUtils.write(response, "计量器具.xls", "数据", MesDvMeteringRespVO.class,
                buildMeteringRespVOList(list));
    }
    @GetMapping("/simple-list")
    @Operation(summary = "获得计量器具精简列表", description = "主要用于前端的下拉选项")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering:query')")
    public CommonResult<List<MesDvMeteringRespVO>> getMeteringSimpleList() {
        List<MesDvMeteringDO> list = meteringService.getMeteringList();
        return success(buildMeteringRespVOList(list));
    }
    // ==================== æ‹¼æŽ¥ VO ====================
    @SuppressWarnings("DuplicatedCode")
    private List<MesDvMeteringRespVO> buildMeteringRespVOList(List<MesDvMeteringDO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 1. æ‰¹é‡èŽ·å–å…³è”æ•°æ®
        int remindDays = meteringService.getRemindDays();
        Map<Long, DeptRespDTO> deptMap = CollUtil.isNotEmpty(convertSet(list, MesDvMeteringDO::getDeptId))
                ? deptApi.getDeptMap(convertSet(list, MesDvMeteringDO::getDeptId)) : Collections.emptyMap();
        Map<Long, AdminUserRespDTO> userMap = CollUtil.isNotEmpty(convertSet(list, MesDvMeteringDO::getChargeUserId))
                ? adminUserApi.getUserMap(convertSet(list, MesDvMeteringDO::getChargeUserId)) : Collections.emptyMap();
        // 2. æ‹¼æŽ¥ VO
        return BeanUtils.toBean(list, MesDvMeteringRespVO.class, vo -> {
            // è®¡ç®—到期状态与剩余天数
            vo.setExpireStatus(MesDvMeteringExpireStatusEnum.compute(vo.getNextCheckDate(), remindDays));
            if (vo.getNextCheckDate() != null) {
                vo.setExpireDays(ChronoUnit.DAYS.between(LocalDate.now(), vo.getNextCheckDate()));
            }
            MapUtils.findAndThen(deptMap, vo.getDeptId(), dept -> vo.setDeptName(dept.getName()));
            MapUtils.findAndThen(userMap, vo.getChargeUserId(),
                    user -> vo.setChargeUserNickname(user.getNickname()));
        });
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDate;
@Schema(description = "管理后台 - MES è®¡é‡å™¨å…·åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesDvMeteringPageReqVO extends PageParam {
    @Schema(description = "计量器具编码", example = "MD-001")
    private String code;
    @Schema(description = "计量器具名称", example = "数显卡尺")
    private String name;
    @Schema(description = "品牌/生产厂家", example = "三丰")
    private String brand;
    @Schema(description = "器具类别", example = "1")
    private Integer category;
    @Schema(description = "使用部门编号", example = "100")
    private Long deptId;
    @Schema(description = "责任人编号", example = "200")
    private Long chargeUserId;
    @Schema(description = "状态", example = "1")
    private Integer status;
    @Schema(description = "到期状态(1 å·²è¿‡æœŸ/2 å³å°†åˆ°æœŸ/3 æ­£å¸¸ï¼‰", example = "2")
    private Integer expireStatus;
    @Schema(description = "下次检测日期开始(由后端根据到期状态自动填充)", hidden = true)
    private LocalDate nextCheckDateBegin;
    @Schema(description = "下次检测日期结束(由后端根据到期状态自动填充)", hidden = true)
    private LocalDate nextCheckDateEnd;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES è®¡é‡å™¨å…· Response VO")
@Data
@ExcelIgnoreUnannotated
public class MesDvMeteringRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @ExcelProperty("编号")
    private Long id;
    @Schema(description = "计量器具编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "MD-001")
    @ExcelProperty("计量器具编码")
    private String code;
    @Schema(description = "计量器具名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "数显卡尺")
    @ExcelProperty("计量器具名称")
    private String name;
    @Schema(description = "规格型号", example = "0-150mm")
    @ExcelProperty("规格型号")
    private String specification;
    @Schema(description = "品牌/生产厂家", example = "三丰")
    @ExcelProperty("品牌/生产厂家")
    private String brand;
    @Schema(description = "器具类别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @ExcelProperty(value = "器具类别", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.MES_DV_METERING_CATEGORY)
    private Integer category;
    @Schema(description = "精度", example = "0.01mm")
    @ExcelProperty("精度")
    private String precision;
    @Schema(description = "量程", example = "0-150mm")
    @ExcelProperty("量程")
    private String range;
    @Schema(description = "使用部门编号", example = "100")
    private Long deptId;
    @Schema(description = "使用部门名称", example = "质检部")
    @ExcelProperty("使用部门")
    private String deptName;
    @Schema(description = "责任人编号", example = "200")
    private Long chargeUserId;
    @Schema(description = "责任人名称", example = "张三")
    @ExcelProperty("责任人")
    private String chargeUserNickname;
    @Schema(description = "校准周期(月)", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
    @ExcelProperty("校准周期(月)")
    private Integer checkCycleCount;
    @Schema(description = "投入使用日期")
    @ExcelProperty("投入使用日期")
    private LocalDate useDate;
    @Schema(description = "上次检测日期")
    @ExcelProperty("上次检测日期")
    private LocalDate lastCheckDate;
    @Schema(description = "下次检测日期(到期日)")
    @ExcelProperty("下次检测日期")
    private LocalDate nextCheckDate;
    @Schema(description = "到期状态(1 å·²è¿‡æœŸ/2 å³å°†åˆ°æœŸ/3 æ­£å¸¸ï¼‰", example = "2")
    @ExcelProperty("到期状态")
    private Integer expireStatus;
    @Schema(description = "距离到期天数(正数未到,负数已过期)", example = "10")
    @ExcelProperty("剩余天数")
    private Long expireDays;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @ExcelProperty(value = "状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.MES_DV_METERING_STATUS)
    private Integer status;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
@Schema(description = "管理后台 - MES è®¡é‡å™¨å…·æ–°å¢ž/修改 Request VO")
@Data
public class MesDvMeteringSaveReqVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "计量器具编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "MD-001")
    @NotEmpty(message = "计量器具编码不能为空")
    private String code;
    @Schema(description = "计量器具名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "数显卡尺")
    @NotEmpty(message = "计量器具名称不能为空")
    private String name;
    @Schema(description = "规格型号", example = "0-150mm")
    private String specification;
    @Schema(description = "品牌/生产厂家", example = "三丰")
    private String brand;
    @Schema(description = "器具类别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "器具类别不能为空")
    private Integer category;
    @Schema(description = "精度", example = "0.01mm")
    private String precision;
    @Schema(description = "量程", example = "0-150mm")
    private String range;
    @Schema(description = "使用部门编号", example = "100")
    private Long deptId;
    @Schema(description = "责任人编号", example = "200")
    private Long chargeUserId;
    @Schema(description = "校准周期(月)", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
    @NotNull(message = "校准周期不能为空")
    private Integer checkCycleCount;
    @Schema(description = "投入使用日期")
    private LocalDate useDate;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "状态不能为空")
    private Integer status;
    @Schema(description = "备注", example = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/MesDvMeteringCheckController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.meteringcheck.MesDvMeteringCheckDO;
import cn.iocoder.yudao.module.mes.service.dv.metering.MesDvMeteringService;
import cn.iocoder.yudao.module.mes.service.dv.meteringcheck.MesDvMeteringCheckService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - MES è®¡é‡æ£€æµ‹è®°å½•")
@RestController
@RequestMapping("/mes/dv/metering-check")
@Validated
public class MesDvMeteringCheckController {
    @Resource
    private MesDvMeteringCheckService meteringCheckService;
    @Resource
    private MesDvMeteringService meteringService;
    @Resource
    private AdminUserApi adminUserApi;
    @PostMapping("/create")
    @Operation(summary = "创建计量检测记录")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:create')")
    public CommonResult<Long> createMeteringCheck(@Valid @RequestBody MesDvMeteringCheckSaveReqVO createReqVO) {
        return success(meteringCheckService.createMeteringCheck(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新计量检测记录")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:update')")
    public CommonResult<Boolean> updateMeteringCheck(@Valid @RequestBody MesDvMeteringCheckSaveReqVO updateReqVO) {
        meteringCheckService.updateMeteringCheck(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除计量检测记录")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:delete')")
    public CommonResult<Boolean> deleteMeteringCheck(@RequestParam("id") Long id) {
        meteringCheckService.deleteMeteringCheck(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得计量检测记录")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:query')")
    public CommonResult<MesDvMeteringCheckRespVO> getMeteringCheck(@RequestParam("id") Long id) {
        MesDvMeteringCheckDO check = meteringCheckService.getMeteringCheck(id);
        if (check == null) {
            return success(null);
        }
        return success(buildMeteringCheckRespVOList(Collections.singletonList(check)).get(0));
    }
    @GetMapping("/page")
    @Operation(summary = "获得计量检测记录分页")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:query')")
    public CommonResult<PageResult<MesDvMeteringCheckRespVO>> getMeteringCheckPage(
            @Valid MesDvMeteringCheckPageReqVO pageReqVO) {
        PageResult<MesDvMeteringCheckDO> pageResult = meteringCheckService.getMeteringCheckPage(pageReqVO);
        return success(new PageResult<>(buildMeteringCheckRespVOList(pageResult.getList()), pageResult.getTotal()));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出计量检测记录 Excel")
    @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportMeteringCheckExcel(@Valid MesDvMeteringCheckPageReqVO pageReqVO,
                                         HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<MesDvMeteringCheckDO> list = meteringCheckService.getMeteringCheckPage(pageReqVO).getList();
        ExcelUtils.write(response, "计量检测记录.xls", "数据", MesDvMeteringCheckRespVO.class,
                buildMeteringCheckRespVOList(list));
    }
    // ==================== æ‹¼æŽ¥ VO ====================
    @SuppressWarnings("DuplicatedCode")
    private List<MesDvMeteringCheckRespVO> buildMeteringCheckRespVOList(List<MesDvMeteringCheckDO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 1. æ‰¹é‡èŽ·å–å…³è”æ•°æ®
        Map<Long, MesDvMeteringDO> meteringMap = meteringService.getMeteringMap(
                convertSet(list, MesDvMeteringCheckDO::getMeteringId));
        Map<Long, AdminUserRespDTO> userMap = CollUtil.isNotEmpty(convertSet(list, MesDvMeteringCheckDO::getCheckUserId))
                ? adminUserApi.getUserMap(convertSet(list, MesDvMeteringCheckDO::getCheckUserId)) : Collections.emptyMap();
        // 2. æ‹¼æŽ¥ VO
        return BeanUtils.toBean(list, MesDvMeteringCheckRespVO.class, vo -> {
            MapUtils.findAndThen(meteringMap, vo.getMeteringId(),
                    metering -> vo.setMeteringCode(metering.getCode()).setMeteringName(metering.getName()));
            MapUtils.findAndThen(userMap, vo.getCheckUserId(),
                    user -> vo.setCheckUserNickname(user.getNickname()));
        });
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDate;
@Schema(description = "管理后台 - MES è®¡é‡æ£€æµ‹è®°å½•分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesDvMeteringCheckPageReqVO extends PageParam {
    @Schema(description = "计量器具编号", example = "100")
    private Long meteringId;
    @Schema(description = "检测编号", example = "CK-001")
    private String checkCode;
    @Schema(description = "检测结论", example = "1")
    private Integer checkResult;
    @Schema(description = "检测日期")
    private LocalDate[] checkDate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,74 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES è®¡é‡æ£€æµ‹è®°å½• Response VO")
@Data
@ExcelIgnoreUnannotated
public class MesDvMeteringCheckRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @ExcelProperty("编号")
    private Long id;
    @Schema(description = "计量器具编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    private Long meteringId;
    @Schema(description = "计量器具编码", example = "MD-001")
    @ExcelProperty("计量器具编码")
    private String meteringCode;
    @Schema(description = "计量器具名称", example = "数显卡尺")
    @ExcelProperty("计量器具名称")
    private String meteringName;
    @Schema(description = "检测编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "CK-001")
    @ExcelProperty("检测编号")
    private String checkCode;
    @Schema(description = "检测日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("检测日期")
    private LocalDate checkDate;
    @Schema(description = "检测机构", example = "市计量院")
    @ExcelProperty("检测机构")
    private String checkOrg;
    @Schema(description = "检测结论", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @ExcelProperty(value = "检测结论", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.MES_DV_METERING_CHECK_RESULT)
    private Integer checkResult;
    @Schema(description = "证书编号", example = "CN-2026-001")
    @ExcelProperty("证书编号")
    private String certNo;
    @Schema(description = "检测人编号", example = "200")
    private Long checkUserId;
    @Schema(description = "检测人名称", example = "李四")
    @ExcelProperty("检测人")
    private String checkUserNickname;
    @Schema(description = "下次检测日期")
    @ExcelProperty("下次检测日期")
    private LocalDate nextCheckDate;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
@Schema(description = "管理后台 - MES è®¡é‡æ£€æµ‹è®°å½•新增/修改 Request VO")
@Data
public class MesDvMeteringCheckSaveReqVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "计量器具编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    @NotNull(message = "计量器具不能为空")
    private Long meteringId;
    @Schema(description = "检测编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "CK-001")
    @NotEmpty(message = "检测编号不能为空")
    private String checkCode;
    @Schema(description = "检测日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "检测日期不能为空")
    private LocalDate checkDate;
    @Schema(description = "检测机构", example = "市计量院")
    private String checkOrg;
    @Schema(description = "检测结论", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "检测结论不能为空")
    private Integer checkResult;
    @Schema(description = "证书编号", example = "CN-2026-001")
    private String certNo;
    @Schema(description = "检测人编号", example = "200")
    private Long checkUserId;
    @Schema(description = "下次检测日期(为空则按 æ£€æµ‹æ—¥æœŸ + æ ¡å‡†å‘¨æœŸ è‡ªåŠ¨è®¡ç®—ï¼‰")
    private LocalDate nextCheckDate;
    @Schema(description = "备注", example = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/MesMdItemController.java
@@ -19,7 +19,6 @@
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
@@ -62,19 +61,6 @@
    @PreAuthorize("@ss.hasPermission('mes:md-item:update')")
    public CommonResult<Boolean> updateItem(@Valid @RequestBody MesMdItemSaveReqVO updateReqVO) {
        itemService.updateItem(updateReqVO);
        return success(true);
    }
    @PutMapping("/update-status")
    @Operation(summary = "更新物料产品状态")
    @Parameters({
            @Parameter(name = "id", description = "编号", required = true),
            @Parameter(name = "status", description = "状态", required = true)
    })
    @PreAuthorize("@ss.hasPermission('mes:md-item:update')")
    public CommonResult<Boolean> updateItemStatus(@RequestParam("id") Long id,
                                                  @RequestParam("status") Integer status) {
        itemService.updateItemStatus(id, status);
        return success(true);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/md/item/vo/MesMdItemSaveReqVO.java
@@ -51,6 +51,9 @@
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "状态", example = "1")
    private Integer status;
    @Schema(description = "是否同步到 MDM", example = "true")
    private Boolean syncMdm;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/project/MesPdProjectController.java
@@ -124,15 +124,24 @@
        return success(true);
    }
    @PutMapping("/audit")
    @Operation(summary = "审核设计项目(待审核→待归档)")
    @PutMapping("/submit-approval")
    @Operation(summary = "提交设计项目审批(待审核→审批中)")
    @Parameter(name = "id", description = "编号", required = true)
    @Parameter(name = "processDefinitionKey", description = "流程定义 Key", required = true)
    @PreAuthorize("@ss.hasPermission('mes:pd-project:update')")
    public CommonResult<Boolean> auditProject(@RequestParam("id") Long id) {
        projectService.auditProject(id);
    public CommonResult<Boolean> submitProjectApproval(@RequestParam("id") Long id,
                                                       @RequestParam("processDefinitionKey") String processDefinitionKey) {
        projectService.submitProjectApproval(id, processDefinitionKey);
        return success(true);
    }
    @GetMapping("/approve-process-list")
    @Operation(summary = "获得设计项目审批流程列表")
    @PreAuthorize("@ss.hasPermission('mes:pd-project:update')")
    public CommonResult<List<Map<String, Object>>> getProjectApproveProcessDefinitionList() {
        return success(projectService.getProjectApproveProcessDefinitionList());
    }
    @PutMapping("/archive")
    @Operation(summary = "归档设计项目(待归档→已归档),同时自动创建归档台账")
    @Parameter(name = "id", description = "编号", required = true)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/project/vo/MesPdProjectRespVO.java
@@ -66,6 +66,9 @@
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "BPM æµç¨‹å®žä¾‹ç¼–号")
    private String processInstanceId;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/MesWmBatchController.java
@@ -6,7 +6,10 @@
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
@@ -15,6 +18,7 @@
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.tm.tool.MesTmToolDO;
@@ -23,6 +27,7 @@
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.tm.tool.MesTmToolService;
@@ -71,6 +76,8 @@
    private MesMdWorkstationService workstationService;
    @Resource
    private MesTmToolService toolService;
    @Resource
    private MesProProcessService processService;
    @GetMapping("/get")
    @Operation(summary = "获得批次详情")
@@ -105,6 +112,42 @@
    public CommonResult<List<MesWmBatchRespVO>> getBackwardList(@RequestParam("code") @Valid String code) {
        List<MesWmBatchDO> list = batchService.getBackwardBatchList(code);
        return success(buildBatchRespVOList(list));
    }
    @GetMapping("/purchase-sources")
    @Operation(summary = "批次采购来源(采购入库单 + IQC)")
    @Parameter(name = "code", description = "批次编码", required = true, example = "BATCH20250314001")
    @PreAuthorize("@ss.hasPermission('mes:wm-batch:query')")
    public CommonResult<List<MesWmBatchPurchaseSourceRespVO>> getPurchaseSourceList(@RequestParam("code") @Valid String code) {
        List<MesWmBatchPurchaseSourceRespVO> list = batchService.getPurchaseSourceList(code);
        return success(buildPurchaseSourceRespVOList(list));
    }
    @GetMapping("/sales-targets")
    @Operation(summary = "批次销售去向(销售出库单 + OQC)")
    @Parameter(name = "code", description = "批次编码", required = true, example = "BATCH20250314001")
    @PreAuthorize("@ss.hasPermission('mes:wm-batch:query')")
    public CommonResult<List<MesWmBatchSalesTargetRespVO>> getSalesTargetList(@RequestParam("code") @Valid String code) {
        List<MesWmBatchSalesTargetRespVO> list = batchService.getSalesTargetList(code);
        return success(buildSalesTargetRespVOList(list));
    }
    @GetMapping("/process-trace")
    @Operation(summary = "批次工序流转(产出/投入事件)")
    @Parameter(name = "code", description = "批次编码", required = true, example = "BATCH20250314001")
    @PreAuthorize("@ss.hasPermission('mes:wm-batch:query')")
    public CommonResult<List<MesWmBatchProcessTraceRespVO>> getProcessTraceList(@RequestParam("code") @Valid String code) {
        List<MesWmBatchProcessTraceRespVO> list = batchService.getProcessTraceList(code);
        return success(buildProcessTraceRespVOList(list));
    }
    @GetMapping("/get-by-sn")
    @Operation(summary = "根据 SN ç åæŸ¥æ‰¹æ¬¡")
    @Parameter(name = "sn", description = "SN åºåˆ—号", required = true, example = "SN20250314001")
    @PreAuthorize("@ss.hasPermission('mes:wm-batch:query')")
    public CommonResult<MesWmBatchRespVO> getBatchBySn(@RequestParam("sn") @Valid String sn) {
        MesWmBatchDO batch = batchService.getBatchBySn(sn);
        return success(buildBatchRespVO(batch));
    }
    // ==================== æ‹¼æŽ¥ VO ====================
@@ -162,4 +205,58 @@
        return buildBatchRespVOList(Collections.singletonList(batch)).get(0);
    }
    private List<MesWmBatchPurchaseSourceRespVO> buildPurchaseSourceRespVOList(List<MesWmBatchPurchaseSourceRespVO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesWmBatchPurchaseSourceRespVO::getItemId));
        Map<Long, SrmSupplierRespDTO> vendorMap = srmSupplierApi.getSupplierList(
                convertSet(list, MesWmBatchPurchaseSourceRespVO::getVendorId)).getCheckedData().stream()
                .collect(Collectors.toMap(SrmSupplierRespDTO::getId, s -> s));
        list.forEach(vo -> {
            MapUtils.findAndThen(itemMap, vo.getItemId(), item -> {
                vo.setItemCode(item.getCode()).setItemName(item.getName());
            });
            MapUtils.findAndThen(vendorMap, vo.getVendorId(),
                    vendor -> vo.setVendorCode(vendor.getCode()).setVendorName(vendor.getName()));
        });
        return list;
    }
    private List<MesWmBatchSalesTargetRespVO> buildSalesTargetRespVOList(List<MesWmBatchSalesTargetRespVO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(
                convertSet(list, MesWmBatchSalesTargetRespVO::getItemId));
        Map<Long, CrmCustomerRespDTO> clientMap = customerApi.getCustomerMap(
                convertSet(list, MesWmBatchSalesTargetRespVO::getClientId));
        list.forEach(vo -> {
            MapUtils.findAndThen(itemMap, vo.getItemId(), item -> {
                vo.setItemCode(item.getCode()).setItemName(item.getName());
            });
            MapUtils.findAndThen(clientMap, vo.getClientId(),
                    client -> vo.setClientName(client.getName()));
        });
        return list;
    }
    private List<MesWmBatchProcessTraceRespVO> buildProcessTraceRespVOList(List<MesWmBatchProcessTraceRespVO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        Map<Long, MesProWorkOrderDO> workOrderMap = workOrderService.getWorkOrderMap(
                convertSet(list, MesWmBatchProcessTraceRespVO::getWorkOrderId));
        Map<Long, MesProProcessDO> processMap = processService.getProcessMap(
                convertSet(list, MesWmBatchProcessTraceRespVO::getProcessId));
        list.forEach(vo -> {
            MapUtils.findAndThen(workOrderMap, vo.getWorkOrderId(),
                    workOrder -> vo.setWorkOrderCode(workOrder.getCode()));
            MapUtils.findAndThen(processMap, vo.getProcessId(),
                    process -> vo.setProcessCode(process.getCode()).setProcessName(process.getName()));
        });
        return list;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchProcessTraceRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ‰¹æ¬¡å·¥åºæµè½¬ Response VO")
@Data
public class MesWmBatchProcessTraceRespVO {
    @Schema(description = "事件类型:IN-工序投入 / OUT-工序产出", example = "OUT")
    private String eventType;
    @Schema(description = "生产工单 ID", example = "1")
    private Long workOrderId;
    @Schema(description = "生产工单编号", example = "WO20250801001")
    private String workOrderCode;
    @Schema(description = "工序 ID", example = "1")
    private Long processId;
    @Schema(description = "工序编码", example = "PROC001")
    private String processCode;
    @Schema(description = "工序名称", example = "CNC åŠ å·¥")
    private String processName;
    @Schema(description = "批次号", example = "BATCH20250801001")
    private String batchCode;
    @Schema(description = "数量", example = "100")
    private BigDecimal quantity;
    @Schema(description = "质量状态(字典 mes_wm_quality_status),仅产出侧有值", example = "1")
    private Integer qualityStatus;
    @Schema(description = "事件时间")
    private LocalDateTime eventTime;
    @Schema(description = "工序序号(排序用)", example = "1")
    private Integer processSort;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchPurchaseSourceRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ‰¹æ¬¡é‡‡è´­æ¥æº Response VO")
@Data
public class MesWmBatchPurchaseSourceRespVO {
    @Schema(description = "采购入库单号", example = "IR20250731001")
    private String receiptCode;
    @Schema(description = "入库日期")
    private LocalDateTime receiptDate;
    @Schema(description = "采购订单编号", example = "PO20250731001")
    private String purchaseOrderCode;
    @Schema(description = "供应商 ID", example = "1")
    private Long vendorId;
    @Schema(description = "供应商编码", example = "V001")
    private String vendorCode;
    @Schema(description = "供应商名称", example = "供应商A")
    private String vendorName;
    @Schema(description = "来料检验单号(IQC)", example = "IQC20250731001")
    private String iqcCode;
    @Schema(description = "IQC æ£€éªŒç»“论(字典 mes_qc_check_result)", example = "1")
    private Integer checkResult;
    @Schema(description = "物料 ID", example = "1")
    private Long itemId;
    @Schema(description = "物料编码", example = "ITEM001")
    private String itemCode;
    @Schema(description = "物料名称", example = "产品A")
    private String itemName;
    @Schema(description = "批次号", example = "BATCH20250731001")
    private String batchCode;
    @Schema(description = "入库数量", example = "100")
    private BigDecimal receivedQuantity;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchSalesTargetRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ‰¹æ¬¡é”€å”®å޻向 Response VO")
@Data
public class MesWmBatchSalesTargetRespVO {
    @Schema(description = "销售出库单号", example = "SO20250801001")
    private String salesCode;
    @Schema(description = "出库日期")
    private LocalDateTime salesDate;
    @Schema(description = "销售订单编号", example = "SO20250801001")
    private String salesOrderCode;
    @Schema(description = "客户 ID", example = "1")
    private Long clientId;
    @Schema(description = "客户编码", example = "C001")
    private String clientCode;
    @Schema(description = "客户名称", example = "客户A")
    private String clientName;
    @Schema(description = "出厂检验单号(OQC)", example = "OQC20250801001")
    private String oqcCode;
    @Schema(description = "OQC æ£€éªŒç»“论(字典 mes_qc_check_result)", example = "1")
    private Integer checkResult;
    @Schema(description = "物料 ID", example = "1")
    private Long itemId;
    @Schema(description = "物料编码", example = "ITEM001")
    private String itemCode;
    @Schema(description = "物料名称", example = "产品A")
    private String itemName;
    @Schema(description = "批次号", example = "BATCH20250801001")
    private String batchCode;
    @Schema(description = "出库数量", example = "100")
    private BigDecimal quantity;
    @Schema(description = "质量状态(字典 mes_wm_quality_status)", example = "1")
    private Integer qualityStatus;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/metering/MesDvMeteringDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDate;
/**
 * MES è®¡é‡å™¨å…· DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_dv_metering")
@KeySequence("mes_dv_metering_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesDvMeteringDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * è®¡é‡å™¨å…·ç¼–码
     */
    private String code;
    /**
     * è®¡é‡å™¨å…·åç§°
     */
    private String name;
    /**
     * è§„格型号
     */
    private String specification;
    /**
     * å“ç‰Œ/生产厂家
     */
    private String brand;
    /**
     * å™¨å…·ç±»åˆ«
     *
     * å­—å…¸ {@link DictTypeConstants#MES_DV_METERING_CATEGORY}
     */
    private Integer category;
    /**
     * ç²¾åº¦
     */
    @TableField("`precision`")
    private String precision;
    /**
     * é‡ç¨‹
     */
    @TableField("`range`")
    private String range;
    /**
     * ä½¿ç”¨éƒ¨é—¨ç¼–号
     */
    private Long deptId;
    /**
     * è´£ä»»äººç¼–号
     */
    private Long chargeUserId;
    /**
     * æ ¡å‡†å‘¨æœŸï¼ˆæœˆï¼‰
     */
    private Integer checkCycleCount;
    /**
     * æŠ•入使用日期
     */
    private LocalDate useDate;
    /**
     * ä¸Šæ¬¡æ£€æµ‹æ—¥æœŸ
     */
    private LocalDate lastCheckDate;
    /**
     * ä¸‹æ¬¡æ£€æµ‹æ—¥æœŸï¼ˆåˆ°æœŸæ—¥ï¼‰
     */
    private LocalDate nextCheckDate;
    /**
     * çŠ¶æ€
     *
     * å­—å…¸ {@link DictTypeConstants#MES_DV_METERING_STATUS}
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/meteringcheck/MesDvMeteringCheckDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.dv.meteringcheck;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDate;
/**
 * MES è®¡é‡æ£€æµ‹è®°å½• DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_dv_metering_check")
@KeySequence("mes_dv_metering_check_seq") // ç”¨äºŽ Oracle、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 MySQL ç­‰æ•°æ®åº“,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesDvMeteringCheckDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * è®¡é‡å™¨å…·ç¼–号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO#getId()}
     */
    private Long meteringId;
    /**
     * æ£€æµ‹ç¼–号
     */
    private String checkCode;
    /**
     * æ£€æµ‹æ—¥æœŸ
     */
    private LocalDate checkDate;
    /**
     * æ£€æµ‹æœºæž„
     */
    private String checkOrg;
    /**
     * æ£€æµ‹ç»“论
     *
     * å­—å…¸ {@link DictTypeConstants#MES_DV_METERING_CHECK_RESULT}
     */
    private Integer checkResult;
    /**
     * è¯ä¹¦ç¼–号
     */
    private String certNo;
    /**
     * æ£€æµ‹äººç¼–号
     */
    private Long checkUserId;
    /**
     * ä¸‹æ¬¡æ£€æµ‹æ—¥æœŸï¼ˆè‡ªåŠ¨è®¡ç®—æˆ–æ‰‹åŠ¨æŒ‡å®šï¼‰
     */
    private LocalDate nextCheckDate;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pd/project/MesPdProjectDO.java
@@ -58,6 +58,14 @@
     */
    private Integer status;
    /**
     * BPM æµç¨‹å®žä¾‹ç¼–号
     */
    private String processInstanceId;
    /**
     * BPM æµç¨‹å®šä¹‰ Key
     */
    private String processDefinitionKey;
    /**
     * ä¸»è®¾è®¡å¸ˆ
     *
     * å…³è” AdminUserDO#getId()
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.dal.mysql.dv.metering;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDate;
import java.util.List;
/**
 * MES è®¡é‡å™¨å…· Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesDvMeteringMapper extends BaseMapperX<MesDvMeteringDO> {
    default PageResult<MesDvMeteringDO> selectPage(MesDvMeteringPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesDvMeteringDO>()
                .likeIfPresent(MesDvMeteringDO::getCode, reqVO.getCode())
                .likeIfPresent(MesDvMeteringDO::getName, reqVO.getName())
                .likeIfPresent(MesDvMeteringDO::getBrand, reqVO.getBrand())
                .eqIfPresent(MesDvMeteringDO::getCategory, reqVO.getCategory())
                .eqIfPresent(MesDvMeteringDO::getDeptId, reqVO.getDeptId())
                .eqIfPresent(MesDvMeteringDO::getChargeUserId, reqVO.getChargeUserId())
                .eqIfPresent(MesDvMeteringDO::getStatus, reqVO.getStatus())
                .geIfPresent(MesDvMeteringDO::getNextCheckDate, reqVO.getNextCheckDateBegin())
                .leIfPresent(MesDvMeteringDO::getNextCheckDate, reqVO.getNextCheckDateEnd())
                .orderByDesc(MesDvMeteringDO::getId));
    }
    default MesDvMeteringDO selectByCode(String code) {
        return selectOne(MesDvMeteringDO::getCode, code);
    }
    default List<MesDvMeteringDO> selectListByNextCheckDateBetween(LocalDate begin, LocalDate end) {
        return selectList(new LambdaQueryWrapperX<MesDvMeteringDO>()
                .ge(MesDvMeteringDO::getNextCheckDate, begin)
                .le(MesDvMeteringDO::getNextCheckDate, end));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/meteringcheck/MesDvMeteringCheckMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.mes.dal.mysql.dv.meteringcheck;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.meteringcheck.MesDvMeteringCheckDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MES è®¡é‡æ£€æµ‹è®°å½• Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesDvMeteringCheckMapper extends BaseMapperX<MesDvMeteringCheckDO> {
    default PageResult<MesDvMeteringCheckDO> selectPage(MesDvMeteringCheckPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesDvMeteringCheckDO>()
                .eqIfPresent(MesDvMeteringCheckDO::getMeteringId, reqVO.getMeteringId())
                .likeIfPresent(MesDvMeteringCheckDO::getCheckCode, reqVO.getCheckCode())
                .eqIfPresent(MesDvMeteringCheckDO::getCheckResult, reqVO.getCheckResult())
                .betweenIfPresent(MesDvMeteringCheckDO::getCheckDate, reqVO.getCheckDate())
                .orderByDesc(MesDvMeteringCheckDO::getId));
    }
    default MesDvMeteringCheckDO selectByCheckCode(String checkCode) {
        return selectOne(MesDvMeteringCheckDO::getCheckCode, checkCode);
    }
    default MesDvMeteringCheckDO selectLatestByMeteringId(Long meteringId) {
        return selectOne(new LambdaQueryWrapperX<MesDvMeteringCheckDO>()
                .eq(MesDvMeteringCheckDO::getMeteringId, meteringId)
                .orderByDesc(MesDvMeteringCheckDO::getCheckDate)
                .orderByDesc(MesDvMeteringCheckDO::getId)
                .last("LIMIT 1"));
    }
    default Long selectCountByMeteringId(Long meteringId) {
        return selectCount(MesDvMeteringCheckDO::getMeteringId, meteringId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java
@@ -7,6 +7,9 @@
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper;
@@ -185,4 +188,28 @@
     */
    List<MesWmBatchDO> selectListByBackward(String batchCode);
    /**
     * æŸ¥è¯¢æ‰¹æ¬¡é‡‡è´­æ¥æºï¼ˆé‡‡è´­å…¥åº“单 + IQC)
     *
     * @param batchCode æ‰¹æ¬¡å·
     * @return é‡‡è´­æ¥æºåˆ—表
     */
    List<MesWmBatchPurchaseSourceRespVO> selectPurchaseSourceList(String batchCode);
    /**
     * æŸ¥è¯¢æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼ˆé”€å”®å‡ºåº“å• + OQC)
     *
     * @param batchCode æ‰¹æ¬¡å·
     * @return é”€å”®åŽ»å‘åˆ—è¡¨
     */
    List<MesWmBatchSalesTargetRespVO> selectSalesTargetList(String batchCode);
    /**
     * æŸ¥è¯¢æ‰¹æ¬¡å·¥åºæµè½¬ï¼ˆäº§å‡º/投入事件)
     *
     * @param batchCode æ‰¹æ¬¡å·
     * @return å·¥åºæµè½¬åˆ—表
     */
    List<MesWmBatchProcessTraceRespVO> selectProcessTraceList(String batchCode);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java
@@ -15,4 +15,20 @@
     */
    String AUTO_CODE = "mes:md:auto_code:";
    /**
     * è®¡é‡å™¨å…·åˆ°æœŸæé†’去重
     *
     * KEY æ ¼å¼ï¼šmes:dv:metering:remind:{meteringId}:{nextCheckDate}
     * VALUE æ•°æ®æ ¼å¼ï¼š1
     */
    String METERING_EXPIRE_REMIND = "mes:dv:metering:remind:";
    /**
     * è®¡é‡å™¨å…·åˆ°æœŸæé†’扫描门闩
     *
     * KEY æ ¼å¼ï¼šmes:dv:metering:remind:scan:{today}
     * VALUE æ•°æ®æ ¼å¼ï¼š1
     */
    String METERING_EXPIRE_REMIND_SCAN = "mes:dv:metering:remind:scan:";
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/dv/metering/MesDvMeteringRemindRedisDAO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.mes.dal.redis.dv.metering;
import cn.iocoder.yudao.module.mes.dal.redis.RedisKeyConstants;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.time.LocalDate;
/**
 * MES è®¡é‡å™¨å…·åˆ°æœŸæé†’ Redis DAO
 *
 * @author è¶…级管理员
 */
@Repository
public class MesDvMeteringRemindRedisDAO {
    /**
     * æé†’去重 KEY çš„过期时间(60 å¤©ï¼‰ï¼ŒåŒä¸€å™¨å…·åŒä¸€åˆ°æœŸæ—¥ 60 å¤©å†…只提醒一次
     */
    private static final Duration REMIND_TIMEOUT = Duration.ofDays(60);
    /**
     * æ‰«æé—¨é—©çš„过期时间(30 åˆ†é’Ÿï¼‰ï¼Œé¿å…å¤šå®žä¾‹/多次并发重复扫描
     */
    private static final Duration SCAN_LOCK_TIMEOUT = Duration.ofMinutes(30);
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    /**
     * å°è¯•标记已提醒(同一器具同一到期日 60 å¤©å†…只提醒一次)
     *
     * @param meteringId è®¡é‡å™¨å…·ç¼–号
     * @param nextCheckDate åˆ°æœŸæ—¥
     * @return true è¡¨ç¤ºé¦–次标记成功,需要发送提醒
     */
    public boolean tryMarkReminded(Long meteringId, LocalDate nextCheckDate) {
        String key = RedisKeyConstants.METERING_EXPIRE_REMIND + meteringId + ":" + nextCheckDate;
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "1", REMIND_TIMEOUT));
    }
    /**
     * å°è¯•获取当天的扫描门闩,避免多实例并发重复扫描
     *
     * @param today å½“天日期
     * @return true è¡¨ç¤ºèŽ·å–æˆåŠŸï¼ˆå¯ä»¥æ‰§è¡Œæ‰«æï¼‰
     */
    public boolean tryAcquireScanLock(LocalDate today) {
        String key = RedisKeyConstants.METERING_EXPIRE_REMIND_SCAN + today;
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "1", SCAN_LOCK_TIMEOUT));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java
@@ -93,6 +93,9 @@
    String MES_DV_CHECK_RESULT = "mes_dv_check_result"; // MES ç‚¹æ£€ç»“æžœ
    String MES_DV_REPAIR_STATUS = "mes_dv_repair_status"; // MES ç»´ä¿®å·¥å•状态
    String MES_DV_REPAIR_RESULT = "mes_dv_repair_result"; // MES ç»´ä¿®ç»“æžœ
    String MES_DV_METERING_CATEGORY = "mes_dv_metering_category"; // MES è®¡é‡å™¨å…·ç±»åˆ«
    String MES_DV_METERING_STATUS = "mes_dv_metering_status"; // MES è®¡é‡å™¨å…·çŠ¶æ€
    String MES_DV_METERING_CHECK_RESULT = "mes_dv_metering_check_result"; // MES è®¡é‡æ£€æµ‹ç»“论
    // ========== æŽ’班日历 (CAL) ==========
    String MES_CAL_HOLIDAY_TYPE = "mes_cal_holiday_type"; // MES å‡æœŸç±»åž‹
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -215,6 +215,15 @@
    // ========== MES è®¾å¤‡ç®¡ç†-点检记录明细(1-040-306-100) ==========
    ErrorCode DV_CHECK_RECORD_LINE_NOT_EXISTS = new ErrorCode(1_040_306_100, "设备点检记录明细不存在");
    // ========== MES è®¾å¤‡ç®¡ç†-计量器具(1-040-307-000) ==========
    ErrorCode DV_METERING_NOT_EXISTS = new ErrorCode(1_040_307_000, "计量器具不存在");
    ErrorCode DV_METERING_CODE_DUPLICATE = new ErrorCode(1_040_307_001, "计量器具编码已存在");
    ErrorCode DV_METERING_HAS_CHECK_RECORD = new ErrorCode(1_040_307_002, "计量器具已关联检测记录,无法删除");
    // ========== MES è®¾å¤‡ç®¡ç†-计量检测记录(1-040-307-100) ==========
    ErrorCode DV_METERING_CHECK_NOT_EXISTS = new ErrorCode(1_040_307_100, "计量检测记录不存在");
    ErrorCode DV_METERING_CHECK_CODE_DUPLICATE = new ErrorCode(1_040_307_101, "计量检测编号已存在");
    // ========== MES å·¥å…·ç®¡ç†-工具类型(1-040-400-000) ==========
    ErrorCode TM_TOOL_TYPE_NOT_EXISTS = new ErrorCode(1_040_400_000, "工具类型不存在");
    ErrorCode TM_TOOL_TYPE_CODE_DUPLICATE = new ErrorCode(1_040_400_001, "工具类型编码已存在");
@@ -767,6 +776,9 @@
    ErrorCode PD_PROJECT_NOT_UPLOAD_FOR_AUDIT = new ErrorCode(1_040_800_005, "只有待审核状态的设计项目才能审核");
    ErrorCode PD_PROJECT_NOT_AUDIT_FOR_ARCHIVE = new ErrorCode(1_040_800_006, "只有待归档状态的设计项目才能归档");
    ErrorCode PD_PROJECT_AUDIT_NOT_FINISHED = new ErrorCode(1_040_800_007, "该项目下存在未审核的资料,请先完成所有资料审核");
    ErrorCode PD_PROJECT_NOT_UPLOAD_FOR_APPROVAL = new ErrorCode(1_040_800_008, "只有待审核状态的设计项目才能提交审批");
    ErrorCode PD_PROJECT_NOT_APPROVING = new ErrorCode(1_040_800_009, "设计项目不处于审批中状态");
    ErrorCode PD_PROJECT_BPM_CATEGORY_NOT_EXISTS = new ErrorCode(1_040_800_010, "设计项目审批分类不存在,请先配置 BPM å®¡æ‰¹æµç¨‹");
    // ========== MES äº§å“è®¾è®¡-资料子表(1-040-801-000) ==========
    ErrorCode PD_DOCUMENT_NOT_EXISTS = new ErrorCode(1_040_801_000, "设计资料不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesPdStatusConstants.java
@@ -32,5 +32,9 @@
     * å·²å½’æ¡£
     */
    public static final int ARCHIVE = 4;
    /**
     * å®¡æ‰¹ä¸­ï¼ˆBPM å®¡æ‰¹è¿›è¡Œä¸­ï¼‰
     */
    public static final int APPROVING = 5;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringCheckResultEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.enums.dv;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES è®¡é‡æ£€æµ‹ç»“论枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesDvMeteringCheckResultEnum implements ArrayValuable<Integer> {
    QUALIFIED(1, "合格"),
    UNQUALIFIED(2, "不合格");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesDvMeteringCheckResultEnum::getResult).toArray(Integer[]::new);
    /**
     * ç»“论值
     */
    private final Integer result;
    /**
     * ç»“论名
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringExpireStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package cn.iocoder.yudao.module.mes.enums.dv;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
/**
 * MES è®¡é‡å™¨å…·åˆ°æœŸçŠ¶æ€æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesDvMeteringExpireStatusEnum implements ArrayValuable<Integer> {
    OVERDUE(1, "已过期"),
    WITHIN(2, "即将到期"),
    NORMAL(3, "正常");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesDvMeteringExpireStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * çŠ¶æ€å€¼
     */
    private final Integer status;
    /**
     * çŠ¶æ€å
     */
    private final String name;
    /**
     * è®¡ç®—到期状态
     *
     * @param nextCheckDate ä¸‹æ¬¡æ£€æµ‹æ—¥æœŸï¼ˆåˆ°æœŸæ—¥ï¼‰
     * @param remindDays æå‰æé†’天数
     * @return åˆ°æœŸçŠ¶æ€
     */
    public static Integer compute(LocalDate nextCheckDate, int remindDays) {
        if (nextCheckDate == null) {
            return NORMAL.getStatus();
        }
        long days = ChronoUnit.DAYS.between(LocalDate.now(), nextCheckDate);
        if (days < 0) {
            return OVERDUE.getStatus();
        }
        if (days <= remindDays) {
            return WITHIN.getStatus();
        }
        return NORMAL.getStatus();
    }
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.enums.dv;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES è®¡é‡å™¨å…·çŠ¶æ€æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesDvMeteringStatusEnum implements ArrayValuable<Integer> {
    IN_USE(1, "在用"),
    STOPPED(2, "停用"),
    SCRAPPED(3, "报废");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesDvMeteringStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * çŠ¶æ€å€¼
     */
    private final Integer status;
    /**
     * çŠ¶æ€å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/pd/MesPdProjectStatusEnum.java
@@ -39,7 +39,7 @@
    /**
     * å¾…å½’æ¡£
     *
     * å¯¹åº” MesPdProjectService#auditProject æ–¹æ³•
     * å¯¹åº” MesPdProjectService#updateProjectAuditStatus æ–¹æ³•(BPM å®¡æ‰¹é€šè¿‡åŽï¼‰
     */
    AUDIT(MesPdStatusConstants.AUDIT, "待归档"),
    /**
@@ -47,7 +47,13 @@
     *
     * å¯¹åº” MesPdProjectService#archiveProject æ–¹æ³•
     */
    ARCHIVE(MesPdStatusConstants.ARCHIVE, "已归档");
    ARCHIVE(MesPdStatusConstants.ARCHIVE, "已归档"),
    /**
     * å®¡æ‰¹ä¸­
     *
     * å¯¹åº” MesPdProjectService#submitProjectApproval æ–¹æ³•
     */
    APPROVING(MesPdStatusConstants.APPROVING, "审批中");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesPdProjectStatusEnum::getStatus).toArray(Integer[]::new);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.mes.service.dv.metering;
/**
 * MES è®¡é‡å™¨å…·åˆ°æœŸæé†’ Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface MesDvMeteringRemindService {
    /**
     * å‘送到期提醒站内信(懒触发,幂等)
     *
     * åœ¨è®¡é‡å™¨å…·åˆ†é¡µæŸ¥è¯¢æ—¶è§¦å‘:先抢当天扫描门闩,再查到期器具,按器具+到期日去重后逐台发站内信。
     */
    void sendExpireRemind();
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.mes.service.dv.metering;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.metering.MesDvMeteringMapper;
import cn.iocoder.yudao.module.mes.dal.redis.dv.metering.MesDvMeteringRemindRedisDAO;
import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * MES è®¡é‡å™¨å…·åˆ°æœŸæé†’ Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Slf4j
@Service
public class MesDvMeteringRemindServiceImpl implements MesDvMeteringRemindService {
    /**
     * æé†’天数配置 KEY(infra_config)
     */
    private static final String REMIND_DAYS_CONFIG_KEY = "mes.dv-metering.remind-days";
    /**
     * æé†’天数默认值
     */
    private static final int DEFAULT_REMIND_DAYS = 30;
    /**
     * åˆ°æœŸæé†’站内信模板编号
     */
    private static final String REMIND_TEMPLATE_CODE = "mes_metering_expire_remind";
    @Resource
    private MesDvMeteringMapper meteringMapper;
    @Resource
    private MesDvMeteringRemindRedisDAO meteringRemindRedisDAO;
    @Resource
    private ConfigApi configApi;
    @Resource
    private NotifyMessageSendApi notifyMessageSendApi;
    @Override
    public void sendExpireRemind() {
        try {
            // 1. æŠ¢å½“天扫描门闩,避免多实例/多次并发重复扫描
            LocalDate today = LocalDate.now();
            if (!meteringRemindRedisDAO.tryAcquireScanLock(today)) {
                return;
            }
            // 2. è¯»å–提醒天数(默认 30)
            int remindDays = getRemindDays();
            // 3. æŸ¥è¯¢ [today, today + remindDays] åˆ°æœŸçš„器具
            List<MesDvMeteringDO> list = meteringMapper.selectListByNextCheckDateBetween(
                    today, today.plusDays(remindDays));
            // 4. é€å°å‘送站内信(按器具 + åˆ°æœŸæ—¥åŽ»é‡ï¼‰
            for (MesDvMeteringDO metering : list) {
                if (metering.getChargeUserId() == null || metering.getNextCheckDate() == null) {
                    continue;
                }
                if (!meteringRemindRedisDAO.tryMarkReminded(metering.getId(), metering.getNextCheckDate())) {
                    continue;
                }
                sendRemindMessage(metering);
            }
        } catch (Exception ex) {
            // best-effort:提醒失败不影响列表查询
            log.error("[sendExpireRemind][发送计量器具到期提醒失败]", ex);
        }
    }
    private int getRemindDays() {
        String value = configApi.getConfigValueByKey(REMIND_DAYS_CONFIG_KEY);
        if (StrUtil.isBlank(value)) {
            return DEFAULT_REMIND_DAYS;
        }
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException ex) {
            log.warn("[getRemindDays][配置 {} çš„值 {} éžæ³•,使用默认值 {}]", REMIND_DAYS_CONFIG_KEY, value, DEFAULT_REMIND_DAYS);
            return DEFAULT_REMIND_DAYS;
        }
    }
    private void sendRemindMessage(MesDvMeteringDO metering) {
        Map<String, Object> params = new HashMap<>();
        params.put("meteringCode", metering.getCode());
        params.put("meteringName", metering.getName());
        params.put("nextCheckDate", metering.getNextCheckDate().toString());
        NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO()
                .setUserId(metering.getChargeUserId())
                .setTemplateCode(REMIND_TEMPLATE_CODE)
                .setTemplateParams(params);
        notifyMessageSendApi.sendSingleMessageToAdmin(reqDTO);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
package cn.iocoder.yudao.module.mes.service.dv.metering;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
import jakarta.validation.Valid;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * MES è®¡é‡å™¨å…· Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface MesDvMeteringService {
    /**
     * åˆ›å»ºè®¡é‡å™¨å…·
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createMetering(@Valid MesDvMeteringSaveReqVO createReqVO);
    /**
     * æ›´æ–°è®¡é‡å™¨å…·
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateMetering(@Valid MesDvMeteringSaveReqVO updateReqVO);
    /**
     * åˆ é™¤è®¡é‡å™¨å…·
     *
     * @param id ç¼–号
     */
    void deleteMetering(Long id);
    /**
     * èŽ·å¾—è®¡é‡å™¨å…·
     *
     * @param id ç¼–号
     * @return è®¡é‡å™¨å…·
     */
    MesDvMeteringDO getMetering(Long id);
    /**
     * èŽ·å¾—è®¡é‡å™¨å…·åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return è®¡é‡å™¨å…·åˆ†é¡µ
     */
    PageResult<MesDvMeteringDO> getMeteringPage(MesDvMeteringPageReqVO pageReqVO);
    /**
     * èŽ·å¾—è®¡é‡å™¨å…·åˆ—è¡¨
     *
     * @return è®¡é‡å™¨å…·åˆ—表
     */
    List<MesDvMeteringDO> getMeteringList();
    /**
     * èŽ·å¾—è®¡é‡å™¨å…·åˆ—è¡¨
     *
     * @param ids ç¼–号数组
     * @return è®¡é‡å™¨å…·åˆ—表
     */
    List<MesDvMeteringDO> getMeteringList(Collection<Long> ids);
    /**
     * èŽ·å¾—è®¡é‡å™¨å…· Map
     *
     * @param ids ç¼–号数组
     * @return è®¡é‡å™¨å…· Map
     */
    default Map<Long, MesDvMeteringDO> getMeteringMap(Collection<Long> ids) {
        return convertMap(getMeteringList(ids), MesDvMeteringDO::getId);
    }
    /**
     * æ ¡éªŒè®¡é‡å™¨å…·å­˜åœ¨
     *
     * @param id ç¼–号
     */
    void validateMeteringExists(Long id);
    /**
     * èŽ·å¾—åˆ°æœŸæé†’å¤©æ•°ï¼ˆé»˜è®¤ 30)
     *
     * @return æé†’天数
     */
    int getRemindDays();
    /**
     * å›žå†™è®¡é‡å™¨å…·çš„æœ€è¿‘检测信息(检测记录新增/修改/删除后调用)
     *
     * @param meteringId è®¡é‡å™¨å…·ç¼–号
     * @param lastCheckDate æœ€è¿‘检测日期(无则置空)
     * @param nextCheckDate ä¸‹æ¬¡æ£€æµ‹æ—¥æœŸï¼ˆæ— åˆ™ç½®ç©ºï¼‰
     */
    void updateMeteringCheckInfo(Long meteringId, LocalDate lastCheckDate, LocalDate nextCheckDate);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,181 @@
package cn.iocoder.yudao.module.mes.service.dv.metering;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.metering.MesDvMeteringMapper;
import cn.iocoder.yudao.module.mes.service.dv.meteringcheck.MesDvMeteringCheckService;
import cn.hutool.core.util.StrUtil;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_CODE_DUPLICATE;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_HAS_CHECK_RECORD;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_NOT_EXISTS;
/**
 * MES è®¡é‡å™¨å…· Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesDvMeteringServiceImpl implements MesDvMeteringService {
    /**
     * æé†’天数配置 KEY(infra_config)
     */
    private static final String REMIND_DAYS_CONFIG_KEY = "mes.dv-metering.remind-days";
    /**
     * æé†’天数默认值
     */
    private static final int DEFAULT_REMIND_DAYS = 30;
    @Resource
    private MesDvMeteringMapper meteringMapper;
    @Resource
    @Lazy // å»¶è¿ŸåŠ è½½ï¼Œé¿å…ä¸Žè®¡é‡æ£€æµ‹è®°å½• Service å¾ªçŽ¯ä¾èµ–
    private MesDvMeteringCheckService meteringCheckService;
    @Resource
    private MesDvMeteringRemindService meteringRemindService;
    @Resource
    private ConfigApi configApi;
    @Override
    public Long createMetering(MesDvMeteringSaveReqVO createReqVO) {
        // æ ¡éªŒç¼–码唯一
        validateMeteringCodeUnique(null, createReqVO.getCode());
        // æ’å…¥
        MesDvMeteringDO metering = BeanUtils.toBean(createReqVO, MesDvMeteringDO.class);
        meteringMapper.insert(metering);
        return metering.getId();
    }
    @Override
    public void updateMetering(MesDvMeteringSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateMeteringExists(updateReqVO.getId());
        // æ ¡éªŒç¼–码唯一
        validateMeteringCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
        // æ›´æ–°
        MesDvMeteringDO updateObj = BeanUtils.toBean(updateReqVO, MesDvMeteringDO.class);
        meteringMapper.updateById(updateObj);
    }
    @Override
    public void deleteMetering(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateMeteringExists(id);
        // æ ¡éªŒæ— å…³è”检测记录
        if (meteringCheckService.getMeteringCheckCountByMeteringId(id) > 0) {
            throw exception(DV_METERING_HAS_CHECK_RECORD);
        }
        // åˆ é™¤
        meteringMapper.deleteById(id);
    }
    @Override
    public void validateMeteringExists(Long id) {
        if (meteringMapper.selectById(id) == null) {
            throw exception(DV_METERING_NOT_EXISTS);
        }
    }
    private void validateMeteringCodeUnique(Long id, String code) {
        if (StrUtil.isBlank(code)) {
            return;
        }
        MesDvMeteringDO metering = meteringMapper.selectByCode(code);
        if (metering == null) {
            return;
        }
        if (ObjUtil.notEqual(metering.getId(), id)) {
            throw exception(DV_METERING_CODE_DUPLICATE);
        }
    }
    @Override
    public MesDvMeteringDO getMetering(Long id) {
        return meteringMapper.selectById(id);
    }
    @Override
    public PageResult<MesDvMeteringDO> getMeteringPage(MesDvMeteringPageReqVO pageReqVO) {
        // 1. åˆ°æœŸçŠ¶æ€è½¬æ¢ä¸ºæ—¥æœŸèŒƒå›´ï¼Œèµ° DB è¿‡æ»¤
        Integer expireStatus = pageReqVO.getExpireStatus();
        if (expireStatus != null) {
            LocalDate today = LocalDate.now();
            int remindDays = getRemindDays();
            switch (expireStatus) {
                case 1 -> { // å·²è¿‡æœŸï¼šåˆ°æœŸæ—¥ < ä»Šå¤©
                    pageReqVO.setNextCheckDateEnd(today.minusDays(1));
                }
                case 2 -> { // å³å°†åˆ°æœŸï¼šåˆ°æœŸæ—¥åœ¨ [今天, ä»Šå¤©+提醒天数]
                    pageReqVO.setNextCheckDateBegin(today);
                    pageReqVO.setNextCheckDateEnd(today.plusDays(remindDays));
                }
                case 3 -> { // æ­£å¸¸ï¼šåˆ°æœŸæ—¥ > ä»Šå¤©+提醒天数
                    pageReqVO.setNextCheckDateBegin(today.plusDays(remindDays + 1));
                }
                default -> {
                }
            }
        }
        // 2. æŸ¥è¯¢
        PageResult<MesDvMeteringDO> pageResult = meteringMapper.selectPage(pageReqVO);
        // 3. æ‡’触发到期提醒站内信(best-effort,不影响查询)
        meteringRemindService.sendExpireRemind();
        return pageResult;
    }
    @Override
    public int getRemindDays() {
        String value = configApi.getConfigValueByKey(REMIND_DAYS_CONFIG_KEY);
        if (StrUtil.isBlank(value)) {
            return DEFAULT_REMIND_DAYS;
        }
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException ex) {
            return DEFAULT_REMIND_DAYS;
        }
    }
    @Override
    public void updateMeteringCheckInfo(Long meteringId, LocalDate lastCheckDate, LocalDate nextCheckDate) {
        meteringMapper.updateById(new MesDvMeteringDO()
                .setId(meteringId)
                .setLastCheckDate(lastCheckDate)
                .setNextCheckDate(nextCheckDate));
    }
    @Override
    public List<MesDvMeteringDO> getMeteringList() {
        return meteringMapper.selectList();
    }
    @Override
    public List<MesDvMeteringDO> getMeteringList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return meteringMapper.selectByIds(ids);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
package cn.iocoder.yudao.module.mes.service.dv.meteringcheck;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.meteringcheck.MesDvMeteringCheckDO;
import jakarta.validation.Valid;
/**
 * MES è®¡é‡æ£€æµ‹è®°å½• Service æŽ¥å£
 *
 * @author è¶…级管理员
 */
public interface MesDvMeteringCheckService {
    /**
     * åˆ›å»ºè®¡é‡æ£€æµ‹è®°å½•
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createMeteringCheck(@Valid MesDvMeteringCheckSaveReqVO createReqVO);
    /**
     * æ›´æ–°è®¡é‡æ£€æµ‹è®°å½•
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateMeteringCheck(@Valid MesDvMeteringCheckSaveReqVO updateReqVO);
    /**
     * åˆ é™¤è®¡é‡æ£€æµ‹è®°å½•
     *
     * @param id ç¼–号
     */
    void deleteMeteringCheck(Long id);
    /**
     * èŽ·å¾—è®¡é‡æ£€æµ‹è®°å½•
     *
     * @param id ç¼–号
     * @return è®¡é‡æ£€æµ‹è®°å½•
     */
    MesDvMeteringCheckDO getMeteringCheck(Long id);
    /**
     * èŽ·å¾—è®¡é‡æ£€æµ‹è®°å½•åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return è®¡é‡æ£€æµ‹è®°å½•分页
     */
    PageResult<MesDvMeteringCheckDO> getMeteringCheckPage(MesDvMeteringCheckPageReqVO pageReqVO);
    /**
     * èŽ·å¾—æŒ‡å®šè®¡é‡å™¨å…·ä¸‹çš„æ£€æµ‹è®°å½•æ•°é‡
     *
     * @param meteringId è®¡é‡å™¨å…·ç¼–号
     * @return æ£€æµ‹è®°å½•数量
     */
    Long getMeteringCheckCountByMeteringId(Long meteringId);
    /**
     * æ ¡éªŒè®¡é‡æ£€æµ‹è®°å½•存在
     *
     * @param id ç¼–号
     */
    void validateMeteringCheckExists(Long id);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,165 @@
package cn.iocoder.yudao.module.mes.service.dv.meteringcheck;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.meteringcheck.MesDvMeteringCheckDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.meteringcheck.MesDvMeteringCheckMapper;
import cn.iocoder.yudao.module.mes.service.dv.metering.MesDvMeteringService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_CHECK_CODE_DUPLICATE;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_CHECK_NOT_EXISTS;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_NOT_EXISTS;
/**
 * MES è®¡é‡æ£€æµ‹è®°å½• Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesDvMeteringCheckServiceImpl implements MesDvMeteringCheckService {
    @Resource
    private MesDvMeteringCheckMapper meteringCheckMapper;
    @Resource
    @Lazy // å»¶è¿ŸåŠ è½½ï¼Œé¿å…ä¸Žè®¡é‡å™¨å…· Service å¾ªçŽ¯ä¾èµ–
    private MesDvMeteringService meteringService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createMeteringCheck(MesDvMeteringCheckSaveReqVO createReqVO) {
        // æ ¡éªŒè®¡é‡å™¨å…·å­˜åœ¨
        MesDvMeteringDO metering = validateMeteringExists(createReqVO.getMeteringId());
        // æ ¡éªŒæ£€æµ‹ç¼–号唯一
        validateMeteringCheckCodeUnique(null, createReqVO.getCheckCode());
        // è®¡ç®—下次检测日期(为空则按校准周期自动计算)
        LocalDate nextCheckDate = computeNextCheckDate(createReqVO.getCheckDate(),
                createReqVO.getNextCheckDate(), metering);
        MesDvMeteringCheckDO check = BeanUtils.toBean(createReqVO, MesDvMeteringCheckDO.class);
        check.setNextCheckDate(nextCheckDate);
        meteringCheckMapper.insert(check);
        // å›žå†™è®¡é‡å™¨å…·çš„æœ€è¿‘检测信息
        refreshMeteringCheckInfo(createReqVO.getMeteringId());
        return check.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateMeteringCheck(MesDvMeteringCheckSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateMeteringCheckExists(updateReqVO.getId());
        // æ ¡éªŒè®¡é‡å™¨å…·å­˜åœ¨
        MesDvMeteringDO metering = validateMeteringExists(updateReqVO.getMeteringId());
        // æ ¡éªŒæ£€æµ‹ç¼–号唯一
        validateMeteringCheckCodeUnique(updateReqVO.getId(), updateReqVO.getCheckCode());
        // è®¡ç®—下次检测日期(为空则按校准周期自动计算)
        LocalDate nextCheckDate = computeNextCheckDate(updateReqVO.getCheckDate(),
                updateReqVO.getNextCheckDate(), metering);
        MesDvMeteringCheckDO updateObj = BeanUtils.toBean(updateReqVO, MesDvMeteringCheckDO.class);
        updateObj.setNextCheckDate(nextCheckDate);
        meteringCheckMapper.updateById(updateObj);
        // å›žå†™è®¡é‡å™¨å…·çš„æœ€è¿‘检测信息
        refreshMeteringCheckInfo(updateReqVO.getMeteringId());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteMeteringCheck(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateMeteringCheckExists(id);
        MesDvMeteringCheckDO check = meteringCheckMapper.selectById(id);
        // åˆ é™¤
        meteringCheckMapper.deleteById(id);
        // å›žå†™è®¡é‡å™¨å…·çš„æœ€è¿‘检测信息
        refreshMeteringCheckInfo(check.getMeteringId());
    }
    @Override
    public MesDvMeteringCheckDO getMeteringCheck(Long id) {
        return meteringCheckMapper.selectById(id);
    }
    @Override
    public PageResult<MesDvMeteringCheckDO> getMeteringCheckPage(MesDvMeteringCheckPageReqVO pageReqVO) {
        return meteringCheckMapper.selectPage(pageReqVO);
    }
    @Override
    public Long getMeteringCheckCountByMeteringId(Long meteringId) {
        return meteringCheckMapper.selectCountByMeteringId(meteringId);
    }
    @Override
    public void validateMeteringCheckExists(Long id) {
        if (meteringCheckMapper.selectById(id) == null) {
            throw exception(DV_METERING_CHECK_NOT_EXISTS);
        }
    }
    private MesDvMeteringDO validateMeteringExists(Long meteringId) {
        MesDvMeteringDO metering = meteringService.getMetering(meteringId);
        if (metering == null) {
            throw exception(DV_METERING_NOT_EXISTS);
        }
        return metering;
    }
    private void validateMeteringCheckCodeUnique(Long id, String checkCode) {
        if (StrUtil.isBlank(checkCode)) {
            return;
        }
        MesDvMeteringCheckDO check = meteringCheckMapper.selectByCheckCode(checkCode);
        if (check == null) {
            return;
        }
        if (ObjUtil.notEqual(check.getId(), id)) {
            throw exception(DV_METERING_CHECK_CODE_DUPLICATE);
        }
    }
    /**
     * è®¡ç®—下次检测日期:手动指定优先,否则按 æ£€æµ‹æ—¥æœŸ + æ ¡å‡†å‘¨æœŸï¼ˆæœˆï¼‰è‡ªåŠ¨è®¡ç®—
     */
    private LocalDate computeNextCheckDate(LocalDate checkDate, LocalDate nextCheckDate, MesDvMeteringDO metering) {
        if (nextCheckDate != null) {
            return nextCheckDate;
        }
        if (checkDate != null && metering.getCheckCycleCount() != null) {
            return checkDate.plusMonths(metering.getCheckCycleCount());
        }
        return nextCheckDate;
    }
    /**
     * æŒ‰"最新一条检测记录"整体重算回写计量器具,正确覆盖编辑非最新记录/删除中间记录等边界
     */
    private void refreshMeteringCheckInfo(Long meteringId) {
        MesDvMeteringCheckDO latest = meteringCheckMapper.selectLatestByMeteringId(meteringId);
        if (latest == null) {
            meteringService.updateMeteringCheckInfo(meteringId, null, null);
        } else {
            meteringService.updateMeteringCheckInfo(meteringId,
                    latest.getCheckDate(), latest.getNextCheckDate());
        }
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -84,7 +84,9 @@
        // æ’å…¥
        MesMdItemDO item = BeanUtils.toBean(createReqVO, MesMdItemDO.class);
        item.setStatus(CommonStatusEnum.DISABLE.getStatus()); // é»˜è®¤ç¦ç”¨ã€‚信息完成后,再启用
        // çŠ¶æ€åªèƒ½ç”±åŸºç¡€æ•°æ®ï¼ˆMDM)控制:MDM åŒæ­¥æ—¶æ²¿ç”¨å…¶çŠ¶æ€ï¼›MES æœ¬åœ°æ–°å¢žé»˜è®¤ç¦ç”¨
        item.setStatus(createReqVO.getMdmItemId() != null && createReqVO.getStatus() != null
                ? createReqVO.getStatus() : CommonStatusEnum.DISABLE.getStatus());
        clearStockIfNotSafe(item); // å¦‚果未启用安全库存,清零库存上下限
        itemMapper.insert(item);
@@ -111,6 +113,10 @@
        // æ›´æ–°
        MesMdItemDO updateObj = BeanUtils.toBean(updateReqVO, MesMdItemDO.class);
        clearStockIfNotSafe(updateObj); // å¦‚果未启用安全库存,清零库存上下限
        // çŠ¶æ€åªèƒ½ç”±åŸºç¡€æ•°æ®ï¼ˆMDM)控制,MES æœ¬åœ°æ›´æ–°ä¸å…è®¸ä¿®æ”¹çŠ¶æ€
        if (updateReqVO.getMdmItemId() == null) {
            updateObj.setStatus(null);
        }
        itemMapper.updateById(updateObj);
        // åŒæ­¥åˆ° MDM(勾选 syncMdm æ—¶ï¼‰
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/project/MesPdProjectService.java
@@ -81,11 +81,29 @@
    void uploadProject(Long id, Long userId);
    /**
     * å®¡æ ¸è®¾è®¡é¡¹ç›®ï¼ˆå¾…审核→待归档)
     * æäº¤è®¾è®¡é¡¹ç›®å®¡æ‰¹ï¼ˆå¾…审核→审批中)
     * <p>
     * åˆ›å»º BPM æµç¨‹å®žä¾‹ï¼Œå®¡æ‰¹é€šè¿‡åŽç”±ç›‘听器回调更新为待归档
     *
     * @param id ç¼–号
     * @param id                  ç¼–号
     * @param processDefinitionKey æµç¨‹å®šä¹‰ Key
     */
    void auditProject(Long id);
    void submitProjectApproval(Long id, String processDefinitionKey);
    /**
     * èŽ·å¾—è®¾è®¡é¡¹ç›®å®¡æ‰¹æµç¨‹åˆ—è¡¨
     *
     * @return æµç¨‹åˆ—表(id/name/version/key)
     */
    List<Map<String, Object>> getProjectApproveProcessDefinitionList();
    /**
     * æ›´æ–°è®¾è®¡é¡¹ç›®å®¡æ‰¹çŠ¶æ€ï¼ˆBPM ç›‘听器回调)
     *
     * @param id       ç¼–号
     * @param bpmResult BPM å®¡æ‰¹ç»“æžœ
     */
    void updateProjectAuditStatus(Long id, Integer bpmResult);
    /**
     * å½’档设计项目(待归档→已归档)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/project/MesPdProjectServiceImpl.java
@@ -1,6 +1,14 @@
package cn.iocoder.yudao.module.mes.service.pd.project;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.mes.controller.admin.pd.project.vo.MesPdProjectPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pd.project.vo.MesPdProjectSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.document.MesPdDocumentDO;
@@ -16,18 +24,24 @@
import cn.iocoder.yudao.module.mes.service.pd.document.MesPdDocumentService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
@@ -37,7 +51,13 @@
 */
@Service
@Validated
@Slf4j
public class MesPdProjectServiceImpl implements MesPdProjectService {
    /**
     * BPM äº§å“é¡¹ç›®å®¡æ‰¹åˆ†ç±»ç¼–码
     */
    private static final String BPM_PROCESS_DEFINITION_CATEGORY_CODE = "mes_pd_project_approve";
    @Resource
    private MesPdProjectMapper projectMapper;
@@ -55,6 +75,15 @@
    @Resource
    @Lazy
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private BpmProcessInstanceApi processInstanceApi;
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -180,27 +209,105 @@
    }
    @Override
    public void auditProject(Long id) {
        // æ ¡éªŒå­˜åœ¨
    @Transactional(rollbackFor = Exception.class)
    public void submitProjectApproval(Long id, String processDefinitionKey) {
        // 1. æ ¡éªŒå­˜åœ¨ + å¾…审核状态
        MesPdProjectDO project = validateProjectExists(id);
        // æ ¡éªŒçŠ¶æ€ä¸ºå¾…å®¡æ ¸
        if (!MesPdProjectStatusEnum.UPLOAD.getStatus().equals(project.getStatus())) {
            throw exception(PD_PROJECT_NOT_UPLOAD_FOR_AUDIT);
            throw exception(PD_PROJECT_NOT_UPLOAD_FOR_APPROVAL);
        }
        // æ ¡éªŒè¯¥é¡¹ç›®ä¸‹æ‰€æœ‰èµ„料审核记录均已审核(不存在待审核的记录)
        // 2. æ ¡éªŒè¯¥é¡¹ç›®ä¸‹æ‰€æœ‰èµ„料审核记录均已审核(不存在待审核的记录)
        List<MesPdDocumentDO> documents = documentService.getDocumentListByProjectId(id);
        List<Long> documentIds = documents.stream().map(MesPdDocumentDO::getId).toList();
        if (documentAuditMapper.existsPendingAuditByDocumentIds(documentIds)) {
            throw exception(PD_PROJECT_AUDIT_NOT_FINISHED);
        }
        // æ›´æ–°çŠ¶æ€
        // 3. åˆ›å»º BPM æµç¨‹å®žä¾‹
        String processInstanceId = processInstanceApi.createProcessInstance(getLoginUserId(),
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
        // 4. æ›´æ–°é¡¹ç›®çŠ¶æ€ä¸ºå®¡æ‰¹ä¸­
        projectMapper.updateById(MesPdProjectDO.builder()
                .id(id)
                .status(MesPdProjectStatusEnum.AUDIT.getStatus())
                .status(MesPdProjectStatusEnum.APPROVING.getStatus())
                .processInstanceId(processInstanceId)
                .processDefinitionKey(processDefinitionKey)
                .build());
    }
    @Override
    public List<Map<String, Object>> getProjectApproveProcessDefinitionList() {
        // 1. æ ¡éªŒåˆ†ç±»æ˜¯å¦å­˜åœ¨
        List<BpmCategoryDO> categories = bpmCategoryService.getCategoryListByCode(
                Collections.singletonList(BPM_PROCESS_DEFINITION_CATEGORY_CODE));
        if (CollUtil.isEmpty(categories)) {
            throw exception(PD_PROJECT_BPM_CATEGORY_NOT_EXISTS);
        }
        // 2. èŽ·å–åˆ†ç±»ä¸‹çš„æµç¨‹å®šä¹‰ä¿¡æ¯
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(BPM_PROCESS_DEFINITION_CATEGORY_CODE);
        if (CollUtil.isEmpty(definitionInfoList)) {
            return Collections.emptyList();
        }
        // 3. è¿‡æ»¤æ¿€æ´»çŠ¶æ€ï¼Œä¿ç•™æœ€æ–°ç‰ˆæœ¬
        Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
        for (BpmProcessDefinitionInfoDO info : definitionInfoList) {
            ProcessDefinition pd = bpmProcessDefinitionService.getProcessDefinition(info.getProcessDefinitionId());
            if (pd == null || pd.isSuspended()) {
                continue;
            }
            ProcessDefinition existing = latestVersionMap.get(pd.getKey());
            if (existing == null || pd.getVersion() > existing.getVersion()) {
                latestVersionMap.put(pd.getKey(), pd);
            }
        }
        // 4. è¿”回流程定义列表
        List<Map<String, Object>> result = new ArrayList<>();
        for (ProcessDefinition pd : latestVersionMap.values()) {
            Map<String, Object> item = new HashMap<>();
            item.put("id", pd.getId());
            item.put("key", pd.getKey());
            item.put("name", pd.getName());
            result.add(item);
        }
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProjectAuditStatus(Long id, Integer bpmResult) {
        // 1. æ ¡éªŒå­˜åœ¨
        MesPdProjectDO project = validateProjectExists(id);
        if (!MesPdProjectStatusEnum.APPROVING.getStatus().equals(project.getStatus())) {
            log.warn("[updateProjectAuditStatus] è®¾è®¡é¡¹ç›®({}) ä¸å¤„于审批中状态,当前状态:{}", id, project.getStatus());
            return;
        }
        // 2. æ ¹æ®å®¡æ‰¹ç»“果更新状态
        Integer newStatus = convertBpmResultToProjectStatus(bpmResult);
        if (newStatus != null) {
            projectMapper.updateById(MesPdProjectDO.builder()
                    .id(id)
                    .status(newStatus)
                    .build());
        }
    }
    /**
     * è½¬æ¢ BPM å®¡æ‰¹ç»“果为设计项目状态
     */
    private Integer convertBpmResultToProjectStatus(Integer bpmResult) {
        if (BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
            return MesPdProjectStatusEnum.AUDIT.getStatus();
        } else if (BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)) {
            return MesPdProjectStatusEnum.UPLOAD.getStatus();
        } else if (BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
            return MesPdProjectStatusEnum.UPLOAD.getStatus();
        }
        return null;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void archiveProject(Long id, Long userId) {
        // æ ¡éªŒå­˜åœ¨
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/project/listener/MesPdProjectStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.mes.service.pd.project.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListener;
import cn.iocoder.yudao.module.mes.service.pd.project.MesPdProjectService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
 * è®¾è®¡é¡¹ç›®å®¡æ‰¹çŠ¶æ€ç›‘å¬å™¨
 *
 * ç›‘听 BPM æµç¨‹å®žä¾‹çŠ¶æ€å˜æ›´äº‹ä»¶ï¼Œæ›´æ–°è®¾è®¡é¡¹ç›®çš„å®¡æ‰¹çŠ¶æ€
 *
 * @author è¶…级管理员
 */
@Component
@Slf4j
public class MesPdProjectStatusListener extends BpmProcessInstanceStatusEventListener {
    /**
     * BPM è®¾è®¡é¡¹ç›®å®¡æ‰¹æµç¨‹å®šä¹‰ key
     */
    private static final String BPM_PROCESS_DEFINITION_KEY = "mes-pd-project-audit";
    @Resource
    private MesPdProjectService projectService;
    @Override
    protected String getProcessDefinitionKey() {
        return BPM_PROCESS_DEFINITION_KEY;
    }
    @Override
    protected void onEvent(BpmProcessInstanceStatusEvent event) {
        Long projectId;
        try {
            projectId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            log.warn("[onEvent] businessKey({}) ä¸æ˜¯æœ‰æ•ˆçš„设计项目编号", event.getBusinessKey());
            return;
        }
        log.info("[onEvent] æ”¶åˆ°BPM审批事件,projectId={}, status={}", projectId, event.getStatus());
        projectService.updateProjectAuditStatus(projectId, event.getStatus());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchService.java
@@ -3,6 +3,9 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import java.util.List;
@@ -96,4 +99,36 @@
     */
    Long getBatchCountByToolId(Long toolId);
    /**
     * èŽ·å¾—æ‰¹æ¬¡é‡‡è´­æ¥æºï¼ˆé‡‡è´­å…¥åº“å• + IQC)
     *
     * @param code æ‰¹æ¬¡å·
     * @return é‡‡è´­æ¥æºåˆ—表
     */
    List<MesWmBatchPurchaseSourceRespVO> getPurchaseSourceList(String code);
    /**
     * èŽ·å¾—æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼ˆé”€å”®å‡ºåº“å• + OQC)
     *
     * @param code æ‰¹æ¬¡å·
     * @return é”€å”®åŽ»å‘åˆ—è¡¨
     */
    List<MesWmBatchSalesTargetRespVO> getSalesTargetList(String code);
    /**
     * èŽ·å¾—æ‰¹æ¬¡å·¥åºæµè½¬ï¼ˆäº§å‡º/投入事件)
     *
     * @param code æ‰¹æ¬¡å·
     * @return å·¥åºæµè½¬åˆ—表
     */
    List<MesWmBatchProcessTraceRespVO> getProcessTraceList(String code);
    /**
     * æ ¹æ® SN ç èŽ·å¾—æ‰¹æ¬¡
     *
     * @param sn SN ç 
     * @return æ‰¹æ¬¡è®°å½•(未命中返回 null)
     */
    MesWmBatchDO getBatchBySn(String sn);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java
@@ -6,9 +6,14 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.sn.MesWmSnDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.batch.MesWmBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.sn.MesWmSnMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
@@ -24,6 +29,7 @@
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -51,6 +57,8 @@
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MesWmSnMapper snMapper;
    @Resource
    private MdmItemBatchConfigApi itemBatchConfigApi;
    @Resource
@@ -372,4 +380,39 @@
        return batchMapper.selectCountByToolId(toolId);
    }
    @Override
    public List<MesWmBatchPurchaseSourceRespVO> getPurchaseSourceList(String code) {
        return batchMapper.selectPurchaseSourceList(code);
    }
    @Override
    public List<MesWmBatchSalesTargetRespVO> getSalesTargetList(String code) {
        return batchMapper.selectSalesTargetList(code);
    }
    @Override
    public List<MesWmBatchProcessTraceRespVO> getProcessTraceList(String code) {
        List<MesWmBatchProcessTraceRespVO> list = batchMapper.selectProcessTraceList(code);
        if (CollUtil.isEmpty(list)) {
            return list;
        }
        // æŒ‰å·¥å•、工序序号、事件时间排序(sort å¯èƒ½ä¸ºç©ºï¼Œç©ºå€¼å…œåº•)
        list.sort(Comparator.comparing(MesWmBatchProcessTraceRespVO::getWorkOrderId,
                        Comparator.nullsLast(Comparator.naturalOrder()))
                .thenComparing(MesWmBatchProcessTraceRespVO::getProcessSort,
                        Comparator.nullsLast(Comparator.naturalOrder()))
                .thenComparing(MesWmBatchProcessTraceRespVO::getEventTime,
                        Comparator.nullsLast(Comparator.naturalOrder())));
        return list;
    }
    @Override
    public MesWmBatchDO getBatchBySn(String sn) {
        MesWmSnDO snDO = snMapper.selectOne(MesWmSnDO::getCode, sn);
        if (snDO == null || StrUtil.isBlank(snDO.getBatchCode())) {
            return null;
        }
        return batchMapper.selectByCode(snDO.getBatchCode());
    }
}
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml
@@ -11,14 +11,15 @@
    <!--
        å‘前追溯:从某个原材料批次出发,查找它被哪些工单消耗后,产出了哪些成品批次
        SQL è·¯å¾„:库存事务(消耗 OUT) â†’ æ¶ˆè€—单 â†’ æŠ¥å·¥è®°å½• â†’ åº“存事务(产出 IN) â†’ æ‰¹æ¬¡
        é“¾è·¯ï¼šåº“存事务(消耗 OUT, biz=123, biz_id=消耗单id) â†’ æ¶ˆè€—单 â†’ æŠ¥å·¥ â†’ äº§å‡ºå•(feedback_id) â†’ åº“存事务(产出 IN, biz=124, biz_id=产出单id) â†’ æ‰¹æ¬¡
    -->
    <select id="selectListByForward" parameterType="String" resultMap="BatchTraceResult">
        SELECT DISTINCT pf.work_order_id, pf.item_id, tx_produce.batch_id, tx_produce.batch_code
        FROM mes_wm_transaction tx_consume
        LEFT JOIN mes_wm_item_consume ic ON tx_consume.biz_id = ic.id AND tx_consume.biz_type = 123 AND ic.deleted = 0
        LEFT JOIN mes_pro_feedback pf ON pf.id = ic.feedback_id AND pf.deleted = 0
        LEFT JOIN mes_wm_transaction tx_produce ON tx_produce.biz_id = pf.id AND tx_produce.biz_type = 124 AND tx_produce.deleted = 0
        JOIN mes_wm_item_consume ic ON tx_consume.biz_id = ic.id AND tx_consume.biz_type = 123 AND ic.deleted = 0
        JOIN mes_pro_feedback pf ON pf.id = ic.feedback_id AND pf.deleted = 0
        JOIN mes_wm_product_produce pp ON pp.feedback_id = pf.id AND pp.deleted = 0
        JOIN mes_wm_transaction tx_produce ON tx_produce.biz_id = pp.id AND tx_produce.biz_type = 124 AND tx_produce.deleted = 0
        WHERE tx_consume.batch_code = #{batchCode}
        AND tx_consume.biz_type = 123
        AND tx_consume.deleted = 0
@@ -27,19 +28,65 @@
    <!--
        å‘后追溯:从某个成品批次出发,查找生产它时消耗了哪些原材料批次
        SQL è·¯å¾„:库存事务(产出 IN) â†’ äº§å‡ºå• â†’ æŠ¥å·¥è®°å½• â†’ åº“存事务(消耗 OUT) â†’ æ‰¹æ¬¡
        é“¾è·¯ï¼šåº“存事务(产出 IN, biz=124, biz_id=产出单id) â†’ äº§å‡ºå• â†’ æŠ¥å·¥ â†’ æ¶ˆè€—单(feedback_id) â†’ åº“存事务(消耗 OUT, biz=123, biz_id=消耗单id) â†’ æ‰¹æ¬¡
    -->
    <select id="selectListByBackward" parameterType="String" resultMap="BatchTraceResult">
        SELECT DISTINCT ic.work_order_id, tx_consume.item_id, tx_consume.batch_id, tx_consume.batch_code
        FROM mes_wm_transaction tx_produce
        LEFT JOIN mes_wm_product_produce pp ON tx_produce.biz_id = pp.id AND tx_produce.biz_type = 124 AND pp.deleted = 0
        LEFT JOIN mes_pro_feedback pf ON pf.id = pp.feedback_id AND pf.deleted = 0
        LEFT JOIN mes_wm_transaction tx_consume ON tx_consume.biz_id = pf.id AND tx_consume.biz_type = 123 AND tx_consume.deleted = 0
        LEFT JOIN mes_wm_item_consume ic ON ic.id = tx_consume.biz_id AND ic.deleted = 0
        JOIN mes_wm_product_produce pp ON tx_produce.biz_id = pp.id AND tx_produce.biz_type = 124 AND pp.deleted = 0
        JOIN mes_pro_feedback pf ON pf.id = pp.feedback_id AND pf.deleted = 0
        JOIN mes_wm_item_consume ic ON ic.feedback_id = pf.id AND ic.deleted = 0
        JOIN mes_wm_transaction tx_consume ON tx_consume.biz_id = ic.id AND tx_consume.biz_type = 123 AND tx_consume.deleted = 0
        WHERE tx_produce.batch_code = #{batchCode}
        AND tx_produce.biz_type = 124
        AND tx_produce.deleted = 0
        AND tx_consume.batch_id IS NOT NULL
    </select>
    <!-- æ‰¹æ¬¡é‡‡è´­æ¥æºï¼šæ‰¹æ¬¡ â†’ é‡‡è´­å…¥åº“行 â†’ é‡‡è´­å…¥åº“单 â†’ IQC -->
    <select id="selectPurchaseSourceList" parameterType="String"
            resultType="cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO">
        SELECT r.code AS receipt_code, r.receipt_date, r.purchase_order_code, r.vendor_id,
               iqc.code AS iqc_code, iqc.check_result AS check_result,
               rl.batch_code, rl.item_id, rl.received_quantity
        FROM mes_wm_item_receipt_line rl
        JOIN mes_wm_item_receipt r ON r.id = rl.receipt_id AND r.deleted = 0
        LEFT JOIN mes_qc_iqc iqc ON iqc.id = r.iqc_id AND iqc.deleted = 0
        WHERE rl.batch_code = #{batchCode} AND rl.deleted = 0
    </select>
    <!-- æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼šæ‰¹æ¬¡ â†’ é”€å”®å‡ºåº“行 â†’ é”€å”®å‡ºåº“单 â†’ OQC -->
    <select id="selectSalesTargetList" parameterType="String"
            resultType="cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO">
        SELECT s.code AS sales_code, s.sales_date, s.sales_order_code, s.client_id,
               oqc.code AS oqc_code, oqc.check_result AS check_result,
               sl.batch_code, sl.item_id, sl.quantity, sl.quality_status
        FROM mes_wm_product_sales_line sl
        JOIN mes_wm_product_sales s ON s.id = sl.sales_id AND s.deleted = 0
        LEFT JOIN mes_qc_oqc oqc ON oqc.id = sl.oqc_id AND oqc.deleted = 0
        WHERE sl.batch_code = #{batchCode} AND sl.deleted = 0
    </select>
    <!-- æ‰¹æ¬¡å·¥åºæµè½¬ï¼šäº§å‡º(OUT)/投入(IN) äº‹ä»¶èšåˆï¼Œæ— éœ€å»ºè¡¨ -->
    <select id="selectProcessTraceList" parameterType="String"
            resultType="cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO">
        SELECT 'OUT' AS event_type, pp.work_order_id, pp.process_id,
               ppl.batch_code, ppl.quantity, ppl.quality_status, pp.produce_date AS event_time,
               wop.sort AS process_sort
        FROM mes_wm_product_produce_line ppl
        JOIN mes_wm_product_produce pp ON pp.id = ppl.produce_id AND pp.deleted = 0
        LEFT JOIN mes_pro_work_order_process wop
               ON wop.work_order_id = pp.work_order_id AND wop.process_id = pp.process_id AND wop.deleted = 0
        WHERE ppl.batch_code = #{batchCode} AND ppl.deleted = 0
        UNION ALL
        SELECT 'IN' AS event_type, ic.work_order_id, ic.process_id,
               icd.batch_code, icd.quantity, NULL AS quality_status, ic.consume_date AS event_time,
               wop.sort AS process_sort
        FROM mes_wm_item_consume_detail icd
        JOIN mes_wm_item_consume ic ON ic.id = icd.consume_id AND ic.deleted = 0
        LEFT JOIN mes_pro_work_order_process wop
               ON wop.work_order_id = ic.work_order_id AND wop.process_id = ic.process_id AND wop.deleted = 0
        WHERE icd.batch_code = #{batchCode} AND icd.deleted = 0
    </select>
</mapper>
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/apply/vo/SrmSupplierApplyRespVO.java
@@ -40,6 +40,12 @@
    @Schema(description = "公司地址", example = "上海市浦东新区XXè·¯XX号")
    private String address;
    @Schema(description = "开户银行", example = "中国工商银行上海分行")
    private String bankName;
    @Schema(description = "银行账号", example = "622202xxxxxxxxxxxx")
    private String bankAccount;
    @Schema(description = "联系人", example = "张三")
    private String contactName;
@@ -49,6 +55,9 @@
    @Schema(description = "邮箱", example = "zhangsan@example.com")
    private String contactEmail;
    @Schema(description = "备注", example = "优先供货")
    private String remark;
    @Schema(description = "申请人ID", example = "1")
    private Long applyUserId;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/apply/vo/SrmSupplierApplySaveReqVO.java
@@ -36,6 +36,12 @@
    @Schema(description = "公司地址", example = "上海市浦东新区XXè·¯XX号")
    private String address;
    @Schema(description = "开户银行", example = "中国工商银行上海分行")
    private String bankName;
    @Schema(description = "银行账号", example = "622202xxxxxxxxxxxx")
    private String bankAccount;
    @Schema(description = "联系人", example = "张三")
    private String contactName;
@@ -45,4 +51,7 @@
    @Schema(description = "邮箱", example = "zhangsan@example.com")
    private String contactEmail;
    @Schema(description = "备注", example = "优先供货")
    private String remark;
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/SrmSupplierCertificateController.java
@@ -6,7 +6,9 @@
import cn.iocoder.yudao.module.srm.controller.admin.certificate.vo.*;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierDO;
import cn.iocoder.yudao.module.srm.service.certificate.SrmSupplierCertificateService;
import cn.iocoder.yudao.module.srm.service.certificate.SrmSupplierCertificateServiceImpl;
import cn.iocoder.yudao.module.srm.service.supplier.SrmSupplierService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -34,6 +36,9 @@
    @Resource
    private SrmSupplierService supplierService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "创建资质证书")
@@ -66,6 +71,7 @@
        var pageResult = certificateService.getCertificatePage(pageReqVO);
        var respList = BeanUtils.toBean(pageResult.getList(), SrmSupplierCertificateRespVO.class);
        enrichSupplierNames(respList);
        enrichAttachments(respList);
        return success(new PageResult<>(respList, pageResult.getTotal()));
    }
@@ -75,6 +81,7 @@
    public CommonResult<List<SrmSupplierCertificateRespVO>> getListBySupplier(@RequestParam("supplierId") Long supplierId) {
        var list = BeanUtils.toBean(certificateService.getCertificateListBySupplierId(supplierId), SrmSupplierCertificateRespVO.class);
        enrichSupplierNames(list);
        enrichAttachments(list);
        return success(list);
    }
@@ -93,6 +100,7 @@
    public CommonResult<SrmSupplierCertificateRespVO> getCertificate(@RequestParam("id") Long id) {
        var resp = BeanUtils.toBean(certificateService.getCertificate(id), SrmSupplierCertificateRespVO.class);
        enrichSupplierName(resp);
        enrichAttachments(List.of(resp));
        return success(resp);
    }
@@ -118,4 +126,10 @@
        }
    }
    private void enrichAttachments(List<SrmSupplierCertificateRespVO> list) {
        if (list == null || list.isEmpty()) return;
        list.forEach(vo -> vo.setAttachmentList(storageAttachmentApi.listAttachments(
                SrmSupplierCertificateServiceImpl.CERTIFICATE_RECORD_TYPE, vo.getId())));
    }
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/vo/SrmSupplierCertificateRespVO.java
@@ -1,10 +1,12 @@
package cn.iocoder.yudao.module.srm.controller.admin.certificate.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - SRM ä¾›åº”商资质证书 Response VO")
@Data
@@ -52,4 +54,7 @@
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    @Schema(description = "证书附件列表")
    private List<StorageBlobRespDTO> attachmentList;
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/vo/SrmSupplierCertificateSaveReqVO.java
@@ -6,6 +6,7 @@
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "管理后台 - SRM ä¾›åº”商资质证书创建/更新 Request VO")
@Data
@@ -47,4 +48,7 @@
    @Schema(description = "备注", example = "年度资质审核")
    private String remark;
    @Schema(description = "证书附件文件ID列表")
    private List<Long> blobIds;
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/supplier/SrmSupplierController.java
@@ -80,6 +80,13 @@
        return success(BeanUtils.toBean(list, SrmSupplierRespVO.class));
    }
    @GetMapping("/list")
    @Operation(summary = "获取供应商档案列表(不过滤准入状态)")
    public CommonResult<List<SrmSupplierRespVO>> getSupplierAllList() {
        List<SrmSupplierDO> list = supplierService.getSupplierAllList();
        return success(BeanUtils.toBean(list, SrmSupplierRespVO.class));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出供应商 Excel")
    @PreAuthorize("@ss.hasPermission('srm:supplier:export')")
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmTenderProjectController.java
@@ -5,6 +5,8 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.*;
import cn.iocoder.yudao.module.srm.service.tender.SrmTenderProjectService;
import cn.iocoder.yudao.module.srm.service.tender.SrmTenderProjectServiceImpl;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -26,6 +28,9 @@
    @Resource
    private SrmTenderProjectService tenderProjectService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "创建招标项目")
@@ -54,14 +59,27 @@
    @Operation(summary = "获取招标项目详情")
    @PreAuthorize("@ss.hasPermission('srm:tender-project:query')")
    public CommonResult<SrmTenderProjectRespVO> getTenderProject(@RequestParam("id") Long id) {
        return success(BeanUtils.toBean(tenderProjectService.getTenderProject(id), SrmTenderProjectRespVO.class));
        var resp = BeanUtils.toBean(tenderProjectService.getTenderProject(id), SrmTenderProjectRespVO.class);
        enrichAttachments(List.of(resp));
        return success(resp);
    }
    @GetMapping("/page")
    @Operation(summary = "获取招标项目分页")
    @PreAuthorize("@ss.hasPermission('srm:tender-project:query')")
    public CommonResult<PageResult<SrmTenderProjectRespVO>> getTenderProjectPage(@Valid SrmTenderProjectPageReqVO pageReqVO) {
        return success(BeanUtils.toBean(tenderProjectService.getTenderProjectPage(pageReqVO), SrmTenderProjectRespVO.class));
        var pageResult = tenderProjectService.getTenderProjectPage(pageReqVO);
        var respList = BeanUtils.toBean(pageResult.getList(), SrmTenderProjectRespVO.class);
        enrichAttachments(respList);
        return success(new PageResult<>(respList, pageResult.getTotal()));
    }
    // ========== å…³è”数据填充 ==========
    private void enrichAttachments(List<SrmTenderProjectRespVO> list) {
        if (list == null || list.isEmpty()) return;
        list.forEach(vo -> vo.setAttachmentList(storageAttachmentApi.listAttachments(
                SrmTenderProjectServiceImpl.TENDER_PROJECT_RECORD_TYPE, vo.getId())));
    }
    @PutMapping("/publish")
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectPartialUpdateReqVO.java
@@ -4,6 +4,8 @@
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - SRM æ‹›æ ‡é¡¹ç›®éƒ¨åˆ†æ›´æ–° Request VO(发布中/投标中阶段)")
@Data
public class SrmTenderProjectPartialUpdateReqVO {
@@ -18,4 +20,7 @@
    @Schema(description = "资质要求", example = "需具备ISO9001认证")
    private String qualificationRequirements;
    @Schema(description = "资质要求附件文件ID列表")
    private List<Long> blobIds;
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectRespVO.java
@@ -1,10 +1,12 @@
package cn.iocoder.yudao.module.srm.controller.admin.tender.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - SRM æ‹›æ ‡é¡¹ç›® Response VO")
@Data
@@ -55,6 +57,9 @@
    @Schema(description = "资质要求", example = "需具备ISO9001认证")
    private String qualificationRequirements;
    @Schema(description = "资质要求附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "采购申请ID", example = "1")
    private Long purchaseRequestId;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectSaveReqVO.java
@@ -7,6 +7,7 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - SRM æ‹›æ ‡é¡¹ç›®åˆ›å»º/更新 Request VO")
@Data
@@ -60,6 +61,9 @@
    @Schema(description = "资质要求", example = "需具备ISO9001认证")
    private String qualificationRequirements;
    @Schema(description = "资质要求附件文件ID列表")
    private List<Long> blobIds;
    @Schema(description = "采购申请ID", example = "1")
    private Long purchaseRequestId;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/dal/dataobject/SrmSupplierApplyDO.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.*;
import java.math.BigDecimal;
@@ -34,6 +35,8 @@
    private String businessLicenseNo;
    private String taxNo;
    private String address;
    private String bankName;
    private String bankAccount;
    private String contactName;
    private String contactMobile;
    private String contactEmail;
@@ -53,5 +56,6 @@
    private Long financeReviewerId;
    private LocalDateTime financeReviewTime;
    private String financeReviewOpinion;
    private String remark;
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/enums/TenderStatusEnum.java
@@ -44,9 +44,9 @@
        return DRAFT.getCode().equals(status);
    }
    /** PUBLISHED æˆ– BIDDING(完整版),或 PRICE_COMPARING(简易版)允许供应商投标 */
    /** BIDDING(完整版),或 PRICE_COMPARING(简易版)允许供应商投标 */
    public static boolean canCreateBid(Integer status) {
        return PUBLISHED.getCode().equals(status) || BIDDING.getCode().equals(status)
        return BIDDING.getCode().equals(status)
                || PRICE_COMPARING.getCode().equals(status);
    }
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyService.java
@@ -61,4 +61,11 @@
     */
    void approve(Long id);
    /**
     * ç¡®ä¿ä¾›åº”商已准入:未准入则自动创建准入申请并置为已通过
     *
     * @param supplierId ä¾›åº”商编号
     */
    void ensureSupplierApproved(Long supplierId);
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyServiceImpl.java
@@ -145,6 +145,43 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void ensureSupplierApproved(Long supplierId) {
        if (supplierId == null) {
            return;
        }
        // æ ¡éªŒä¾›åº”商存在
        SrmSupplierDO supplier = supplierMapper.selectById(supplierId);
        if (supplier == null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.SUPPLIER_NOT_EXISTS);
        }
        // å·²å‡†å…¥åˆ™æ— éœ€å¤„理
        if (supplierApplyMapper.existsApprovedBySupplierId(supplierId)) {
            return;
        }
        // è‡ªåŠ¨åˆ›å»ºå‡†å…¥ç”³è¯·å¹¶ç›´æŽ¥ç½®ä¸ºå·²é€šè¿‡
        SrmSupplierApplyDO apply = new SrmSupplierApplyDO();
        apply.setApplyNo(generateApplyNo());
        apply.setSupplierId(supplierId);
        apply.setSupplierName(supplier.getName());
        apply.setSupplierType(supplier.getType());
        apply.setCompanyNature(supplier.getCompanyNature());
        apply.setRegisteredCapital(supplier.getRegisteredCapital());
        apply.setBusinessLicenseNo(supplier.getBusinessLicenseNo());
        apply.setTaxNo(supplier.getTaxNo());
        apply.setAddress(supplier.getAddress());
        apply.setBankName(supplier.getBankName());
        apply.setBankAccount(supplier.getBankAccount());
        apply.setContactName(supplier.getContactName());
        apply.setContactMobile(supplier.getContactMobile());
        apply.setContactEmail(supplier.getContactEmail());
        apply.setRemark("中标生成采购订单时自动准入并通过");
        apply.setApplyStatus(SupplierApplyStatusEnum.APPROVED.getCode());
        apply.setApplyTime(LocalDateTime.now());
        supplierApplyMapper.insert(apply);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void approve(Long id) {
        SrmSupplierApplyDO apply = validateApplyExists(id);
        if (!SupplierApplyStatusEnum.APPROVED.getCode().equals(apply.getApplyStatus())) {
@@ -159,9 +196,12 @@
            supplier.setBusinessLicenseNo(apply.getBusinessLicenseNo());
            supplier.setTaxNo(apply.getTaxNo());
            supplier.setAddress(apply.getAddress());
            supplier.setBankName(apply.getBankName());
            supplier.setBankAccount(apply.getBankAccount());
            supplier.setContactName(apply.getContactName());
            supplier.setContactMobile(apply.getContactMobile());
            supplier.setContactEmail(apply.getContactEmail());
            supplier.setRemark(apply.getRemark());
            supplier.setStatus(0);
            supplierMapper.insert(supplier);
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/certificate/SrmSupplierCertificateServiceImpl.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.srm.service.certificate;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.srm.controller.admin.certificate.vo.SrmSupplierCertificatePageReqVO;
@@ -8,8 +9,10 @@
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierCertificateMapper;
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
@@ -19,28 +22,47 @@
@Validated
public class SrmSupplierCertificateServiceImpl implements SrmSupplierCertificateService {
    /**
     * èµ„质证书附件的业务记录类型,对应 {@link cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum#SRM_SUPPLIER_CERTIFICATE}
     */
    public static final String CERTIFICATE_RECORD_TYPE = "srm_supplier_certificate";
    @Resource
    private SrmSupplierCertificateMapper certificateMapper;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createCertificate(SrmSupplierCertificateSaveReqVO createReqVO) {
        SrmSupplierCertificateDO certificate = BeanUtils.toBean(createReqVO, SrmSupplierCertificateDO.class);
        updateStatus(certificate);
        certificateMapper.insert(certificate);
        // ç»‘定证书附件
        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
            storageAttachmentApi.bindAttachments("file", CERTIFICATE_RECORD_TYPE, certificate.getId(), createReqVO.getBlobIds());
        }
        return certificate.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateCertificate(SrmSupplierCertificateSaveReqVO updateReqVO) {
        validateCertificateExists(updateReqVO.getId());
        SrmSupplierCertificateDO updateObj = BeanUtils.toBean(updateReqVO, SrmSupplierCertificateDO.class);
        updateStatus(updateObj);
        certificateMapper.updateById(updateObj);
        // å¢žé‡æ›´æ–°è¯ä¹¦é™„ä»¶
        storageAttachmentApi.updateAttachments("file", CERTIFICATE_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteCertificate(Long id) {
        validateCertificateExists(id);
        // åˆ é™¤è¯ä¹¦é™„ä»¶
        storageAttachmentApi.deleteAttachmentsByRecord(CERTIFICATE_RECORD_TYPE, id);
        certificateMapper.deleteById(id);
    }
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierService.java
@@ -46,6 +46,11 @@
    List<SrmSupplierDO> getSupplierSimpleList();
    /**
     * èŽ·å–å…¨éƒ¨ä¾›åº”å•†æ¡£æ¡ˆåˆ—è¡¨ï¼ˆä¸è¿‡æ»¤å‡†å…¥çŠ¶æ€ï¼Œç”¨äºŽæŠ•æ ‡é€‰æ‹©ä¾›åº”å•†ï¼‰
     */
    List<SrmSupplierDO> getSupplierAllList();
    /**
     * èŽ·å–ä¾›åº”å•†åˆ—è¡¨
     */
    List<SrmSupplierDO> getSupplierList(Collection<Long> ids);
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierServiceImpl.java
@@ -3,17 +3,24 @@
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.srm.controller.admin.supplier.vo.SrmSupplierPageReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.supplier.vo.SrmSupplierSaveReqVO;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierDO;
import cn.iocoder.yudao.module.srm.dal.dataobject.*;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmBidAwardMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmBidEvaluationMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierApplyMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierCategoryMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierContactMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierCertificateMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierMapper;
import cn.iocoder.yudao.module.srm.dal.mysql.SrmTenderBidMapper;
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
@@ -34,6 +41,21 @@
    @Resource
    private SrmSupplierCategoryMapper supplierCategoryMapper;
    @Resource
    private SrmSupplierApplyMapper supplierApplyMapper;
    @Resource
    private SrmSupplierCertificateMapper supplierCertificateMapper;
    @Resource
    private SrmTenderBidMapper tenderBidMapper;
    @Resource
    private SrmBidEvaluationMapper bidEvaluationMapper;
    @Resource
    private SrmBidAwardMapper bidAwardMapper;
    @Override
    public Long createSupplier(SrmSupplierSaveReqVO createReqVO) {
@@ -57,10 +79,13 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteSupplier(Long id) {
        // æ ¡éªŒå­˜åœ¨æ€§
        validateSupplierExists(id);
        // åˆ é™¤å…³è”数据
        // å…³è”数据存在时,不允许删除
        validateNoReference(id);
        // å¯ç›´æŽ¥æ¸…理的关联数据
        supplierContactMapper.deleteBySupplierId(id);
        supplierCategoryMapper.deleteBySupplierId(id);
        // é€»è¾‘删除
@@ -83,6 +108,12 @@
    }
    @Override
    public List<SrmSupplierDO> getSupplierAllList() {
        return supplierMapper.selectList(new LambdaQueryWrapperX<SrmSupplierDO>()
                .orderByAsc(SrmSupplierDO::getId));
    }
    @Override
    public List<SrmSupplierDO> getSupplierList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
@@ -105,4 +136,15 @@
        }
    }
    private void validateNoReference(Long id) {
        boolean hasReference = supplierApplyMapper.existsApprovedBySupplierId(id)
                || supplierCertificateMapper.selectCount(SrmSupplierCertificateDO::getSupplierId, id) > 0
                || tenderBidMapper.selectCount(SrmTenderBidDO::getSupplierId, id) > 0
                || bidEvaluationMapper.selectCount(SrmBidEvaluationDO::getSupplierId, id) > 0
                || bidAwardMapper.selectCount(SrmBidAwardDO::getSupplierId, id) > 0;
        if (hasReference) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.SUPPLIER_EXISTS_REFERENCE);
        }
    }
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardServiceImpl.java
@@ -14,6 +14,7 @@
import cn.iocoder.yudao.module.srm.enums.BidStatusEnum;
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.srm.enums.TenderStatusEnum;
import cn.iocoder.yudao.module.srm.service.apply.SrmSupplierApplyService;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -61,6 +62,9 @@
    @Resource
    private ErpPurchaseOrderApi erpPurchaseOrderApi;
    @Resource
    private SrmSupplierApplyService supplierApplyService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -162,6 +166,9 @@
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_SUPPLIER_NOT_SYNCED);
        }
        // ä¾›åº”商未准入则自动准入并通过,保证采购订单流程可用
        supplierApplyService.ensureSupplierApproved(srmSupplier.getId());
        // 3. æŸ¥æ‹›æ ‡ç‰©æ–™åˆ—表
        List<SrmTenderMaterialDO> materials = tenderMaterialMapper
                .selectListByTenderProjectId(award.getTenderProjectId());
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderProjectServiceImpl.java
@@ -7,6 +7,7 @@
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderCreateReqDTO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderMaterialSaveReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPageReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPartialUpdateReqVO;
@@ -18,6 +19,7 @@
import cn.iocoder.yudao.module.srm.enums.BidStatusEnum;
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.srm.enums.TenderStatusEnum;
import cn.iocoder.yudao.module.srm.service.apply.SrmSupplierApplyService;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.starter.annotation.LogRecord;
@@ -41,6 +43,11 @@
@Slf4j
public class SrmTenderProjectServiceImpl implements SrmTenderProjectService {
    /**
     * æ‹›æ ‡é¡¹ç›®èµ„质要求附件的业务记录类型,对应 {@link cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum#SRM_TENDER_PROJECT}
     */
    public static final String TENDER_PROJECT_RECORD_TYPE = "srm_tender_project";
    @Resource
    private SrmTenderProjectMapper tenderProjectMapper;
    @Resource
@@ -60,8 +67,13 @@
    private MdmItemApi mdmItemApi;
    @Resource
    private ErpPurchaseOrderApi erpPurchaseOrderApi;
    @Resource
    private SrmSupplierApplyService supplierApplyService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createTenderProject(SrmTenderProjectSaveReqVO createReqVO) {
        SrmTenderProjectDO project = BeanUtils.toBean(createReqVO, SrmTenderProjectDO.class);
        project.setTenderStatus(TenderStatusEnum.DRAFT.getCode());
@@ -69,10 +81,15 @@
            project.setBiddingMode(tenderConfigService.getConfig().getDefaultBiddingMode());
        }
        tenderProjectMapper.insert(project);
        // ç»‘定资质要求附件
        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
            storageAttachmentApi.bindAttachments("file", TENDER_PROJECT_RECORD_TYPE, project.getId(), createReqVO.getBlobIds());
        }
        return project.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = LOG_TENDER_TYPE, subType = LOG_TENDER_UPDATE_SUB_TYPE,
            bizNo = "{{#updateReqVO.id}}", success = LOG_TENDER_UPDATE_SUCCESS)
    public void updateTenderProject(SrmTenderProjectSaveReqVO updateReqVO) {
@@ -84,6 +101,8 @@
            SrmTenderProjectDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderProjectDO.class);
            updateObj.setTenderStatus(status); // çŠ¶æ€ä¸å…è®¸å‰ç«¯ä¼ å…¥è¦†ç›–
            tenderProjectMapper.updateById(updateObj);
            // å¢žé‡æ›´æ–°èµ„质要求附件
            storageAttachmentApi.updateAttachments("file", TENDER_PROJECT_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
            LogRecordContext.putVariable("tenderName", project.getTenderName());
            LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc());
            LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc());
@@ -99,6 +118,8 @@
            updateObj.setTenderDesc(updateReqVO.getTenderDesc());
            updateObj.setQualificationRequirements(updateReqVO.getQualificationRequirements());
            tenderProjectMapper.updateById(updateObj);
            // å¢žé‡æ›´æ–°èµ„质要求附件
            storageAttachmentApi.updateAttachments("file", TENDER_PROJECT_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
            LogRecordContext.putVariable("tenderName", project.getTenderName());
            LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc());
            LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc());
@@ -112,6 +133,7 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteTenderProject(Long id) {
        SrmTenderProjectDO project = validateTenderExists(id);
        if (!TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) {
@@ -121,6 +143,8 @@
        if (tenderBidMapper.countByTenderProjectId(id) > 0) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_HAS_BIDS_CANNOT_DELETE);
        }
        // åˆ é™¤èµ„质要求附件
        storageAttachmentApi.deleteAttachmentsByRecord(TENDER_PROJECT_RECORD_TYPE, id);
        tenderMaterialMapper.deleteByTenderProjectId(id);
        tenderProjectMapper.deleteById(id);
    }
@@ -284,6 +308,9 @@
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_SUPPLIER_NOT_SYNCED);
        }
        // ä¾›åº”商未准入则自动准入并通过,保证采购订单流程可用
        supplierApplyService.ensureSupplierApproved(supplier.getId());
        // æŸ¥æ‹›æ ‡ç‰©æ–™
        List<SrmTenderMaterialDO> materials = tenderMaterialMapper
                .selectListByTenderProjectId(award.getTenderProjectId());
@@ -368,6 +395,7 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = LOG_TENDER_TYPE, subType = LOG_TENDER_UPDATE_SUB_TYPE,
            bizNo = "{{#updateReqVO.id}}", success = LOG_TENDER_UPDATE_SUCCESS)
    public void updateTenderProjectPartial(SrmTenderProjectPartialUpdateReqVO updateReqVO) {
@@ -380,6 +408,8 @@
        updateObj.setTenderDesc(updateReqVO.getTenderDesc());
        updateObj.setQualificationRequirements(updateReqVO.getQualificationRequirements());
        tenderProjectMapper.updateById(updateObj);
        // å¢žé‡æ›´æ–°èµ„质要求附件
        storageAttachmentApi.updateAttachments("file", TENDER_PROJECT_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
        LogRecordContext.putVariable("tenderName", project.getTenderName());
        LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc());
        LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc());
yudao-module-srm/src/main/resources/sql/srm_init.sql
@@ -95,6 +95,8 @@
    business_license_no VARCHAR(100) DEFAULT NULL COMMENT '营业执照编号',
    tax_no VARCHAR(100) DEFAULT NULL COMMENT '税号',
    address VARCHAR(500) DEFAULT NULL COMMENT '公司地址',
    bank_name VARCHAR(255) DEFAULT NULL COMMENT '开户银行',
    bank_account VARCHAR(100) DEFAULT NULL COMMENT '银行账号',
    contact_name VARCHAR(50) DEFAULT NULL COMMENT '联系人',
    contact_mobile VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
    contact_email VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
@@ -111,6 +113,7 @@
    finance_reviewer_id BIGINT DEFAULT NULL COMMENT '财务审核人ID',
    finance_review_time DATETIME DEFAULT NULL COMMENT '财务审核时间',
    finance_review_opinion VARCHAR(500) DEFAULT NULL COMMENT '财务审核意见',
    remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
    creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
@@ -362,4 +365,3 @@
    KEY idx_supplier_id (supplier_id),
    KEY idx_purchase_order_id (purchase_order_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定标记录表';
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
@@ -60,7 +60,11 @@
    AFTER_SALE_RETURN("aftersales_after_sale_return", "售后退货申请"),
    // ===== MES =====
    MES_PD_DOCUMENT("mes_pd_document", "MES è®¾è®¡èµ„æ–™");
    MES_PD_DOCUMENT("mes_pd_document", "MES è®¾è®¡èµ„æ–™"),
    // ===== SRM =====
    SRM_SUPPLIER_CERTIFICATE("srm_supplier_certificate", "SRM ä¾›åº”商资质证书"),
    SRM_TENDER_PROJECT("srm_tender_project", "SRM æ‹›æ ‡é¡¹ç›®");
    private final String type;
    private final String description;
yudao-server/src/main/resources/application-yzfx.yaml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,324 @@
server:
  port: 48080
--- #################### æ•°æ®åº“相关配置 ####################
spring:
  autoconfigure:
    # noinspection SpringBootApplicationYaml
    exclude:
      - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # é»˜è®¤ local çŽ¯å¢ƒï¼Œä¸å¼€å¯ Quartz çš„自动配置
      - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Qdrant,手动创建
      - org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Redis,手动创建
      - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # ç¦ç”¨ AI æ¨¡å—çš„ Milvus,手动创建
      - org.dromara.trans.config.TransServiceConfig # TODO èŠ‹è‰¿ï¼šeasy-trans å…¼å®¹ Spring Boot 4 åŽç§»é™¤ï¼ˆTransServiceConfig å¼•用了已移除的 RestTemplateBuilder)
      # ç¦ç”¨æœªé…ç½® API Key çš„æ¨¡åž‹è‡ªåŠ¨é…ç½®ï¼ˆåªä¿ç•™é€šä¹‰åƒé—® Embedding çš„æ‰‹åŠ¨é…ç½®ï¼‰
      - org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiAudioTranscriptionAutoConfiguration
      - org.springframework.ai.model.openai.autoconfigure.OpenAiModerationAutoConfiguration
      - org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration
      - org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.chat.GoogleGenAiChatAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiEmbeddingConnectionAutoConfiguration
      - org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiTextEmbeddingAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaApiAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration
      - org.springframework.ai.model.ollama.autoconfigure.OllamaEmbeddingAutoConfiguration
      - org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration
  # æ•°æ®æºé…ç½®é¡¹
  datasource:
    druid: # Druid ã€ç›‘控】相关的全局配置
      web-stat-filter:
        enabled: true
      stat-view-servlet:
        enabled: true
        allow: # è®¾ç½®ç™½åå•,不填则允许所有访问
        url-pattern: /druid/*
        login-username: # æŽ§åˆ¶å°ç®¡ç†ç”¨æˆ·åå’Œå¯†ç 
        login-password:
      filter:
        stat:
          enabled: true
          log-slow-sql: true # æ…¢ SQL è®°å½•
          slow-sql-millis: 100
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
    dynamic: # å¤šæ•°æ®æºé…ç½®
      druid: # Druid ã€è¿žæŽ¥æ± ã€‘相关的全局配置
        initial-size: 1 # åˆå§‹è¿žæŽ¥æ•°
        min-idle: 1 # æœ€å°è¿žæŽ¥æ± æ•°é‡
        max-active: 20 # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
        max-wait: 60000 # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´ï¼Œå•ä½ï¼šæ¯«ç§’ï¼ˆ1 åˆ†é’Ÿï¼‰
        time-between-eviction-runs-millis: 60000 # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 åˆ†é’Ÿï¼‰
        min-evictable-idle-time-millis: 600000 # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位:毫秒(10 åˆ†é’Ÿï¼‰
        max-evictable-idle-time-millis: 1800000 # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位:毫秒(30 åˆ†é’Ÿï¼‰
        validation-query: SELECT 1 FROM DUAL # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        pool-prepared-statements: true # æ˜¯å¦å¼€å¯ PreparedStatement ç¼“å­˜
        max-pool-prepared-statement-per-connection-size: 20 # æ¯ä¸ªè¿žæŽ¥ç¼“存的 PreparedStatement æ•°é‡
      primary: master
      datasource:
        master:
          url: jdbc:mysql://172.17.0.1:9002/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X è¿žæŽ¥çš„示例
          #          url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL è¿žæŽ¥çš„示例
          #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle è¿žæŽ¥çš„示例
          #          url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro;SelectMethod=cursor;encrypt=false;rewriteBatchedStatements=true;useUnicode=true;characterEncoding=utf-8 # SQLServer è¿žæŽ¥çš„示例
          #          url: jdbc:dm://127.0.0.1:5236?schema=RUOYI_VUE_PRO # DM è¿žæŽ¥çš„示例
          #          url: jdbc:kingbase8://127.0.0.1:54321/test  # äººå¤§é‡‘仓 KingbaseES è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss è¿žæŽ¥çš„示例
          #          url: jdbc:postgresql://127.0.0.1:5866/ruoyi-vue-pro # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
          username: root
          password: yzfx@123456..
          #          username: sa # SQL Server è¿žæŽ¥çš„示例
          #          password: Yudao@2024 # SQL Server è¿žæŽ¥çš„示例
          #          username: SYSDBA # DM è¿žæŽ¥çš„示例
          #          password: SYSDBA001 # DM è¿žæŽ¥çš„示例
          #          username: root # OpenGauss è¿žæŽ¥çš„示例
          #          password: Yudao@2024 # OpenGauss è¿žæŽ¥çš„示例
          #          username: root # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
          #          password: 123456 # ç€šé«˜ HighGo è¿žæŽ¥çš„示例
        slave: # æ¨¡æ‹Ÿä»Žåº“,可根据自己需要修改
          lazy: true # å¼€å¯æ‡’加载,保证启动速度
          url: jdbc:mysql://172.17.0.1:9002/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
          username: root
          password: yzfx@123456
#        tdengine: # IoT æ•°æ®åº“(需要 IoT ç‰©è”网再开启噢!)
#          lazy: true # å¼€å¯æ‡’加载,保证启动速度
#          url: jdbc:TAOS-WS://127.0.0.1:6041/ruoyi_vue_pro?varcharAsString=true&enableAutoReconnect=true # å‚见:https://t.zsxq.com/qaX9c,开启 TDengine WebSocket è‡ªåŠ¨é‡è¿ž
#          driver-class-name: com.taosdata.jdbc.ws.WebSocketDriver
#          username: root
#          password: taosdata
#          druid:
#            validation-query: SELECT SERVER_STATUS() # TDengine æ•°æ®æºçš„æœ‰æ•ˆæ€§æ£€æŸ¥ SQL
  # Redis é…ç½®ã€‚Redisson é»˜è®¤çš„配置足够使用,一般不需要进行调优
  data:
    redis:
      host: 172.17.0.1 # åœ°å€
      port: 6379 # ç«¯å£
      database: 1 # æ•°æ®åº“索引
#      password: dev # å¯†ç ï¼Œå»ºè®®ç”Ÿäº§çŽ¯å¢ƒå¼€å¯
--- #################### å®šæ—¶ä»»åŠ¡ç›¸å…³é…ç½® ####################
# Quartz é…ç½®é¡¹ï¼Œå¯¹åº” QuartzProperties é…ç½®ç±»
spring:
  quartz:
    auto-startup: true # æœ¬åœ°å¼€å‘环境,尽量不要开启 Job
    scheduler-name: schedulerName # Scheduler åå­—。默认为 schedulerName
    job-store-type: jdbc # Job å­˜å‚¨å™¨ç±»åž‹ã€‚默认为 memory è¡¨ç¤ºå†…存,可选 jdbc ä½¿ç”¨æ•°æ®åº“。
    wait-for-jobs-to-complete-on-shutdown: true # åº”用关闭时,是否等待定时任务执行完成。默认为 false ï¼Œå»ºè®®è®¾ç½®ä¸º true
    properties: # æ·»åŠ  Quartz Scheduler é™„加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html æ–‡æ¡£
      org:
        quartz:
          # Scheduler ç›¸å…³é…ç½®
          scheduler:
            instanceName: schedulerName
            instanceId: AUTO # è‡ªåŠ¨ç”Ÿæˆ instance ID
          # JobStore ç›¸å…³é…ç½®
          jobStore:
            # JobStore å®žçŽ°ç±»ã€‚å¯è§åšå®¢ï¼šhttps://blog.csdn.net/weixin_42458219/article/details/122247162
            class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
            isClustered: true # æ˜¯é›†ç¾¤æ¨¡å¼
            clusterCheckinInterval: 15000 # é›†ç¾¤æ£€æŸ¥é¢‘率,单位:毫秒。默认为 15000,即 15 ç§’
            misfireThreshold: 60000 # misfire é˜€å€¼ï¼Œå•位:毫秒。
          # çº¿ç¨‹æ± ç›¸å…³é…ç½®
          threadPool:
            threadCount: 25 # çº¿ç¨‹æ± å¤§å°ã€‚默认为 10 ã€‚
            threadPriority: 5 # çº¿ç¨‹ä¼˜å…ˆçº§
            class: org.quartz.simpl.SimpleThreadPool # çº¿ç¨‹æ± ç±»åž‹
    jdbc: # ä½¿ç”¨ JDBC çš„ JobStore çš„æ—¶å€™ï¼ŒJDBC çš„配置
      initialize-schema: NEVER # æ˜¯å¦è‡ªåŠ¨ä½¿ç”¨ SQL åˆå§‹åŒ– Quartz è¡¨ç»“构。这里设置成 never ï¼Œæˆ‘们手动创建表结构。
--- #################### æ¶ˆæ¯é˜Ÿåˆ—相关 ####################
# rocketmq é…ç½®é¡¹ï¼Œå¯¹åº” RocketMQProperties é…ç½®ç±»
rocketmq:
  name-server: 127.0.0.1:9876 # RocketMQ Namesrv
spring:
  # RabbitMQ é…ç½®é¡¹ï¼Œå¯¹åº” RabbitProperties é…ç½®ç±»
  rabbitmq:
    host: 127.0.0.1 # RabbitMQ æœåŠ¡çš„åœ°å€
    port: 5672 # RabbitMQ æœåŠ¡çš„ç«¯å£
    username: rabbit # RabbitMQ æœåŠ¡çš„è´¦å·
    password: rabbit # RabbitMQ æœåŠ¡çš„å¯†ç 
  # Kafka é…ç½®é¡¹ï¼Œå¯¹åº” KafkaProperties é…ç½®ç±»
  kafka:
    bootstrap-servers: 127.0.0.1:9092 # æŒ‡å®š Kafka Broker åœ°å€ï¼Œå¯ä»¥è®¾ç½®å¤šä¸ªï¼Œä»¥é€—号分隔
--- #################### æœåŠ¡ä¿éšœç›¸å…³é…ç½® ####################
# Lock4j é…ç½®é¡¹
lock4j:
  acquire-timeout: 3000 # èŽ·å–åˆ†å¸ƒå¼é”è¶…æ—¶æ—¶é—´ï¼Œé»˜è®¤ä¸º 3000 æ¯«ç§’
  expire: 30000 # åˆ†å¸ƒå¼é”çš„超时时间,默认为 30000 æ¯«ç§’
--- #################### ç›‘控相关配置 ####################
# Actuator ç›‘控端点的配置项
management:
  endpoints:
    web:
      base-path: /actuator # Actuator æä¾›çš„ API æŽ¥å£çš„æ ¹ç›®å½•。默认为 /actuator
      exposure:
        include: '*' # éœ€è¦å¼€æ”¾çš„端点。默认值只打开 health å’Œ info ä¸¤ä¸ªç«¯ç‚¹ã€‚通过设置 * ï¼Œå¯ä»¥å¼€æ”¾æ‰€æœ‰ç«¯ç‚¹ã€‚
# Spring Boot Admin é…ç½®é¡¹
spring:
  boot:
    admin:
      # Spring Boot Admin Client å®¢æˆ·ç«¯çš„相关配置
      client:
        url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # è®¾ç½® Spring Boot Admin Server åœ°å€
        instance:
          service-host-type: IP # æ³¨å†Œå®žä¾‹æ—¶ï¼Œä¼˜å…ˆä½¿ç”¨ IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
        username: admin
        password: admin
      # Spring Boot Admin Server æœåŠ¡ç«¯çš„ç›¸å…³é…ç½®
      context-path: /admin # é…ç½® Spring
      # å…è®¸åµŒå…¥ iframe çš„域名(支持通配符),实际部署时,可以改为 "'self' [你的公网域名]"
      frame-ancestors: "'self' localhost localhost:48080 127.0.0.1 127.0.0.1:48080"
# æ—¥å¿—文件配置
logging:
  file:
    name: logs/${spring.application.name}.log # æ—¥å¿—文件名,相对于项目根目录
  level:
    # é…ç½®è‡ªå·±å†™çš„ MyBatis Mapper æ‰“印日志
    cn.iocoder.yudao.module.bpm.dal.mysql: debug
    cn.iocoder.yudao.module.infra.dal.mysql: debug
    cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiErrorLogMapper: INFO # é…ç½® ApiErrorLogMapper çš„æ—¥å¿—级别为 info,避免和 GlobalExceptionHandler é‡å¤æ‰“印
    cn.iocoder.yudao.module.infra.dal.mysql.job.JobLogMapper: INFO # é…ç½® JobLogMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper: INFO # é…ç½® FileConfigMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.pay.dal.mysql: debug
    cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper: INFO # é…ç½® PayNotifyTaskMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.system.dal.mysql: debug
    cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper: INFO # é…ç½® SmsChannelMapper çš„æ—¥å¿—级别为 info
    cn.iocoder.yudao.module.tool.dal.mysql: debug
    cn.iocoder.yudao.module.member.dal.mysql: debug
    cn.iocoder.yudao.module.trade.dal.mysql: debug
    cn.iocoder.yudao.module.promotion.dal.mysql: debug
    cn.iocoder.yudao.module.statistics.dal.mysql: debug
    cn.iocoder.yudao.module.crm.dal.mysql: debug
    cn.iocoder.yudao.module.erp.dal.mysql: debug
    cn.iocoder.yudao.module.mes.dal.mysql: debug
    cn.iocoder.yudao.module.wms.dal.mysql: debug
    cn.iocoder.yudao.module.iot.dal.mysql: debug
    cn.iocoder.yudao.module.iot.dal.tdengine: DEBUG
    cn.iocoder.yudao.module.iot.service.rule: debug
    cn.iocoder.yudao.module.ai.dal.mysql: debug
    cn.iocoder.yudao.module.im.dal.mysql: debug
    org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO èŠ‹è‰¿ï¼šå…ˆç¦ç”¨ï¼ŒSpring Boot 3.X å­˜åœ¨éƒ¨åˆ†é”™è¯¯çš„ WARN æç¤º
debug: false
yudao:
  ai:
    vector-store:
      type: milvus # å‘量库类型:milvus
      milvus:
        initialize-schema: true
        database-name: test
        collection-name: knowledge_segment_yzfx
        host: 36.213.180.217
        port: 19530
--- #################### å¾®ä¿¡å…¬ä¼—号、小程序相关配置 ####################
wx:
  mp: # å…¬ä¼—号配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md æ–‡æ¡£
#    app-id: wx041349c6f39b268b # æµ‹è¯•号(牛希尧提供的)
#    secret: 5abee519483bc9f8cb37ce280e814bd0
#    app-id: wx5b23ba7a5589ecbb # æµ‹è¯•号(自己的)
#    secret: 2a7b3b20c537e52e74afd395eb85f61f
    app-id: wxf56b1542b9e85f8a # æµ‹è¯•号(Kongdy æä¾›çš„)
    secret: 496379dcef1ba869e9234de8d598cfd3
    # å­˜å‚¨é…ç½®ï¼Œè§£å†³ AccessToken çš„跨节点的共享
    config-storage:
      type: RedisTemplate # é‡‡ç”¨ RedisTemplate æ“ä½œ Redis,会自动从 Spring ä¸­èŽ·å–
      key-prefix: wx # Redis Key çš„前缀
      http-client-type: HttpComponents # é‡‡ç”¨ HttpComponents(HttpClient 5)请求微信公众号平台
  miniapp: # å°ç¨‹åºé…ç½®ï¼ˆå¿…填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md æ–‡æ¡£
    #    appid: wx62056c0d5e8db250 # æµ‹è¯•号(牛希尧提供的)
    #    secret: 333ae72f41552af1e998fe1f54e1584a
#    appid: wx63c280fe3248a3e7 # wenhualian的接口测试号
#    secret: 6f270509224a7ae1296bbf1c8cb97aed
    appid: wxc4598c446f8a9cb3 # æµ‹è¯•号(Kongdy æä¾›çš„)
    secret: 4a1a04e07f6a4a0751b39c3064a92c8b
#    appid: wx66186af0759f47c9 # æµ‹è¯•号(puhui æä¾›çš„)
#    secret: 3218bcbd112cbc614c7264ceb20144ac
    config-storage:
      type: RedisTemplate # é‡‡ç”¨ RedisTemplate æ“ä½œ Redis,会自动从 Spring ä¸­èŽ·å–
      key-prefix: wa # Redis Key çš„前缀
      http-client-type: HttpComponents # é‡‡ç”¨ HttpComponents(HttpClient 5)请求微信小程序平台
--- #################### è¶…级管理员相关配置 ####################
# è¶…级管理员配置项,设置当前项目所有自定义的配置
yudao:
  captcha:
    enable: false # æœ¬åœ°çŽ¯å¢ƒï¼Œæš‚æ—¶å…³é—­å›¾ç‰‡éªŒè¯ç ï¼Œæ–¹ä¾¿ç™»å½•ç­‰æŽ¥å£çš„æµ‹è¯•ï¼›
  security:
    mock-enable: true
  pay:
    order-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/order # æ”¯ä»˜æ¸ é“的【支付】回调地址
    refund-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/refund # æ”¯ä»˜æ¸ é“的【退款】回调地址
    transfer-notify-url: https://yutou.mynatapp.cc/admin-api/pay/notify/transfer # æ”¯ä»˜æ¸ é“的【转账】回调地址
  access-log: # è®¿é—®æ—¥å¿—的配置项
    enable: false
  demo: false # å…³é—­æ¼”示模式
  wxa-code:
    env-version: develop # å°ç¨‹åºç‰ˆæœ¬: æ­£å¼ç‰ˆä¸º "release";体验版为 "trial";开发版为 "develop"
  wxa-subscribe-message:
    miniprogram-state: developer # è·³è½¬å°ç¨‹åºç±»åž‹ï¼šå¼€å‘版为 â€œdeveloper”;体验版为 â€œtrial”为;正式版为 â€œformal”
  tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ åœ°å›¾çš„密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
  storage: # æ–‡ä»¶ä¸Šä¼ é…ç½®
    path: /data/javaWork/mom/file # æ–‡ä»¶å­˜å‚¨æ ¹ç›®å½•
    url-prefix: /admin-api/system/storage-blob # URL路径前缀
    domain: http://42.63.70.231:9001 # åŸŸåå‰ç¼€ï¼ˆæŒ‰å®žé™…域名配置),前端配置好像不太对现在是直连后端,所有要后端端口
    expired: 120 # token过期时间(分钟)
    use-limit: 10 # token使用次数上限
    jwt-secret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG # JWT密钥
    compress: true # æ˜¯å¦åŽ‹ç¼©å›¾ç‰‡
    need-compress-size: 10MB # è§¦å‘压缩的文件大小阈值
    compress-quality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
justauth:
  enabled: true
  type:
    DINGTALK: # é’‰é’‰
      client-id: dingvrnreaje3yqvzhxg
      client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI
      ignore-check-redirect-uri: true
    WECHAT_ENTERPRISE: # ä¼ä¸šå¾®ä¿¡
      client-id: wwd411c69a39ad2e54
      client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
      agent-id: 1000004
      ignore-check-redirect-uri: true
    # noinspection SpringBootApplicationYaml
    WECHAT_MINI_PROGRAM: # å¾®ä¿¡å°ç¨‹åº
      client-id: ${wx.miniapp.appid}
      client-secret: ${wx.miniapp.secret}
      ignore-check-redirect-uri: true
      ignore-check-state: true # å¾®ä¿¡å°ç¨‹åºï¼Œä¸ä¼šä½¿ç”¨åˆ° state,所以不进行校验
    WECHAT_MP: # å¾®ä¿¡å…¬ä¼—号
      client-id: ${wx.mp.app-id}
      client-secret: ${wx.mp.secret}
      ignore-check-redirect-uri: true
    ALIPAY: # æ”¯ä»˜å®å°ç¨‹åº
      client-id: xx
      client-secret: xx
      alipay-public-key: xx
      ignore-check-redirect-uri: true
      ignore-check-state: true
  cache:
    type: REDIS
    prefix: 'social_auth_state:' # ç¼“存前缀,目前只对 Redis ç¼“存生效,默认 JUSTAUTH::STATE::
    timeout: 24h # è¶…时时长,目前只对 Redis ç¼“存生效,默认 3 åˆ†é’Ÿ