2026-07-01 6b5f7c66fc40d7f6099d561e31a34fbd50dd20d3
初始化项目
已添加129个文件
已修改43个文件
14510 ■■■■■ 文件已修改
.claude/settings.local.json 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/contract_generate_order_frontend.md 276 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/crm_erp_integration_frontend.md 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/crm_erp_integration_plan.md 193 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/crm_erp_integration_sql.sql 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/erp_purchase_frontend_change.md 288 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/erp_purchase_request_frontend.md 386 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/file_upload_guide.md 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/im_module_tables.sql 401 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/marketing_report_frontend.md 361 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_frontend_change.md 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_frontend_change_phase1.md 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_frontend_change_phase2.md 556 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_migration_plan.md 260 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_product_integration_frontend.md 276 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sales_traceability_frontend.md 349 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/bpm_purchase_approval_init.sql 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/erp_purchase_enhance.sql 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/mdm_migration_01_basic_data.sql 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/mdm_migration_02_item_data.sql 291 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/mdm_migration_03_update_refs.sql 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/mdm_migration_04_alter_columns.sql 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/mdm_schema.sql 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/pom.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApi.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/customer/dto/CrmCustomerRespDTO.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/CrmReportApi.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/dto/CrmBusinessAnalyzeRespDTO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/dto/CrmContractAnalyzeRespDTO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/dto/CrmCustomerAnalyzeRespDTO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/dto/CrmSalesFunnelRespDTO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/dto/CrmSalesPerformanceRespDTO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/trace/CrmSaleTraceApi.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/trace/dto/CrmSaleTraceRespDTO.java 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/pom.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApi.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApiImpl.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/dto/CrmCustomerRespDTO.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/report/CrmReportApiImpl.java 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/trace/CrmSaleTraceApiImpl.java 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/CrmContractDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/CrmContractMapper.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/product/CrmProductServiceImpl.java 167 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/pom.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApi.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/dto/ErpSaleOrderCreateReqDTO.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/dto/ErpSaleOrderRespDTO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/pom.xml 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApiImpl.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/ErpProductController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestPageReqVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinanceReceiptDO.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestDO.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOutDO.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleReturnDO.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestItemMapper.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestMapper.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseRequestStatusEnum.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinanceReceiptServiceImpl.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductServiceImpl.java 240 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java 403 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseRequestStatusListener.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/pom.xml 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemApi.java 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemCreateReqDTO.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemRespDTO.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemUpdateReqDTO.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/enums/MdmItemTypeEnum.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/pom.xml 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/brand/MdmBrandApi.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/brand/MdmBrandApiImpl.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/brand/dto/MdmBrandRespDTO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApi.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApiImpl.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/dto/MdmItemCategoryRespDTO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemApi.java 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemApiImpl.java 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemSkuApi.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemSkuApiImpl.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemCreateReqDTO.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemRespDTO.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemSkuRespDTO.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemUpdateReqDTO.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/unit/MdmUnitMeasureApi.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/unit/MdmUnitMeasureApiImpl.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/unit/dto/MdmUnitMeasureRespDTO.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/warehouse/MdmWarehouseApi.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/warehouse/MdmWarehouseApiImpl.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/warehouse/dto/MdmWarehouseRespDTO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/brand/MdmBrandController.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/brand/vo/MdmBrandPageReqVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/brand/vo/MdmBrandSaveReqVO.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/MdmItemCategoryController.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategoryPageReqVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategoryRespVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategorySaveReqVO.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemPageReqVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemRespVO.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemStatusUpdateReqVO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/MdmUnitMeasureController.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasurePageReqVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/warehouse/MdmWarehouseController.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/warehouse/vo/MdmWarehousePageReqVO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/warehouse/vo/MdmWarehouseRespVO.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/warehouse/vo/MdmWarehouseSaveReqVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/brand/MdmBrandDO.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/category/MdmItemCategoryDO.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/item/MdmItemBatchConfigDO.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/item/MdmItemDO.java 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/item/MdmItemSkuDO.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/unit/MdmUnitMeasureDO.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/warehouse/MdmWarehouseDO.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/brand/MdmBrandMapper.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/category/MdmItemCategoryMapper.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/item/MdmItemBatchConfigMapper.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/item/MdmItemMapper.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/item/MdmItemSkuMapper.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/unit/MdmUnitMeasureMapper.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/warehouse/MdmWarehouseMapper.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/enums/ErrorCodeConstants.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/enums/MdmItemTypeEnum.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/brand/MdmBrandService.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/brand/MdmBrandServiceImpl.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryService.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryServiceImpl.java 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemService.java 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemSkuService.java 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemSkuServiceImpl.java 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/unit/MdmUnitMeasureService.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/unit/MdmUnitMeasureServiceImpl.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/warehouse/MdmWarehouseService.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/warehouse/MdmWarehouseServiceImpl.java 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-wms/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemServiceImpl.java 100 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/settings.local.json
@@ -14,7 +14,18 @@
      "mcp__ide__getDiagnostics",
      "WebFetch(domain:help.jimureport.com)",
      "WebSearch",
      "Bash(python3 *)"
      "Bash(python3 *)",
      "Bash(mvn compile *)",
      "Bash(mysql -u root -p123456 -D ruoyi-vue-pro)",
      "Bash(mysql -h127.0.0.1 -P3306 -uroot -p123456 ruoyi-vue-pro)",
      "Bash(where mysql *)",
      "Bash(dir /s /b \"C:\\\\Program Files\\\\MySQL\\\\*\\\\bin\\\\mysql.exe\")",
      "Bash(dir /s /b \"C:\\\\Program Files \\(x86\\)\\\\MySQL\\\\*\\\\bin\\\\mysql.exe\")",
      "Bash(cmd.exe /c \"where mysql\")",
      "Bash(cmd.exe /c \"dir /s /b C:\\\\ProgramData\\\\MySQL\\\\*\\\\mysql.exe\")",
      "Bash(cmd.exe /c \"dir /s /b C:\\\\Program Files\\\\MySQL\\\\*\\\\mysql.exe\")",
      "Bash(mysql -u root -p123456 ruoyi-vue-pro -e \"SELECT id, name, code, deleted FROM bpm_category WHERE code = 'purchase_approve' OR name LIKE '%采购审核%';\")",
      "Bash(dir /s /b \"D:\\\\牛马\\\\南通\\\\后端\\\\mom-pro2-after\\\\yudao-module-mdm-api\\\\*.java\")"
    ],
    "deny": [
      "Bash(rm -rf *)",
docs/contract_generate_order_frontend.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,276 @@
# CRM åˆåŒç”Ÿæˆ ERP é”€å”®è®¢å• - å‰ç«¯æ–‡æ¡£
## æ¦‚è¿°
本次改造实现 CRM åˆåŒå®¡æ‰¹é€šè¿‡åŽï¼Œå¯ç›´æŽ¥ç”Ÿæˆ ERP é”€å”®è®¢å•,打通合同到订单的业务流程。
---
## ä¸€ã€æ”¹é€ æ¸…单
### åŽç«¯æ”¹é€ ï¼ˆå·²å®Œæˆï¼‰
| æ–‡ä»¶ | æ”¹åŠ¨è¯´æ˜Ž |
|------|----------|
| `yudao-module-erp-api/` | æ–°å¢ž ERP é”€å”®è®¢å• API æ¨¡å— |
| `ErpSaleOrderApi.java` | é”€å”®è®¢å• API æŽ¥å£ |
| `ErpSaleOrderCreateReqDTO.java` | é”€å”®è®¢å•创建请求 DTO |
| `ErpSaleOrderRespDTO.java` | é”€å”®è®¢å•响应 DTO |
| `ErpSaleOrderApiImpl.java` | ERP é”€å”®è®¢å• API å®žçް |
| `CrmContractDO.java` | æ–°å¢ž orderId、orderNo å­—段 |
| `ErpSaleOrderDO.java` | æ–°å¢ž contractId、contractNo å­—段 |
| `CrmContractService.java` | æ–°å¢ž generateSaleOrder æ–¹æ³• |
| `CrmContractServiceImpl.java` | å®žçŽ°ç”Ÿæˆé”€å”®è®¢å•é€»è¾‘ |
| `CrmContractController.java` | æ–°å¢žç”Ÿæˆé”€å”®è®¢å• API |
| `CrmContractRespVO.java` | æ–°å¢ž orderId、orderNo å­—段 |
---
## äºŒã€æ¶‰åŠé¡µé¢
### CRM åˆåŒç®¡ç†
| é¡µé¢ | è·¯å¾„ | æ”¹åŠ¨è¯´æ˜Ž |
|------|------|----------|
| åˆåŒåˆ—表 | `/crm/contract/index` | æ·»åŠ "生成销售订单"按钮 |
| åˆåŒè¯¦æƒ… | `/crm/contract/detail` | æ˜¾ç¤ºå…³è”的销售订单信息 |
### ERP é”€å”®è®¢å•管理
| é¡µé¢ | è·¯å¾„ | æ”¹åŠ¨è¯´æ˜Ž |
|------|------|----------|
| é”€å”®è®¢å•列表 | `/erp/sale/order/index` | æ˜¾ç¤ºå…³è”合同信息 |
| é”€å”®è®¢å•详情 | `/erp/sale/order/detail` | æ˜¾ç¤ºå…³è”合同信息 |
---
## ä¸‰ã€API æŽ¥å£
### CRM åˆåŒ API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /crm/contract/generate-sale-order | æ ¹æ®åˆåŒç”Ÿæˆé”€å”®è®¢å• |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | åˆåŒç¼–号 |
**响应:**
```json
{
  "code": 0,
  "data": 12345,
  "msg": "操作成功"
}
```
### ERP é”€å”®è®¢å• API(新增 RPC)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /rpc-api/erp/sale-order/create | åˆ›å»ºé”€å”®è®¢å• |
| GET | /rpc-api/erp/sale-order/get | èŽ·å–é”€å”®è®¢å• |
| GET | /rpc-api/erp/sale-order/get-by-contract-id | æ ¹æ®åˆåŒç¼–号获取订单 |
---
## å››ã€å‰ç«¯ä¿®æ”¹ç‚¹
### 4.1 åˆåŒåˆ—表页面
**文件路径:** `/src/views/crm/contract/index.vue`
**新增按钮:**
```html
<!-- åœ¨åˆåŒæ“ä½œåˆ—添加"生成销售订单"按钮 -->
<el-button
  v-if="row.auditStatus === 20 && !row.orderId"
  type="success"
  size="small"
  @click="handleGenerateOrder(row)"
>
  ç”Ÿæˆé”€å”®è®¢å•
</el-button>
<el-button
  v-if="row.orderId"
  type="primary"
  size="small"
  @click="handleViewOrder(row)"
>
  æŸ¥çœ‹è®¢å•
</el-button>
```
**新增方法:**
```js
methods: {
  // ç”Ÿæˆé”€å”®è®¢å•
  async handleGenerateOrder(row) {
    try {
      await this.$confirm('确认根据该合同生成销售订单?', '提示')
      const { data } = await generateSaleOrder(row.id)
      this.$message.success('生成销售订单成功,订单号:' + data)
      this.getList() // åˆ·æ–°åˆ—表
    } catch (e) {
      if (e !== 'cancel') {
        this.$message.error(e.message || '生成失败')
      }
    }
  },
  // æŸ¥çœ‹é”€å”®è®¢å•
  handleViewOrder(row) {
    this.$router.push({
      path: '/erp/sale/order/detail',
      query: { id: row.orderId }
    })
  }
}
```
**新增 API:**
```js
// api/crm/contract.js
export function generateSaleOrder(id) {
  return request({
    url: '/crm/contract/generate-sale-order',
    method: 'post',
    params: { id }
  })
}
```
### 4.2 åˆåŒè¯¦æƒ…页面
**文件路径:** `/src/views/crm/contract/detail.vue`
**新增显示字段:**
```html
<!-- åœ¨åˆåŒåŸºæœ¬ä¿¡æ¯åŒºåŸŸæ·»åŠ  -->
<el-descriptions-item label="销售订单">
  <span v-if="contract.orderId">
    <el-link type="primary" @click="handleViewOrder">
      {{ contract.orderNo }}
    </el-link>
  </span>
  <span v-else>
    <el-button
      v-if="contract.auditStatus === 20"
      type="success"
      size="small"
      @click="handleGenerateOrder"
    >
      ç”Ÿæˆé”€å”®è®¢å•
    </el-button>
    <span v-else>未生成</span>
  </span>
</el-descriptions-item>
```
### 4.3 é”€å”®è®¢å•列表页面
**文件路径:** `/src/views/erp/sale/order/index.vue`
**新增列显示:**
```html
<el-table-column label="关联合同" prop="contractNo" width="150">
  <template #default="{ row }">
    <span v-if="row.contractId">
      <el-link type="primary" @click="handleViewContract(row)">
        {{ row.contractNo }}
      </el-link>
    </span>
    <span v-else>-</span>
  </template>
</el-table-column>
```
### 4.4 é”€å”®è®¢å•详情页面
**文件路径:** `/src/views/erp/sale/order/detail.vue`
**新增显示字段:**
```html
<el-descriptions-item label="关联合同">
  <span v-if="order.contractId">
    <el-link type="primary" @click="handleViewContract">
      {{ order.contractNo }}
    </el-link>
  </span>
  <span v-else>-</span>
</el-descriptions-item>
```
---
## äº”、数据模型变更
### CRM åˆåŒè¡¨å˜æ›´
```sql
ALTER TABLE `crm_contract`
ADD COLUMN `order_id` bigint DEFAULT NULL COMMENT '关联 ERP é”€å”®è®¢å•编号' AFTER `total_price`,
ADD COLUMN `order_no` varchar(32) DEFAULT NULL COMMENT 'ERP é”€å”®è®¢å•单号' AFTER `order_id`;
```
### ERP é”€å”®è®¢å•表变更
```sql
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`;
```
---
## å…­ã€ä¸šåŠ¡è§„åˆ™
### 6.1 ç”Ÿæˆé”€å”®è®¢å•条件
- åˆåŒå¿…须审批通过(auditStatus = 20)
- åˆåŒæœªå…³è”销售订单(orderId ä¸ºç©ºï¼‰
- åˆåŒå¿…须包含产品信息
### 6.2 é”€å”®è®¢å•数据来源
- å®¢æˆ·ï¼šåˆåŒå®¢æˆ·
- äº§å“ï¼šåˆåŒäº§å“åˆ—表
- é‡‘额:合同金额(扣除折扣)
- å¤‡æ³¨ï¼šè‡ªåŠ¨æ·»åŠ "由合同【xxx】自动生成"
### 6.3 æ•°æ®å…³è”
- é”€å”®è®¢å•创建后,自动回填合同 orderId、orderNo
- é”€å”®è®¢å•记录 contractId、contractNo ç”¨äºŽåå‘查询
---
## ä¸ƒã€æ³¨æ„äº‹é¡¹
1. **状态控制**:只有审批通过的合同才能生成订单
2. **唯一性**:一个合同只能生成一个销售订单
3. **数据同步**:生成订单后,合同与订单双向关联
4. **权限要求**:需要有合同编辑权限才能生成订单
5. **订单状态**:生成的销售订单默认为"未审核"状态,需要在 ERP ä¸­å®¡æ‰¹
---
## å…«ã€åŽç»­è®¡åˆ’
| Phase | å†…容 | çŠ¶æ€ |
|-------|------|------|
| Phase 1 | å®¢æˆ·æ•°æ®ç»Ÿä¸€ - ERP ä½¿ç”¨ CRM å®¢æˆ· | **已完成** |
| Phase 2 | äº§å“æ•°æ®ç»Ÿä¸€ - CRM/ERP ä½¿ç”¨ MDM ç‰©æ–™ | **已完成** |
| Phase 3 | åˆåŒç”Ÿæˆé”€å”®è®¢å• | **已完成** |
| Phase 4 | é”€å”®å…¨æµç¨‹è¿½æº¯ | å¾…开始 |
| Phase 5 | è¥é”€æ•°æ®æŠ¥è¡¨ | å¾…开始 |
| Phase 6 | æ•°æ®è¿ç§»ä¸Žæ¸…理 | å¾…开始 |
docs/crm_erp_integration_frontend.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,140 @@
# CRM ä¸Ž ERP é”€å”®ä¸€ä½“化改造 - å‰ç«¯æ–‡æ¡£
## æ¦‚è¿°
本次改造将 CRM å®¢æˆ·ä½œä¸ºç»Ÿä¸€å®¢æˆ·ä¸»æ•°æ®ï¼ŒERP é”€å”®æ¨¡å—(销售订单、销售出库、销售退货、收款单)统一使用 CRM å®¢æˆ·ã€‚
---
## ä¸€ã€æ”¹é€ æ¸…单
### åŽç«¯æ”¹é€ ï¼ˆå·²å®Œæˆï¼‰
| æ–‡ä»¶ | æ”¹åŠ¨è¯´æ˜Ž |
|------|----------|
| `yudao-module-crm-api/` | æ–°å¢ž CRM å®¢æˆ· API æ¨¡å— |
| `CrmCustomerApi.java` | æ–°å¢žå®¢æˆ· API æŽ¥å£ |
| `CrmCustomerRespDTO.java` | æ–°å¢žå®¢æˆ· DTO |
| `CrmCustomerApiImpl.java` | å®žçް CRM å®¢æˆ· API |
| `ErpSaleOrderServiceImpl.java` | ä½¿ç”¨ CRM å®¢æˆ· API |
| `ErpFinanceReceiptServiceImpl.java` | ä½¿ç”¨ CRM å®¢æˆ· API |
| `ErpSaleOrderController.java` | ä½¿ç”¨ CRM å®¢æˆ· API |
| `ErpSaleOrderDO.java` | æ›´æ–°æ³¨é‡Šå…³è” CRM å®¢æˆ· |
| `ErpSaleOutDO.java` | æ›´æ–°æ³¨é‡Šå…³è” CRM å®¢æˆ· |
| `ErpSaleReturnDO.java` | æ›´æ–°æ³¨é‡Šå…³è” CRM å®¢æˆ· |
| `ErpFinanceReceiptDO.java` | æ›´æ–°æ³¨é‡Šå…³è” CRM å®¢æˆ· |
### åŽç«¯æ”¹é€ ï¼ˆå¾…完成)
| æ–‡ä»¶ | æ”¹åŠ¨è¯´æ˜Ž |
|------|----------|
| `ErpSaleOutController.java` | ä½¿ç”¨ CRM å®¢æˆ· API |
| `ErpSaleReturnController.java` | ä½¿ç”¨ CRM å®¢æˆ· API |
| `ErpFinanceReceiptController.java` | ä½¿ç”¨ CRM å®¢æˆ· API |
---
## äºŒã€æ¶‰åŠé¡µé¢
### ERP é”€å”®æ¨¡å—
| é¡µé¢ | è·¯å¾„ | æ”¹åŠ¨è¯´æ˜Ž |
|------|------|----------|
| é”€å”®è®¢å•列表 | `/erp/sale/order/index` | å®¢æˆ·é€‰æ‹©æ”¹ä¸º CRM å®¢æˆ·é€‰æ‹©ç»„ä»¶ |
| é”€å”®è®¢å•表单 | `/erp/sale/order/form` | å®¢æˆ·é€‰æ‹©æ”¹ä¸º CRM å®¢æˆ·é€‰æ‹©ç»„ä»¶ |
| é”€å”®å‡ºåº“列表 | `/erp/sale/out/index` | å®¢æˆ·é€‰æ‹©æ”¹ä¸º CRM å®¢æˆ·é€‰æ‹©ç»„ä»¶ |
| é”€å”®å‡ºåº“表单 | `/erp/sale/out/form` | å®¢æˆ·é€‰æ‹©æ”¹ä¸º CRM å®¢æˆ·é€‰æ‹©ç»„ä»¶ |
| é”€å”®é€€è´§åˆ—表 | `/erp/sale/return/index` | å®¢æˆ·é€‰æ‹©æ”¹ä¸º CRM å®¢æˆ·é€‰æ‹©ç»„ä»¶ |
| é”€å”®é€€è´§è¡¨å• | `/erp/sale/return/form` | å®¢æˆ·é€‰æ‹©æ”¹ä¸º CRM å®¢æˆ·é€‰æ‹©ç»„ä»¶ |
| æ”¶æ¬¾å•列表 | `/erp/finance/receipt/index` | å®¢æˆ·é€‰æ‹©æ”¹ä¸º CRM å®¢æˆ·é€‰æ‹©ç»„ä»¶ |
| æ”¶æ¬¾å•表单 | `/erp/finance/receipt/form` | å®¢æˆ·é€‰æ‹©æ”¹ä¸º CRM å®¢æˆ·é€‰æ‹©ç»„ä»¶ |
---
## ä¸‰ã€å®¢æˆ·é€‰æ‹©ç»„ä»¶
### 3.1 ä½¿ç”¨ CRM å®¢æˆ·é€‰æ‹©ç»„ä»¶
ERP é”€å”®æ¨¡å—的客户选择统一使用 CRM å®¢æˆ·é€‰æ‹©ç»„件。
```html
<!-- åŽŸæ¥ï¼šERP å®¢æˆ·é€‰æ‹© -->
<el-select v-model="form.customerId" placeholder="请选择客户">
  <el-option
    v-for="item in customerList"
    :key="item.id"
    :label="item.name"
    :value="item.id"
  />
</el-select>
<!-- æ”¹ä¸ºï¼šCRM å®¢æˆ·é€‰æ‹©ç»„ä»¶ -->
<CrmCustomerSelect v-model="form.customerId" />
```
### 3.2 CRM å®¢æˆ· API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /crm/customer/simple-list | èŽ·å–å®¢æˆ·ç²¾ç®€åˆ—è¡¨ï¼ˆç”¨äºŽä¸‹æ‹‰é€‰æ‹©ï¼‰ |
| GET | /crm/customer/list-by-ids | æ‰¹é‡èŽ·å–å®¢æˆ·ä¿¡æ¯ |
**客户精简列表响应:**
```json
{
  "code": 0,
  "data": [
    {
      "id": 1,
      "name": "客户A",
      "mobile": "13800138000",
      "telephone": "0571-12345678",
      "ownerUserName": "张三"
    }
  ]
}
```
---
## å››ã€åºŸå¼ƒçš„ ERP å®¢æˆ·ç®¡ç†
### 4.1 ERP å®¢æˆ·ç®¡ç†é¡µé¢åºŸå¼ƒ
以下页面标记为废弃,建议在菜单中隐藏或删除:
- `/erp/customer/index` - ERP å®¢æˆ·åˆ—表
- `/erp/customer/form` - ERP å®¢æˆ·è¡¨å•
---
## äº”、SQL æ‰§è¡Œ
见 `docs/crm_erp_integration_sql.sql`
---
## å…­ã€æ”¹é€ å®ŒæˆçŠ¶æ€
| Phase | å†…容 | çŠ¶æ€ |
|-------|------|------|
| Phase 1 | å®¢æˆ·æ•°æ®ç»Ÿä¸€ - ERP ä½¿ç”¨ CRM å®¢æˆ· | **已完成** |
| Phase 2 | äº§å“æ•°æ®ç»Ÿä¸€ - CRM/ERP ä½¿ç”¨ MDM ç‰©æ–™ | **已完成** |
| Phase 3 | åˆåŒç”Ÿæˆé”€å”®è®¢å• | **已完成** |
| Phase 4 | é”€å”®å…¨æµç¨‹è¿½æº¯ | **已完成** |
| Phase 5 | è¥é”€æ•°æ®æŠ¥è¡¨ | **已完成** |
| Phase 6 | æ•°æ®è¿ç§»ä¸Žæ¸…理 | **已完成** |
---
## ä¸ƒã€äº§å‡ºæ–‡æ¡£æ¸…单
| æ–‡æ¡£ | è¯´æ˜Ž |
|------|------|
| `docs/crm_erp_integration_frontend.md` | CRM å®¢æˆ·ä¸€ä½“化前端文档 |
| `docs/crm_erp_integration_sql.sql` | SQL å˜æ›´è„šæœ¬ |
| `docs/mdm_product_integration_frontend.md` | MDM äº§å“ä¸€ä½“化前端文档 |
| `docs/contract_generate_order_frontend.md` | åˆåŒç”Ÿæˆè®¢å•前端文档 |
| `docs/sales_traceability_frontend.md` | é”€å”®è¿½æº¯å‰ç«¯æ–‡æ¡£ |
| `docs/marketing_report_frontend.md` | è¥é”€æŠ¥è¡¨å‰ç«¯æ–‡æ¡£ |
docs/crm_erp_integration_plan.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,193 @@
# CRM ä¸Ž ERP é”€å”®ä¸€ä½“化改造计划
## ä¸€ã€çŽ°çŠ¶åˆ†æž
### 1.1 å½“前问题
| é—®é¢˜ | è¯´æ˜Ž |
|------|------|
| **客户数据独立** | CRM å®¢æˆ· (`crm_customer`) å’Œ ERP å®¢æˆ· (`erp_customer`) æ˜¯ä¸¤å¼ å®Œå…¨ç‹¬ç«‹çš„表,无法共享 |
| **产品数据独立** | CRM äº§å“ (`crm_product`) å’Œ ERP äº§å“ (`erp_product`) ç‹¬ç«‹ç»´æŠ¤ |
| **合同与订单脱节** | CRM åˆåŒå®¡æ‰¹é€šè¿‡åŽæ— æ³•自动生成 ERP é”€å”®è®¢å• |
| **回款数据独立** | CRM å›žæ¬¾ (`crm_receivable`) å’Œ ERP æ”¶æ¬¾å• (`erp_finance_receipt`) æ— å…³è” |
| **销售流程断裂** | çº¿ç´¢ â†’ å®¢æˆ· â†’ å•†æœº â†’ åˆåŒ â†’ è®¢å• â†’ å‡ºåº“ â†’ æ”¶æ¬¾ï¼Œæµç¨‹åœ¨åˆåŒå’Œè®¢å•之间断裂 |
### 1.2 æ”¹é€ ç›®æ ‡
根据需求描述,改造目标为:
1. **统一客户管理** - CRM å®¢æˆ·ä½œä¸ºç»Ÿä¸€å®¢æˆ·ä¸»æ•°æ®ï¼ŒERP ä½¿ç”¨ CRM å®¢æˆ·
2. **统一产品管理** - ERP äº§å“ä½œä¸ºäº§å“ä¸»æ•°æ®ï¼ŒCRM ä½¿ç”¨ ERP äº§å“
3. **销售流程打通** - åˆåŒå®¡æ‰¹é€šè¿‡åŽå¯ç”Ÿæˆé”€å”®è®¢å•,实现全流程追溯
4. **销售数据联动** - é”€å”®è®¢å•状态实时同步,支持销售决策分析
5. **营销数据报表** - æ•´åˆ CRM å’Œ ERP æ•°æ®ï¼Œæä¾›å®Œæ•´çš„销售分析报表
---
## äºŒã€åˆ†æ‰¹æ”¹é€ è®¡åˆ’
### Phase 1: å®¢æˆ·æ•°æ®ç»Ÿä¸€ï¼ˆé¢„计 2 å¤©ï¼‰
**目标**: ç»Ÿä¸€å®¢æˆ·ä¸»æ•°æ®ï¼ŒERP é”€å”®æ¨¡å—使用 CRM å®¢æˆ·
**改造内容**:
| åºå· | æ”¹é€ é¡¹ | è¯´æ˜Ž |
|------|--------|------|
| 1.1 | åˆ›å»º CRM å®¢æˆ· API æ¨¡å— | æš´éœ²å®¢æˆ·æŸ¥è¯¢æŽ¥å£ä¾› ERP è°ƒç”¨ |
| 1.2 | ERP é”€å”®è®¢å•关联 CRM å®¢æˆ· | `erp_sale_order.customer_id` æ”¹ä¸ºå…³è” `crm_customer.id` |
| 1.3 | ERP é”€å”®å‡ºåº“关联 CRM å®¢æˆ· | `erp_sale_out.customer_id` æ”¹ä¸ºå…³è” `crm_customer.id` |
| 1.4 | ERP é”€å”®é€€è´§å…³è” CRM å®¢æˆ· | `erp_sale_return.customer_id` æ”¹ä¸ºå…³è” `crm_customer.id` |
| 1.5 | ERP æ”¶æ¬¾å•关联 CRM å®¢æˆ· | `erp_finance_receipt.customer_id` æ”¹ä¸ºå…³è” `crm_customer.id` |
| 1.6 | åºŸå¼ƒ ERP å®¢æˆ·è¡¨ | æ ‡è®°åºŸå¼ƒï¼Œä¿ç•™æ•°æ®è¿ç§»æŽ¥å£ |
**涉及文件**:
- æ–°å¢ž: `yudao-module-crm-api/` æ¨¡å—
- ä¿®æ”¹: `ErpSaleOrderDO.java`, `ErpSaleOutDO.java`, `ErpSaleReturnDO.java`
- ä¿®æ”¹: `ErpSaleOrderServiceImpl.java`, `ErpSaleOutServiceImpl.java`, `ErpSaleReturnServiceImpl.java`
---
### Phase 2: äº§å“æ•°æ®ç»Ÿä¸€ï¼ˆé¢„计 1 å¤©ï¼‰
**目标**: ç»Ÿä¸€äº§å“ä¸»æ•°æ®ï¼ŒCRM ä½¿ç”¨ ERP äº§å“
**改造内容**:
| åºå· | æ”¹é€ é¡¹ | è¯´æ˜Ž |
|------|--------|------|
| 2.1 | åˆ›å»º ERP äº§å“ API æ¨¡å— | æš´éœ²äº§å“æŸ¥è¯¢æŽ¥å£ä¾› CRM è°ƒç”¨ |
| 2.2 | CRM å•†æœºäº§å“å…³è” ERP äº§å“ | `crm_business_product.product_id` æ”¹ä¸ºå…³è” `erp_product.id` |
| 2.3 | CRM åˆåŒäº§å“å…³è” ERP äº§å“ | `crm_contract_product.product_id` æ”¹ä¸ºå…³è” `erp_product.id` |
| 2.4 | åºŸå¼ƒ CRM äº§å“è¡¨ | æ ‡è®°åºŸå¼ƒï¼Œä¿ç•™æ•°æ®è¿ç§»æŽ¥å£ |
**涉及文件**:
- æ–°å¢ž: `yudao-module-erp-api/` æ¨¡å—(如果不存在)
- ä¿®æ”¹: `CrmBusinessProductDO.java`, `CrmContractProductDO.java`
- ä¿®æ”¹: CRM å•†æœº/合同 Service å±‚
---
### Phase 3: åˆåŒç”Ÿæˆé”€å”®è®¢å•(预计 2 å¤©ï¼‰
**目标**: CRM åˆåŒå®¡æ‰¹é€šè¿‡åŽå¯ç”Ÿæˆ ERP é”€å”®è®¢å•
**改造内容**:
| åºå· | æ”¹é€ é¡¹ | è¯´æ˜Ž |
|------|--------|------|
| 3.1 | ERP é”€å”®è®¢å•增加关联合同字段 | æ–°å¢ž `contract_id`, `contract_no` å­—段 |
| 3.2 | CRM åˆåŒå¢žåŠ è®¢å•å…³è”å­—æ®µ | æ–°å¢ž `order_id`, `order_no` å­—段 |
| 3.3 | åˆåŒç”Ÿæˆè®¢å•接口 | åˆåŒå®¡æ‰¹é€šè¿‡åŽï¼Œè°ƒç”¨ ERP æœåŠ¡ç”Ÿæˆé”€å”®è®¢å• |
| 3.4 | åˆåŒçŠ¶æ€ç›‘å¬å™¨ | ç›‘听合同审批通过事件,自动/手动触发订单生成 |
**涉及文件**:
- ä¿®æ”¹: `ErpSaleOrderDO.java`, `CrmContractDO.java`
- æ–°å¢ž: `CrmContractToOrderService.java`
- ä¿®æ”¹: `CrmContractStatusListener.java`
---
### Phase 4: é”€å”®å…¨æµç¨‹è¿½æº¯ï¼ˆé¢„计 1 å¤©ï¼‰
**目标**: å®žçŽ°ä»Žçº¿ç´¢åˆ°æ”¶æ¬¾çš„å®Œæ•´é”€å”®æµç¨‹è¿½æº¯
**改造内容**:
| åºå· | æ”¹é€ é¡¹ | è¯´æ˜Ž |
|------|--------|------|
| 4.1 | å•†æœºå…³è”销售订单 | `crm_business` å¢žåŠ è®¢å•å…³è”ï¼Œæ”¯æŒå•†æœºç›´æŽ¥ä¸‹å• |
| 4.2 | é”€å”®è®¢å•状态回写 | å‡ºåº“/退货/收款状态实时回写到订单和关联合同 |
| 4.3 | å…¨æµç¨‹æŸ¥è¯¢æŽ¥å£ | æä¾›æŒ‰å®¢æˆ·/商机/合同查询完整销售链路的接口 |
**涉及文件**:
- ä¿®æ”¹: `CrmBusinessDO.java`
- æ–°å¢ž: `CrmSaleTraceService.java`
---
### Phase 5: è¥é”€æ•°æ®æŠ¥è¡¨ï¼ˆé¢„计 2 å¤©ï¼‰
**目标**: æ•´åˆ CRM å’Œ ERP æ•°æ®ï¼Œæä¾›é”€å”®å†³ç­–报表
**改造内容**:
| åºå· | æ”¹é€ é¡¹ | è¯´æ˜Ž |
|------|--------|------|
| 5.1 | é”€å”®æ¼æ–—分析增强 | æ•´åˆ ERP è®¢å•数据,展示从商机到订单的转化率 |
| 5.2 | å®¢æˆ·é”€å”®åˆ†æž | æŒ‰å®¢æˆ·ç»Ÿè®¡ï¼šå•†æœºé‡‘额、合同金额、订单金额、出库金额、收款金额 |
| 5.3 | é”€å”®ä¸šç»©åˆ†æž | æ•´åˆ CRM å•†æœº/合同和 ERP è®¢å•/收款,计算销售业绩 |
| 5.4 | é”€å”®è¶‹åŠ¿åˆ†æž | æŒ‰æ—¶é—´ç»´åº¦åˆ†æžé”€å”®è¶‹åŠ¿ |
| 5.5 | é”€å”®æŽ’行榜 | ç»¼åˆæŽ’名:商机、合同、订单、收款 |
**涉及文件**:
- ä¿®æ”¹: CRM ç»Ÿè®¡ç›¸å…³ Service
- æ–°å¢ž: `CrmErpSalesStatisticsService.java`
---
### Phase 6: æ•°æ®è¿ç§»ä¸Žæ¸…理(预计 1 å¤©ï¼‰
**目标**: å°†çŽ°æœ‰æ•°æ®è¿ç§»åˆ°ç»Ÿä¸€ç»“æž„
**改造内容**:
| åºå· | æ”¹é€ é¡¹ | è¯´æ˜Ž |
|------|--------|------|
| 6.1 | ERP å®¢æˆ·è¿ç§»åˆ° CRM | æ•°æ®è¿ç§» SQL è„šæœ¬ |
| 6.2 | CRM äº§å“è¿ç§»åˆ° ERP | æ•°æ®è¿ç§» SQL è„šæœ¬ |
| 6.3 | æ•°æ®æ ¡éªŒè„šæœ¬ | éªŒè¯è¿ç§»åŽæ•°æ®å®Œæ•´æ€§ |
---
## ä¸‰ã€æ”¹é€ ä¼˜å…ˆçº§å»ºè®®
| ä¼˜å…ˆçº§ | Phase | è¯´æ˜Ž |
|--------|-------|------|
| P0 | Phase 1 | å®¢æˆ·ç»Ÿä¸€æ˜¯åŸºç¡€ï¼Œå¿…须最先完成 |
| P0 | Phase 2 | äº§å“ç»Ÿä¸€æ˜¯åŸºç¡€ï¼Œå¿…须最先完成 |
| P1 | Phase 3 | åˆåŒç”Ÿæˆè®¢å•是核心业务流程 |
| P1 | Phase 4 | å…¨æµç¨‹è¿½æº¯æ˜¯åˆè§„要求 |
| P2 | Phase 5 | æŠ¥è¡¨åˆ†æžå¯åŽç»­å®Œå–„ |
| P2 | Phase 6 | æ•°æ®è¿ç§»å¯åœ¨ç³»ç»Ÿç¨³å®šåŽè¿›è¡Œ |
---
## å››ã€æŠ€æœ¯æ–¹æ¡ˆè¦ç‚¹
### 4.1 è·¨æ¨¡å—调用方案
使用模块间 API è°ƒç”¨ï¼š
- CRM æä¾› `CrmCustomerApi` ä¾› ERP è°ƒç”¨
- ERP æä¾› `ErpProductApi`、`ErpSaleOrderApi` ä¾› CRM è°ƒç”¨
### 4.2 æ•°æ®ä¸€è‡´æ€§æ–¹æ¡ˆ
- ä½¿ç”¨ Spring Event è¿›è¡Œæ¨¡å—间事件通知
- å…³é”®æ“ä½œä½¿ç”¨äº‹åŠ¡ä¿è¯ä¸€è‡´æ€§
- å¢žåŠ æ•°æ®åŒæ­¥çŠ¶æ€å­—æ®µï¼Œæ”¯æŒå¼‚å¸¸æ¢å¤
### 4.3 æƒé™æŽ§åˆ¶æ–¹æ¡ˆ
- CRM å®¢æˆ·æ²¿ç”¨çŽ°æœ‰çš„å›¢é˜Ÿæƒé™æœºåˆ¶
- ERP æ¨¡å—通过 API æŸ¥è¯¢å®¢æˆ·æ—¶ï¼Œéœ€æ ¡éªŒæ•°æ®æƒé™
---
## äº”、风险与注意事项
| é£Žé™© | åº”对措施 |
|------|----------|
| çŽ°æœ‰æ•°æ®è¿ç§»å¤æ‚ | æä¾›è¿ç§»è„šæœ¬å’Œå›žæ»šè„šæœ¬ï¼Œåˆ†æ‰¹è¿ç§» |
| è·¨æ¨¡å—调用性能 | ä½¿ç”¨ç¼“存减少 API è°ƒç”¨ï¼Œæ‰¹é‡æŸ¥è¯¢ä¼˜åŒ– |
| æƒé™æŽ§åˆ¶å¤æ‚ | ç»Ÿä¸€æƒé™æ ¡éªŒé€»è¾‘,复用 CRM æƒé™æœåŠ¡ |
| ä¸šåŠ¡æµç¨‹å˜æ›´ | å……分测试,提供用户培训文档 |
---
## å…­ã€äº¤ä»˜ç‰©
每个 Phase å®ŒæˆåŽäº¤ä»˜ï¼š
1. **SQL è„šæœ¬** - è¡¨ç»“构变更、数据迁移
2. **前端文档** - æŽ¥å£å˜æ›´ã€é¡µé¢ä¿®æ”¹è¯´æ˜Ž
3. **测试用例** - åŠŸèƒ½æµ‹è¯•ã€å›žå½’æµ‹è¯•
docs/crm_erp_integration_sql.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
-- =============================================
-- CRM ä¸Ž ERP é”€å”®ä¸€ä½“化改造 - SQL æ‰§è¡Œè®°å½•
-- =============================================
--
-- æ‰§è¡ŒçŠ¶æ€ï¼š
-- Phase 1: å®¢æˆ·æ•°æ®ç»Ÿä¸€ - å·²å®Œæˆï¼ˆæ— æ–°å¢žè¡¨ï¼‰
-- Phase 2: äº§å“æ•°æ®ç»Ÿä¸€ - å·²å®Œæˆï¼ˆMDM ç‰©æ–™è¡¨å·²å­˜åœ¨ï¼‰
-- Phase 3: åˆåŒè®¢å•关联 - SQL å·²æ‰§è¡Œ âœ“
-- Phase 4: å•†æœºè®¢å•关联 - SQL å·²æ‰§è¡Œ âœ“
-- Phase 5: è¥é”€æŠ¥è¡¨ - æ— éœ€æ–°å¢žè¡¨
-- Phase 6: æ•°æ®è¿ç§»æ¸…理 - ç”¨æˆ·æ‰‹åŠ¨æ‰§è¡Œå®Œæˆ âœ“
--
-- =============================================
-- =============================================
-- Phase 1 SQL
-- å®¢æˆ·æ•°æ®ç»Ÿä¸€ï¼šERP é”€å”®æ¨¡å—使用 CRM å®¢æˆ·
-- =============================================
-- 1. åˆ›å»º CRM å®¢æˆ· API æ¨¡å—相关表(无新增表,使用现有 crm_customer)
-- 2. æ•°æ®è¿ç§»ï¼šå°† ERP å®¢æˆ·æ•°æ®è¿ç§»åˆ° CRM å®¢æˆ·ï¼ˆå¯é€‰ï¼Œæ ¹æ®å®žé™…情况执行)
-- æ³¨æ„ï¼šæ­¤æ­¥éª¤éœ€è¦æ ¹æ®å®žé™…数据情况进行调整
-- 2.1 æŸ¥çœ‹ ERP å®¢æˆ·æ•°æ®
-- SELECT * FROM erp_customer WHERE deleted = 0;
-- 2.2 è¿ç§» ERP å®¢æˆ·åˆ° CRM å®¢æˆ·ï¼ˆç¤ºä¾‹è„šæœ¬ï¼Œéœ€æ ¹æ®å®žé™…æƒ…å†µè°ƒæ•´ï¼‰
-- INSERT INTO crm_customer (id, name, mobile, telephone, email, detail_address,
--     follow_up_status, lock_status, deal_status, owner_user_id, owner_time,
--     creator, create_time, updater, update_time, deleted, tenant_id)
-- SELECT
--     id,
--     name,
--     mobile,
--     telephone,
--     email,
--     NULL as detail_address,
--     0 as follow_up_status,
--     0 as lock_status,
--     0 as deal_status,
--     NULL as owner_user_id,
--     NOW() as owner_time,
--     creator,
--     create_time,
--     updater,
--     update_time,
--     deleted,
--     tenant_id
-- FROM erp_customer WHERE deleted = 0;
-- 3. åºŸå¼ƒ ERP å®¢æˆ·è¡¨ï¼ˆä¿ç•™è¡¨ç»“构,仅标记说明)
-- æ³¨æ„ï¼šerp_customer è¡¨ä¿ç•™ï¼Œä½†ä¸å†ä½œä¸ºä¸»æ•°æ®ä½¿ç”¨
-- åŽç»­å¯åˆ é™¤æˆ–å½’æ¡£
-- =============================================
-- Phase 3 SQL(预告)
-- åˆåŒç”Ÿæˆé”€å”®è®¢å•相关
-- =============================================
-- 4. ERP é”€å”®è®¢å•增加关联合同字段
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`;
-- 5. CRM åˆåŒå¢žåŠ å…³è”è®¢å•å­—æ®µ
ALTER TABLE `crm_contract`
ADD COLUMN `order_id` bigint DEFAULT NULL COMMENT '关联 ERP é”€å”®è®¢å•编号' AFTER `total_price`,
ADD COLUMN `order_no` varchar(32) DEFAULT NULL COMMENT 'ERP é”€å”®è®¢å•单号' AFTER `order_id`;
-- =============================================
-- Phase 4 SQL(预告)
-- å•†æœºå…³è”销售订单
-- =============================================
-- 6. CRM å•†æœºå¢žåŠ å…³è”è®¢å•å­—æ®µ
ALTER TABLE `crm_business`
ADD COLUMN `order_id` bigint DEFAULT NULL COMMENT '关联 ERP é”€å”®è®¢å•编号' AFTER `total_price`,
ADD COLUMN `order_no` varchar(32) DEFAULT NULL COMMENT 'ERP é”€å”®è®¢å•单号' AFTER `order_id`;
-- =============================================
-- æ³¨æ„äº‹é¡¹
-- =============================================
-- 1. æ‰§è¡Œ SQL å‰è¯·å¤‡ä»½æ•°æ®
-- 2. æ•°æ®è¿ç§»è„šæœ¬éœ€è¦æ ¹æ®å®žé™…æ•°æ®æƒ…å†µè°ƒæ•´
-- 3. åºŸå¼ƒçš„ ERP å®¢æˆ·è¡¨å»ºè®®ä¿ç•™ä¸€æ®µæ—¶é—´åŽå†åˆ é™¤
-- 4. åˆåŒå’Œå•†æœºå…³è”订单字段可先添加,后续功能实现时再使用
docs/erp_purchase_frontend_change.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,288 @@
# ERP é‡‡è´­ç®¡ç†æ¨¡å—完善 - å‰ç«¯å˜æ›´æ–‡æ¡£
## æ¦‚è¿°
完善 ERP é‡‡è´­ç®¡ç†æ¨¡å—,实现从采购申请到采购入库的完整流程管理。
**流程说明**:
- **采购申请**:需要 BPM å®¡æ‰¹ï¼Œå®¡æ‰¹é€šè¿‡åŽå¯ç”Ÿæˆé‡‡è´­è®¢å•
- **采购订单**:由采购申请生成,无需审批,直接可进行入库操作
---
## ä¸€ã€å‰ç½®æ¡ä»¶
### BPM æµç¨‹é…ç½®ï¼ˆé‡‡è´­ç”³è¯·ï¼‰
采购申请审批需要 BPM æµç¨‹æ”¯æŒï¼Œå¿…须先完成以下配置:
1. **创建采购申请审批分类**:在 BPM æµç¨‹åˆ†ç±»ä¸­åˆ›å»º `purchase_request_approve` åˆ†ç±»
2. **创建采购申请审批流程**:在 BPM æµç¨‹æ¨¡åž‹ä¸­åˆ›å»ºé‡‡è´­ç”³è¯·å®¡æ‰¹æµç¨‹
**执行 SQL**:
```sql
-- åˆ›å»º BPM é‡‡è´­ç”³è¯·å®¡æ‰¹åˆ†ç±»
INSERT INTO bpm_category (id, name, code, description, status, sort, creator, create_time, updater, update_time, deleted)
VALUES (101, '采购申请审批', 'purchase_request_approve', '采购申请审批流程分类', 0, 11, '1', NOW(), '1', NOW(), 0);
```
---
## äºŒã€åŠŸèƒ½æ¸…å•
| åºå· | åŠŸèƒ½æ¨¡å— | é¡µé¢è·¯å¾„ | åŠŸèƒ½è¯´æ˜Ž |
|------|----------|----------|----------|
| 1 | é‡‡è´­ç”³è¯· | `/erp/purchase/request` | éœ€æ±‚部门发起采购需求、BPM å®¡æ‰¹ã€ç”Ÿæˆé‡‡è´­è®¢å• |
| 2 | ä¾›åº”商管理 | `/erp/purchase/supplier` | ä¾›åº”商主数据档案管理 |
| 3 | é‡‡è´­è®¢å• | `/erp/purchase/order` | é‡‡è´­è®¢å•管理(无需审批,由采购申请生成) |
| 4 | é‡‡è´­å…¥åº“ | `/erp/purchase/in` | åˆ°è´§å…¥åº“、验收管理 |
| 5 | é‡‡è´­é€€è´§ | `/erp/purchase/return` | é‡‡è´­é€€è´§ç®¡ç† |
| 6 | é‡‡è´­ç»Ÿè®¡ | `/erp/purchase/statistics` | ç»Ÿè®¡åˆ†æžæŠ¥è¡¨ |
### é‡‡è´­æµç¨‹è¯´æ˜Ž
完整的采购流程为:
**采购申请 -> å®¡æ‰¹ -> é‡‡è´­è®¢å• -> å…¥åº“ -> ä»˜æ¬¾**
1. **采购申请**:需求部门发起采购需求,填写申请理由和产品明细
2. **审批**:通过 BPM å·¥ä½œæµè¿›è¡Œå®¡æ‰¹
3. **采购订单**:审批通过后,选择供应商生成采购订单(无需再审批)
4. **入库**:供应商发货后,进行入库验收
5. **付款**:根据入库情况进行付款
**详细文档**:采购申请功能详见 `docs/erp_purchase_request_frontend.md`
---
## ä¸‰ã€ä¾›åº”商管理页面
### æ¶‰åŠé¡µé¢
- `/erp/purchase/supplier/index` - ä¾›åº”商列表
- `/erp/purchase/supplier/form` - ä¾›åº”商表单
### æ–°å¢žå­—段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| creditCode | String | ç»Ÿä¸€ç¤¾ä¼šä¿¡ç”¨ä»£ç  |
| bankName | String | å¼€æˆ·é“¶è¡Œ |
| bankAccount | String | é“¶è¡Œè´¦å· |
| address | String | åœ°å€ |
| contact | String | è”系人 |
| phone | String | è”系电话 |
| email | String | é‚®ç®± |
| taxNo | String | ç¨Žå· |
| qualification | String | èµ„质信息 |
| cooperationTime | Date | åˆä½œå¼€å§‹æ—¶é—´ |
### å‰ç«¯è¡¨å•修改
```html
<el-form :model="form" :rules="rules">
  <el-row :gutter="20">
    <el-col :span="8">
      <el-form-item label="供应商名称" prop="name">
        <el-input v-model="form.name" />
      </el-form-item>
    </el-col>
    <el-col :span="8">
      <el-form-item label="统一社会信用代码" prop="creditCode">
        <el-input v-model="form.creditCode" />
      </el-form-item>
    </el-col>
    <el-col :span="8">
      <el-form-item label="税号" prop="taxNo">
        <el-input v-model="form.taxNo" />
      </el-form-item>
    </el-col>
  </el-row>
  <el-row :gutter="20">
    <el-col :span="8">
      <el-form-item label="联系人" prop="contact">
        <el-input v-model="form.contact" />
      </el-form-item>
    </el-col>
    <el-col :span="8">
      <el-form-item label="联系电话" prop="phone">
        <el-input v-model="form.phone" />
      </el-form-item>
    </el-col>
    <el-col :span="8">
      <el-form-item label="邮箱" prop="email">
        <el-input v-model="form.email" />
      </el-form-item>
    </el-col>
  </el-row>
  <el-row :gutter="20">
    <el-col :span="8">
      <el-form-item label="开户银行" prop="bankName">
        <el-input v-model="form.bankName" />
      </el-form-item>
    </el-col>
    <el-col :span="8">
      <el-form-item label="银行账号" prop="bankAccount">
        <el-input v-model="form.bankAccount" />
      </el-form-item>
    </el-col>
    <el-col :span="8">
      <el-form-item label="合作开始时间" prop="cooperationTime">
        <el-date-picker v-model="form.cooperationTime" type="date" />
      </el-form-item>
    </el-col>
  </el-row>
  <el-row :gutter="20">
    <el-col :span="16">
      <el-form-item label="地址" prop="address">
        <el-input v-model="form.address" />
      </el-form-item>
    </el-col>
    <el-col :span="8">
      <el-form-item label="状态" prop="status">
        <el-radio-group v-model="form.status">
          <el-radio :label="0">启用</el-radio>
          <el-radio :label="1">禁用</el-radio>
        </el-radio-group>
      </el-form-item>
    </el-col>
  </el-row>
  <el-form-item label="资质信息" prop="qualification">
    <el-input v-model="form.qualification" type="textarea" :rows="3" />
  </el-form-item>
</el-form>
```
---
## å››ã€é‡‡è´­å…¥åº“页面(验收功能)
### æ–°å¢žå­—段
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| inspectionStatus | Integer | éªŒæ”¶çŠ¶æ€ï¼ˆ0:待验收 1:已验收 2:验收不合格) |
| inspectionTime | DateTime | éªŒæ”¶æ—¶é—´ |
| inspectorId | Long | é‡æ”¶äººå‘˜ç¼–号 |
| inspectionResult | String | éªŒæ”¶ç»“æžœ |
### å‰ç«¯ä¿®æ”¹
```html
<!-- éªŒæ”¶æŒ‰é’® -->
<el-button @click="handleInspection(row)" v-if="row.inspectionStatus === 0">
  éªŒæ”¶
</el-button>
<!-- éªŒæ”¶å¼¹çª— -->
<el-dialog title="采购入库验收" v-model="inspectionDialogVisible">
  <el-form :model="inspectionForm">
    <el-form-item label="验收状态">
      <el-radio-group v-model="inspectionForm.inspectionStatus">
        <el-radio :label="1">验收合格</el-radio>
        <el-radio :label="2">验收不合格</el-radio>
      </el-radio-group>
    </el-form-item>
    <el-form-item label="验收结果">
      <el-input v-model="inspectionForm.inspectionResult" type="textarea" />
    </el-form-item>
  </el-form>
  <template #footer>
    <el-button @click="inspectionDialogVisible = false">取消</el-button>
    <el-button type="primary" @click="submitInspection">确定</el-button>
  </template>
</el-dialog>
```
---
## äº”、采购统计页面
### æ¶‰åŠé¡µé¢
- `/erp/purchase/statistics/index` - é‡‡è´­ç»Ÿè®¡é¦–页
- `/erp/purchase/statistics/supplier` - ä¾›åº”商供货统计
- `/erp/purchase/statistics/price` - é‡‡è´­ä»·æ ¼åˆ†æž
- `/erp/purchase/statistics/uncompleted` - æœªå®Œæˆè®¢å•跟踪
### API å˜æ›´
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /erp/purchase-statistics/summary | é‡‡è´­æ±‡æ€»ç»Ÿè®¡ |
| GET | /erp/purchase-statistics/supplier | ä¾›åº”商供货统计 |
| GET | /erp/purchase-statistics/price | é‡‡è´­ä»·æ ¼åˆ†æž |
| GET | /erp/purchase-statistics/uncompleted | æœªå®Œæˆè®¢å•跟踪 |
#### ä¾›åº”商供货统计响应
```json
{
  "code": 0,
  "data": [
    {
      "supplierId": 1,
      "supplierName": "供应商A",
      "orderCount": 10,
      "orderAmount": 100000.00,
      "inCount": 8,
      "inAmount": 80000.00,
      "returnCount": 1,
      "returnAmount": 5000.00
    }
  ],
  "msg": "操作成功"
}
```
#### é‡‡è´­ä»·æ ¼åˆ†æžå“åº”
```json
{
  "code": 0,
  "data": [
    {
      "productId": 1,
      "productName": "产品A",
      "productBarCode": "12345678",
      "avgPrice": 50.00,
      "minPrice": 45.00,
      "maxPrice": 55.00,
      "latestPrice": 52.00,
      "priceChangeRate": 4.0
    }
  ],
  "msg": "操作成功"
}
```
#### æœªå®Œæˆè®¢å•跟踪响应
```json
{
  "code": 0,
  "data": [
    {
      "orderId": 1,
      "orderNo": "PO202401001",
      "supplierName": "供应商A",
      "orderTime": "2024-01-01",
      "productNames": "产品A,产品B",
      "orderAmount": 10000.00,
      "inAmount": 5000.00,
      "uncompletedAmount": 5000.00,
      "daysOverdue": 5
    }
  ],
  "msg": "操作成功"
}
```
---
## å…­ã€æ³¨æ„äº‹é¡¹
1. **数据完整性**:供应商主数据需填写完整信息,包括信用代码、联系方式、银行信息等
2. **验收管理**:采购入库需进行验收,验收合格后才正式入库
3. **权限控制**:每个功能模块都有独立的权限控制,需配置角色权限
4. **数据追溯**:所有操作记录都有完整的创建人、创建时间、审批人、审批时间等信息
docs/erp_purchase_request_frontend.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,386 @@
# ERP é‡‡è´­ç”³è¯·åŠŸèƒ½ - å‰ç«¯å˜æ›´æ–‡æ¡£
## æ¦‚è¿°
新增 ERP é‡‡è´­ç”³è¯·åŠŸèƒ½ï¼Œå®žçŽ°ä»Žéœ€æ±‚éƒ¨é—¨å‘èµ·é‡‡è´­éœ€æ±‚åˆ°ç”Ÿæˆé‡‡è´­è®¢å•çš„å®Œæ•´æµç¨‹ç®¡ç†ã€‚
**重要说明**:采购申请审批需要集成 BPM å·¥ä½œæµï¼Œå¿…须先在 BPM æ¨¡å—创建 `purchase_request_approve` åˆ†ç±»å’Œå¯¹åº”的审批流程。
---
## ä¸€ã€å‰ç½®æ¡ä»¶
### BPM æµç¨‹é…ç½®
采购申请审批需要 BPM æµç¨‹æ”¯æŒï¼Œå¿…须先完成以下配置:
1. **创建采购申请审批分类**:在 BPM æµç¨‹åˆ†ç±»ä¸­åˆ›å»º `purchase_request_approve` åˆ†ç±»
2. **创建采购申请审批流程**:在 BPM æµç¨‹æ¨¡åž‹ä¸­åˆ›å»ºé‡‡è´­ç”³è¯·å®¡æ‰¹æµç¨‹
   - **流程标识(Key)必须为**:`erp-purchase-request-audit`
   - å…³è”到 `purchase_request_approve` åˆ†ç±»
   - å‘布并激活流程
**执行 SQL**:
```sql
-- åˆ›å»º BPM é‡‡è´­ç”³è¯·å®¡æ‰¹åˆ†ç±»
INSERT INTO bpm_category (id, name, code, description, status, sort, creator, create_time, updater, update_time, deleted)
VALUES (101, '采购申请审批', 'purchase_request_approve', '采购申请审批流程分类', 0, 11, '1', NOW(), '1', NOW(), 0);
```
**校验逻辑**:
- æäº¤é‡‡è´­ç”³è¯·æ—¶ï¼Œç³»ç»Ÿä¼šæ ¡éªŒ BPM é‡‡è´­ç”³è¯·å®¡æ‰¹åˆ†ç±»æ˜¯å¦å­˜åœ¨
- å¦‚果分类不存在,会抛出异常:`采购申请审批分类不存在,请先在BPM模块创建采购申请审批流程分类`
- å¦‚果流程标识为 `erp-purchase-request-audit` çš„æµç¨‹æœªå‘布或未激活,会抛出异常:`采购申请审批流程未发布或未激活,请先创建流程标识为[erp-purchase-request-audit]的流程模型并发布`
---
## äºŒã€æ¶‰åŠé¡µé¢
- `/erp/purchase/request/index` - é‡‡è´­ç”³è¯·åˆ—表
- `/erp/purchase/request/form` - é‡‡è´­ç”³è¯·è¡¨å•(新增/编辑)
- `/erp/purchase/request/detail` - é‡‡è´­ç”³è¯·è¯¦æƒ…
---
## ä¸‰ã€åŠŸèƒ½æ¸…å•
| åºå· | åŠŸèƒ½ | è¯´æ˜Ž |
|------|------|------|
| 1 | æ–°å¢žé‡‡è´­ç”³è¯· | åˆ›å»ºè‰ç¨¿çŠ¶æ€çš„é‡‡è´­ç”³è¯· |
| 2 | ç¼–辑采购申请 | ä¿®æ”¹è‰ç¨¿çŠ¶æ€çš„é‡‡è´­ç”³è¯· |
| 3 | åˆ é™¤é‡‡è´­ç”³è¯· | åˆ é™¤è‰ç¨¿çŠ¶æ€çš„é‡‡è´­ç”³è¯· |
| 4 | æäº¤å®¡æ‰¹ | æäº¤ BPM æµç¨‹å®¡æ‰¹ |
| 5 | ç”Ÿæˆé‡‡è´­è®¢å• | å®¡æ‰¹é€šè¿‡åŽç”Ÿæˆé‡‡è´­è®¢å• |
| 6 | å¯¼å‡º Excel | å¯¼å‡ºé‡‡è´­ç”³è¯·åˆ—表 |
---
## å››ã€API æŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /erp/purchase-request/create | åˆ›å»ºé‡‡è´­ç”³è¯· |
| PUT | /erp/purchase-request/update | æ›´æ–°é‡‡è´­ç”³è¯· |
| PUT | /erp/purchase-request/submit | æäº¤é‡‡è´­ç”³è¯·å®¡æ‰¹ |
| DELETE | /erp/purchase-request/delete | åˆ é™¤é‡‡è´­ç”³è¯· |
| GET | /erp/purchase-request/get | èŽ·å¾—é‡‡è´­ç”³è¯·è¯¦æƒ… |
| GET | /erp/purchase-request/page | èŽ·å¾—é‡‡è´­ç”³è¯·åˆ†é¡µ |
| GET | /erp/purchase-request/export-excel | å¯¼å‡ºé‡‡è´­ç”³è¯· Excel |
| POST | /erp/purchase-request/generate-order | ç”Ÿæˆé‡‡è´­è®¢å• |
| GET | /erp/purchase-request/approve-process-list | èŽ·å–å®¡æ‰¹æµç¨‹åˆ—è¡¨ |
> **文件上传**:使用通用接口 `POST /infra/file/upload`,参数 `file` + `directory=purchase-request`,返回文件 URL。详见 `docs/file_upload_guide.md`。
### 4.1 åˆ›å»ºé‡‡è´­ç”³è¯·
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| requestTime | DateTime | æ˜¯ | ç”³è¯·æ—¶é—´ |
| requestReason | String | å¦ | ç”³è¯·ç†ç”± |
| supplierId | Long | å¦ | ä¾›åº”商编号(可选) |
| discountPercent | BigDecimal | å¦ | ä¼˜æƒ çŽ‡ï¼Œç™¾åˆ†æ¯” |
| fileUrl | String | å¦ | é™„件地址 |
| remark | String | å¦ | å¤‡æ³¨ |
| items | List | æ˜¯ | ç”³è¯·æ˜Žç»†åˆ—表 |
**items æ˜Žç»†å‚数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| productId | Long | æ˜¯ | äº§å“ç¼–号 |
| productUnitId | Long | æ˜¯ | äº§å“å•位编号 |
| productPrice | BigDecimal | å¦ | äº§å“å•价(参考价) |
| count | BigDecimal | æ˜¯ | æ•°é‡ |
| taxPercent | BigDecimal | å¦ | ç¨ŽçŽ‡ï¼Œç™¾åˆ†æ¯” |
| demandTime | Date | å¦ | éœ€æ±‚日期 |
| remark | String | å¦ | å¤‡æ³¨ |
**响应:** `{ "code": 0, "data": 1001, "msg": "操作成功" }`
### 4.2 æäº¤é‡‡è´­ç”³è¯·å®¡æ‰¹
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | é‡‡è´­ç”³è¯·ç¼–号 |
| processDefinitionKey | String | æ˜¯ | æµç¨‹å®šä¹‰ Key(用户从流程列表中选择) |
**响应:** `{ "code": 0, "msg": "操作成功" }`
> **流程选择**:提交审批前,需先调用 `/erp/purchase-request/approve-process-list` èŽ·å–å¯é€‰æµç¨‹åˆ—è¡¨ï¼Œè®©ç”¨æˆ·é€‰æ‹©åŽä¼ å…¥ `processDefinitionKey`。
### 4.3 ç”Ÿæˆé‡‡è´­è®¢å•
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | æ˜¯ | é‡‡è´­ç”³è¯·ç¼–号 |
| supplierId | Long | æ˜¯ | ä¾›åº”商编号 |
**响应:** `{ "code": 0, "data": 2001, "msg": "操作成功" }`(返回采购订单编号)
### 4.4 åˆ†é¡µæŸ¥è¯¢å“åº”
```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "no": "CGSQ20260701000001",
        "status": 20,
        "requestUserId": 1,
        "requestUserName": "张三",
        "requestDeptId": 100,
        "requestDeptName": "生产部",
        "requestTime": "2026-07-01 10:00:00",
        "requestReason": "生产需要",
        "supplierId": 1,
        "supplierName": "供应商A",
        "totalCount": 100.00,
        "totalPrice": 10000.00,
        "productNames": "产品A,产品B",
        "orderId": 2001,
        "orderNo": "CGDD20260701000001",
        "createTime": "2026-07-01 10:00:00"
      }
    ],
    "total": 1
  },
  "msg": "操作成功"
}
```
---
## äº”、状态流转
| çŠ¶æ€å€¼ | çŠ¶æ€å | è¯´æ˜Ž |
|--------|--------|------|
| 0 | æœªæäº¤ | è‰ç¨¿çŠ¶æ€ï¼Œå¯ç¼–è¾‘ã€å¯åˆ é™¤ |
| 10 | å®¡æ‰¹ä¸­ | å·²æäº¤å®¡æ‰¹ï¼Œä¸å¯ä¿®æ”¹ |
| 20 | å®¡æ ¸é€šè¿‡ | å®¡æ‰¹é€šè¿‡ï¼Œå¯ç”Ÿæˆé‡‡è´­è®¢å• |
| 30 | å®¡æ ¸ä¸é€šè¿‡ | å®¡æ‰¹è¢«æ‹’绝,可重新提交 |
| 40 | å·²å–消 | æµç¨‹è¢«å–消 |
**状态流转图:**
```
草稿(0) -> æäº¤ -> å®¡æ‰¹ä¸­(10) -> å®¡æ‰¹é€šè¿‡(20) -> ç”Ÿæˆé‡‡è´­è®¢å•
                          |
                          +-> å®¡æ‰¹ä¸é€šè¿‡(30) -> é‡æ–°æäº¤ -> å®¡æ‰¹ä¸­(10)
                          |
                          +-> å·²å–消(40)
```
---
## å…­ã€å‰ç«¯ä¿®æ”¹ç‚¹
### 6.1 åˆ—表页按钮
```html
<!-- æœç´¢æ  -->
<el-form :model="queryParams" ref="queryFormRef" :inline="true">
  <el-form-item label="申请单号" prop="no">
    <el-input v-model="queryParams.no" placeholder="请输入申请单号" clearable />
  </el-form-item>
  <el-form-item label="申请人" prop="requestUserId">
    <el-select v-model="queryParams.requestUserId" placeholder="请选择申请人" clearable>
      <!-- ç”¨æˆ·åˆ—表 -->
    </el-select>
  </el-form-item>
  <el-form-item label="审批状态" prop="status">
    <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
      <el-option label="未提交" :value="0" />
      <el-option label="审批中" :value="10" />
      <el-option label="审核通过" :value="20" />
      <el-option label="审核不通过" :value="30" />
      <el-option label="已取消" :value="40" />
    </el-select>
  </el-form-item>
  <el-form-item label="申请时间" prop="requestTime">
    <el-date-picker v-model="queryParams.requestTime" type="daterange" />
  </el-form-item>
  <el-form-item>
    <el-button type="primary" @click="handleQuery">搜索</el-button>
    <el-button @click="resetQuery">重置</el-button>
  </el-form-item>
</el-form>
<!-- æ“ä½œæ  -->
<el-row :gutter="10" class="mb8">
  <el-col :span="1.5">
    <el-button type="primary" @click="handleAdd" v-hasPermi="['erp:purchase-request:create']">新增</el-button>
  </el-col>
  <el-col :span="1.5">
    <el-button type="success" @click="handleExport" v-hasPermi="['erp:purchase-request:export']">导出</el-button>
  </el-col>
</el-row>
<!-- è¡¨æ ¼æ“ä½œåˆ— -->
<el-table-column label="操作" align="center" width="200">
  <template #default="scope">
    <el-button link type="primary" @click="handleDetail(scope.row)">详情</el-button>
    <el-button link type="primary" @click="handleUpdate(scope.row)"
               v-if="scope.row.status === 0" v-hasPermi="['erp:purchase-request:update']">编辑</el-button>
    <el-button link type="primary" @click="handleSubmit(scope.row)"
               v-if="scope.row.status === 0" v-hasPermi="['erp:purchase-request:submit']">提交</el-button>
    <el-button link type="primary" @click="handleGenerateOrder(scope.row)"
               v-if="scope.row.status === 20 && !scope.row.orderId" v-hasPermi="['erp:purchase-request:generate-order']">生成订单</el-button>
    <el-button link type="danger" @click="handleDelete(scope.row)"
               v-if="scope.row.status === 0" v-hasPermi="['erp:purchase-request:delete']">删除</el-button>
  </template>
</el-table-column>
```
### 6.2 data æ•°æ®
```js
data() {
  return {
    // æŸ¥è¯¢å‚æ•°
    queryParams: {
      pageNo: 1,
      pageSize: 10,
      no: undefined,
      requestUserId: undefined,
      requestDeptId: undefined,
      requestTime: [],
      status: undefined,
      productId: undefined,
    },
    // çŠ¶æ€å­—å…¸
    statusOptions: [
      { value: 0, label: '未提交' },
      { value: 10, label: '审批中' },
      { value: 20, label: '审核通过' },
      { value: 30, label: '审核不通过' },
      { value: 40, label: '已取消' },
    ],
  }
}
```
### 6.3 æ–¹æ³•
```js
methods: {
  // æäº¤å®¡æ‰¹
  async handleSubmit(row) {
    try {
      await this.$modal.confirm('确认提交该采购申请进行审批?')
      await submitPurchaseRequest(row.id)
      this.$modal.msgSuccess('提交成功')
      this.getList()
    } catch {}
  },
  // ç”Ÿæˆé‡‡è´­è®¢å•
  async handleGenerateOrder(row) {
    try {
      // å¼¹çª—选择供应商
      const { value: supplierId } = await this.$prompt('请选择供应商', '生成采购订单', {
        inputType: 'select',
        inputOptions: this.supplierOptions,
        inputValidator: (value) => {
          if (!value) return '请选择供应商'
          return true
        }
      })
      const orderId = await generatePurchaseOrder(row.id, supplierId)
      this.$modal.msgSuccess(`生成采购订单成功,订单编号:${orderId}`)
      this.getList()
    } catch {}
  },
}
```
### 6.4 æ–‡ä»¶ä¸Šä¼ ç»„ä»¶
> ä½¿ç”¨é€šç”¨æ–‡ä»¶ä¸Šä¼ æŽ¥å£ `POST /infra/file/upload`,详见 `docs/file_upload_guide.md`。
```html
<!-- è¡¨å•中的附件上传 -->
<el-form-item label="附件" prop="fileUrl">
  <el-upload
    :action="uploadUrl"
    :headers="headers"
    :data="{ directory: 'purchase-request' }"
    :on-success="handleUploadSuccess"
    :before-upload="beforeUpload"
    :file-list="fileList"
    :limit="1"
  >
    <el-button type="primary">点击上传</el-button>
    <template #tip>
      <div class="el-upload__tip">支持 pdf、doc、docx、xls、xlsx æ ¼å¼ï¼Œæœ€å¤§ 10MB</div>
    </template>
  </el-upload>
</el-form-item>
```
```js
data() {
  return {
    // æ–‡ä»¶ä¸Šä¼ é…ç½®ï¼ˆä½¿ç”¨é€šç”¨æŽ¥å£ï¼‰
    uploadUrl: process.env.VUE_APP_BASE_API + '/infra/file/upload',
    headers: { Authorization: 'Bearer ' + getToken() },
    fileList: [],
  }
},
methods: {
  handleUploadSuccess(response) {
    if (response.code === 0) {
      this.form.fileUrl = response.data
      this.$modal.msgSuccess('上传成功')
    } else {
      this.$modal.msgError(response.msg || '上传失败')
    }
  },
  beforeUpload(file) {
    const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                          'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
    const isAllowed = allowedTypes.includes(file.type) || file.name.endsWith('.pdf') || file.name.endsWith('.doc') || file.name.endsWith('.docx')
                       || file.name.endsWith('.xls') || file.name.endsWith('.xlsx')
    if (!isAllowed) {
      this.$message.error('只支持 pdf、doc、docx、xls、xlsx æ ¼å¼!')
      return false
    }
    const isLt10M = file.size / 1024 / 1024 < 10
    if (!isLt10M) {
      this.$message.error('文件大小不能超过 10MB!')
      return false
    }
    return true
  },
}
```
---
## ä¸ƒã€æƒé™é…ç½®
| æƒé™æ ‡è¯† | è¯´æ˜Ž |
|---------|------|
| erp:purchase-request:query | æŸ¥è¯¢é‡‡è´­ç”³è¯· |
| erp:purchase-request:create | æ–°å¢žé‡‡è´­ç”³è¯· |
| erp:purchase-request:update | ä¿®æ”¹é‡‡è´­ç”³è¯· |
| erp:purchase-request:delete | åˆ é™¤é‡‡è´­ç”³è¯· |
| erp:purchase-request:submit | æäº¤å®¡æ‰¹ |
| erp:purchase-request:export | å¯¼å‡ºé‡‡è´­ç”³è¯· |
| erp:purchase-request:generate-order | ç”Ÿæˆé‡‡è´­è®¢å• |
---
## å…«ã€æ³¨æ„äº‹é¡¹
1. **状态限制**:只有草稿状态(status=0)的申请单才能编辑和删除
2. **审批流程**:提交审批前请确保 BPM æµç¨‹å·²é…ç½®å®Œæˆ
3. **生成订单**:只有审批通过(status=20)且未生成订单的申请单才能生成采购订单
4. **供应商选择**:生成采购订单时必须选择供应商,申请时可预先指定供应商
5. **数据完整性**:申请明细需填写完整的产品信息和数量
docs/file_upload_guide.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,244 @@
# æ–‡ä»¶ä¸Šä¼ é›†æˆæŒ‡å—
## æ¦‚è¿°
本系统提供了**统一的文件上传服务**,支持多种存储方式(本地、S3/OSS、数据库、FTP/SFTP)。
> **重要**:所有业务模块必须使用通用文件上传接口,**禁止为每个模块新增上传接口**。
---
## ä¸€ã€é€šç”¨ä¸Šä¼ æŽ¥å£
### 1.1 æŽ¥å£ä¿¡æ¯
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/infra/file/upload` | é€šç”¨æ–‡ä»¶ä¸Šä¼  |
### 1.2 è¯·æ±‚参数
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| file | MultipartFile | æ˜¯ | ä¸Šä¼ çš„æ–‡ä»¶ |
| directory | String | å¦ | å­˜å‚¨ç›®å½•,如 `purchase-request`、`contract` |
### 1.3 å“åº”示例
```json
{
  "code": 0,
  "data": "https://xxx.com/xxx.pdf",
  "msg": "操作成功"
}
```
返回值 `data` å³ä¸ºæ–‡ä»¶è®¿é—® URL,可直接存储到数据库。
---
## äºŒã€å‰ç«¯é›†æˆ
### 2.1 ä¸Šä¼ ç»„件示例
```html
<el-upload
  :action="uploadUrl"
  :headers="headers"
  :data="{ directory: 'purchase-request' }"
  :on-success="handleUploadSuccess"
  :before-upload="beforeUpload"
>
  <el-button type="primary">点击上传</el-button>
</el-upload>
```
### 2.2 é…ç½®è¯´æ˜Ž
```js
data() {
  return {
    // é€šç”¨ä¸Šä¼ æŽ¥å£
    uploadUrl: process.env.VUE_APP_BASE_API + '/infra/file/upload',
    // è®¤è¯å¤´
    headers: { Authorization: 'Bearer ' + getToken() },
  }
},
methods: {
  handleUploadSuccess(response) {
    if (response.code === 0) {
      this.form.fileUrl = response.data  // ä¿å­˜æ–‡ä»¶ URL
    }
  },
  beforeUpload(file) {
    // ä¸šåŠ¡å±‚æ ¡éªŒæ–‡ä»¶ç±»åž‹å’Œå¤§å°
    const isLt10M = file.size / 1024 / 1024 < 10
    if (!isLt10M) {
      this.$message.error('文件大小不能超过 10MB!')
    }
    return isLt10M
  }
}
```
---
## ä¸‰ã€ç›®å½•规范
各业务模块通过 `directory` å‚数区分存储目录,**禁止新增业务上传接口**:
| æ¨¡å— | directory | è¯´æ˜Ž |
|------|-----------|------|
| ç”¨æˆ·å¤´åƒ | `avatar` | ç”¨æˆ·å¤´åƒå›¾ç‰‡ |
| åˆåŒé™„ä»¶ | `contract` | åˆåŒç›¸å…³æ–‡ä»¶ |
| é‡‡è´­ç”³è¯· | `purchase-request` | é‡‡è´­ç”³è¯·é™„ä»¶ |
| é‡‡è´­è®¢å• | `purchase-order` | é‡‡è´­è®¢å•附件 |
| äº§å“å›¾ç‰‡ | `product` | äº§å“ç›¸å…³å›¾ç‰‡ |
| é€šç”¨é™„ä»¶ | `attachment` | é€šç”¨ä¸šåС附件 |
> **命名规范**:使用小写字母和连字符,如 `purchase-request`,与模块名保持一致。
---
## å››ã€åŽç«¯é›†æˆï¼ˆç‰¹æ®Šåœºæ™¯ï¼‰
### 4.1 åŽç«¯éœ€è¦ä¸»åŠ¨ä¸Šä¼ æ–‡ä»¶æ—¶
极少数场景下,后端需要主动上传文件(如生成报表后保存),可注入 `FileApi`:
```java
@Resource
private FileApi fileApi;
// ä¸Šä¼ æ–‡ä»¶
String url = fileApi.createFile(bytes, "报表.xlsx", "report", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
```
### 4.2 èŽ·å–ç§æœ‰æ–‡ä»¶é¢„ç­¾ååœ°å€
```java
// ç”Ÿæˆä¸´æ—¶è®¿é—®åœ°å€ï¼Œ1小时有效
String presignedUrl = fileApi.presignGetUrl(fileUrl, 3600);
```
---
## äº”、存储配置
存储配置通过管理后台动态配置,无需修改代码:
1. è®¿é—® **基础设施 > æ–‡ä»¶ç®¡ç† > æ–‡ä»¶é…ç½®**
2. æ–°å¢žé…ç½®ï¼Œé€‰æ‹©å­˜å‚¨ç±»åž‹å¹¶å¡«å†™å‚æ•°
3. ç‚¹å‡»"设置为主配置"激活使用
### æ”¯æŒçš„存储类型
| ç±»åž‹ | è¯´æ˜Ž | é€‚用场景 |
|------|------|----------|
| DB | å­˜å‚¨åˆ°æ•°æ®åº“ | å°æ–‡ä»¶ã€ä¸´æ—¶æ–‡ä»¶ |
| LOCAL | æœ¬åœ°ç£ç›˜å­˜å‚¨ | å†…网部署、开发环境 |
| FTP/SFTP | FTP æœåС噍 | å…¼å®¹è€ç³»ç»Ÿ |
| S3 | é˜¿é‡Œäº‘ OSS / è…¾è®¯äº‘ COS / MinIO | ç”Ÿäº§çŽ¯å¢ƒ |
---
## å…­ã€æ–‡ä»¶å­˜å‚¨æ–¹å¼
### 6.1 å¤šæ–‡ä»¶å­˜å‚¨ï¼ˆæŽ¨èï¼‰
使用 `List<String>` + `StringListTypeHandler`,参考 CRM è·Ÿè¿›è®°å½•模块:
**实体类:**
```java
@TableName(value = "crm_follow_up_record", autoResultMap = true)
public class CrmFollowUpRecordDO extends BaseDO {
    /**
     * å›¾ç‰‡
     */
    @TableField(typeHandler = StringListTypeHandler.class)
    private List<String> picUrls;
    /**
     * é™„ä»¶
     */
    @TableField(typeHandler = StringListTypeHandler.class)
    private List<String> fileUrls;
}
```
**数据库字段**:`VARCHAR`,存储 JSON æ•°ç»„格式如 `["url1","url2","url3"]`
**VO ç±»ï¼š**
```java
@Schema(description = "附件")
private List<String> fileUrls;
```
**前端使用:**
```js
// ä¸Šä¼ æˆåŠŸåŽè¿½åŠ 
handleUploadSuccess(response) {
  if (response.code === 0) {
    if (!this.form.fileUrls) {
      this.form.fileUrls = []
    }
    this.form.fileUrls.push(response.data)
  }
}
// åˆ é™¤æ–‡ä»¶
handleRemove(index) {
  this.form.fileUrls.splice(index, 1)
}
```
### 6.2 å•文件存储
```java
// å®žä½“ç±»
private String fileUrl;
// VO ç±»
@Schema(description = "附件地址")
private String fileUrl;
```
### 6.3 `infra_file` è¡¨çš„作用
系统 `infra_file` è¡¨è®°å½•所有上传文件的元数据(name、path、url、type、size),不与业务关联,仅用于:
- æ–‡ä»¶ç®¡ç†åŽå°æŸ¥çœ‹ä¸Šä¼ è®°å½•
- ç»Ÿè®¡å­˜å‚¨ä½¿ç”¨æƒ…况
---
## ä¸ƒã€æ³¨æ„äº‹é¡¹
1. **使用通用接口**:所有文件上传必须使用 `/infra/file/upload`,禁止新增业务上传接口
2. **区分目录**:通过 `directory` å‚数区分业务模块,便于管理和清理
3. **文件大小**:默认限制 10MB,可在 Nginx æˆ–应用层调整
4. **类型校验**:前端应校验允许的文件类型,防止上传恶意文件
5. **URL å­˜å‚¨**:上传成功后,将返回的 URL å­˜å‚¨åˆ°ä¸šåŠ¡è¡¨çš„ `fileUrl` å­—段
---
## ä¸ƒã€å¸¸è§é—®é¢˜
**Q: ä¸ºä»€ä¹ˆä¸èƒ½ä¸ºæ¯ä¸ªæ¨¡å—新增上传接口?**
A: ç»Ÿä¸€æŽ¥å£ä¾¿äºŽï¼š
- ç»Ÿä¸€ç®¡ç†å­˜å‚¨é…ç½®
- ç»Ÿä¸€æƒé™æŽ§åˆ¶å’Œå®¡è®¡
- é¿å…ä»£ç é‡å¤
- åŽç»­ç»´æŠ¤å’Œè¿ç§»
**Q: å¦‚何区分不同模块的文件?**
A: ä½¿ç”¨ `directory` å‚数,文件会存储在对应目录下。
**Q: å‰ç«¯å¦‚何限制文件类型?**
A: åœ¨ `beforeUpload` æ–¹æ³•中校验 `file.type` æˆ– `file.name` åŽç¼€ã€‚
docs/im_module_tables.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,401 @@
-- IM æ¨¡å— MySQL è¡¨ç»“æž„
-- åŸºäºŽ yudao-module-im æ¨¡å—实体类提取
-- ç”Ÿæˆæ—¶é—´: 2026-06-30
-- ========================================
-- 1. im_channel - IM é¢‘道表
-- ========================================
DROP TABLE IF EXISTS `im_channel`;
CREATE TABLE `im_channel` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `code` VARCHAR(64) NOT NULL COMMENT '频道业务码',
    `name` VARCHAR(128) NOT NULL COMMENT '频道名称',
    `avatar` VARCHAR(512) DEFAULT NULL COMMENT '频道头像',
    `sort` INT DEFAULT 0 COMMENT '排序',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态(0正常 1禁用)',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM é¢‘道表';
-- ========================================
-- 2. im_channel_material - IM é¢‘道素材表
-- ========================================
DROP TABLE IF EXISTS `im_channel_material`;
CREATE TABLE `im_channel_material` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `channel_id` BIGINT NOT NULL COMMENT '频道编号',
    `type` TINYINT NOT NULL COMMENT '素材内容类型',
    `title` VARCHAR(256) NOT NULL COMMENT '标题',
    `cover_url` VARCHAR(512) DEFAULT NULL COMMENT '封面图',
    `summary` VARCHAR(1024) DEFAULT NULL COMMENT '摘要',
    `content` TEXT DEFAULT NULL COMMENT '富文本 HTML',
    `url` VARCHAR(512) DEFAULT NULL COMMENT '跳转链接',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    KEY `idx_channel_id` (`channel_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM é¢‘道素材表';
-- ========================================
-- 3. im_conversation_read - IM ä¼šè¯è¯»ä½ç½®è¡¨
-- ========================================
DROP TABLE IF EXISTS `im_conversation_read`;
CREATE TABLE `im_conversation_read` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `user_id` BIGINT NOT NULL COMMENT '用户编号',
    `conversation_type` TINYINT NOT NULL COMMENT '会话类型(1私聊 2群聊 3频道)',
    `target_id` BIGINT NOT NULL COMMENT '目标编号',
    `message_id` BIGINT NOT NULL COMMENT '最大已读消息编号',
    `read_time` DATETIME DEFAULT NULL COMMENT '最近已读时间',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    UNIQUE KEY `uk_user_conv` (`user_id`, `conversation_type`, `target_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM ä¼šè¯è¯»ä½ç½®è¡¨';
-- ========================================
-- 4. im_face_pack - IM è¡¨æƒ…包表
-- ========================================
DROP TABLE IF EXISTS `im_face_pack`;
CREATE TABLE `im_face_pack` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `name` VARCHAR(128) NOT NULL COMMENT '表情包名称',
    `icon` VARCHAR(512) DEFAULT NULL COMMENT '表情包图标',
    `sort` INT DEFAULT 0 COMMENT '排序',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态(0正常 1禁用)',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM è¡¨æƒ…包表';
-- ========================================
-- 5. im_face_pack_item - IM è¡¨æƒ…包项表
-- ========================================
DROP TABLE IF EXISTS `im_face_pack_item`;
CREATE TABLE `im_face_pack_item` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `pack_id` BIGINT NOT NULL COMMENT '所属表情包编号',
    `url` VARCHAR(512) NOT NULL COMMENT '表情图 URL',
    `name` VARCHAR(64) DEFAULT NULL COMMENT '表情名',
    `width` INT DEFAULT NULL COMMENT '渲染宽度(像素)',
    `height` INT DEFAULT NULL COMMENT '渲染高度(像素)',
    `sort` INT DEFAULT 0 COMMENT '排序',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态(0正常 1禁用)',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    KEY `idx_pack_id` (`pack_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM è¡¨æƒ…包项表';
-- ========================================
-- 6. im_face_user_item - IM ç”¨æˆ·ç§æœ‰è¡¨æƒ…表
-- ========================================
DROP TABLE IF EXISTS `im_face_user_item`;
CREATE TABLE `im_face_user_item` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `user_id` BIGINT NOT NULL COMMENT '所属用户编号',
    `url` VARCHAR(512) NOT NULL COMMENT '表情图 URL',
    `name` VARCHAR(64) DEFAULT NULL COMMENT '表情名',
    `width` INT DEFAULT NULL COMMENT '渲染宽度(像素)',
    `height` INT DEFAULT NULL COMMENT '渲染高度(像素)',
    `sort` INT DEFAULT 0 COMMENT '排序',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM ç”¨æˆ·ç§æœ‰è¡¨æƒ…表';
-- ========================================
-- 7. im_friend - IM å¥½å‹å…³ç³»è¡¨
-- ========================================
DROP TABLE IF EXISTS `im_friend`;
CREATE TABLE `im_friend` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `user_id` BIGINT NOT NULL COMMENT '用户编号',
    `friend_user_id` BIGINT NOT NULL COMMENT '好友用户编号',
    `silent` BIT(1) DEFAULT b'0' COMMENT '是否免打扰',
    `display_name` VARCHAR(128) DEFAULT NULL COMMENT '好友展示备注',
    `add_source` TINYINT DEFAULT NULL COMMENT '添加来源',
    `pinned` BIT(1) DEFAULT b'0' COMMENT '是否置顶联系人',
    `blocked` BIT(1) DEFAULT b'0' COMMENT '是否拉黑',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '好友状态(0正常 1禁用/已删除)',
    `add_time` DATETIME DEFAULT NULL COMMENT '添加好友时间',
    `delete_time` DATETIME DEFAULT NULL COMMENT '删除好友时间',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    UNIQUE KEY `uk_user_friend` (`user_id`, `friend_user_id`),
    KEY `idx_friend_user_id` (`friend_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM å¥½å‹å…³ç³»è¡¨';
-- ========================================
-- 8. im_friend_request - IM å¥½å‹ç”³è¯·è®°å½•表
-- ========================================
DROP TABLE IF EXISTS `im_friend_request`;
CREATE TABLE `im_friend_request` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `from_user_id` BIGINT NOT NULL COMMENT '发起方用户编号',
    `to_user_id` BIGINT NOT NULL COMMENT '接收方用户编号',
    `apply_content` VARCHAR(256) DEFAULT NULL COMMENT '申请理由',
    `display_name` VARCHAR(128) DEFAULT NULL COMMENT '发起方对接收方的备注',
    `add_source` TINYINT DEFAULT NULL COMMENT '添加来源',
    `handle_result` TINYINT DEFAULT NULL COMMENT '处理结果',
    `handle_content` VARCHAR(256) DEFAULT NULL COMMENT '处理理由',
    `handle_time` DATETIME DEFAULT NULL COMMENT '处理时间',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    KEY `idx_from_user_id` (`from_user_id`),
    KEY `idx_to_user_id` (`to_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM å¥½å‹ç”³è¯·è®°å½•表';
-- ========================================
-- 9. im_group - IM ç¾¤ä¿¡æ¯è¡¨
-- ========================================
DROP TABLE IF EXISTS `im_group`;
CREATE TABLE `im_group` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `name` VARCHAR(128) NOT NULL COMMENT '群名称',
    `owner_user_id` BIGINT NOT NULL COMMENT '群主用户编号',
    `avatar` VARCHAR(512) DEFAULT NULL COMMENT '群头像',
    `notice` VARCHAR(1024) DEFAULT NULL COMMENT '群公告',
    `join_approval` BIT(1) DEFAULT b'0' COMMENT '进群是否需审批',
    `banned` BIT(1) DEFAULT b'0' COMMENT '是否封禁',
    `banned_reason` VARCHAR(256) DEFAULT NULL COMMENT '封禁原因',
    `banned_time` DATETIME DEFAULT NULL COMMENT '封禁时间',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '群状态(0正常 1已解散)',
    `dissolved_time` DATETIME DEFAULT NULL COMMENT '解散时间',
    `muted_all` BIT(1) DEFAULT b'0' COMMENT '是否全群禁言',
    `pinned_message_ids` VARCHAR(1024) DEFAULT NULL COMMENT '群置顶消息编号列表',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    KEY `idx_owner_user_id` (`owner_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM ç¾¤ä¿¡æ¯è¡¨';
-- ========================================
-- 10. im_group_member - IM ç¾¤æˆå‘˜è¡¨
-- ========================================
DROP TABLE IF EXISTS `im_group_member`;
CREATE TABLE `im_group_member` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `group_id` BIGINT NOT NULL COMMENT '群编号',
    `user_id` BIGINT NOT NULL COMMENT '用户编号',
    `display_user_name` VARCHAR(128) DEFAULT NULL COMMENT '组内显示名',
    `group_remark` VARCHAR(256) DEFAULT NULL COMMENT '群备注',
    `silent` BIT(1) DEFAULT b'0' COMMENT '是否免打扰',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '成员状态(0正常 1禁用)',
    `role` TINYINT NOT NULL DEFAULT 0 COMMENT '成员角色(0普通成员 1管理员 2群主)',
    `join_time` DATETIME DEFAULT NULL COMMENT '入群时间',
    `add_source` TINYINT DEFAULT NULL COMMENT '加入来源',
    `inviter_user_id` BIGINT DEFAULT NULL COMMENT '邀请人用户编号',
    `quit_time` DATETIME DEFAULT NULL COMMENT '退群时间',
    `mute_end_time` DATETIME DEFAULT NULL COMMENT '禁言到期时间',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    UNIQUE KEY `uk_group_user` (`group_id`, `user_id`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM ç¾¤æˆå‘˜è¡¨';
-- ========================================
-- 11. im_group_request - IM åŠ ç¾¤ç”³è¯·è®°å½•è¡¨
-- ========================================
DROP TABLE IF EXISTS `im_group_request`;
CREATE TABLE `im_group_request` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `group_id` BIGINT NOT NULL COMMENT '群编号',
    `user_id` BIGINT NOT NULL COMMENT '申请人/被邀请人用户编号',
    `inviter_user_id` BIGINT DEFAULT NULL COMMENT '邀请人用户编号',
    `apply_content` VARCHAR(256) DEFAULT NULL COMMENT '申请理由',
    `add_source` TINYINT DEFAULT NULL COMMENT '加入来源',
    `handle_result` TINYINT DEFAULT NULL COMMENT '处理结果',
    `handle_user_id` BIGINT DEFAULT NULL COMMENT '处理人用户编号',
    `handle_content` VARCHAR(256) DEFAULT NULL COMMENT '处理理由',
    `handle_time` DATETIME DEFAULT NULL COMMENT '处理时间',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    KEY `idx_group_id` (`group_id`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM åŠ ç¾¤ç”³è¯·è®°å½•è¡¨';
-- ========================================
-- 12. im_channel_message - IM é¢‘道消息表
-- ========================================
DROP TABLE IF EXISTS `im_channel_message`;
CREATE TABLE `im_channel_message` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `channel_id` BIGINT NOT NULL COMMENT '频道编号',
    `material_id` BIGINT DEFAULT NULL COMMENT '关联素材编号',
    `type` TINYINT NOT NULL COMMENT '消息类型',
    `content` TEXT DEFAULT NULL COMMENT '消息内容 JSON',
    `receiver_user_ids` VARCHAR(1024) DEFAULT NULL COMMENT '接收人编号列表',
    `send_time` DATETIME DEFAULT NULL COMMENT '发送时间',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    KEY `idx_channel_id` (`channel_id`),
    KEY `idx_send_time` (`send_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM é¢‘道消息表';
-- ========================================
-- 13. im_group_message - IM ç¾¤èŠæ¶ˆæ¯è¡¨
-- ========================================
DROP TABLE IF EXISTS `im_group_message`;
CREATE TABLE `im_group_message` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `client_message_id` VARCHAR(64) DEFAULT NULL COMMENT '客户端消息编号',
    `sender_id` BIGINT NOT NULL COMMENT '发送人编号',
    `group_id` BIGINT NOT NULL COMMENT '群编号',
    `type` TINYINT NOT NULL COMMENT '消息类型',
    `content` TEXT DEFAULT NULL COMMENT '消息内容 JSON',
    `status` TINYINT DEFAULT NULL COMMENT '消息状态',
    `send_time` DATETIME DEFAULT NULL COMMENT '发送时间',
    `receiver_user_ids` VARCHAR(1024) DEFAULT NULL COMMENT '定向接收用户编号列表',
    `at_user_ids` VARCHAR(1024) DEFAULT NULL COMMENT '@目标用户编号列表',
    `receipt_status` TINYINT DEFAULT NULL COMMENT '回执状态',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    UNIQUE KEY `uk_client_message_id` (`client_message_id`),
    KEY `idx_group_id` (`group_id`),
    KEY `idx_sender_id` (`sender_id`),
    KEY `idx_send_time` (`send_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM ç¾¤èŠæ¶ˆæ¯è¡¨';
-- ========================================
-- 14. im_private_message - IM ç§èŠæ¶ˆæ¯è¡¨
-- ========================================
DROP TABLE IF EXISTS `im_private_message`;
CREATE TABLE `im_private_message` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `client_message_id` VARCHAR(64) DEFAULT NULL COMMENT '客户端消息编号',
    `sender_id` BIGINT NOT NULL COMMENT '发送人编号',
    `receiver_id` BIGINT NOT NULL COMMENT '接收人编号',
    `type` TINYINT NOT NULL COMMENT '消息类型',
    `content` TEXT DEFAULT NULL COMMENT '消息内容 JSON',
    `status` TINYINT DEFAULT NULL COMMENT '消息状态',
    `receipt_status` TINYINT DEFAULT NULL COMMENT '回执状态',
    `send_time` DATETIME DEFAULT NULL COMMENT '发送时间',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    UNIQUE KEY `uk_client_message_id` (`client_message_id`),
    KEY `idx_sender_id` (`sender_id`),
    KEY `idx_receiver_id` (`receiver_id`),
    KEY `idx_send_time` (`send_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM ç§èŠæ¶ˆæ¯è¡¨';
-- ========================================
-- 15. im_rtc_call - IM é€šè¯è®°å½•表
-- ========================================
DROP TABLE IF EXISTS `im_rtc_call`;
CREATE TABLE `im_rtc_call` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `room` VARCHAR(64) NOT NULL COMMENT '业务通话编号(UUID)',
    `conversation_type` TINYINT NOT NULL COMMENT '会话类型',
    `media_type` TINYINT NOT NULL COMMENT '媒体类型',
    `inviter_user_id` BIGINT NOT NULL COMMENT '发起人用户编号',
    `group_id` BIGINT DEFAULT NULL COMMENT '群编号',
    `status` TINYINT NOT NULL COMMENT '通话状态',
    `end_reason` TINYINT DEFAULT NULL COMMENT '结束原因',
    `start_time` DATETIME DEFAULT NULL COMMENT '发起时间',
    `accept_time` DATETIME DEFAULT NULL COMMENT '接通时间',
    `end_time` DATETIME DEFAULT NULL COMMENT '通话结束时间',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    UNIQUE KEY `uk_room` (`room`),
    KEY `idx_inviter_user_id` (`inviter_user_id`),
    KEY `idx_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM é€šè¯è®°å½•表';
-- ========================================
-- 16. im_rtc_participant - IM é€šè¯å‚与者表
-- ========================================
DROP TABLE IF EXISTS `im_rtc_participant`;
CREATE TABLE `im_rtc_participant` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `call_id` BIGINT NOT NULL COMMENT '通话编号',
    `room` VARCHAR(64) NOT NULL COMMENT '业务通话编号',
    `user_id` BIGINT NOT NULL COMMENT '参与者用户编号',
    `role` TINYINT NOT NULL COMMENT '参与角色',
    `status` TINYINT NOT NULL COMMENT '参与状态',
    `invite_time` DATETIME DEFAULT NULL COMMENT '被邀请时间',
    `accept_time` DATETIME DEFAULT NULL COMMENT '接听时间',
    `leave_time` DATETIME DEFAULT NULL COMMENT '离开时间',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    KEY `idx_call_id` (`call_id`),
    KEY `idx_room` (`room`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM é€šè¯å‚与者表';
-- ========================================
-- 17. im_sensitive_word - IM æ•æ„Ÿè¯è¡¨
-- ========================================
DROP TABLE IF EXISTS `im_sensitive_word`;
CREATE TABLE `im_sensitive_word` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '编号',
    `word` VARCHAR(128) NOT NULL COMMENT '敏感词',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态(0正常 1禁用)',
    `creator` VARCHAR(64) DEFAULT '' COMMENT '创建者',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` VARCHAR(64) 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`),
    UNIQUE KEY `uk_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM æ•æ„Ÿè¯è¡¨';
docs/marketing_report_frontend.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,361 @@
# CRM è¥é”€æ•°æ®æŠ¥è¡¨ - å‰ç«¯æ–‡æ¡£
## æ¦‚è¿°
本次改造实现营销数据报表功能,提供客户分析、商机分析、合同分析、销售漏斗、销售业绩等多维度统计分析。
---
## ä¸€ã€æ”¹é€ æ¸…单
### åŽç«¯æ”¹é€ ï¼ˆå·²å®Œæˆï¼‰
| æ–‡ä»¶ | æ”¹åŠ¨è¯´æ˜Ž |
|------|----------|
| `CrmReportApi.java` | è¥é”€æŠ¥è¡¨ API æŽ¥å£ |
| `CrmCustomerAnalyzeRespDTO.java` | å®¢æˆ·åˆ†æžç»Ÿè®¡ DTO |
| `CrmBusinessAnalyzeRespDTO.java` | å•†æœºåˆ†æžç»Ÿè®¡ DTO |
| `CrmContractAnalyzeRespDTO.java` | åˆåŒåˆ†æžç»Ÿè®¡ DTO |
| `CrmSalesFunnelRespDTO.java` | é”€å”®æ¼æ–—统计 DTO |
| `CrmSalesPerformanceRespDTO.java` | é”€å”®ä¸šç»©ç»Ÿè®¡ DTO |
| `CrmReportApiImpl.java` | æŠ¥è¡¨ API å®žçŽ°ç±» |
| `CrmCustomerMapper.java` | æ–°å¢žç»Ÿè®¡æ–¹æ³• |
| `CrmBusinessMapper.java` | æ–°å¢žç»Ÿè®¡æ–¹æ³• |
| `CrmContractMapper.java` | æ–°å¢žç»Ÿè®¡æ–¹æ³• |
| `CrmReceivableMapper.java` | æ–°å¢žç»Ÿè®¡æ–¹æ³• |
| `CrmReceivableService.java` | æ–°å¢žç»Ÿè®¡æ–¹æ³• |
---
## äºŒã€æ¶‰åŠé¡µé¢
### CRM æŠ¥è¡¨æ¨¡å—
| é¡µé¢ | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| æŠ¥è¡¨æ¦‚览 | `/crm/report/dashboard` | æ•°æ®æ¦‚览页面 |
| å®¢æˆ·åˆ†æž | `/crm/report/customer` | å®¢æˆ·ç»Ÿè®¡åˆ†æž |
| å•†æœºåˆ†æž | `/crm/report/business` | å•†æœºç»Ÿè®¡åˆ†æž |
| åˆåŒåˆ†æž | `/crm/report/contract` | åˆåŒç»Ÿè®¡åˆ†æž |
| é”€å”®æ¼æ–— | `/crm/report/funnel` | é”€å”®æ¼æ–—图 |
| é”€å”®ä¸šç»© | `/crm/report/performance` | é”€å”®ä¸šç»©æŽ’行 |
---
## ä¸‰ã€API æŽ¥å£
### CRM è¥é”€æŠ¥è¡¨ API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /rpc-api/crm/report/get-customer-analyze | èŽ·å–å®¢æˆ·åˆ†æžç»Ÿè®¡ |
| GET | /rpc-api/crm/report/get-business-analyze | èŽ·å–å•†æœºåˆ†æžç»Ÿè®¡ |
| GET | /rpc-api/crm/report/get-contract-analyze | èŽ·å–åˆåŒåˆ†æžç»Ÿè®¡ |
| GET | /rpc-api/crm/report/get-sales-funnel | èŽ·å–é”€å”®æ¼æ–—ç»Ÿè®¡ |
| GET | /rpc-api/crm/report/get-sales-performance | èŽ·å–é”€å”®ä¸šç»©ç»Ÿè®¡ |
### è¯·æ±‚参数
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| startTime | LocalDateTime | å¦ | å¼€å§‹æ—¶é—´ |
| endTime | LocalDateTime | å¦ | ç»“束时间 |
### å“åº”示例
**客户分析:**
```json
{
  "code": 0,
  "data": {
    "totalCount": 1000,
    "todayCount": 15,
    "dealCount": 300,
    "dealRate": 30.00,
    "lockCount": 50,
    "poolCount": 200
  }
}
```
**商机分析:**
```json
{
  "code": 0,
  "data": {
    "totalCount": 500,
    "todayCount": 8,
    "totalPrice": 5000000.00,
    "successCount": 150,
    "successPrice": 2000000.00,
    "successRate": 30.00,
    "processingCount": 250,
    "failCount": 100
  }
}
```
**合同分析:**
```json
{
  "code": 0,
  "data": {
    "totalCount": 400,
    "todayCount": 5,
    "totalPrice": 3000000.00,
    "approveCount": 300,
    "approvePrice": 2500000.00,
    "processCount": 20,
    "receivablePrice": 1500000.00,
    "receivableRate": 60.00
  }
}
```
**销售漏斗:**
```json
{
  "code": 0,
  "data": {
    "stages": [
      { "name": "客户", "count": 1000, "price": 0, "conversionRate": 100.00 },
      { "name": "商机", "count": 500, "price": 5000000.00, "conversionRate": 50.00 },
      { "name": "合同", "count": 300, "price": 2500000.00, "conversionRate": 30.00 },
      { "name": "回款", "count": null, "price": 1500000.00, "conversionRate": 15.00 }
    ]
  }
}
```
**销售业绩:**
```json
{
  "code": 0,
  "data": {
    "items": [
      {
        "userId": 1,
        "userName": "张三",
        "deptName": "销售一部",
        "contractCount": 50,
        "contractPrice": 500000.00,
        "receivablePrice": 300000.00,
        "businessCount": 80,
        "businessPrice": 800000.00
      }
    ]
  }
}
```
---
## å››ã€å‰ç«¯ä¿®æ”¹ç‚¹
### 4.1 æŠ¥è¡¨æ¦‚览页面
**文件路径:** `/src/views/crm/report/dashboard.vue`
```html
<template>
  <div class="report-dashboard">
    <!-- ç»Ÿè®¡å¡ç‰‡ -->
    <el-row :gutter="20">
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">客户总数</div>
          <div class="stat-value">{{ customerAnalyze.totalCount }}</div>
          <div class="stat-footer">今日新增:{{ customerAnalyze.todayCount }}</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">商机总数</div>
          <div class="stat-value">{{ businessAnalyze.totalCount }}</div>
          <div class="stat-footer">转化率:{{ businessAnalyze.successRate }}%</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">合同金额</div>
          <div class="stat-value">Â¥{{ contractAnalyze.totalPrice }}</div>
          <div class="stat-footer">已回款:¥{{ contractAnalyze.receivablePrice }}</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">回款率</div>
          <div class="stat-value">{{ contractAnalyze.receivableRate }}%</div>
          <div class="stat-footer">审批通过:{{ contractAnalyze.approveCount }}份</div>
        </el-card>
      </el-col>
    </el-row>
    <!-- é”€å”®æ¼æ–— -->
    <el-card header="销售漏斗">
      <FunnelChart :data="funnelData" />
    </el-card>
    <!-- é”€å”®ä¸šç»©æŽ’行 -->
    <el-card header="销售业绩排行">
      <el-table :data="performanceData" stripe>
        <el-table-column prop="userName" label="销售人员" />
        <el-table-column prop="deptName" label="部门" />
        <el-table-column prop="contractCount" label="合同数量" width="100" />
        <el-table-column prop="contractPrice" label="合同金额" width="150" />
        <el-table-column prop="receivablePrice" label="回款金额" width="150" />
        <el-table-column prop="businessCount" label="商机数量" width="100" />
        <el-table-column prop="businessPrice" label="商机金额" width="150" />
      </el-table>
    </el-card>
  </div>
</template>
```
### 4.2 é”€å”®æ¼æ–—组件
**文件路径:** `/src/components/FunnelChart.vue`
```html
<template>
  <div class="funnel-chart">
    <div v-for="(stage, index) in data" :key="index" class="funnel-stage">
      <div class="funnel-bar" :style="getBarStyle(stage, index)">
        <span class="stage-name">{{ stage.name }}</span>
        <span class="stage-value">{{ stage.price > 0 ? 'Â¥' + stage.price : stage.count }}</span>
      </div>
      <div class="conversion-rate">转化率:{{ stage.conversionRate }}%</div>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    data: Array
  },
  methods: {
    getBarStyle(stage, index) {
      const maxWidth = 100
      const width = maxWidth - (index * 15)
      return {
        width: `${width}%`,
        backgroundColor: this.getColor(index)
      }
    },
    getColor(index) {
      const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C']
      return colors[index] || '#909399'
    }
  }
}
</script>
```
### 4.3 API å®šä¹‰
**文件路径:** `/src/api/crm/report.js`
```js
import request from '@/utils/request'
// èŽ·å–å®¢æˆ·åˆ†æžç»Ÿè®¡
export function getCustomerAnalyze(startTime, endTime) {
  return request({
    url: '/crm/report/get-customer-analyze',
    method: 'get',
    params: { startTime, endTime }
  })
}
// èŽ·å–å•†æœºåˆ†æžç»Ÿè®¡
export function getBusinessAnalyze(startTime, endTime) {
  return request({
    url: '/crm/report/get-business-analyze',
    method: 'get',
    params: { startTime, endTime }
  })
}
// èŽ·å–åˆåŒåˆ†æžç»Ÿè®¡
export function getContractAnalyze(startTime, endTime) {
  return request({
    url: '/crm/report/get-contract-analyze',
    method: 'get',
    params: { startTime, endTime }
  })
}
// èŽ·å–é”€å”®æ¼æ–—ç»Ÿè®¡
export function getSalesFunnel() {
  return request({
    url: '/crm/report/get-sales-funnel',
    method: 'get'
  })
}
// èŽ·å–é”€å”®ä¸šç»©ç»Ÿè®¡
export function getSalesPerformance(startTime, endTime) {
  return request({
    url: '/crm/report/get-sales-performance',
    method: 'get',
    params: { startTime, endTime }
  })
}
```
---
## äº”、图表展示建议
### 5.1 å®¢æˆ·åˆ†æžå›¾è¡¨
- **客户增长趋势图**:折线图展示每日/每周/每月新增客户
- **客户成交率饼图**:已成交 vs æœªæˆäº¤å®¢æˆ·æ¯”例
- **客户来源分布**:饼图展示不同来源的客户占比
### 5.2 å•†æœºåˆ†æžå›¾è¡¨
- **商机阶段分布**:饼图展示不同状态的商机数量
- **商机金额分布**:柱状图展示不同金额区间的商机数量
- **商机转化趋势**:折线图展示转化率变化
### 5.3 åˆåŒåˆ†æžå›¾è¡¨
- **合同金额趋势**:折线图展示合同金额变化
- **合同状态分布**:饼图展示不同审批状态的合同
- **回款进度图**:进度条展示回款完成度
### 5.4 é”€å”®æ¼æ–—图
- ä½¿ç”¨æ¨ªå‘漏斗图展示各阶段转化情况
- æ¯ä¸ªé˜¶æ®µæ˜¾ç¤ºæ•°é‡å’Œé‡‘额
- è‡ªåŠ¨è®¡ç®—è½¬åŒ–çŽ‡
---
## å…­ã€æ³¨æ„äº‹é¡¹
1. **数据权限**:报表数据需要根据用户权限过滤
2. **统计口径**:明确各类统计数据的计算逻辑
3. **时间范围**:支持自定义时间范围查询
4. **数据刷新**:考虑数据缓存和刷新策略
5. **导出功能**:支持报表数据导出为 Excel
---
## ä¸ƒã€åŽç»­è®¡åˆ’
| Phase | å†…容 | çŠ¶æ€ |
|-------|------|------|
| Phase 1 | å®¢æˆ·æ•°æ®ç»Ÿä¸€ - ERP ä½¿ç”¨ CRM å®¢æˆ· | **已完成** |
| Phase 2 | äº§å“æ•°æ®ç»Ÿä¸€ - CRM/ERP ä½¿ç”¨ MDM ç‰©æ–™ | **已完成** |
| Phase 3 | åˆåŒç”Ÿæˆé”€å”®è®¢å• | **已完成** |
| Phase 4 | é”€å”®å…¨æµç¨‹è¿½æº¯ | **已完成** |
| Phase 5 | è¥é”€æ•°æ®æŠ¥è¡¨ | **已完成** |
| Phase 6 | æ•°æ®è¿ç§»ä¸Žæ¸…理 | å¾…开始 |
docs/mdm_frontend_change.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,241 @@
# MDM ç‰©æ–™ä¸»æ•°æ®æ¨¡å—改造 - å‰ç«¯å˜æ›´æ–‡æ¡£
## æ¦‚è¿°
本次改造将 ERP、MES、WMS ä¸‰ä¸ªæ¨¡å—的物料/产品数据统一到 MDM(主数据管理)模块,消除数据孤岛。
## æ¨¡å—变更
### 1. MDM æ¨¡å—(新增)
新增 MDM ç‰©æ–™ä¸»æ•°æ®ç®¡ç†æ¨¡å—,包含以下页面:
| é¡µé¢ | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| ç‰©æ–™ç®¡ç† | /mdm/item | ç‰©æ–™ä¸»æ•°æ®ç®¡ç† |
| ç‰©æ–™åˆ†ç±» | /mdm/category | ç‰©æ–™åˆ†ç±»ç®¡ç† |
| è®¡é‡å•位 | /mdm/unit | è®¡é‡å•位管理 |
| å“ç‰Œç®¡ç† | /mdm/brand | å“ç‰Œç®¡ç† |
| ä»“库管理 | /mdm/warehouse | ä»“库主数据管理 |
### 2. ERP æ¨¡å—
ERP äº§å“æ•°æ®æ¥æºäºŽ MDM,变更如下:
| åŽŸé¡µé¢ | å˜åŒ– |
|--------|------|
| /erp/product | äº§å“æ•°æ®ä»Ž MDM èŽ·å–ï¼Œé¡µé¢ä¿ç•™ä½†æ•°æ®æºå˜æ›´ |
### 3. MES æ¨¡å—
MES ç‰©æ–™æ•°æ®æ¥æºäºŽ MDM,变更如下:
| åŽŸé¡µé¢ | å˜åŒ– |
|--------|------|
| /mes/md/item | ç‰©æ–™æ•°æ®ä»Ž MDM èŽ·å–ï¼Œé¡µé¢ä¿ç•™ä½†æ•°æ®æºå˜æ›´ |
### 4. WMS æ¨¡å—
WMS å•†å“å’Œ SKU æ•°æ®æ¥æºäºŽ MDM,变更如下:
| åŽŸé¡µé¢ | å˜åŒ– |
|--------|------|
| /wms/item | å•†å“æ•°æ®ä»Ž MDM èŽ·å–ï¼Œé¡µé¢ä¿ç•™ä½†æ•°æ®æºå˜æ›´ |
| /wms/item-sku | SKU æ•°æ®ä»Ž MDM èŽ·å–ï¼Œé¡µé¢ä¿ç•™ä½†æ•°æ®æºå˜æ›´ |
---
## API å˜æ›´è¯´æ˜Ž
### MDM ç‰©æ–™ API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mdm/item/create | åˆ›å»ºç‰©æ–™ |
| PUT | /mdm/item/update | æ›´æ–°ç‰©æ–™ |
| PUT | /mdm/item/update-status | æ›´æ–°ç‰©æ–™çŠ¶æ€ |
| DELETE | /mdm/item/delete | åˆ é™¤ç‰©æ–™ï¼ˆå®žé™…为禁用) |
| GET | /mdm/item/get | èŽ·å–ç‰©æ–™ |
| GET | /mdm/item/page | èŽ·å–ç‰©æ–™åˆ†é¡µ |
| GET | /mdm/item/list-by-ids | èŽ·å–ç‰©æ–™åˆ—è¡¨ |
**请求参数(创建):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| code | String | æ˜¯ | ç‰©æ–™ç¼–码 |
| name | String | æ˜¯ | ç‰©æ–™åç§° |
| barCode | String | å¦ | ç‰©æ–™æ¡ç  |
| specification | String | å¦ | è§„格型号 |
| categoryId | Long | æ˜¯ | ç‰©æ–™åˆ†ç±»ç¼–号 |
| unitMeasureId | Long | æ˜¯ | è®¡é‡å•位编号 |
| brandId | Long | å¦ | å“ç‰Œç¼–号 |
| itemType | Integer | æ˜¯ | ç‰©æ–™ç±»åž‹ï¼ˆ1:物料, 2:产品, 3:半成品) |
| status | Integer | å¦ | çŠ¶æ€ï¼ˆ0:启用, 1:禁用) |
| purchasePrice | BigDecimal | å¦ | é‡‡è´­ä»·æ ¼ |
| salesPrice | BigDecimal | å¦ | é”€å”®ä»·æ ¼ |
| costPrice | BigDecimal | å¦ | æˆæœ¬ä»·æ ¼ |
| safetyStock | BigDecimal | å¦ | å®‰å…¨åº“å­˜ |
| expiryDays | Integer | å¦ | ä¿è´¨æœŸå¤©æ•° |
| isBatchManaged | Boolean | å¦ | æ˜¯å¦æ‰¹æ¬¡ç®¡ç† |
| remark | String | å¦ | å¤‡æ³¨ |
**响应:**
```json
{
  "code": 0,
  "data": 1,
  "msg": "操作成功"
}
```
### MDM ç‰©æ–™åˆ†ç±» API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mdm/category/create | åˆ›å»ºåˆ†ç±» |
| PUT | /mdm/category/update | æ›´æ–°åˆ†ç±» |
| DELETE | /mdm/category/delete | åˆ é™¤åˆ†ç±» |
| GET | /mdm/category/get | èŽ·å–åˆ†ç±» |
| GET | /mdm/category/list | èŽ·å–åˆ†ç±»åˆ—è¡¨ |
| GET | /mdm/category/list-all-simple | èŽ·å–å¯ç”¨çš„åˆ†ç±»åˆ—è¡¨ |
### MDM è®¡é‡å•位 API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mdm/unit/create | åˆ›å»ºå•位 |
| PUT | /mdm/unit/update | æ›´æ–°å•位 |
| DELETE | /mdm/unit/delete | åˆ é™¤å•位 |
| GET | /mdm/unit/get | èŽ·å–å•ä½ |
| GET | /mdm/unit/page | èŽ·å–å•ä½åˆ†é¡µ |
| GET | /mdm/unit/list-by-ids | èŽ·å–å•ä½åˆ—è¡¨ |
### MDM å“ç‰Œ API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mdm/brand/create | åˆ›å»ºå“ç‰Œ |
| PUT | /mdm/brand/update | æ›´æ–°å“ç‰Œ |
| DELETE | /mdm/brand/delete | åˆ é™¤å“ç‰Œ |
| GET | /mdm/brand/get | èŽ·å–å“ç‰Œ |
| GET | /mdm/brand/page | èŽ·å–å“ç‰Œåˆ†é¡µ |
| GET | /mdm/brand/list-by-ids | èŽ·å–å“ç‰Œåˆ—è¡¨ |
### MDM ä»“库 API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mdm/warehouse/create | åˆ›å»ºä»“库 |
| PUT | /mdm/warehouse/update | æ›´æ–°ä»“库 |
| PUT | /mdm/warehouse/update-status | æ›´æ–°ä»“库状态 |
| DELETE | /mdm/warehouse/delete | åˆ é™¤ä»“库 |
| GET | /mdm/warehouse/get | èŽ·å–ä»“åº“ |
| GET | /mdm/warehouse/page | èŽ·å–ä»“åº“åˆ†é¡µ |
| GET | /mdm/warehouse/list-by-ids | èŽ·å–ä»“åº“åˆ—è¡¨ |
---
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. ERP äº§å“é¡µé¢
ERP äº§å“é¡µé¢æ”¹ä¸ºä»Ž MDM èŽ·å–æ•°æ®ï¼Œå‰ç«¯éœ€è¦ä¿®æ”¹ï¼š
**API è°ƒç”¨å˜æ›´ï¼š**
```js
// åŽŸè°ƒç”¨ ERP äº§å“ API
import { getProductPage, getProduct } from '@/api/erp/product'
// æ”¹ä¸ºè°ƒç”¨ MDM ç‰©æ–™ API(itemType=2 äº§å“ï¼‰
import { getItemPage, getItem } from '@/api/mdm/item'
// æŸ¥è¯¢æ—¶éœ€è¦ä¼ å…¥ itemType=2(产品类型)
getItemPage({ itemType: 2, pageNo: 1, pageSize: 10 })
```
**字段映射:**
| ERP å­—段 | MDM å­—段 | è¯´æ˜Ž |
|----------|----------|------|
| id | id | ç¼–号 |
| barCode | code | ç¼–码(原条码改为编码) |
| name | name | åç§° |
| spec | specification | è§„格型号 |
| categoryId | categoryId | åˆ†ç±»ç¼–号 |
| unitId | unitMeasureId | å•位编号 |
| brandId | brandId | å“ç‰Œç¼–号 |
| status | status | çŠ¶æ€ |
| purchasePrice | purchasePrice | é‡‡è´­ä»·æ ¼ |
| salePrice | salesPrice | é”€å”®ä»·æ ¼ |
| minPrice | costPrice | æœ€ä½Žä»·æ ¼æ”¹ä¸ºæˆæœ¬ä»· |
### 2. MES ç‰©æ–™é¡µé¢
MES ç‰©æ–™é¡µé¢æ”¹ä¸ºä»Ž MDM èŽ·å–æ•°æ®ï¼š
**API è°ƒç”¨å˜æ›´ï¼š**
```js
// åŽŸè°ƒç”¨ MES ç‰©æ–™ API
import { getItemPage, getItem } from '@/api/mes/md/item'
// æ”¹ä¸ºè°ƒç”¨ MDM ç‰©æ–™ API(itemType=1 ç‰©æ–™ï¼‰
import { getItemPage, getItem } from '@/api/mdm/item'
// æŸ¥è¯¢æ—¶éœ€è¦ä¼ å…¥ itemType=1(物料类型)
getItemPage({ itemType: 1, pageNo: 1, pageSize: 10 })
```
**MES ç‰¹æœ‰åŠŸèƒ½ä¿ç•™ï¼š**
- æ‰¹æ¬¡å±žæ€§é…ç½®ï¼šä»ç”± MES æ¨¡å—管理
- BOM é…ç½®ï¼šä»ç”± MES æ¨¡å—管理
- SOP/SIP é…ç½®ï¼šä»ç”± MES æ¨¡å—管理
### 3. WMS å•†å“/SKU é¡µé¢
WMS å•†å“å’Œ SKU é¡µé¢æ”¹ä¸ºä»Ž MDM èŽ·å–æ•°æ®ï¼š
**API è°ƒç”¨å˜æ›´ï¼š**
```js
// åŽŸè°ƒç”¨ WMS å•†å“ API
import { getItemPage, getItem } from '@/api/wms/item'
// æ”¹ä¸ºè°ƒç”¨ MDM ç‰©æ–™ API
import { getItemPage, getItem } from '@/api/mdm/item'
// SKU æ•°æ®
import { getItemSkuList } from '@/api/mdm/item-sku'
```
---
## æ³¨æ„äº‹é¡¹
1. **物料类型区分**:
   - itemType=1:物料(MES ä½¿ç”¨ï¼‰
   - itemType=2:产品(ERP ä½¿ç”¨ï¼‰
   - itemType=3:半成品
2. **删除操作**:MDM ç‰©æ–™åˆ é™¤å®žé™…为禁用操作,不做物理删除
3. **SKU æ¨¡å¼**:所有物料必须配置 SKU,ERP/MES æ•°æ®è¿ç§»æ—¶è‡ªåŠ¨åˆ›å»ºé»˜è®¤ SKU
4. **向后兼容**:原模块的 API æŽ¥å£ä¿ç•™ï¼Œå†…部调用 MDM API
---
## æ•°æ®åº“表
| è¡¨å | è¯´æ˜Ž |
|------|------|
| mdm_item | ç‰©æ–™ä¸»æ•°æ®è¡¨ |
| mdm_item_sku | ç‰©æ–™ SKU è¡¨ |
| mdm_item_category | ç‰©æ–™åˆ†ç±»è¡¨ |
| mdm_unit_measure | è®¡é‡å•位表 |
| mdm_brand | å“ç‰Œè¡¨ |
| mdm_warehouse | ä»“库表 |
| mdm_item_batch_config | æ‰¹æ¬¡å±žæ€§é…ç½®è¡¨ |
docs/mdm_frontend_change_phase1.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,173 @@
# MDM ä¸»æ•°æ®æ¨¡å—改造 - å‰ç«¯å˜æ›´æ–‡æ¡£ï¼ˆç¬¬ä¸€é˜¶æ®µï¼‰
## æ¦‚è¿°
本次改造将 ERP、MES、WMS ä¸‰ä¸ªæ¨¡å—的物料/产品数据统一到 MDM(主数据管理)模块。
**改造范围**:基础数据(单位)
**注意**:
- ç‰©æ–™ç±»åž‹ï¼ˆitemType)为枚举字段(1:物料, 2:产品, 3:半成品),不需要单独维护页面
- å“ç‰Œæ•°æ®ç”± WMS ç»´æŠ¤ï¼Œä¸éœ€è¦å•独的 MDM å“ç‰Œé¡µé¢
---
## ä¸€ã€MDM è®¡é‡å•位页面
### æ¶‰åŠé¡µé¢
- æ–°å¢žé¡µé¢ï¼š`/mdm/unit` - MDM è®¡é‡å•位管理
### API å˜æ›´
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mdm/unit/create | åˆ›å»ºå•位 |
| PUT | /mdm/unit/update | æ›´æ–°å•位 |
| DELETE | /mdm/unit/delete | åˆ é™¤å•位 |
| GET | /mdm/unit/get | èŽ·å–å•ä½ |
| GET | /mdm/unit/page | èŽ·å–å•ä½åˆ†é¡µ |
| GET | /mdm/unit/list-by-ids | èŽ·å–å•ä½åˆ—è¡¨ |
**请求参数(创建/更新):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ | å•位编号(更新时必填) |
| code | String | æ˜¯ | å•位编码 |
| name | String | æ˜¯ | å•位名称 |
| primaryFlag | Boolean | å¦ | æ˜¯å¦ä¸»å•位 |
| primaryId | Long | å¦ | ä¸»å•位编号 |
| changeRate | BigDecimal | å¦ | ä¸Žä¸»å•位换算比例 |
| status | Integer | æ˜¯ | çŠ¶æ€ï¼ˆ0:启用, 1:禁用) |
| remark | String | å¦ | å¤‡æ³¨ |
**响应:**
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "code": "PCS",
    "name": "个",
    "primaryFlag": true,
    "primaryId": null,
    "changeRate": 1,
    "status": 0,
    "remark": "",
    "createTime": "2024-01-01 12:00:00"
  },
  "msg": "操作成功"
}
```
---
## äºŒã€ERP äº§å“ç®¡ç†é¡µé¢ä¿®æ”¹
### æ¶‰åŠé¡µé¢
- `/erp/product` - ERP äº§å“ç®¡ç†ï¼ˆä¿ç•™ï¼Œæ•°æ®æºå˜æ›´ï¼‰
### å‰ç«¯ä¿®æ”¹ç‚¹
物料类型固定为 `itemType = 2`(产品),前端无需维护该字段。
---
## ä¸‰ã€MES ç‰©æ–™ç®¡ç†é¡µé¢ä¿®æ”¹
### æ¶‰åŠé¡µé¢
- `/mes/md/item` - MES ç‰©æ–™ç®¡ç†ï¼ˆä¿ç•™ï¼Œæ•°æ®æºå˜æ›´ï¼‰
### å‰ç«¯ä¿®æ”¹ç‚¹
物料类型固定为 `itemType = 1`(物料),前端无需维护该字段。
---
## å››ã€WMS å•†å“ç®¡ç†é¡µé¢ä¿®æ”¹
### æ¶‰åŠé¡µé¢
- `/wms/item` - WMS å•†å“ç®¡ç†ï¼ˆä¿ç•™ï¼Œæ•°æ®æºå˜æ›´ï¼‰
### å‰ç«¯ä¿®æ”¹ç‚¹
物料类型固定为 `itemType = 1`(物料),前端无需维护该字段。
---
## äº”、新增前端 API æ–‡ä»¶
### æ–‡ä»¶è·¯å¾„:`src/api/mdm/unit.js`
```js
import request from '@/utils/request'
// åˆ›å»ºè®¡é‡å•位
export function createUnit(data) {
  return request({
    url: '/mdm/unit/create',
    method: 'post',
    data: data
  })
}
// æ›´æ–°è®¡é‡å•位
export function updateUnit(data) {
  return request({
    url: '/mdm/unit/update',
    method: 'put',
    data: data
  })
}
// åˆ é™¤è®¡é‡å•位
export function deleteUnit(id) {
  return request({
    url: '/mdm/unit/delete?id=' + id,
    method: 'delete'
  })
}
// èŽ·å–è®¡é‡å•ä½
export function getUnit(id) {
  return request({
    url: '/mdm/unit/get?id=' + id,
    method: 'get'
  })
}
// èŽ·å–è®¡é‡å•ä½åˆ†é¡µ
export function getUnitPage(params) {
  return request({
    url: '/mdm/unit/page',
    method: 'get',
    params: params
  })
}
// èŽ·å–è®¡é‡å•ä½åˆ—è¡¨
export function getUnitListByIds(ids) {
  return request({
    url: '/mdm/unit/list-by-ids?ids=' + ids,
    method: 'get'
  })
}
```
---
## å…­ã€æ³¨æ„äº‹é¡¹
1. **数据兼容**:迁移期间,原模块页面保留,但数据源变更为 MDM
2. **物料类型字段**:`itemType` ä¸ºæžšä¸¾å­—段,不需要单独维护页面
   - ERP äº§å“ï¼š`itemType = 2`(产品)
   - MES ç‰©æ–™ï¼š`itemType = 1`(物料)
   - WMS å•†å“ï¼š`itemType = 1`(物料)
3. **品牌数据**:由 WMS æ¨¡å—维护,不需要单独的 MDM å“ç‰Œé¡µé¢
4. **ID æ˜ å°„**:部分数据 ID æœ‰åç§»ï¼Œæ³¨æ„æŸ¥è¯¢å…³è”数据
5. **权限配置**:需要为新页面配置菜单和权限
docs/mdm_frontend_change_phase2.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,556 @@
# MDM ä¸»æ•°æ®æ¨¡å—改造 - å‰ç«¯å˜æ›´æ–‡æ¡£ï¼ˆç¬¬äºŒé˜¶æ®µï¼‰
## æ¦‚è¿°
本次改造将 ERP、MES、WMS ä¸‰ä¸ªæ¨¡å—的物料/产品数据统一到 MDM(主数据管理)模块。
**改造范围**:物料/产品数据迁移及 SKU ç®¡ç†
---
## ä¸€ã€MDM ç‰©æ–™é¡µé¢
### æ¶‰åŠé¡µé¢
- æ–°å¢žé¡µé¢ï¼š`/mdm/item` - MDM ç‰©æ–™ä¸»æ•°æ®ç®¡ç†
### API å˜æ›´
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mdm/item/create | åˆ›å»ºç‰©æ–™ |
| PUT | /mdm/item/update | æ›´æ–°ç‰©æ–™ |
| PUT | /mdm/item/update-status | æ›´æ–°ç‰©æ–™çŠ¶æ€ |
| DELETE | /mdm/item/delete | åˆ é™¤ç‰©æ–™ï¼ˆå®žé™…为禁用) |
| GET | /mdm/item/get | èŽ·å–ç‰©æ–™ |
| GET | /mdm/item/page | èŽ·å–ç‰©æ–™åˆ†é¡µ |
| GET | /mdm/item/list-by-ids | èŽ·å–ç‰©æ–™åˆ—è¡¨ |
**请求参数(创建):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| code | String | æ˜¯ | ç‰©æ–™ç¼–码 |
| name | String | æ˜¯ | ç‰©æ–™åç§° |
| barCode | String | å¦ | ç‰©æ–™æ¡ç  |
| specification | String | å¦ | è§„格型号 |
| categoryId | Long | æ˜¯ | ç‰©æ–™åˆ†ç±»ç¼–号 |
| unitMeasureId | Long | æ˜¯ | è®¡é‡å•位编号 |
| brandId | Long | å¦ | å“ç‰Œç¼–号 |
| itemType | Integer | æ˜¯ | ç‰©æ–™ç±»åž‹ï¼ˆ1:物料, 2:产品, 3:半成品) |
| status | Integer | å¦ | çŠ¶æ€ï¼ˆ0:启用, 1:禁用) |
| purchasePrice | BigDecimal | å¦ | é‡‡è´­ä»·æ ¼ |
| salesPrice | BigDecimal | å¦ | é”€å”®ä»·æ ¼ |
| costPrice | BigDecimal | å¦ | æˆæœ¬ä»·æ ¼ |
| safetyStock | BigDecimal | å¦ | å®‰å…¨åº“å­˜ |
| minStock | BigDecimal | å¦ | æœ€ä½Žåº“å­˜ |
| maxStock | BigDecimal | å¦ | æœ€é«˜åº“å­˜ |
| expiryDay | Integer | å¦ | ä¿è´¨æœŸå¤©æ•° |
| isBatchManaged | Boolean | å¦ | æ˜¯å¦æ‰¹æ¬¡ç®¡ç† |
| weight | BigDecimal | å¦ | é‡é‡ï¼ˆkg) |
| remark | String | å¦ | å¤‡æ³¨ |
**响应:**
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "code": "ITEM001",
    "name": "螺丝",
    "barCode": "12345678",
    "specification": "M8*20",
    "categoryId": 1,
    "categoryName": "五金配件",
    "unitMeasureId": 1,
    "unitMeasureName": "个",
    "brandId": null,
    "brandName": null,
    "itemType": 1,
    "status": 0,
    "purchasePrice": 10.00,
    "salesPrice": 15.00,
    "costPrice": 8.00,
    "safetyStock": 100,
    "expiryDay": null,
    "isBatchManaged": false,
    "weight": 0.01,
    "remark": "",
    "createTime": "2024-01-01 12:00:00"
  },
  "msg": "操作成功"
}
```
---
## äºŒã€ERP äº§å“é¡µé¢ä¿®æ”¹
### æ¶‰åŠé¡µé¢
- `/erp/product` - ERP äº§å“ç®¡ç†ï¼ˆä¿ç•™ï¼Œæ•°æ®æºå˜æ›´ï¼‰
### å‰ç«¯ä¿®æ”¹ç‚¹
#### 1. API è°ƒç”¨å˜æ›´
```js
// åŽŸè°ƒç”¨ ERP äº§å“ API
import { getProductPage, getProduct, createProduct, updateProduct } from '@/api/erp/product'
// æ”¹ä¸ºè°ƒç”¨ MDM ç‰©æ–™ API(itemType=2 äº§å“ï¼‰
import { getItemPage, getItem, createItem, updateItem } from '@/api/mdm/item'
```
#### 2. å­—段映射
| ERP å­—段 | MDM å­—段 | è¯´æ˜Ž |
|----------|----------|------|
| id | id | ç¼–号 |
| barCode | barCode | æ¡ç ï¼ˆåŽŸ ERP ç”¨ barCode,现统一) |
| barCode | code | ç¼–码(原条码改为编码) |
| name | name | åç§° |
| standard | specification | è§„格型号 |
| categoryId | categoryId | åˆ†ç±»ç¼–号 |
| unitId | unitMeasureId | å•位编号 |
| status | status | çŠ¶æ€ |
| purchasePrice | purchasePrice | é‡‡è´­ä»·æ ¼ |
| salePrice | salesPrice | é”€å”®ä»·æ ¼ |
| minPrice | minPrice | æœ€ä½Žä»·æ ¼ |
| expiryDay | expiryDay | ä¿è´¨æœŸå¤©æ•° |
| weight | weight | é‡é‡ |
| remark | remark | å¤‡æ³¨ |
| - | itemType = 2 | äº§å“ç±»åž‹ |
#### 3. data æ•°æ®ä¿®æ”¹
```js
data() {
  return {
    // æŸ¥è¯¢å‚æ•°
    queryParams: {
      pageNo: 1,
      pageSize: 10,
      code: '',
      name: '',
      categoryId: null,
      itemType: 2,  // å›ºå®šä¸ºäº§å“ç±»åž‹
      status: null
    }
  }
}
```
#### 4. æŸ¥è¯¢æ–¹æ³•修改
```js
getList() {
  this.loading = true
  getItemPage({
    ...this.queryParams,
    itemType: 2  // äº§å“ç±»åž‹
  }).then(response => {
    this.list = response.data.list
    this.total = response.data.total
    this.loading = false
  })
}
```
#### 5. è¡¨å•字段映射修改
```html
<!-- åŽŸå­—æ®µå -->
<el-input v-model="form.barCode" placeholder="产品条码" />
<!-- æ”¹ä¸º -->
<el-input v-model="form.barCode" placeholder="产品条码" />
<el-input v-model="form.code" placeholder="产品编码" />
```
#### 6. è¡¨æ ¼åˆ—修改
```html
<!-- åŽŸè¡¨æ ¼åˆ— -->
<el-table-column prop="barCode" label="产品条码" />
<el-table-column prop="standard" label="规格" />
<!-- æ”¹ä¸º -->
<el-table-column prop="code" label="产品编码" />
<el-table-column prop="barCode" label="产品条码" />
<el-table-column prop="specification" label="规格型号" />
```
---
## ä¸‰ã€MES ç‰©æ–™é¡µé¢ä¿®æ”¹
### æ¶‰åŠé¡µé¢
- `/mes/md/item` - MES ç‰©æ–™ç®¡ç†ï¼ˆä¿ç•™ï¼Œæ•°æ®æºå˜æ›´ï¼‰
### å‰ç«¯ä¿®æ”¹ç‚¹
#### 1. API è°ƒç”¨å˜æ›´
```js
// åŽŸè°ƒç”¨ MES ç‰©æ–™ API
import { getItemPage, getItem, createItem, updateItem } from '@/api/mes/md/item'
// æ”¹ä¸ºè°ƒç”¨ MDM ç‰©æ–™ API(itemType=1 ç‰©æ–™ï¼‰
import { getItemPage, getItem, createItem, updateItem } from '@/api/mdm/item'
```
#### 2. å­—段映射
| MES å­—段 | MDM å­—段 | è¯´æ˜Ž |
|----------|----------|------|
| id | id | ç‰©æ–™ç¼–号 |
| code | code | ç‰©æ–™ç¼–码 |
| name | name | ç‰©æ–™åç§° |
| specification | specification | è§„格型号 |
| itemTypeId | categoryId | åˆ†ç±»ç¼–号 |
| unitMeasureId | unitMeasureId | å•位编号 |
| status | status | çŠ¶æ€ |
| minStock | minStock | æœ€ä½Žåº“å­˜ |
| maxStock | maxStock | æœ€é«˜åº“å­˜ |
| batchFlag | isBatchManaged | æ˜¯å¦æ‰¹æ¬¡ç®¡ç† |
| highValue | highValue | æ˜¯å¦é«˜å€¼ç‰©æ–™ |
| safeStockFlag | - | æ˜¯å¦å¯ç”¨å®‰å…¨åº“存(计算 safetyStock) |
| remark | remark | å¤‡æ³¨ |
| - | itemType = 1 | ç‰©æ–™ç±»åž‹ |
#### 3. MES ç‰¹æœ‰åŠŸèƒ½ä¿ç•™
以下功能仍由 MES æ¨¡å—管理,不在 MDM ä¸­ï¼š
- **批次属性配置**:`/mes/md/item/batch-config` - ç‰©æ–™æ‰¹æ¬¡å±žæ€§é…ç½®
- **BOM é…ç½®**:`/mes/md/product-bom` - äº§å“ BOM é…ç½®
- **SOP é…ç½®**:`/mes/md/product-sop` - äº§å“ SOP é…ç½®
- **SIP é…ç½®**:`/mes/md/product-sip` - äº§å“ SIP é…ç½®
这些页面需要通过物料 ID å…³è” MDM ç‰©æ–™æ•°æ®ï¼ŒæŸ¥è¯¢æ—¶ä½¿ç”¨ `list-by-ids` æŽ¥å£èŽ·å–ç‰©æ–™åŸºç¡€ä¿¡æ¯ã€‚
#### 4. æ‰¹æ¬¡é…ç½®é¡µé¢ä¿®æ”¹
```js
// èŽ·å–ç‰©æ–™ä¿¡æ¯æ—¶ä½¿ç”¨ MDM API
import { getItemListByIds } from '@/api/mdm/item'
// æ‰¹æ¬¡é…ç½®åˆ—表页面
methods: {
  // èŽ·å–ç‰©æ–™åç§°
  getItemNames(itemIds) {
    getItemListByIds(itemIds).then(response => {
      const itemMap = {}
      response.data.forEach(item => {
        itemMap[item.id] = item.name
      })
      this.itemMap = itemMap
    })
  }
}
```
---
## å››ã€WMS å•†å“/SKU é¡µé¢ä¿®æ”¹
### æ¶‰åŠé¡µé¢
- `/wms/item` - WMS å•†å“ç®¡ç†ï¼ˆä¿ç•™ï¼Œæ•°æ®æºå˜æ›´ï¼‰
- `/wms/item-sku` - WMS SKU ç®¡ç†ï¼ˆä¿ç•™ï¼Œæ•°æ®æºå˜æ›´ï¼‰
### å‰ç«¯ä¿®æ”¹ç‚¹
#### 1. API è°ƒç”¨å˜æ›´
```js
// åŽŸè°ƒç”¨ WMS å•†å“ API
import { getItemPage, getItem } from '@/api/wms/item'
// æ”¹ä¸ºè°ƒç”¨ MDM ç‰©æ–™ API
import { getItemPage, getItem } from '@/api/mdm/item'
// SKU æ•°æ®
import { getSkuPage, getSku } from '@/api/mdm/item-sku'
```
#### 2. å­—段映射
| WMS å•†å“å­—段 | MDM ç‰©æ–™å­—段 | è¯´æ˜Ž |
|--------------|--------------|------|
| id | id | å•†å“ç¼–号 |
| code | code | å•†å“ç¼–码 |
| name | name | å•†å“åç§° |
| unit | - | å•位(需从 MDM å•位表获取名称) |
| categoryId | categoryId | åˆ†ç±»ç¼–号 |
| brandId | brandId | å“ç‰Œç¼–号 |
| remark | remark | å¤‡æ³¨ |
| WMS SKU å­—段 | MDM SKU å­—段 | è¯´æ˜Ž |
|--------------|--------------|------|
| id | id | SKU ç¼–号 |
| itemId | itemId | å•†å“ç¼–号 |
| code | code | SKU ç¼–码 |
| name | name | SKU åç§° |
| barCode | barCode | SKU æ¡ç  |
| length | length | é•¿åº¦ï¼ˆcm) |
| width | width | å®½åº¦ï¼ˆcm) |
| height | height | é«˜åº¦ï¼ˆcm) |
| grossWeight | grossWeight | æ¯›é‡ï¼ˆkg) |
| netWeight | netWeight | å‡€é‡ï¼ˆkg) |
| costPrice | costPrice | æˆæœ¬ä»· |
| sellingPrice | salePrice | é”€å”®ä»· |
#### 3. å•位名称获取
```js
// WMS å•†å“åŽŸæ¥ unit æ˜¯å­—符串,现在需要从 MDM å•位获取
import { getUnitListByIds } from '@/api/mdm/unit'
methods: {
  // èŽ·å–å•ä½åç§°
  loadUnitNames() {
    const unitIds = this.list.map(item => item.unitMeasureId).filter(id => id)
    if (unitIds.length > 0) {
      getUnitListByIds(unitIds.join(',')).then(response => {
        const unitMap = {}
        response.data.forEach(unit => {
          unitMap[unit.id] = unit.name
        })
        this.unitMap = unitMap
      })
    }
  }
}
```
---
## äº”、新增前端 API æ–‡ä»¶
### æ–‡ä»¶è·¯å¾„:`src/api/mdm/item.js`
```js
import request from '@/utils/request'
// åˆ›å»ºç‰©æ–™
export function createItem(data) {
  return request({
    url: '/mdm/item/create',
    method: 'post',
    data: data
  })
}
// æ›´æ–°ç‰©æ–™
export function updateItem(data) {
  return request({
    url: '/mdm/item/update',
    method: 'put',
    data: data
  })
}
// æ›´æ–°ç‰©æ–™çŠ¶æ€
export function updateItemStatus(id, status) {
  return request({
    url: '/mdm/item/update-status?id=' + id + '&status=' + status,
    method: 'put'
  })
}
// åˆ é™¤ç‰©æ–™
export function deleteItem(id) {
  return request({
    url: '/mdm/item/delete?id=' + id,
    method: 'delete'
  })
}
// èŽ·å–ç‰©æ–™
export function getItem(id) {
  return request({
    url: '/mdm/item/get?id=' + id,
    method: 'get'
  })
}
// èŽ·å–ç‰©æ–™åˆ†é¡µ
export function getItemPage(params) {
  return request({
    url: '/mdm/item/page',
    method: 'get',
    params: params
  })
}
// èŽ·å–ç‰©æ–™åˆ—è¡¨
export function getItemListByIds(ids) {
  return request({
    url: '/mdm/item/list-by-ids?ids=' + ids,
    method: 'get'
  })
}
```
### æ–‡ä»¶è·¯å¾„:`src/api/mdm/item-sku.js`
```js
import request from '@/utils/request'
// åˆ›å»º SKU
export function createSku(data) {
  return request({
    url: '/mdm/item-sku/create',
    method: 'post',
    data: data
  })
}
// æ›´æ–° SKU
export function updateSku(data) {
  return request({
    url: '/mdm/item-sku/update',
    method: 'put',
    data: data
  })
}
// åˆ é™¤ SKU
export function deleteSku(id) {
  return request({
    url: '/mdm/item-sku/delete?id=' + id,
    method: 'delete'
  })
}
// èŽ·å– SKU
export function getSku(id) {
  return request({
    url: '/mdm/item-sku/get?id=' + id,
    method: 'get'
  })
}
// èŽ·å– SKU åˆ†é¡µ
export function getSkuPage(params) {
  return request({
    url: '/mdm/item-sku/page',
    method: 'get',
    params: params
  })
}
// èŽ·å–ç‰©æ–™çš„ SKU åˆ—表
export function getSkuListByItemId(itemId) {
  return request({
    url: '/mdm/item-sku/list-by-item-id?itemId=' + itemId,
    method: 'get'
  })
}
```
---
## å…­ã€é€‰æ‹©å™¨ç»„件修改
### ç‰©æ–™é€‰æ‹©å™¨ç»„ä»¶
各模块使用物料选择器时,需要传入 `itemType` å‚数:
```vue
<!-- ERP äº§å“é€‰æ‹©å™¨ -->
<mdm-item-selector :item-type="2" @select="handleProductSelect" />
<!-- MES ç‰©æ–™é€‰æ‹©å™¨ -->
<mdm-item-selector :item-type="1" @select="handleItemSelect" />
<!-- WMS å•†å“é€‰æ‹©å™¨ -->
<mdm-item-selector :item-type="1" @select="handleItemSelect" />
```
### ç»„件实现示例
```vue
<template>
  <el-dialog title="选择物料" :visible.sync="visible" width="800px">
    <el-table :data="list" @row-click="handleRowClick">
      <el-table-column prop="code" label="编码" width="120" />
      <el-table-column prop="name" label="名称" width="180" />
      <el-table-column prop="specification" label="规格型号" width="120" />
      <el-table-column prop="categoryName" label="分类" width="120" />
      <el-table-column prop="unitMeasureName" label="单位" width="80" />
    </el-table>
    <pagination :total="total" :page.sync="queryParams.pageNo" @pagination="getList" />
  </el-dialog>
</template>
<script>
import { getItemPage } from '@/api/mdm/item'
export default {
  name: 'MdmItemSelector',
  props: {
    itemType: {
      type: Number,
      default: null
    }
  },
  data() {
    return {
      visible: false,
      list: [],
      total: 0,
      queryParams: {
        pageNo: 1,
        pageSize: 10
      }
    }
  },
  methods: {
    show() {
      this.visible = true
      this.getList()
    },
    getList() {
      getItemPage({
        ...this.queryParams,
        itemType: this.itemType,
        status: 0  // åªé€‰æ‹©å¯ç”¨çš„
      }).then(response => {
        this.list = response.data.list
        this.total = response.data.total
      })
    },
    handleRowClick(row) {
      this.$emit('select', row)
      this.visible = false
    }
  }
}
</script>
```
---
## ä¸ƒã€æ³¨æ„äº‹é¡¹
1. **物料类型区分**:
   - `itemType=1`:物料(MES ä½¿ç”¨ï¼‰
   - `itemType=2`:产品(ERP ä½¿ç”¨ï¼‰
   - `itemType=3`:半成品
2. **删除操作**:MDM ç‰©æ–™åˆ é™¤å®žé™…为禁用操作,不做物理删除
3. **SKU æ¨¡å¼**:所有物料必须配置 SKU,ERP/MES æ•°æ®è¿ç§»æ—¶è‡ªåŠ¨åˆ›å»ºé»˜è®¤ SKU
4. **向后兼容**:原模块的 API æŽ¥å£ä¿ç•™ï¼Œå†…部调用 MDM API
5. **数据关联**:
   - MES æ‰¹æ¬¡é…ç½®ã€BOM ç­‰é€šè¿‡ç‰©æ–™ ID å…³è”
   - WMS åº“存、出入库等通过 SKU ID å…³è”
6. **权限配置**:需要为新页面配置菜单和权限
docs/mdm_migration_plan.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,260 @@
# MDM ä¸»æ•°æ®æ¨¡å—改造计划
## ä¸€ã€æ”¹é€ èƒŒæ™¯
当前 ERP、MES、WMS ä¸‰ä¸ªæ¨¡å—各自维护产品/物料数据,存在数据孤岛问题:
- **ERP**:维护产品数据(erp_product)、产品分类(erp_product_category)、计量单位(erp_product_unit)
- **MES**:维护物料数据(mes_md_item)、物料分类(mes_md_item_type)、计量单位(mes_md_unit_measure)
- **WMS**:维护商品数据(wms_item)、商品分类(wms_item_category)、品牌(wms_item_brand)、SKU(wms_item_sku)
改造目标:将基础产品数据统一到 MDM æ¨¡å—管理,三个模块通过 API è°ƒç”¨èŽ·å–æ•°æ®ã€‚
---
## äºŒã€æ”¹é€ é˜¶æ®µè§„划
### ç¬¬ä¸€é˜¶æ®µï¼šåŸºç¡€æ•°æ®è¿ç§»ï¼ˆé¢„计 1 å¤©ï¼‰
**目标**:完成分类、单位、品牌等基础数据的迁移
| åºå· | ä»»åŠ¡ | æ¨¡å— | ä¼˜å…ˆçº§ |
|------|------|------|--------|
| 1.1 | è¿ç§» ERP äº§å“åˆ†ç±»åˆ° MDM ç‰©æ–™åˆ†ç±» | ERP | P0 |
| 1.2 | è¿ç§» MES ç‰©æ–™åˆ†ç±»åˆ° MDM ç‰©æ–™åˆ†ç±» | MES | P0 |
| 1.3 | è¿ç§» WMS å•†å“åˆ†ç±»åˆ° MDM ç‰©æ–™åˆ†ç±» | WMS | P0 |
| 1.4 | è¿ç§» ERP è®¡é‡å•位到 MDM è®¡é‡å•位 | ERP | P0 |
| 1.5 | è¿ç§» MES è®¡é‡å•位到 MDM è®¡é‡å•位 | MES | P0 |
| 1.6 | è¿ç§» WMS å•†å“å“ç‰Œåˆ° MDM å“ç‰Œ | WMS | P0 |
**产出物**:
- SQL æ•°æ®è¿ç§»è„šæœ¬
- å‰ç«¯å˜æ›´æ–‡æ¡£
---
### ç¬¬äºŒé˜¶æ®µï¼šç‰©æ–™æ•°æ®è¿ç§»ï¼ˆé¢„计 1 å¤©ï¼‰
**目标**:完成物料/产品主数据迁移
| åºå· | ä»»åŠ¡ | æ¨¡å— | ä¼˜å…ˆçº§ |
|------|------|------|--------|
| 2.1 | è¿ç§» MES ç‰©æ–™åˆ° MDM(itemType=1 ç‰©æ–™ï¼‰ | MES | P0 |
| 2.2 | è¿ç§» ERP äº§å“åˆ° MDM(itemType=2 äº§å“ï¼‰ | ERP | P0 |
| 2.3 | è¿ç§» WMS å•†å“åˆ° MDM | WMS | P0 |
| 2.4 | åˆ›å»ºç‰©æ–™ SKU æ•°æ® | MDM | P0 |
**产出物**:
- SQL æ•°æ®è¿ç§»è„šæœ¬
- å‰ç«¯å˜æ›´æ–‡æ¡£
---
### ç¬¬ä¸‰é˜¶æ®µï¼šæ¨¡å—适配改造(预计 4 å¤©ï¼‰
**目标**:ERP/MES/WMS æ¨¡å—适配 MDM æ•°æ®æº
| åºå· | ä»»åŠ¡ | æ¨¡å— | ä¼˜å…ˆçº§ |
|------|------|------|--------|
| 3.1 | ERP æ¨¡å—引用 MDM API,修改 Service å±‚ | ERP | P0 |
| 3.2 | ERP æ¨¡å—修改 Controller å±‚响应结构 | ERP | P1 |
| 3.3 | MES æ¨¡å—引用 MDM API,修改 Service å±‚ | MES | P0 |
| 3.4 | MES æ¨¡å—修改 Controller å±‚响应结构 | MES | P1 |
| 3.5 | WMS æ¨¡å—引用 MDM API,修改 Service å±‚ | WMS | P0 |
| 3.6 | WMS æ¨¡å—修改 Controller å±‚响应结构 | WMS | P1 |
**产出物**:
- åŽç«¯ä»£ç æ”¹é€ 
- å‰ç«¯å˜æ›´æ–‡æ¡£
---
### ç¬¬å››é˜¶æ®µï¼šå‰ç«¯é€‚配(预计 3 å¤©ï¼‰
**目标**:前端页面适配新数据结构
| åºå· | ä»»åŠ¡ | æ¨¡å— | ä¼˜å…ˆçº§ |
|------|------|------|--------|
| 4.1 | ERP äº§å“ç®¡ç†é¡µé¢é€‚配 | ERP | P1 |
| 4.2 | MES ç‰©æ–™ç®¡ç†é¡µé¢é€‚配 | MES | P1 |
| 4.3 | WMS å•†å“ç®¡ç†é¡µé¢é€‚配 | WMS | P1 |
| 4.4 | å„模块选择器组件适配 | å…¨éƒ¨ | P1 |
**产出物**:
- å‰ç«¯ä»£ç æ”¹é€ 
- å‰ç«¯å˜æ›´æ–‡æ¡£
---
## ä¸‰ã€æ•°æ®å­—段映射
### 3.1 ERP äº§å“ â†’ MDM ç‰©æ–™
| ERP å­—段 | MDM å­—段 | è¯´æ˜Ž |
|----------|----------|------|
| id | id | äº§å“ç¼–号 |
| name | name | äº§å“åç§° |
| barCode | barCode | äº§å“æ¡ç  |
| categoryId | categoryId | åˆ†ç±»ç¼–号 |
| unitId | unitMeasureId | å•位编号 |
| standard | specification | è§„格型号 |
| status | status | çŠ¶æ€ |
| purchasePrice | purchasePrice | é‡‡è´­ä»·æ ¼ |
| salePrice | salesPrice | é”€å”®ä»·æ ¼ |
| minPrice | minPrice | æœ€ä½Žä»·æ ¼ |
| expiryDay | expiryDay | ä¿è´¨æœŸå¤©æ•° |
| weight | weight | é‡é‡ |
| remark | remark | å¤‡æ³¨ |
| - | itemType = 2 | äº§å“ç±»åž‹ |
### 3.2 MES ç‰©æ–™ â†’ MDM ç‰©æ–™
| MES å­—段 | MDM å­—段 | è¯´æ˜Ž |
|----------|----------|------|
| id | id | ç‰©æ–™ç¼–号 |
| code | code | ç‰©æ–™ç¼–码 |
| name | name | ç‰©æ–™åç§° |
| specification | specification | è§„格型号 |
| itemTypeId | categoryId | åˆ†ç±»ç¼–号 |
| unitMeasureId | unitMeasureId | å•位编号 |
| status | status | çŠ¶æ€ |
| minStock | minStock | æœ€ä½Žåº“å­˜ |
| maxStock | maxStock | æœ€é«˜åº“å­˜ |
| batchFlag | isBatchManaged | æ˜¯å¦æ‰¹æ¬¡ç®¡ç† |
| highValue | highValue | æ˜¯å¦é«˜å€¼ç‰©æ–™ |
| safeStockFlag | - | æ˜¯å¦å¯ç”¨å®‰å…¨åº“存(转为 safetyStock è®¡ç®—) |
| remark | remark | å¤‡æ³¨ |
| - | itemType = 1 | ç‰©æ–™ç±»åž‹ |
### 3.3 WMS å•†å“ â†’ MDM ç‰©æ–™
| WMS å­—段 | MDM å­—段 | è¯´æ˜Ž |
|----------|----------|------|
| id | id | å•†å“ç¼–号 |
| code | code | å•†å“ç¼–码 |
| name | name | å•†å“åç§° |
| unit | - | å•位(需关联 MDM å•位) |
| categoryId | categoryId | åˆ†ç±»ç¼–号 |
| brandId | brandId | å“ç‰Œç¼–号 |
| remark | remark | å¤‡æ³¨ |
| - | itemType = 1 | é»˜è®¤ç‰©æ–™ç±»åž‹ |
### 3.4 WMS SKU â†’ MDM SKU
| WMS SKU å­—段 | MDM SKU å­—段 | è¯´æ˜Ž |
|--------------|--------------|------|
| id | id | SKU ç¼–号 |
| itemId | itemId | ç‰©æ–™ç¼–号 |
| code | code | SKU ç¼–码 |
| name | name | SKU åç§° |
| barCode | barCode | SKU æ¡ç  |
| length | length | é•¿åº¦ |
| width | width | å®½åº¦ |
| height | height | é«˜åº¦ |
| grossWeight | grossWeight | æ¯›é‡ |
| netWeight | netWeight | å‡€é‡ |
| costPrice | costPrice | æˆæœ¬ä»· |
| sellingPrice | salePrice | é”€å”®ä»· |
---
## å››ã€æ³¨æ„äº‹é¡¹
1. **数据合并策略**:分类、单位等基础数据可能存在重复,需按编码去重合并
2. **ID å…¼å®¹**:迁移后保持原 ID ä¸å˜ï¼Œé¿å…å…³è”失效
3. **向后兼容**:原模块 API ä¿ç•™ï¼Œå†…部调用 MDM API å®žçް
4. **数据验证**:每阶段完成后进行数据完整性验证
---
## äº”、风险评估
| é£Žé™©é¡¹ | é£Žé™©ç­‰çº§ | åº”对措施 |
|--------|----------|----------|
| æ•°æ®åˆå¹¶å†²çª | ä¸­ | åˆ¶å®šåˆå¹¶è§„则,人工审核冲突数据 |
| å‰ç«¯é€‚配遗漏 | ä¸­ | ç¼–写详细变更文档,逐页面核对 |
| æ€§èƒ½ä¸‹é™ | ä½Ž | MDM API å¢žåŠ ç¼“å­˜æœºåˆ¶ |
---
## å…­ã€äº§å‡ºç‰©æ¸…单
| æ–‡ä»¶ | è¯´æ˜Ž | è·¯å¾„ |
|------|------|------|
| mdm_migration_plan.md | æ”¹é€ è®¡åˆ’总文档 | docs/ |
| mdm_migration_01_basic_data.sql | ç¬¬ä¸€é˜¶æ®µ SQL:基础数据迁移 | docs/sql/ |
| mdm_migration_02_item_data.sql | ç¬¬äºŒé˜¶æ®µ SQL:物料数据迁移 | docs/sql/ |
| mdm_frontend_change_phase1.md | ç¬¬ä¸€é˜¶æ®µå‰ç«¯å˜æ›´æ–‡æ¡£ | docs/ |
| mdm_frontend_change_phase2.md | ç¬¬äºŒé˜¶æ®µå‰ç«¯å˜æ›´æ–‡æ¡£ | docs/ |
| mdm_frontend_change.md | MDM API æ€»è§ˆæ–‡æ¡£ | docs/ |
---
## ä¸ƒã€è¿›åº¦è·Ÿè¸ª
| é˜¶æ®µ | å¼€å§‹æ—¥æœŸ | ç»“束日期 | çŠ¶æ€ | å®Œæˆçއ |
|------|----------|----------|------|--------|
| ç¬¬ä¸€é˜¶æ®µ | SQL脚本已准备 | - | âœ… å·²å®Œæˆï¼ˆéœ€æ‰‹åŠ¨æ‰§è¡ŒSQL) | 100% |
| ç¬¬äºŒé˜¶æ®µ | SQL脚本已准备 | - | âœ… å·²å®Œæˆï¼ˆéœ€æ‰‹åŠ¨æ‰§è¡ŒSQL) | 100% |
| ç¬¬ä¸‰é˜¶æ®µ | åŽç«¯ä»£ç æ”¹é€ å®Œæˆ | - | âœ… å·²å®Œæˆ | 100% |
| ç¬¬å››é˜¶æ®µ | - | - | âŒ æœªå¼€å§‹ï¼ˆéœ€å‰ç«¯å¼€å‘) | 0% |
---
## å…«ã€æ”¹é€ è¯¦æƒ…
### ç¬¬ä¸€é˜¶æ®µå®Œæˆæƒ…况
| åºå· | ä»»åŠ¡ | çŠ¶æ€ |
|------|------|------|
| 1.1 | è¿ç§» ERP äº§å“åˆ†ç±»åˆ° MDM ç‰©æ–™åˆ†ç±» | âœ… SQL脚本已准备 |
| 1.2 | è¿ç§» MES ç‰©æ–™åˆ†ç±»åˆ° MDM ç‰©æ–™åˆ†ç±» | âœ… SQL脚本已准备 |
| 1.3 | è¿ç§» WMS å•†å“åˆ†ç±»åˆ° MDM ç‰©æ–™åˆ†ç±» | âœ… SQL脚本已准备 |
| 1.4 | è¿ç§» ERP è®¡é‡å•位到 MDM è®¡é‡å•位 | âœ… SQL脚本已准备 |
| 1.5 | è¿ç§» MES è®¡é‡å•位到 MDM è®¡é‡å•位 | âœ… SQL脚本已准备 |
| 1.6 | è¿ç§» WMS å•†å“å“ç‰Œåˆ° MDM å“ç‰Œ | âœ… SQL脚本已准备 |
**产出物**:`docs/sql/mdm_migration_01_basic_data.sql`
### ç¬¬äºŒé˜¶æ®µå®Œæˆæƒ…况
| åºå· | ä»»åŠ¡ | çŠ¶æ€ |
|------|------|------|
| 2.1 | è¿ç§» MES ç‰©æ–™åˆ° MDM(itemType=1 ç‰©æ–™ï¼‰ | âœ… SQL脚本已准备 |
| 2.2 | è¿ç§» ERP äº§å“åˆ° MDM(itemType=2 äº§å“ï¼‰ | âœ… SQL脚本已准备 |
| 2.3 | è¿ç§» WMS å•†å“åˆ° MDM | âœ… SQL脚本已准备 |
| 2.4 | åˆ›å»ºç‰©æ–™ SKU æ•°æ® | âœ… SQL脚本已准备 |
**产出物**:`docs/sql/mdm_migration_02_item_data.sql`
### ç¬¬ä¸‰é˜¶æ®µå®Œæˆæƒ…况
| åºå· | ä»»åŠ¡ | çŠ¶æ€ |
|------|------|------|
| 3.1 | ERP æ¨¡å—引用 MDM API,修改 Service å±‚ | âœ… å·²å®Œæˆ |
| 3.2 | ERP æ¨¡å—修改 Controller å±‚响应结构 | âœ… ä¿æŒåŽŸç»“æž„ï¼Œæ— éœ€ä¿®æ”¹ |
| 3.3 | MES æ¨¡å—引用 MDM API,修改 Service å±‚ | âœ… å·²å®Œæˆ |
| 3.4 | MES æ¨¡å—修改 Controller å±‚响应结构 | âœ… ä¿æŒåŽŸç»“æž„ï¼Œæ— éœ€ä¿®æ”¹ |
| 3.5 | WMS æ¨¡å—引用 MDM API,修改 Service å±‚ | âœ… å·²å®Œæˆ |
| 3.6 | WMS æ¨¡å—修改 Controller å±‚响应结构 | âœ… ä¿æŒåŽŸç»“æž„ï¼Œæ— éœ€ä¿®æ”¹ |
**改动文件**:
- `yudao-module-erp/pom.xml` - æ·»åŠ  MDM ä¾èµ–
- `yudao-module-erp/.../ErpProductServiceImpl.java` - æ”¹ä¸ºè°ƒç”¨ MDM API
- `yudao-module-mes/pom.xml` - æ·»åŠ  MDM ä¾èµ–
- `yudao-module-mes/.../MesMdItemServiceImpl.java` - æ”¹ä¸ºè°ƒç”¨ MDM API
- `yudao-module-wms/pom.xml` - æ·»åŠ  MDM ä¾èµ–
- `yudao-module-wms/.../WmsItemServiceImpl.java` - æ”¹ä¸ºè°ƒç”¨ MDM API
- `yudao-module-mdm/.../MdmItemRespDTO.java` - æ·»åŠ  minStock、maxStock å­—段
### ç¬¬å››é˜¶æ®µå¾…办事项
| åºå· | ä»»åŠ¡ | çŠ¶æ€ | å¤‡æ³¨ |
|------|------|------|------|
| 4.1 | ERP äº§å“ç®¡ç†é¡µé¢é€‚配 | âŒ å¾…开始 | éœ€å‰ç«¯å¼€å‘ |
| 4.2 | MES ç‰©æ–™ç®¡ç†é¡µé¢é€‚配 | âŒ å¾…开始 | éœ€å‰ç«¯å¼€å‘ |
| 4.3 | WMS å•†å“ç®¡ç†é¡µé¢é€‚配 | âŒ å¾…开始 | éœ€å‰ç«¯å¼€å‘ |
| 4.4 | å„模块选择器组件适配 | âŒ å¾…开始 | éœ€å‰ç«¯å¼€å‘ |
**前端变更文档**:
- `docs/mdm_frontend_change_phase1.md`
- `docs/mdm_frontend_change_phase2.md`
docs/mdm_product_integration_frontend.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,276 @@
# MDM äº§å“æ•°æ®ä¸€ä½“化改造 - å‰ç«¯æ–‡æ¡£
## æ¦‚è¿°
本次改造将 MDM ç‰©æ–™ä½œä¸ºç»Ÿä¸€äº§å“ä¸»æ•°æ®ï¼ŒCRM å’Œ ERP æ¨¡å—的产品数据统一使用 MDM ç‰©æ–™ï¼ˆitemType=2 äº§å“ç±»åž‹ï¼‰ã€‚
---
## ä¸€ã€æ”¹é€ æ¸…单
### åŽç«¯æ”¹é€ ï¼ˆå·²å®Œæˆï¼‰
| æ–‡ä»¶ | æ”¹åŠ¨è¯´æ˜Ž |
|------|----------|
| `yudao-module-mdm-api/` | MDM ç‰©æ–™ API æ¨¡å— |
| `MdmItemApi.java` | ç‰©æ–™ API æŽ¥å£ |
| `MdmItemRespDTO.java` | ç‰©æ–™å“åº” DTO |
| `MdmItemCreateReqDTO.java` | ç‰©æ–™åˆ›å»ºè¯·æ±‚ DTO |
| `MdmItemUpdateReqDTO.java` | ç‰©æ–™æ›´æ–°è¯·æ±‚ DTO |
| `MdmItemTypeEnum.java` | ç‰©æ–™ç±»åž‹æžšä¸¾ |
| `ErpProductServiceImpl.java` | ä½¿ç”¨ MDM ç‰©æ–™ API |
| `CrmProductServiceImpl.java` | ä½¿ç”¨ MDM ç‰©æ–™ API |
---
## äºŒã€æ¶‰åŠé¡µé¢
### ERP äº§å“ç®¡ç†
| é¡µé¢ | è·¯å¾„ | æ”¹åŠ¨è¯´æ˜Ž |
|------|------|----------|
| äº§å“åˆ—表 | `/erp/product/index` | æ— éœ€æ”¹åŠ¨ï¼ŒæŽ¥å£å…¼å®¹ |
| äº§å“è¡¨å• | `/erp/product/form` | æ— éœ€æ”¹åŠ¨ï¼ŒæŽ¥å£å…¼å®¹ |
### CRM äº§å“ç®¡ç†
| é¡µé¢ | è·¯å¾„ | æ”¹åŠ¨è¯´æ˜Ž |
|------|------|----------|
| äº§å“åˆ—表 | `/crm/product/index` | æ— éœ€æ”¹åŠ¨ï¼ŒæŽ¥å£å…¼å®¹ |
| äº§å“è¡¨å• | `/crm/product/form` | æ— éœ€æ”¹åŠ¨ï¼ŒæŽ¥å£å…¼å®¹ |
### MDM ç‰©æ–™ç®¡ç†
| é¡µé¢ | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| ç‰©æ–™åˆ—表 | `/mdm/item/index` | ç»Ÿä¸€äº§å“ä¸»æ•°æ®å…¥å£ |
| ç‰©æ–™è¡¨å• | `/mdm/item/form` | åˆ›å»º/编辑物料 |
---
## ä¸‰ã€æ•°æ®æ¨¡åž‹æ˜ å°„
### MDM ç‰©æ–™ç±»åž‹
| ç±»åž‹å€¼ | è¯´æ˜Ž | ç”¨é€” |
|--------|------|------|
| 1 | ç‰©æ–™ | åŽŸææ–™ã€è¾…æ–™ç­‰ |
| 2 | äº§å“ | é”€å”®äº§å“ï¼ˆCRM、ERP ä½¿ç”¨ï¼‰ |
| 3 | åŠæˆå“ | ç”Ÿäº§ä¸­é—´ä»¶ |
### å­—段映射(MDM -> ERP)
| MDM å­—段 | ERP å­—段 | è¯´æ˜Ž |
|----------|----------|------|
| id | id | ç¼–号 |
| name | name | åç§° |
| barCode | barCode | æ¡ç  |
| specification | standard | è§„æ ¼ |
| categoryId | categoryId | åˆ†ç±»ç¼–号 |
| unitMeasureId | unitId | å•位编号 |
| status | status | çŠ¶æ€ |
| purchasePrice | purchasePrice | é‡‡è´­ä»·æ ¼ |
| salesPrice | salePrice | é”€å”®ä»·æ ¼ |
| costPrice | minPrice | æˆæœ¬ä»· -> æœ€ä½Žä»· |
| expiryDay | expiryDay | ä¿è´¨æœŸ |
| weight | weight | é‡é‡ |
| remark | remark | å¤‡æ³¨ |
### å­—段映射(MDM -> CRM)
| MDM å­—段 | CRM å­—段 | è¯´æ˜Ž |
|----------|----------|------|
| id | id | ç¼–号 |
| code | no | ç¼–码 |
| name | name | åç§° |
| unitMeasureId | unit | å•位 |
| salesPrice | price | ä»·æ ¼ |
| status | status | çŠ¶æ€ |
| categoryId | categoryId | åˆ†ç±»ç¼–号 |
| remark | description | æè¿° |
---
## å››ã€API æŽ¥å£
### MDM ç‰©æ–™ API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /rpc-api/mdm/item/create | åˆ›å»ºç‰©æ–™ |
| PUT | /rpc-api/mdm/item/update | æ›´æ–°ç‰©æ–™ |
| GET | /rpc-api/mdm/item/get | èŽ·å–ç‰©æ–™ |
| POST | /rpc-api/mdm/item/list | èŽ·å–ç‰©æ–™åˆ—è¡¨ |
| POST | /rpc-api/mdm/item/page | èŽ·å–ç‰©æ–™åˆ†é¡µ |
| GET | /rpc-api/mdm/item/validate-exists | æ ¡éªŒç‰©æ–™å­˜åœ¨ |
| GET | /rpc-api/mdm/item/get-by-code | æ ¹æ®ç¼–码获取物料 |
| GET | /rpc-api/mdm/item/get-by-bar-code | æ ¹æ®æ¡ç èŽ·å–ç‰©æ–™ |
### ERP äº§å“ API(保持不变)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /erp/product/create | åˆ›å»ºäº§å“ |
| PUT | /erp/product/update | æ›´æ–°äº§å“ |
| DELETE | /erp/product/delete | åˆ é™¤äº§å“ |
| GET | /erp/product/get | èŽ·å–äº§å“ |
| GET | /erp/product/page | èŽ·å–äº§å“åˆ†é¡µ |
| GET | /erp/product/simple-list | èŽ·å–äº§å“ç²¾ç®€åˆ—è¡¨ |
### CRM äº§å“ API(保持不变)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /crm/product/create | åˆ›å»ºäº§å“ |
| PUT | /crm/product/update | æ›´æ–°äº§å“ |
| DELETE | /crm/product/delete | åˆ é™¤äº§å“ |
| GET | /crm/product/get | èŽ·å–äº§å“ |
| GET | /crm/product/page | èŽ·å–äº§å“åˆ†é¡µ |
| GET | /crm/product/simple-list | èŽ·å–äº§å“ç²¾ç®€åˆ—è¡¨ |
---
## äº”、前端修改点
### 5.1 äº§å“é€‰æ‹©ç»„件(可选优化)
如果需要统一产品选择体验,可以创建 MDM ç‰©æ–™é€‰æ‹©ç»„件:
```html
<!-- MdmItemSelect.vue -->
<template>
  <el-select v-model="selectedValue" placeholder="请选择产品" filterable>
    <el-option
      v-for="item in itemList"
      :key="item.id"
      :label="item.name"
      :value="item.id"
    >
      <span>{{ item.name }}</span>
      <span style="color: #999; margin-left: 8px;">{{ item.barCode }}</span>
    </el-option>
  </el-select>
</template>
<script>
import { getItemList } from '@/api/mdm/item'
export default {
  props: {
    value: [Number, String],
    itemType: {
      type: Number,
      default: 2 // é»˜è®¤äº§å“ç±»åž‹
    }
  },
  data() {
    return {
      itemList: []
    }
  },
  computed: {
    selectedValue: {
      get() {
        return this.value
      },
      set(val) {
        this.$emit('input', val)
      }
    }
  },
  created() {
    this.loadItemList()
  },
  methods: {
    async loadItemList() {
      const { data } = await getItemList({ itemType: this.itemType, status: 0 })
      this.itemList = data
    }
  }
}
</script>
```
### 5.2 ä½¿ç”¨æ–¹å¼
```html
<!-- ERP é”€å”®è®¢å•中选择产品 -->
<MdmItemSelect v-model="form.productId" :item-type="2" />
<!-- é‡‡è´­è®¢å•中选择物料 -->
<MdmItemSelect v-model="form.itemId" :item-type="1" />
```
---
## å…­ã€æ³¨æ„äº‹é¡¹
### 6.1 æ•°æ®è¿ç§»
如果已有 ERP æˆ– CRM äº§å“æ•°æ®ï¼Œéœ€è¦è¿ç§»åˆ° MDM ç‰©æ–™è¡¨ï¼š
```sql
-- è¿ç§» ERP äº§å“åˆ° MDM ç‰©æ–™ï¼ˆç¤ºä¾‹ï¼‰
INSERT INTO mdm_item (id, code, name, bar_code, specification, category_id, unit_measure_id,
    item_type, status, purchase_price, sales_price, cost_price, expiry_day, weight, remark,
    creator, create_time, updater, update_time, deleted, tenant_id)
SELECT
    id,
    bar_code AS code,
    name,
    bar_code,
    standard AS specification,
    category_id,
    unit_id AS unit_measure_id,
    2 AS item_type, -- äº§å“ç±»åž‹
    status,
    purchase_price,
    sale_price AS sales_price,
    min_price AS cost_price,
    expiry_day,
    weight,
    remark,
    creator,
    create_time,
    updater,
    update_time,
    deleted,
    tenant_id
FROM erp_product
WHERE deleted = 0;
-- è¿ç§» CRM äº§å“åˆ° MDM ç‰©æ–™ï¼ˆéœ€è¦å¤„理 ID å†²çªï¼‰
-- æ³¨æ„ï¼šå¦‚æžœ ID ä¸Ž ERP äº§å“å†²çªï¼Œéœ€è¦é‡æ–°åˆ†é… ID å¹¶æ›´æ–°å…³è”数据
```
### 6.2 æŽ¥å£å…¼å®¹æ€§
- ERP å’Œ CRM çš„产品接口保持不变,前端无需修改
- æ•°æ®å®žé™…从 MDM èŽ·å–ï¼Œå¯¹å‰ç«¯é€æ˜Ž
- åˆ›å»º/更新操作通过 MDM API æ‰§è¡Œ
### 6.3 æƒé™ç®¡ç†
- CRM äº§å“ä¿ç•™æœ¬åœ°è®°å½•用于权限管理
- äº§å“ ID ä¸Ž MDM ç‰©æ–™ ID ä¿æŒä¸€è‡´
- æƒé™éªŒè¯ä»ç„¶é€šè¿‡ CRM æƒé™ç³»ç»Ÿ
### 6.4 åºŸå¼ƒè¯´æ˜Ž
- `erp_product` è¡¨ï¼šä¿ç•™ä½†ä¸å†ä½œä¸ºä¸»æ•°æ®æº
- `crm_product` è¡¨ï¼šä¿ç•™ç”¨äºŽæƒé™ç®¡ç†ï¼Œæ•°æ®åŒæ­¥è‡ª MDM
- å»ºè®®åŽç»­ç‰ˆæœ¬åœ¨å‰ç«¯èœå•中统一使用 MDM ç‰©æ–™ç®¡ç†
---
## ä¸ƒã€åŽç»­è®¡åˆ’
| Phase | å†…容 | çŠ¶æ€ |
|-------|------|------|
| Phase 1 | å®¢æˆ·æ•°æ®ç»Ÿä¸€ - ERP ä½¿ç”¨ CRM å®¢æˆ· | **已完成** |
| Phase 2 | äº§å“æ•°æ®ç»Ÿä¸€ - CRM/ERP ä½¿ç”¨ MDM ç‰©æ–™ | **已完成** |
| Phase 3 | åˆåŒç”Ÿæˆé”€å”®è®¢å• | å¾…开始 |
| Phase 4 | é”€å”®å…¨æµç¨‹è¿½æº¯ | å¾…开始 |
| Phase 5 | è¥é”€æ•°æ®æŠ¥è¡¨ | å¾…开始 |
| Phase 6 | æ•°æ®è¿ç§»ä¸Žæ¸…理 | å¾…开始 |
docs/sales_traceability_frontend.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,349 @@
# CRM é”€å”®å…¨æµç¨‹è¿½æº¯ - å‰ç«¯æ–‡æ¡£
## æ¦‚è¿°
本次改造实现销售全流程追溯功能,可从商机、合同、客户等多个维度查看完整的销售流程信息,包括商机 -> åˆåŒ -> é”€å”®è®¢å• -> å‡ºåº“ -> å›žæ¬¾ -> é€€è´§çš„全链路数据。
---
## ä¸€ã€æ”¹é€ æ¸…单
### åŽç«¯æ”¹é€ ï¼ˆå·²å®Œæˆï¼‰
| æ–‡ä»¶ | æ”¹åŠ¨è¯´æ˜Ž |
|------|----------|
| `CrmSaleTraceApi.java` | é”€å”®è¿½æº¯ API æŽ¥å£ |
| `CrmSaleTraceRespDTO.java` | é”€å”®è¿½æº¯å“åº” DTO |
| `CrmSaleTraceApiImpl.java` | é”€å”®è¿½æº¯ API å®žçް |
| `CrmBusinessDO.java` | æ–°å¢ž orderId、orderNo å­—段 |
| `CrmBusinessRespVO.java` | æ–°å¢ž orderId、orderNo å­—段 |
| `CrmContractMapper.java` | æ–°å¢ž selectByBusinessId、selectListByCustomerId æ–¹æ³• |
| `CrmReceivableService.java` | æ–°å¢ž getReceivablePriceByContractId æ–¹æ³• |
---
## äºŒã€æ¶‰åŠé¡µé¢
### CRM æ¨¡å—
| é¡µé¢ | è·¯å¾„ | æ”¹åŠ¨è¯´æ˜Ž |
|------|------|----------|
| å•†æœºè¯¦æƒ… | `/crm/business/detail` | æ·»åŠ é”€å”®æµç¨‹è¿½æº¯å…¥å£ |
| åˆåŒè¯¦æƒ… | `/crm/contract/detail` | æ·»åŠ é”€å”®æµç¨‹è¿½æº¯å…¥å£ |
| å®¢æˆ·è¯¦æƒ… | `/crm/customer/detail` | æ·»åŠ é”€å”®æµç¨‹è¿½æº¯åˆ—è¡¨ |
### æ–°å¢žé¡µé¢
| é¡µé¢ | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| é”€å”®è¿½æº¯è¯¦æƒ… | `/crm/sale-trace/detail` | é”€å”®æµç¨‹è¿½æº¯è¯¦æƒ…页 |
---
## ä¸‰ã€API æŽ¥å£
### CRM é”€å”®è¿½æº¯ API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /rpc-api/crm/sale-trace/get-by-business-id | æ ¹æ®å•†æœºç¼–号获取追溯信息 |
| GET | /rpc-api/crm/sale-trace/get-by-contract-id | æ ¹æ®åˆåŒç¼–号获取追溯信息 |
| GET | /rpc-api/crm/sale-trace/get-list-by-customer-id | æ ¹æ®å®¢æˆ·ç¼–号获取追溯列表 |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "businessId": 1,
    "businessName": "商机A",
    "businessPrice": 100000,
    "businessStatus": "进行中",
    "businessCreateTime": "2024-01-01 10:00:00",
    "customerId": 1,
    "customerName": "客户A",
    "contractId": 1,
    "contractName": "合同A",
    "contractPrice": 95000,
    "contractStatus": 20,
    "contractCreateTime": "2024-01-15 10:00:00",
    "orderId": 1,
    "orderNo": "SO001",
    "orderPrice": 95000,
    "orderStatus": 0,
    "orderCreateTime": "2024-01-16 10:00:00",
    "receivablePrice": 50000,
    "receivableItems": [
      {
        "receivableId": 1,
        "receivableNo": "RK001",
        "price": 30000,
        "receivableTime": "2024-02-01"
      },
      {
        "receivableId": 2,
        "receivableNo": "RK002",
        "price": 20000,
        "receivableTime": "2024-03-01"
      }
    ]
  }
}
```
---
## å››ã€å‰ç«¯ä¿®æ”¹ç‚¹
### 4.1 å•†æœºè¯¦æƒ…页面
**文件路径:** `/src/views/crm/business/detail.vue`
**新增组件:**
```html
<!-- é”€å”®æµç¨‹è¿½æº¯å¡ç‰‡ -->
<el-card class="sale-trace-card" header="销售流程追溯">
  <div v-if="traceInfo">
    <!-- æµç¨‹æ—¶é—´çº¿ -->
    <el-timeline>
      <el-timeline-item
        :timestamp="traceInfo.businessCreateTime"
        placement="top"
        color="#409EFF"
      >
        <el-card>
          <h4>商机创建</h4>
          <p>商机名称:{{ traceInfo.businessName }}</p>
          <p>商机金额:¥{{ traceInfo.businessPrice }}</p>
        </el-card>
      </el-timeline-item>
      <el-timeline-item
        v-if="traceInfo.contractId"
        :timestamp="traceInfo.contractCreateTime"
        placement="top"
        color="#67C23A"
      >
        <el-card>
          <h4>合同签订</h4>
          <p>合同名称:{{ traceInfo.contractName }}</p>
          <p>合同金额:¥{{ traceInfo.contractPrice }}</p>
          <p>合同状态:{{ getContractStatusText(traceInfo.contractStatus) }}</p>
        </el-card>
      </el-timeline-item>
      <el-timeline-item
        v-if="traceInfo.orderId"
        :timestamp="traceInfo.orderCreateTime"
        placement="top"
        color="#E6A23C"
      >
        <el-card>
          <h4>销售订单</h4>
          <p>订单号:<el-link type="primary" @click="viewOrder(traceInfo.orderId)">{{ traceInfo.orderNo }}</el-link></p>
          <p>订单金额:¥{{ traceInfo.orderPrice }}</p>
          <p>订单状态:{{ getOrderStatusText(traceInfo.orderStatus) }}</p>
        </el-card>
      </el-timeline-item>
      <el-timeline-item
        v-if="traceInfo.receivablePrice > 0"
        placement="top"
        color="#F56C6C"
      >
        <el-card>
          <h4>回款记录</h4>
          <p>已回款金额:¥{{ traceInfo.receivablePrice }}</p>
          <el-table v-if="traceInfo.receivableItems" :data="traceInfo.receivableItems" size="small">
            <el-table-column prop="receivableNo" label="回款单号" />
            <el-table-column prop="price" label="回款金额" />
            <el-table-column prop="receivableTime" label="回款时间" />
          </el-table>
        </el-card>
      </el-timeline-item>
    </el-timeline>
  </div>
  <div v-else>
    <el-empty description="暂无销售流程数据" />
  </div>
</el-card>
```
**新增方法:**
```js
import { getTraceByBusinessId } from '@/api/crm/saleTrace'
data() {
  return {
    traceInfo: null
  }
},
methods: {
  async loadTraceInfo() {
    const { data } = await getTraceByBusinessId(this.businessId)
    this.traceInfo = data
  },
  getContractStatusText(status) {
    const statusMap = { 0: '草稿', 10: '审批中', 20: '审批通过', 30: '审批不通过' }
    return statusMap[status] || '未知'
  },
  getOrderStatusText(status) {
    const statusMap = { 0: '未审核', 10: '已审核' }
    return statusMap[status] || '未知'
  },
  viewOrder(orderId) {
    this.$router.push({ path: '/erp/sale/order/detail', query: { id: orderId } })
  }
}
```
### 4.2 åˆåŒè¯¦æƒ…页面
**文件路径:** `/src/views/crm/contract/detail.vue`
**新增方法:**
```js
import { getTraceByContractId } from '@/api/crm/saleTrace'
methods: {
  async loadTraceInfo() {
    const { data } = await getTraceByContractId(this.contractId)
    this.traceInfo = data
  }
}
```
### 4.3 å®¢æˆ·è¯¦æƒ…页面
**文件路径:** `/src/views/crm/customer/detail.vue`
**新增列表:**
```html
<el-card header="销售流程追溯">
  <el-table :data="traceList" size="small">
    <el-table-column prop="businessName" label="商机名称">
      <template #default="{ row }">
        <el-link v-if="row.businessId" type="primary" @click="viewBusiness(row.businessId)">
          {{ row.businessName }}
        </el-link>
        <span v-else>-</span>
      </template>
    </el-table-column>
    <el-table-column prop="contractName" label="合同名称">
      <template #default="{ row }">
        <el-link v-if="row.contractId" type="primary" @click="viewContract(row.contractId)">
          {{ row.contractName }}
        </el-link>
        <span v-else>-</span>
      </template>
    </el-table-column>
    <el-table-column prop="contractPrice" label="合同金额" width="120" />
    <el-table-column prop="orderNo" label="销售订单">
      <template #default="{ row }">
        <el-link v-if="row.orderId" type="primary" @click="viewOrder(row.orderId)">
          {{ row.orderNo }}
        </el-link>
        <span v-else>-</span>
      </template>
    </el-table-column>
    <el-table-column prop="receivablePrice" label="已回款" width="120" />
    <el-table-column label="回款率" width="100">
      <template #default="{ row }">
        <span v-if="row.contractPrice > 0">
          {{ ((row.receivablePrice / row.contractPrice) * 100).toFixed(1) }}%
        </span>
        <span v-else>-</span>
      </template>
    </el-table-column>
  </el-table>
</el-card>
```
### 4.4 API å®šä¹‰
**文件路径:** `/src/api/crm/saleTrace.js`
```js
import request from '@/utils/request'
// æ ¹æ®å•†æœºç¼–号获取销售流程追溯信息
export function getTraceByBusinessId(businessId) {
  return request({
    url: '/crm/sale-trace/get-by-business-id',
    method: 'get',
    params: { businessId }
  })
}
// æ ¹æ®åˆåŒç¼–号获取销售流程追溯信息
export function getTraceByContractId(contractId) {
  return request({
    url: '/crm/sale-trace/get-by-contract-id',
    method: 'get',
    params: { contractId }
  })
}
// æ ¹æ®å®¢æˆ·ç¼–号获取销售流程追溯列表
export function getTraceListByCustomerId(customerId) {
  return request({
    url: '/crm/sale-trace/get-list-by-customer-id',
    method: 'get',
    params: { customerId }
  })
}
```
---
## äº”、数据模型变更
### CRM å•†æœºè¡¨å˜æ›´
```sql
ALTER TABLE `crm_business`
ADD COLUMN `order_id` bigint DEFAULT NULL COMMENT '关联 ERP é”€å”®è®¢å•编号' AFTER `total_price`,
ADD COLUMN `order_no` varchar(32) DEFAULT NULL COMMENT 'ERP é”€å”®è®¢å•单号' AFTER `order_id`;
```
---
## å…­ã€ä¸šåŠ¡æµç¨‹å›¾
```
商机创建 â†’ å•†æœºè·Ÿè¿› â†’ åˆåŒç­¾è®¢ â†’ åˆåŒå®¡æ‰¹ â†’ ç”Ÿæˆè®¢å• â†’ è®¢å•审批 â†’ é”€å”®å‡ºåº“ â†’ å®¢æˆ·å›žæ¬¾
    â†“           â†“          â†“          â†“          â†“          â†“          â†“          â†“
  10:00      12:00     15:00      16:00      17:00      18:00      20:00      25:00
                            ï¼ˆé‡‘额变化追踪)
商机金额(100K) â†’ åˆåŒé‡‘额(95K) â†’ è®¢å•金额(95K) â†’ å‡ºåº“金额(95K) â†’ å›žæ¬¾é‡‘额(50K)
```
---
## ä¸ƒã€æ³¨æ„äº‹é¡¹
1. **数据关联**:追溯数据来源于多个模块(CRM、ERP),需确保数据一致性
2. **权限控制**:追溯查询需要对应模块的查看权限
3. **数据完整性**:部分业务可能只到某个环节(如只签合同未生成订单)
4. **时间线展示**:按时间顺序展示各环节信息
5. **金额汇总**:展示每个环节的金额变化情况
---
## å…«ã€åŽç»­è®¡åˆ’
| Phase | å†…容 | çŠ¶æ€ |
|-------|------|------|
| Phase 1 | å®¢æˆ·æ•°æ®ç»Ÿä¸€ - ERP ä½¿ç”¨ CRM å®¢æˆ· | **已完成** |
| Phase 2 | äº§å“æ•°æ®ç»Ÿä¸€ - CRM/ERP ä½¿ç”¨ MDM ç‰©æ–™ | **已完成** |
| Phase 3 | åˆåŒç”Ÿæˆé”€å”®è®¢å• | **已完成** |
| Phase 4 | é”€å”®å…¨æµç¨‹è¿½æº¯ | **已完成** |
| Phase 5 | è¥é”€æ•°æ®æŠ¥è¡¨ | å¾…开始 |
| Phase 6 | æ•°æ®è¿ç§»ä¸Žæ¸…理 | å¾…开始 |
docs/sql/bpm_purchase_approval_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,86 @@
-- =====================================================
-- BPM é‡‡è´­å®¡æ ¸æµç¨‹ - SQL åˆå§‹åŒ–
-- =====================================================
-- =====================================================
-- 01. åˆ›å»ºæµç¨‹åˆ†ç±»ï¼ˆé‡‡è´­ç®¡ç†ï¼‰
-- =====================================================
INSERT INTO bpm_category (id, name, code, description, status, sort, creator, create_time, updater, update_time, deleted)
VALUES (100, '采购审核', 'purchase_approve', '采购订单审批流程分类', 0, 10, '1', NOW(), '1', NOW(), 0)
ON DUPLICATE KEY UPDATE name = VALUES(name), code = VALUES(code);
-- =====================================================
-- 02. åˆ›å»ºé‡‡è´­è®¢å•审批表单
-- =====================================================
INSERT INTO bpm_form (id, name, status, conf, fields, remark, creator, create_time, updater, update_time, deleted)
VALUES (
    100,
    '采购订单审批表单',
    0,
    '{"formRef":"elForm","size":"medium","labelPosition":"right","labelWidth":100,"formRules":"rules","gutter":15,"disabled":false,"span":24,"formBtns":true}',
    '[
        {"type":"input","label":"订单编号","placeholder":"请输入订单编号","field":"orderNo","defaultValue":"","required":true},
        {"type":"input","label":"供应商名称","placeholder":"请输入供应商名称","field":"supplierName","defaultValue":"","required":true},
        {"type":"input","label":"采购金额","placeholder":"请输入采购金额","field":"amount","defaultValue":"","required":true},
        {"type":"input","label":"订单数量","placeholder":"请输入订单数量","field":"quantity","defaultValue":"","required":true},
        {"type":"textarea","label":"订单明细","placeholder":"请输入订单明细","field":"orderDetail","defaultValue":"","required":false},
        {"type":"textarea","label":"备注","placeholder":"请输入备注","field":"remark","defaultValue":"","required":false}
    ]',
    '采购订单审批表单',
    '1',
    NOW(),
    '1',
    NOW(),
    0
) ON DUPLICATE KEY UPDATE name = VALUES(name);
-- =====================================================
-- 03. è¯´æ˜Žï¼šæµç¨‹æ¨¡åž‹å’Œå®šä¹‰éœ€è¦é€šè¿‡å‰ç«¯ç•Œé¢åˆ›å»º
-- =====================================================
--
-- é‡‡è´­å®¡æ‰¹æµç¨‹å»ºè®®é…ç½®ï¼š
--
-- æµç¨‹åç§°ï¼šé‡‡è´­è®¢å•审批
-- æµç¨‹åˆ†ç±»ï¼šé‡‡è´­å®¡æ ¸ (code = purchase_approve)
-- è¡¨å•类型:动态表单(关联上面的表单 id=100)
--
-- æµç¨‹èŠ‚ç‚¹é…ç½®ï¼ˆSimple è®¾è®¡å™¨ï¼‰ï¼š
--
-- 1. å¼€å§‹èŠ‚ç‚¹
--    - å‘起人:全员可发起
--
-- 2. ä¸»ç®¡å®¡æ‰¹èŠ‚ç‚¹
--    - å®¡æ‰¹äººï¼šå‘起人的直属主管
--    - å®¡æ‰¹ç±»åž‹ï¼šå®¡æ‰¹
--
-- 3. é‡‡è´­ç»ç†å®¡æ‰¹èŠ‚ç‚¹ï¼ˆé‡‘é¢ > 5000 æ—¶ï¼‰
--    - æ¡ä»¶åˆ†æ”¯ï¼šé‡‡è´­é‡‘额 > 5000
--    - å®¡æ‰¹äººï¼šé‡‡è´­ç»ç†è§’色或指定用户
--    - å®¡æ‰¹ç±»åž‹ï¼šå®¡æ‰¹
--
-- 4. è´¢åŠ¡å®¡æ‰¹èŠ‚ç‚¹ï¼ˆé‡‘é¢ > 10000 æ—¶ï¼‰
--    - æ¡ä»¶åˆ†æ”¯ï¼šé‡‡è´­é‡‘额 > 10000
--    - å®¡æ‰¹äººï¼šè´¢åŠ¡ç»ç†è§’è‰²æˆ–æŒ‡å®šç”¨æˆ·
--    - å®¡æ‰¹ç±»åž‹ï¼šå®¡æ‰¹
--
-- 5. æ€»ç»ç†å®¡æ‰¹èŠ‚ç‚¹ï¼ˆé‡‘é¢ > 50000 æ—¶ï¼‰
--    - æ¡ä»¶åˆ†æ”¯ï¼šé‡‡è´­é‡‘额 > 50000
--    - å®¡æ‰¹äººï¼šæ€»ç»ç†è§’色或指定用户
--    - å®¡æ‰¹ç±»åž‹ï¼šå®¡æ‰¹
--
-- 6. ç»“束节点
--    - å®¡æ‰¹é€šè¿‡åŽè‡ªåŠ¨ç»“æŸ
--
-- =====================================================
-- =====================================================
-- 04. æŸ¥è¯¢éªŒè¯
-- =====================================================
-- éªŒè¯åˆ†ç±»åˆ›å»º
SELECT * FROM bpm_category WHERE code = 'purchase_approve';
-- éªŒè¯è¡¨å•创建
SELECT * FROM bpm_form WHERE name = '采购订单审批表单';
docs/sql/erp_purchase_enhance.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
-- =====================================================
-- ERP é‡‡è´­ç®¡ç†æ¨¡å—完善 - SQL è„šæœ¬
-- =====================================================
-- =====================================================
-- 00. åˆ›å»º BPM é‡‡è´­å®¡æ ¸åˆ†ç±»ï¼ˆå¿…须在采购订单前创建)
-- =====================================================
INSERT INTO bpm_category (id, name, code, description, status, sort, creator, create_time, updater, update_time, deleted)
VALUES (100, '采购审核', 'purchase_approve', '采购订单审批流程分类', 0, 10, '1', NOW(), '1', NOW(), 0)
ON DUPLICATE KEY UPDATE name = VALUES(name);
-- =====================================================
-- 01. é‡‡è´­å…¥åº“表增加验收字段
-- =====================================================
ALTER TABLE `erp_purchase_in` ADD COLUMN IF NOT EXISTS `inspection_status` tinyint DEFAULT 0 COMMENT '验收状态(0:待验收 1:已验收 2:验收不合格)';
ALTER TABLE `erp_purchase_in` ADD COLUMN IF NOT EXISTS `inspection_time` datetime DEFAULT NULL COMMENT '验收时间';
ALTER TABLE `erp_purchase_in` ADD COLUMN IF NOT EXISTS `inspector_id` bigint DEFAULT NULL COMMENT '验收人员编号';
ALTER TABLE `erp_purchase_in` ADD COLUMN IF NOT EXISTS `inspection_result` varchar(500) DEFAULT NULL COMMENT '验收结果';
-- =====================================================
-- 02. ä¾›åº”商表增加字段
-- =====================================================
ALTER TABLE `erp_supplier` ADD COLUMN IF NOT EXISTS `credit_code` varchar(50) DEFAULT NULL COMMENT '统一社会信用代码';
ALTER TABLE `erp_supplier` ADD COLUMN IF NOT EXISTS `bank_name` varchar(100) DEFAULT NULL COMMENT '开户银行';
ALTER TABLE `erp_supplier` ADD COLUMN IF NOT EXISTS `bank_account` varchar(50) DEFAULT NULL COMMENT '银行账号';
ALTER TABLE `erp_supplier` ADD COLUMN IF NOT EXISTS `address` varchar(200) DEFAULT NULL COMMENT '地址';
ALTER TABLE `erp_supplier` ADD COLUMN IF NOT EXISTS `contact` varchar(50) DEFAULT NULL COMMENT '联系人';
ALTER TABLE `erp_supplier` ADD COLUMN IF NOT EXISTS `phone` varchar(20) DEFAULT NULL COMMENT '联系电话';
ALTER TABLE `erp_supplier` ADD COLUMN IF NOT EXISTS `email` varchar(100) DEFAULT NULL COMMENT '邮箱';
ALTER TABLE `erp_supplier` ADD COLUMN IF NOT EXISTS `tax_no` varchar(50) DEFAULT NULL COMMENT '税号';
ALTER TABLE `erp_supplier` ADD COLUMN IF NOT EXISTS `qualification` varchar(500) DEFAULT NULL COMMENT '资质信息';
ALTER TABLE `erp_supplier` ADD COLUMN IF NOT EXISTS `cooperation_time` date DEFAULT NULL COMMENT '合作开始时间';
-- =====================================================
-- 03. é‡‡è´­è®¢å•表增加审批字段
-- =====================================================
ALTER TABLE `erp_purchase_order` ADD COLUMN IF NOT EXISTS `approve_time` datetime DEFAULT NULL COMMENT '审批时间';
ALTER TABLE `erp_purchase_order` ADD COLUMN IF NOT EXISTS `approver_id` bigint DEFAULT NULL COMMENT '审批人员编号';
-- =====================================================
-- 04. åˆå§‹åŒ–菜单数据
-- =====================================================
-- é‡‡è´­ç®¡ç†ç›®å½•
INSERT INTO system_menu (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 ('采购管理', '', 1, 10, 0, '/erp/purchase', 'ep:goods', NULL, NULL, 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- èŽ·å–é‡‡è´­ç®¡ç†èœå•ID
SET @purchase_menu_id = LAST_INSERT_ID();
-- ä¾›åº”商管理菜单
INSERT INTO system_menu (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 ('供应商管理', '', 2, 1, @purchase_menu_id, 'supplier', 'ep:office-building', 'erp/purchase/supplier/index', 'ErpSupplier', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
SET @supplier_menu_id = LAST_INSERT_ID();
-- ä¾›åº”商按钮权限
INSERT INTO system_menu (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
('供应商查询', 'erp:supplier:query', 3, 1, @supplier_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('供应商新增', 'erp:supplier:create', 3, 2, @supplier_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('供应商修改', 'erp:supplier:update', 3, 3, @supplier_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('供应商删除', 'erp:supplier:delete', 3, 4, @supplier_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('供应商导出', 'erp:supplier:export', 3, 5, @supplier_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- é‡‡è´­è®¢å•菜单
INSERT INTO system_menu (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 ('采购订单', '', 2, 2, @purchase_menu_id, 'order', 'ep:document-checked', 'erp/purchase/order/index', 'ErpPurchaseOrder', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
SET @order_menu_id = LAST_INSERT_ID();
-- é‡‡è´­è®¢å•按钮权限
INSERT INTO system_menu (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
('采购订单查询', 'erp:purchase-order:query', 3, 1, @order_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('采购订单新增', 'erp:purchase-order:create', 3, 2, @order_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('采购订单修改', 'erp:purchase-order:update', 3, 3, @order_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('采购订单删除', 'erp:purchase-order:delete', 3, 4, @order_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('采购订单审批', 'erp:purchase-order:update-status', 3, 5, @order_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('采购订单导出', 'erp:purchase-order:export', 3, 6, @order_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- é‡‡è´­å…¥åº“菜单
INSERT INTO system_menu (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 ('采购入库', '', 2, 3, @purchase_menu_id, 'in', 'ep:box', 'erp/purchase/in/index', 'ErpPurchaseIn', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- é‡‡è´­é€€è´§èœå•
INSERT INTO system_menu (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 ('采购退货', '', 2, 4, @purchase_menu_id, 'return', 'ep:back', 'erp/purchase/return/index', 'ErpPurchaseReturn', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- é‡‡è´­ç»Ÿè®¡èœå•
INSERT INTO system_menu (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 ('采购统计', '', 2, 5, @purchase_menu_id, 'statistics', 'ep:data-analysis', 'erp/purchase/statistics/index', 'ErpPurchaseStatistics', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
SET @statistics_menu_id = LAST_INSERT_ID();
-- é‡‡è´­ç»Ÿè®¡æŒ‰é’®æƒé™
INSERT INTO system_menu (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
('采购统计查询', 'erp:purchase-statistics:query', 3, 1, @statistics_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('供应商供货统计', 'erp:purchase-statistics:supplier', 3, 2, @statistics_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('采购价格分析', 'erp:purchase-statistics:price', 3, 3, @statistics_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
('未完成订单跟踪', 'erp:purchase-statistics:uncompleted', 3, 4, @statistics_menu_id, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
docs/sql/mdm_migration_01_basic_data.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,191 @@
-- =====================================================
-- MDM ä¸»æ•°æ®è¿ç§» - ç¬¬ä¸€é˜¶æ®µï¼šåŸºç¡€æ•°æ®è¿ç§»
-- æ‰§è¡Œé¡ºåºï¼š01 -> 02 -> 03 -> 04 -> 05 -> 06
-- é€‚用场景:新项目初始化,无历史业务数据
-- =====================================================
-- =====================================================
-- 00. åˆ›å»ºé»˜è®¤å•位(确保 WMS å•†å“å•位不为空)
-- =====================================================
-- åˆ›å»ºä¸€ä¸ªé»˜è®¤å•位,用于 WMS å•†å“å•位无法匹配的情况
INSERT INTO mdm_unit_measure (id, code, name, primary_flag, primary_id, change_rate, status, remark, creator, create_time, updater, update_time, deleted)
VALUES (1, 'PCS', '个', TRUE, NULL, 1, 0, '默认单位', '1', NOW(), '1', NOW(), 0)
ON DUPLICATE KEY UPDATE name = VALUES(name);
-- =====================================================
-- 01. è¿ç§»åˆ†ç±»æ•°æ®
-- =====================================================
-- 1.1 è¿ç§» ERP äº§å“åˆ†ç±»åˆ° MDM ç‰©æ–™åˆ†ç±»
-- ERP åˆ†ç±»é»˜è®¤å…³è”产品类型(itemType=2)
INSERT INTO mdm_item_category (id, parent_id, code, name, item_type, sort, status, creator, create_time, updater, update_time, deleted)
SELECT
    id,
    COALESCE(parent_id, 0) AS parent_id,
    code,
    name,
    2 AS item_type,  -- äº§å“ç±»åž‹
    sort,
    status,
    '1' AS creator,
    create_time,
    '1' AS updater,
    update_time,
    0 AS deleted
FROM erp_product_category
WHERE deleted = 0
ON DUPLICATE KEY UPDATE
    name = VALUES(name),
    code = VALUES(code);
-- 1.2 è¿ç§» MES ç‰©æ–™åˆ†ç±»åˆ° MDM ç‰©æ–™åˆ†ç±»
-- MES åˆ†ç±»æ ¹æ® itemOrProduct å­—段判断类型
INSERT INTO mdm_item_category (id, parent_id, code, name, item_type, sort, status, remark, creator, create_time, updater, update_time, deleted)
SELECT
    id + 10000 AS id,  -- MES åˆ†ç±»ID偏移10000避免冲突
    COALESCE(parent_id, 0) +
        CASE WHEN parent_id IS NOT NULL AND parent_id > 0 THEN 10000 ELSE 0 END AS parent_id,
    code,
    name,
    CASE item_or_product
        WHEN 'ITEM' THEN 1    -- ç‰©æ–™
        WHEN 'PRODUCT' THEN 2 -- äº§å“
        ELSE NULL             -- å…¨éƒ¨
    END AS item_type,
    sort,
    status,
    remark,
    '1' AS creator,
    create_time,
    '1' AS updater,
    update_time,
    0 AS deleted
FROM mes_md_item_type
WHERE deleted = 0
ON DUPLICATE KEY UPDATE
    name = VALUES(name),
    code = VALUES(code);
-- 1.3 è¿ç§» WMS å•†å“åˆ†ç±»åˆ° MDM ç‰©æ–™åˆ†ç±»
INSERT INTO mdm_item_category (id, parent_id, code, name, item_type, sort, status, creator, create_time, updater, update_time, deleted)
SELECT
    id + 20000 AS id,  -- WMS åˆ†ç±»ID偏移20000避免冲突
    COALESCE(parent_id, 0) +
        CASE WHEN parent_id IS NOT NULL AND parent_id > 0 THEN 20000 ELSE 0 END AS parent_id,
    code,
    name,
    1 AS item_type,  -- é»˜è®¤ç‰©æ–™ç±»åž‹
    sort,
    status,
    '1' AS creator,
    create_time,
    '1' AS updater,
    update_time,
    0 AS deleted
FROM wms_item_category
WHERE deleted = 0
ON DUPLICATE KEY UPDATE
    name = VALUES(name),
    code = VALUES(code);
-- =====================================================
-- 02. è¿ç§»è®¡é‡å•位数据
-- =====================================================
-- 2.1 è¿ç§» ERP è®¡é‡å•位到 MDM(ID ä»Ž 100 å¼€å§‹ï¼‰
INSERT INTO mdm_unit_measure (id, code, name, primary_flag, primary_id, change_rate, status, remark, creator, create_time, updater, update_time, deleted)
SELECT
    id + 100 AS id,  -- ERP å•位ID偏移100避免与默认单位冲突
    name AS code,  -- ERP å•位没有编码,使用名称作为编码
    name,
    TRUE AS primary_flag,  -- é»˜è®¤ä¸ºä¸»å•位
    NULL AS primary_id,
    1 AS change_rate,
    status,
    NULL AS remark,
    '1' AS creator,
    create_time,
    '1' AS updater,
    update_time,
    0 AS deleted
FROM erp_product_unit
WHERE deleted = 0
ON DUPLICATE KEY UPDATE
    name = VALUES(name);
-- 2.2 è¿ç§» MES è®¡é‡å•位到 MDM(ID ä»Ž 1000 å¼€å§‹ï¼‰
INSERT INTO mdm_unit_measure (id, code, name, primary_flag, primary_id, change_rate, status, remark, creator, create_time, updater, update_time, deleted)
SELECT
    id + 1000 AS id,  -- MES å•位ID偏移1000避免冲突
    code,
    name,
    COALESCE(primary_flag, FALSE) AS primary_flag,
    CASE WHEN primary_id IS NOT NULL THEN primary_id + 1000 ELSE NULL END AS primary_id,
    change_rate,
    status,
    remark,
    '1' AS creator,
    create_time,
    '1' AS updater,
    update_time,
    0 AS deleted
FROM mes_md_unit_measure
WHERE deleted = 0
ON DUPLICATE KEY UPDATE
    name = VALUES(name),
    code = VALUES(code);
-- =====================================================
-- 03. è¿ç§»å“ç‰Œæ•°æ®
-- =====================================================
-- 3.1 è¿ç§» WMS å“ç‰Œåˆ° MDM
INSERT INTO mdm_brand (id, code, name, status, remark, creator, create_time, updater, update_time, deleted)
SELECT
    id,
    code,
    name,
    0 AS status,  -- é»˜è®¤å¯ç”¨
    NULL AS remark,
    '1' AS creator,
    create_time,
    '1' AS updater,
    update_time,
    0 AS deleted
FROM wms_item_brand
WHERE deleted = 0
ON DUPLICATE KEY UPDATE
    name = VALUES(name),
    code = VALUES(code);
-- =====================================================
-- 04. æ•°æ®éªŒè¯
-- =====================================================
-- éªŒè¯åˆ†ç±»æ•°æ®è¿ç§»
SELECT 'ERP分类迁移' AS source,
       (SELECT COUNT(*) FROM erp_product_category WHERE deleted = 0) AS original_count,
       (SELECT COUNT(*) FROM mdm_item_category WHERE id < 10000 AND deleted = 0) AS migrated_count;
SELECT 'MES分类迁移' AS source,
       (SELECT COUNT(*) FROM mes_md_item_type WHERE deleted = 0) AS original_count,
       (SELECT COUNT(*) FROM mdm_item_category WHERE id >= 10000 AND id < 20000 AND deleted = 0) AS migrated_count;
SELECT 'WMS分类迁移' AS source,
       (SELECT COUNT(*) FROM wms_item_category WHERE deleted = 0) AS original_count,
       (SELECT COUNT(*) FROM mdm_item_category WHERE id >= 20000 AND deleted = 0) AS migrated_count;
-- éªŒè¯å•位数据迁移
SELECT 'ERP单位迁移' AS source,
       (SELECT COUNT(*) FROM erp_product_unit WHERE deleted = 0) AS original_count,
       (SELECT COUNT(*) FROM mdm_unit_measure WHERE id >= 100 AND id < 1000 AND deleted = 0) AS migrated_count;
SELECT 'MES单位迁移' AS source,
       (SELECT COUNT(*) FROM mes_md_unit_measure WHERE deleted = 0) AS original_count,
       (SELECT COUNT(*) FROM mdm_unit_measure WHERE id >= 1000 AND deleted = 0) AS migrated_count;
-- éªŒè¯å“ç‰Œæ•°æ®è¿ç§»
SELECT 'WMS品牌迁移' AS source,
       (SELECT COUNT(*) FROM wms_item_brand WHERE deleted = 0) AS original_count,
       (SELECT COUNT(*) FROM mdm_brand WHERE deleted = 0) AS migrated_count;
docs/sql/mdm_migration_02_item_data.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,291 @@
-- =====================================================
-- MDM ä¸»æ•°æ®è¿ç§» - ç¬¬äºŒé˜¶æ®µï¼šç‰©æ–™æ•°æ®è¿ç§»
-- æ‰§è¡Œé¡ºåºï¼šå…ˆæ‰§è¡Œç¬¬ä¸€é˜¶æ®µ SQL
-- é€‚用场景:新项目初始化,无历史业务数据
-- =====================================================
-- =====================================================
-- 01. è¿ç§» MES ç‰©æ–™æ•°æ®
-- =====================================================
-- MES ç‰©æ–™ç±»åž‹ä¸º itemType=1(物料)
INSERT INTO mdm_item (id, code, name, bar_code, specification, category_id, unit_measure_id, brand_id,
    item_type, status, purchase_price, sales_price, cost_price, min_price, safety_stock, min_stock, max_stock,
    min_order_qty, lead_time, is_batch_managed, is_serial_managed, barcode_rule_id, expiry_day, high_value,
    weight, remark, creator, create_time, updater, update_time, deleted)
SELECT
    m.id AS id,
    m.code,
    m.name,
    NULL AS bar_code,  -- MES ç‰©æ–™æ²¡æœ‰æ¡ç å­—段
    m.specification,
    -- MES åˆ†ç±»ID偏移了10000
    m.item_type_id + 10000 AS category_id,
    -- MES å•位ID偏移了1000
    m.unit_measure_id + 1000 AS unit_measure_id,
    NULL AS brand_id,
    1 AS item_type,  -- ç‰©æ–™ç±»åž‹
    m.status,
    NULL AS purchase_price,
    NULL AS sales_price,
    NULL AS cost_price,
    NULL AS min_price,
    NULL AS safety_stock,
    m.min_stock,
    m.max_stock,
    NULL AS min_order_qty,
    NULL AS lead_time,
    COALESCE(m.batch_flag, FALSE) AS is_batch_managed,
    FALSE AS is_serial_managed,
    NULL AS barcode_rule_id,
    NULL AS expiry_day,
    COALESCE(m.high_value, FALSE) AS high_value,
    NULL AS weight,
    m.remark,
    '1' AS creator,
    m.create_time,
    '1' AS updater,
    m.update_time,
    0 AS deleted
FROM mes_md_item m
WHERE m.deleted = 0
ON DUPLICATE KEY UPDATE
    name = VALUES(name),
    code = VALUES(code);
-- =====================================================
-- 02. è¿ç§» ERP äº§å“æ•°æ®
-- =====================================================
-- ERP äº§å“ç±»åž‹ä¸º itemType=2(产品)
INSERT INTO mdm_item (id, code, name, bar_code, specification, category_id, unit_measure_id, brand_id,
    item_type, status, purchase_price, sales_price, cost_price, min_price, safety_stock, min_stock, max_stock,
    min_order_qty, lead_time, is_batch_managed, is_serial_managed, barcode_rule_id, expiry_day, high_value,
    weight, remark, creator, create_time, updater, update_time, deleted)
SELECT
    p.id AS id,
    p.bar_code AS code,  -- ERP ä½¿ç”¨æ¡ç ä½œä¸ºç¼–码
    p.name,
    p.bar_code,
    p.standard AS specification,
    -- ERP åˆ†ç±»ID无偏移
    p.category_id,
    -- ERP å•位ID偏移了100
    p.unit_id + 100 AS unit_measure_id,
    NULL AS brand_id,
    2 AS item_type,  -- äº§å“ç±»åž‹
    p.status,
    p.purchase_price,
    p.sale_price AS sales_price,
    NULL AS cost_price,
    p.min_price,
    NULL AS safety_stock,
    NULL AS min_stock,
    NULL AS max_stock,
    NULL AS min_order_qty,
    NULL AS lead_time,
    FALSE AS is_batch_managed,
    FALSE AS is_serial_managed,
    NULL AS barcode_rule_id,
    p.expiry_day,
    FALSE AS high_value,
    p.weight,
    p.remark,
    '1' AS creator,
    p.create_time,
    '1' AS updater,
    p.update_time,
    0 AS deleted
FROM erp_product p
WHERE p.deleted = 0
ON DUPLICATE KEY UPDATE
    name = VALUES(name),
    bar_code = VALUES(bar_code);
-- =====================================================
-- 03. è¿ç§» WMS å•†å“æ•°æ®
-- =====================================================
-- WMS å•†å“ç±»åž‹ä¸º itemType=1(物料)
-- æ³¨æ„ï¼šWMS å•†å“çš„ unit å­—段是字符串,需要匹配 MDM å•位名称,找不到则使用默认单位(id=1)
INSERT INTO mdm_item (id, code, name, bar_code, specification, category_id, unit_measure_id, brand_id,
    item_type, status, purchase_price, sales_price, cost_price, min_price, safety_stock, min_stock, max_stock,
    min_order_qty, lead_time, is_batch_managed, is_serial_managed, barcode_rule_id, expiry_day, high_value,
    weight, remark, creator, create_time, updater, update_time, deleted)
SELECT
    w.id + 30000 AS id,  -- WMS å•†å“ID偏移30000避免冲突
    w.code,
    w.name,
    NULL AS bar_code,
    NULL AS specification,
    -- WMS åˆ†ç±»ID偏移了20000
    w.category_id + 20000 AS category_id,
    -- WMS å•†å“å•位为字符串,需匹配MDM单位名称,找不到则使用默认单位
    COALESCE(
        (SELECT id FROM mdm_unit_measure WHERE name = w.unit AND deleted = 0 LIMIT 1),
        1  -- é»˜è®¤å•位ID
    ) AS unit_measure_id,
    -- WMS å“ç‰ŒID无偏移
    w.brand_id,
    1 AS item_type,  -- é»˜è®¤ç‰©æ–™ç±»åž‹
    0 AS status,  -- é»˜è®¤å¯ç”¨
    NULL AS purchase_price,
    NULL AS sales_price,
    NULL AS cost_price,
    NULL AS min_price,
    NULL AS safety_stock,
    NULL AS min_stock,
    NULL AS max_stock,
    NULL AS min_order_qty,
    NULL AS lead_time,
    FALSE AS is_batch_managed,
    FALSE AS is_serial_managed,
    NULL AS barcode_rule_id,
    NULL AS expiry_day,
    FALSE AS high_value,
    NULL AS weight,
    w.remark,
    '1' AS creator,
    w.create_time,
    '1' AS updater,
    w.update_time,
    0 AS deleted
FROM wms_item w
WHERE w.deleted = 0
ON DUPLICATE KEY UPDATE
    name = VALUES(name),
    code = VALUES(code);
-- =====================================================
-- 04. è¿ç§» WMS SKU æ•°æ®å¹¶åˆ›å»º MDM SKU
-- =====================================================
-- è¿ç§» WMS SKU åˆ° MDM SKU
INSERT INTO mdm_item_sku (id, item_id, code, name, bar_code, length, width, height,
    gross_weight, net_weight, cost_price, sale_price, status, remark,
    creator, create_time, updater, update_time, deleted)
SELECT
    s.id + 40000 AS id,  -- SKU ID偏移40000避免冲突
    -- WMS å•†å“ID偏移了30000
    s.item_id + 30000 AS item_id,
    s.code,
    s.name,
    s.bar_code,
    s.length,
    s.width,
    s.height,
    s.gross_weight,
    s.net_weight,
    s.cost_price,
    s.selling_price AS sale_price,
    0 AS status,
    NULL AS remark,
    '1' AS creator,
    s.create_time,
    '1' AS updater,
    s.update_time,
    0 AS deleted
FROM wms_item_sku s
WHERE s.deleted = 0
ON DUPLICATE KEY UPDATE
    name = VALUES(name),
    code = VALUES(code);
-- =====================================================
-- 05. ä¸º MES/ERP ç‰©æ–™åˆ›å»ºé»˜è®¤ SKU
-- =====================================================
-- ä¸º MES ç‰©æ–™åˆ›å»ºé»˜è®¤ SKU(如果不存在)
INSERT INTO mdm_item_sku (item_id, code, name, bar_code, length, width, height,
    gross_weight, net_weight, cost_price, sale_price, status, remark,
    creator, create_time, updater, update_time, deleted)
SELECT
    m.id AS item_id,
    CONCAT(m.code, '-DEFAULT') AS code,
    CONCAT(m.name, '-默认SKU') AS name,
    NULL AS bar_code,
    NULL AS length,
    NULL AS width,
    NULL AS height,
    NULL AS gross_weight,
    NULL AS net_weight,
    NULL AS cost_price,
    NULL AS sale_price,
    0 AS status,
    '系统自动创建' AS remark,
    '1' AS creator,
    NOW() AS create_time,
    '1' AS updater,
    NOW() AS update_time,
    0 AS deleted
FROM mdm_item m
WHERE m.item_type = 1  -- MES ç‰©æ–™
  AND m.deleted = 0
  AND m.id < 30000  -- æŽ’除 WMS å•†å“ï¼ˆå·²æœ‰ SKU)
  AND NOT EXISTS (
      SELECT 1 FROM mdm_item_sku s WHERE s.item_id = m.id AND s.deleted = 0
  );
-- ä¸º ERP äº§å“åˆ›å»ºé»˜è®¤ SKU(如果不存在)
INSERT INTO mdm_item_sku (item_id, code, name, bar_code, length, width, height,
    gross_weight, net_weight, cost_price, sale_price, status, remark,
    creator, create_time, updater, update_time, deleted)
SELECT
    p.id AS item_id,
    CONCAT(p.bar_code, '-DEFAULT') AS code,
    CONCAT(p.name, '-默认SKU') AS name,
    p.bar_code,
    NULL AS length,
    NULL AS width,
    NULL AS height,
    NULL AS gross_weight,
    NULL AS net_weight,
    NULL AS cost_price,
    NULL AS sale_price,
    0 AS status,
    '系统自动创建' AS remark,
    '1' AS creator,
    NOW() AS create_time,
    '1' AS updater,
    NOW() AS update_time,
    0 AS deleted
FROM mdm_item p
WHERE p.item_type = 2  -- ERP äº§å“
  AND p.deleted = 0
  AND NOT EXISTS (
      SELECT 1 FROM mdm_item_sku s WHERE s.item_id = p.id AND s.deleted = 0
  );
-- =====================================================
-- 06. æ•°æ®éªŒè¯
-- =====================================================
-- éªŒè¯ç‰©æ–™æ•°æ®è¿ç§»
SELECT 'MES物料迁移' AS source,
       (SELECT COUNT(*) FROM mes_md_item WHERE deleted = 0) AS original_count,
       (SELECT COUNT(*) FROM mdm_item WHERE item_type = 1 AND id < 30000 AND deleted = 0) AS migrated_count;
SELECT 'ERP产品迁移' AS source,
       (SELECT COUNT(*) FROM erp_product WHERE deleted = 0) AS original_count,
       (SELECT COUNT(*) FROM mdm_item WHERE item_type = 2 AND deleted = 0) AS migrated_count;
SELECT 'WMS商品迁移' AS source,
       (SELECT COUNT(*) FROM wms_item WHERE deleted = 0) AS original_count,
       (SELECT COUNT(*) FROM mdm_item WHERE id >= 30000 AND deleted = 0) AS migrated_count;
-- éªŒè¯ SKU æ•°æ®
SELECT 'SKU总数' AS metric, COUNT(*) AS count FROM mdm_item_sku WHERE deleted = 0;
SELECT '有SKU的物料数' AS metric, COUNT(DISTINCT item_id) AS count FROM mdm_item_sku WHERE deleted = 0;
-- =====================================================
-- 07. æ£€æŸ¥ä½¿ç”¨é»˜è®¤å•位的 WMS å•†å“
-- =====================================================
-- æŸ¥çœ‹ WMS å•†å“ä¸­å•位无法匹配的情况
SELECT w.id, w.code, w.name, w.unit
FROM wms_item w
WHERE w.deleted = 0
  AND NOT EXISTS (
      SELECT 1 FROM mdm_unit_measure u WHERE u.name = w.unit AND u.deleted = 0
  )
LIMIT 20;
docs/sql/mdm_migration_03_update_refs.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
-- =====================================================
-- MDM ä¸»æ•°æ®è¿ç§» - è¡¥å……:添加缺失的字段
-- æ‰§è¡Œé¡ºåºï¼šåœ¨ mdm_migration_02_item_data.sql ä¹‹åŽæ‰§è¡Œ
-- =====================================================
-- =====================================================
-- 01. æ£€æŸ¥å¹¶æ·»åŠ  mes_wm_material_stock è¡¨çš„ item_type_id å­—段
-- =====================================================
-- æ·»åŠ  item_type_id å­—段(如果不存在)
-- MySQL ä¸æ”¯æŒ IF NOT EXISTS ç”¨äºŽ ADD COLUMN,使用存储过程或直接执行
-- å…ˆå°è¯•添加,如果已存在会报错,可以忽略
-- æ–¹å¼1:直接添加(如果字段已存在会报错,忽略即可)
ALTER TABLE mes_wm_material_stock ADD COLUMN item_type_id bigint DEFAULT NULL COMMENT '物料分类编号' AFTER id;
-- æ–¹å¼2:如果方式1报错说字段已存在,说明字段已经存在,跳过即可
-- =====================================================
-- 02. ä¸º item_type_id åˆ›å»ºç´¢å¼•(可选)
-- =====================================================
-- åˆ›å»ºç´¢å¼•(如果不存在)
-- CREATE INDEX idx_item_type_id ON mes_wm_material_stock(item_type_id);
-- =====================================================
-- 03. æ›´æ–°åº“存表中的 item_type_id
-- æ ¹æ® item_id å…³è” mes_md_item è¡¨èŽ·å– item_type_id
-- =====================================================
-- ä»Žç‰©æ–™è¡¨èŽ·å–åˆ†ç±»ID并更新到库存表
UPDATE mes_wm_material_stock s
INNER JOIN mes_md_item i ON s.item_id = i.id
SET s.item_type_id = i.item_type_id + 10000  -- MDM分类ID偏移10000
WHERE s.item_type_id IS NULL
  AND s.deleted = 0;
-- =====================================================
-- 04. éªŒè¯ç»“æžœ
-- =====================================================
-- æŸ¥çœ‹åº“存表字段
SELECT * FROM mes_wm_material_stock LIMIT 5;
-- éªŒè¯ item_type_id æ˜¯å¦æ›´æ–°
SELECT COUNT(*) AS updated_count
FROM mes_wm_material_stock
WHERE item_type_id IS NOT NULL AND item_type_id >= 10000 AND deleted = 0;
docs/sql/mdm_migration_04_alter_columns.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
-- =====================================================
-- MDM ä¸»æ•°æ®è¿ç§» - è¡¥å……:修改字段约束
-- æ‰§è¡Œé¡ºåºï¼šåœ¨ mdm_schema.sql ä¹‹åŽæ‰§è¡Œï¼ˆå¦‚果表已存在)
-- =====================================================
-- ä¿®æ”¹ mdm_item è¡¨å­—段约束,允许 category_id å’Œ unit_measure_id ä¸ºç©º
ALTER TABLE mdm_item MODIFY COLUMN category_id bigint DEFAULT NULL COMMENT '物料分类编号';
ALTER TABLE mdm_item MODIFY COLUMN unit_measure_id bigint DEFAULT NULL COMMENT '计量单位编号';
-- å¦‚果需要删除 brand_id å­—段
ALTER TABLE mdm_item DROP COLUMN IF EXISTS brand_id;
docs/sql/mdm_schema.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,194 @@
-- =====================================================
-- MDM ä¸»æ•°æ®æ¨¡å— - æ•°æ®åº“表结构
-- æ‰§è¡Œé¡ºåºï¼šå…ˆæ‰§è¡Œæ­¤è„šæœ¬åˆ›å»ºè¡¨ï¼Œå†æ‰§è¡Œæ•°æ®è¿ç§»è„šæœ¬
-- =====================================================
-- =====================================================
-- 01. ç‰©æ–™åˆ†ç±»è¡¨
-- =====================================================
DROP TABLE IF EXISTS `mdm_item_category`;
CREATE TABLE `mdm_item_category` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '分类编号',
    `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '父分类编号(根节点=0)',
    `code` varchar(50) DEFAULT NULL COMMENT '分类编码',
    `name` varchar(100) NOT NULL COMMENT '分类名称',
    `item_type` tinyint DEFAULT NULL COMMENT '适用物料类型(1:物料 2:产品 3:半成品 NULL:全部)',
    `sort` int NOT NULL DEFAULT 0 COMMENT '排序',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态(0正常 1停用)',
    `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 '更新者',
    `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`),
    KEY `idx_parent_id` (`parent_id`),
    KEY `idx_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MDM ç‰©æ–™åˆ†ç±»è¡¨';
-- =====================================================
-- 02. è®¡é‡å•位表
-- =====================================================
DROP TABLE IF EXISTS `mdm_unit_measure`;
CREATE TABLE `mdm_unit_measure` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '单位编号',
    `code` varchar(50) NOT NULL COMMENT '单位编码',
    `name` varchar(50) NOT NULL COMMENT '单位名称',
    `primary_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否主单位',
    `primary_id` bigint DEFAULT NULL COMMENT '主单位编号',
    `change_rate` decimal(24,6) DEFAULT 1.000000 COMMENT '与主单位换算比例',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态(0正常 1停用)',
    `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 '更新者',
    `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`),
    UNIQUE KEY `uk_code` (`code`, `deleted`),
    KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MDM è®¡é‡å•位表';
-- =====================================================
-- 03. å“ç‰Œè¡¨
-- =====================================================
DROP TABLE IF EXISTS `mdm_brand`;
CREATE TABLE `mdm_brand` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '品牌编号',
    `code` varchar(50) NOT NULL COMMENT '品牌编码',
    `name` varchar(100) NOT NULL COMMENT '品牌名称',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态(0正常 1停用)',
    `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 '更新者',
    `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`),
    UNIQUE KEY `uk_code` (`code`, `deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MDM å“ç‰Œè¡¨';
-- =====================================================
-- 04. ç‰©æ–™ä¸»æ•°æ®è¡¨
-- =====================================================
DROP TABLE IF EXISTS `mdm_item`;
CREATE TABLE `mdm_item` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '物料编号',
    `code` varchar(50) NOT NULL COMMENT '物料编码',
    `name` varchar(200) NOT NULL COMMENT '物料名称',
    `bar_code` varchar(100) DEFAULT NULL COMMENT '物料条码',
    `specification` varchar(200) DEFAULT NULL COMMENT '规格型号',
    `category_id` bigint DEFAULT NULL COMMENT '物料分类编号',
    `unit_measure_id` bigint DEFAULT NULL COMMENT '计量单位编号',
    `item_type` tinyint NOT NULL DEFAULT 1 COMMENT '物料类型(1:物料 2:产品 3:半成品)',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态(0正常 1停用)',
    `purchase_price` decimal(24,2) DEFAULT NULL COMMENT '采购价',
    `sales_price` decimal(24,2) DEFAULT NULL COMMENT '销售价',
    `cost_price` decimal(24,2) DEFAULT NULL COMMENT '成本价',
    `min_price` decimal(24,2) DEFAULT NULL COMMENT '最低价',
    `safety_stock` decimal(24,6) DEFAULT NULL COMMENT '安全库存',
    `min_stock` decimal(24,6) DEFAULT NULL COMMENT '最低库存量',
    `max_stock` decimal(24,6) DEFAULT NULL COMMENT '最高库存量',
    `min_order_qty` decimal(24,6) DEFAULT NULL COMMENT '最小订货量',
    `lead_time` int DEFAULT NULL COMMENT '采购提前期(天)',
    `is_batch_managed` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否批次管理',
    `is_serial_managed` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否序列号管理',
    `barcode_rule_id` bigint DEFAULT NULL COMMENT '条码规则ID',
    `expiry_day` int DEFAULT NULL COMMENT '保质期天数',
    `high_value` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否高值物料',
    `weight` decimal(24,6) DEFAULT NULL COMMENT '重量(kg)',
    `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 '更新者',
    `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`),
    UNIQUE KEY `uk_code` (`code`, `deleted`),
    KEY `idx_name` (`name`),
    KEY `idx_category_id` (`category_id`),
    KEY `idx_item_type` (`item_type`),
    KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MDM ç‰©æ–™ä¸»æ•°æ®è¡¨';
-- =====================================================
-- 05. ç‰©æ–™SKU表
-- =====================================================
DROP TABLE IF EXISTS `mdm_item_sku`;
CREATE TABLE `mdm_item_sku` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'SKU编号',
    `item_id` bigint NOT NULL COMMENT '物料编号',
    `code` varchar(50) NOT NULL COMMENT 'SKU编码',
    `name` varchar(200) NOT NULL COMMENT 'SKU名称',
    `bar_code` varchar(100) DEFAULT NULL COMMENT 'SKU条码',
    `length` decimal(24,6) DEFAULT NULL COMMENT '长度(cm)',
    `width` decimal(24,6) DEFAULT NULL COMMENT '宽度(cm)',
    `height` decimal(24,6) DEFAULT NULL COMMENT '高度(cm)',
    `gross_weight` decimal(24,6) DEFAULT NULL COMMENT '毛重(kg)',
    `net_weight` decimal(24,6) DEFAULT NULL COMMENT '净重(kg)',
    `cost_price` decimal(24,2) DEFAULT NULL COMMENT '成本价',
    `sale_price` decimal(24,2) DEFAULT NULL COMMENT '销售价',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态(0正常 1停用)',
    `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 '更新者',
    `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`),
    UNIQUE KEY `uk_code` (`code`, `deleted`),
    KEY `idx_item_id` (`item_id`),
    KEY `idx_bar_code` (`bar_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MDM ç‰©æ–™SKU表';
-- =====================================================
-- 06. ç‰©æ–™æ‰¹æ¬¡å±žæ€§é…ç½®è¡¨
-- =====================================================
DROP TABLE IF EXISTS `mdm_item_batch_config`;
CREATE TABLE `mdm_item_batch_config` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '配置编号',
    `item_id` bigint NOT NULL COMMENT '物料编号',
    `produce_date_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用生产日期',
    `expire_date_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用失效日期',
    `receipt_date_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用入库日期',
    `vendor_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用供应商',
    `client_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用客户',
    `sales_order_code_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用销售订单号',
    `purchase_order_code_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用采购订单号',
    `work_order_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用生产工单',
    `task_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用生产任务',
    `workstation_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用工位',
    `tool_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用工装夹具',
    `mold_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用模具',
    `lot_number_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用批次号',
    `quality_status_flag` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否启用质量状态',
    `creator` varchar(64) DEFAULT '' COMMENT '创建者',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updater` varchar(64) 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`),
    UNIQUE KEY `uk_item_id` (`item_id`, `deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MDM ç‰©æ–™æ‰¹æ¬¡å±žæ€§é…ç½®è¡¨';
-- =====================================================
-- 07. ä»“库表
-- =====================================================
DROP TABLE IF EXISTS `mdm_warehouse`;
CREATE TABLE `mdm_warehouse` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '仓库编号',
    `code` varchar(50) NOT NULL COMMENT '仓库编码',
    `name` varchar(100) NOT NULL COMMENT '仓库名称',
    `type` tinyint DEFAULT NULL COMMENT '仓库类型',
    `address` varchar(500) DEFAULT NULL COMMENT '仓库地址',
    `principal` varchar(64) DEFAULT NULL COMMENT '负责人',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态(0正常 1停用)',
    `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 '更新者',
    `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`),
    UNIQUE KEY `uk_code` (`code`, `deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='MDM ä»“库表';
pom.xml
@@ -22,11 +22,16 @@
<!--        <module>yudao-module-pay</module>-->
<!--        <module>yudao-module-mall</module>-->
        <module>yudao-module-crm</module>
        <module>yudao-module-crm-api</module>
        <module>yudao-module-erp</module>
        <module>yudao-module-erp-api</module>
<!--        <module>yudao-module-iot</module>-->
        <module>yudao-module-mes</module>
        <module>yudao-module-wms</module>
        <module>yudao-module-im</module>
        <!-- MDM ä¸»æ•°æ®ç®¡ç†æ¨¡å— -->
        <module>yudao-module-mdm-api</module>
        <module>yudao-module-mdm</module>
        <!-- è¯·å‚考 https://doc.iocoder.cn/ai/build/ æ–‡æ¡£ï¼Œå®Œæˆ AI æ¨¡å—的启动!!! -->
<!--        <module>yudao-module-ai</module>-->
    </modules>
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/TerminalEnum.java
@@ -1,8 +1,6 @@
package cn.iocoder.yudao.framework.common.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
@@ -11,8 +9,6 @@
 *
 * @author èŠ‹é“æºç 
 */
@RequiredArgsConstructor
@Getter
public enum TerminalEnum implements ArrayValuable<Integer> {
    UNKNOWN(0, "未知"), // ç›®çš„:在无法解析到 terminal æ—¶ï¼Œä½¿ç”¨å®ƒ
@@ -33,6 +29,19 @@
     */
    private final String name;
    TerminalEnum(Integer terminal, String name) {
        this.terminal = terminal;
        this.name = name;
    }
    public Integer getTerminal() {
        return terminal;
    }
    public String getName() {
        return name;
    }
    @Override
    public Integer[] array() {
        return ARRAYS;
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java
@@ -24,4 +24,10 @@
                new LambdaQueryWrapperX<BpmProcessDefinitionInfoDO>().eq(BpmProcessDefinitionInfoDO::getModelId, modelId));
    }
    default List<BpmProcessDefinitionInfoDO> selectListByCategory(String category) {
        return selectList(new LambdaQueryWrapperX<BpmProcessDefinitionInfoDO>()
                .eq(BpmProcessDefinitionInfoDO::getCategory, category)
                .eq(BpmProcessDefinitionInfoDO::getVisible, true));
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java
@@ -178,4 +178,12 @@
     */
    Deployment getDeployment(String id);
    /**
     * èŽ·å¾—æŒ‡å®šåˆ†ç±»ä¸‹çš„æµç¨‹å®šä¹‰ä¿¡æ¯åˆ—è¡¨
     *
     * @param category åˆ†ç±»ç¼–码
     * @return æµç¨‹å®šä¹‰ä¿¡æ¯åˆ—表
     */
    List<BpmProcessDefinitionInfoDO> getProcessDefinitionInfoListByCategory(String category);
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
@@ -244,4 +244,9 @@
        return query.list();
    }
    @Override
    public List<BpmProcessDefinitionInfoDO> getProcessDefinitionInfoListByCategory(String category) {
        return processDefinitionMapper.selectListByCategory(category);
    }
}
yudao-module-crm-api/pom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>yudao-module-crm-api</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        crm æ¨¡å—下,客户关系管理的 API æŽ¥å£å®šä¹‰ï¼Œä¾›å…¶ä»–模块引用
    </description>
    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-common</artifactId>
        </dependency>
        <!-- Swagger æ³¨è§£ -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.crm.api.customer;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
 * CRM å®¢æˆ· API æŽ¥å£
 *
 * æä¾›ç»™å…¶å®ƒæ¨¡å—(如 ERP)调用
 *
 * @author èŠ‹é“æºç 
 */
public interface CrmCustomerApi {
    /**
     * èŽ·å¾—å®¢æˆ·ä¿¡æ¯
     *
     * @param id å®¢æˆ·ç¼–号
     * @return å®¢æˆ·ä¿¡æ¯
     */
    CrmCustomerRespDTO getCustomer(Long id);
    /**
     * èŽ·å¾—å®¢æˆ·ä¿¡æ¯åˆ—è¡¨
     *
     * @param ids å®¢æˆ·ç¼–号列表
     * @return å®¢æˆ·ä¿¡æ¯åˆ—表
     */
    List<CrmCustomerRespDTO> getCustomerList(Collection<Long> ids);
    /**
     * èŽ·å¾—å®¢æˆ·ä¿¡æ¯ Map
     *
     * @param ids å®¢æˆ·ç¼–号列表
     * @return å®¢æˆ·ä¿¡æ¯ Map
     */
    Map<Long, CrmCustomerRespDTO> getCustomerMap(Collection<Long> ids);
    /**
     * æ ¡éªŒå®¢æˆ·æ˜¯å¦å­˜åœ¨
     *
     * @param id å®¢æˆ·ç¼–号
     * @return å®¢æˆ·ä¿¡æ¯ï¼ˆæ ¡éªŒé€šè¿‡ï¼‰
     */
    CrmCustomerRespDTO validateCustomer(Long id);
}
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/customer/dto/CrmCustomerRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
package cn.iocoder.yudao.module.crm.api.customer.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * CRM å®¢æˆ· Response DTO
 *
 * æä¾›ç»™å…¶å®ƒæ¨¡å—(如 ERP)使用
 *
 * @author èŠ‹é“æºç 
 */
@Data
public class CrmCustomerRespDTO {
    /**
     * ç¼–号
     */
    private Long id;
    /**
     * å®¢æˆ·åç§°
     */
    private String name;
    /**
     * æ‰‹æœº
     */
    private String mobile;
    /**
     * ç”µè¯
     */
    private String telephone;
    /**
     * QQ
     */
    private String qq;
    /**
     * wechat
     */
    private String wechat;
    /**
     * email
     */
    private String email;
    /**
     * æ‰€åœ¨åœ°
     */
    private Integer areaId;
    /**
     * è¯¦ç»†åœ°å€
     */
    private String detailAddress;
    /**
     * æ‰€å±žè¡Œä¸š
     */
    private Integer industryId;
    /**
     * å®¢æˆ·ç­‰çº§
     */
    private Integer level;
    /**
     * å®¢æˆ·æ¥æº
     */
    private Integer source;
    /**
     * è´Ÿè´£äººçš„用户编号
     */
    private Long ownerUserId;
    /**
     * æˆä¸ºè´Ÿè´£äººçš„æ—¶é—´
     */
    private LocalDateTime ownerTime;
    /**
     * é”å®šçŠ¶æ€
     */
    private Boolean lockStatus;
    /**
     * æˆäº¤çŠ¶æ€
     */
    private Boolean dealStatus;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/CrmReportApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.crm.api.report;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.crm.api.report.dto.CrmBusinessAnalyzeRespDTO;
import cn.iocoder.yudao.module.crm.api.report.dto.CrmContractAnalyzeRespDTO;
import cn.iocoder.yudao.module.crm.api.report.dto.CrmCustomerAnalyzeRespDTO;
import cn.iocoder.yudao.module.crm.api.report.dto.CrmSalesFunnelRespDTO;
import cn.iocoder.yudao.module.crm.api.report.dto.CrmSalesPerformanceRespDTO;
import java.time.LocalDateTime;
/**
 * CRM è¥é”€æŠ¥è¡¨ API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface CrmReportApi {
    String PREFIX = "/rpc-api/crm/report";
    /**
     * èŽ·å–å®¢æˆ·åˆ†æžç»Ÿè®¡
     *
     * @param startTime å¼€å§‹æ—¶é—´
     * @param endTime   ç»“束时间
     * @return ç»Ÿè®¡æ•°æ®
     */
    CommonResult<CrmCustomerAnalyzeRespDTO> getCustomerAnalyze(LocalDateTime startTime, LocalDateTime endTime);
    /**
     * èŽ·å–å•†æœºåˆ†æžç»Ÿè®¡
     *
     * @param startTime å¼€å§‹æ—¶é—´
     * @param endTime   ç»“束时间
     * @return ç»Ÿè®¡æ•°æ®
     */
    CommonResult<CrmBusinessAnalyzeRespDTO> getBusinessAnalyze(LocalDateTime startTime, LocalDateTime endTime);
    /**
     * èŽ·å–åˆåŒåˆ†æžç»Ÿè®¡
     *
     * @param startTime å¼€å§‹æ—¶é—´
     * @param endTime   ç»“束时间
     * @return ç»Ÿè®¡æ•°æ®
     */
    CommonResult<CrmContractAnalyzeRespDTO> getContractAnalyze(LocalDateTime startTime, LocalDateTime endTime);
    /**
     * èŽ·å–é”€å”®æ¼æ–—ç»Ÿè®¡
     *
     * @return ç»Ÿè®¡æ•°æ®
     */
    CommonResult<CrmSalesFunnelRespDTO> getSalesFunnel();
    /**
     * èŽ·å–é”€å”®ä¸šç»©ç»Ÿè®¡
     *
     * @param startTime å¼€å§‹æ—¶é—´
     * @param endTime   ç»“束时间
     * @return ç»Ÿè®¡æ•°æ®
     */
    CommonResult<CrmSalesPerformanceRespDTO> getSalesPerformance(LocalDateTime startTime, LocalDateTime endTime);
}
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/dto/CrmBusinessAnalyzeRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.crm.api.report.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * CRM å•†æœºåˆ†æžç»Ÿè®¡ Response DTO
 */
@Schema(description = "RPC æœåŠ¡ - CRM å•†æœºåˆ†æžç»Ÿè®¡ Response DTO")
@Data
public class CrmBusinessAnalyzeRespDTO {
    @Schema(description = "商机总数")
    private Long totalCount;
    @Schema(description = "新增商机数(今日)")
    private Long todayCount;
    @Schema(description = "商机总金额")
    private BigDecimal totalPrice;
    @Schema(description = "成交商机数")
    private Long successCount;
    @Schema(description = "成交商机金额")
    private BigDecimal successPrice;
    @Schema(description = "商机转化率(百分比)")
    private BigDecimal successRate;
    @Schema(description = "进行中商机数")
    private Long processingCount;
    @Schema(description = "失败商机数")
    private Long failCount;
}
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/dto/CrmContractAnalyzeRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.crm.api.report.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * CRM åˆåŒåˆ†æžç»Ÿè®¡ Response DTO
 */
@Schema(description = "RPC æœåŠ¡ - CRM åˆåŒåˆ†æžç»Ÿè®¡ Response DTO")
@Data
public class CrmContractAnalyzeRespDTO {
    @Schema(description = "合同总数")
    private Long totalCount;
    @Schema(description = "新增合同数(今日)")
    private Long todayCount;
    @Schema(description = "合同总金额")
    private BigDecimal totalPrice;
    @Schema(description = "审批通过合同数")
    private Long approveCount;
    @Schema(description = "审批通过合同金额")
    private BigDecimal approvePrice;
    @Schema(description = "审批中合同数")
    private Long processCount;
    @Schema(description = "已回款金额")
    private BigDecimal receivablePrice;
    @Schema(description = "回款率(百分比)")
    private BigDecimal receivableRate;
}
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/dto/CrmCustomerAnalyzeRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.crm.api.report.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * CRM å®¢æˆ·åˆ†æžç»Ÿè®¡ Response DTO
 */
@Schema(description = "RPC æœåŠ¡ - CRM å®¢æˆ·åˆ†æžç»Ÿè®¡ Response DTO")
@Data
public class CrmCustomerAnalyzeRespDTO {
    @Schema(description = "客户总数")
    private Long totalCount;
    @Schema(description = "新增客户数(今日)")
    private Long todayCount;
    @Schema(description = "成交客户数")
    private Long dealCount;
    @Schema(description = "成交率(百分比)")
    private BigDecimal dealRate;
    @Schema(description = "锁定客户数")
    private Long lockCount;
    @Schema(description = "公海客户数")
    private Long poolCount;
}
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/dto/CrmSalesFunnelRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.crm.api.report.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
 * CRM é”€å”®æ¼æ–—统计 Response DTO
 */
@Schema(description = "RPC æœåŠ¡ - CRM é”€å”®æ¼æ–—统计 Response DTO")
@Data
public class CrmSalesFunnelRespDTO {
    @Schema(description = "漏斗阶段列表")
    private List<FunnelStage> stages;
    @Schema(description = "漏斗阶段")
    @Data
    public static class FunnelStage {
        @Schema(description = "阶段名称")
        private String name;
        @Schema(description = "数量")
        private Long count;
        @Schema(description = "金额")
        private BigDecimal price;
        @Schema(description = "转化率(百分比)")
        private BigDecimal conversionRate;
    }
}
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/report/dto/CrmSalesPerformanceRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.crm.api.report.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
 * CRM é”€å”®ä¸šç»©ç»Ÿè®¡ Response DTO
 */
@Schema(description = "RPC æœåŠ¡ - CRM é”€å”®ä¸šç»©ç»Ÿè®¡ Response DTO")
@Data
public class CrmSalesPerformanceRespDTO {
    @Schema(description = "销售人员业绩列表")
    private List<SalesItem> items;
    @Schema(description = "销售业绩项")
    @Data
    public static class SalesItem {
        @Schema(description = "销售人员编号")
        private Long userId;
        @Schema(description = "销售人员姓名")
        private String userName;
        @Schema(description = "部门名称")
        private String deptName;
        @Schema(description = "合同数量")
        private Long contractCount;
        @Schema(description = "合同金额")
        private BigDecimal contractPrice;
        @Schema(description = "回款金额")
        private BigDecimal receivablePrice;
        @Schema(description = "商机数量")
        private Long businessCount;
        @Schema(description = "商机金额")
        private BigDecimal businessPrice;
    }
}
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/trace/CrmSaleTraceApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.crm.api.trace;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.crm.api.trace.dto.CrmSaleTraceRespDTO;
/**
 * CRM é”€å”®è¿½æº¯ API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface CrmSaleTraceApi {
    String PREFIX = "/rpc-api/crm/sale-trace";
    /**
     * æ ¹æ®å•†æœºç¼–号获取销售流程追溯信息
     *
     * @param businessId å•†æœºç¼–号
     * @return è¿½æº¯ä¿¡æ¯
     */
    CommonResult<CrmSaleTraceRespDTO> getTraceByBusinessId(Long businessId);
    /**
     * æ ¹æ®åˆåŒç¼–号获取销售流程追溯信息
     *
     * @param contractId åˆåŒç¼–号
     * @return è¿½æº¯ä¿¡æ¯
     */
    CommonResult<CrmSaleTraceRespDTO> getTraceByContractId(Long contractId);
    /**
     * æ ¹æ®å®¢æˆ·ç¼–号获取销售流程追溯信息列表
     *
     * @param customerId å®¢æˆ·ç¼–号
     * @return è¿½æº¯ä¿¡æ¯åˆ—表
     */
    CommonResult<java.util.List<CrmSaleTraceRespDTO>> getTraceListByCustomerId(Long customerId);
}
yudao-module-crm-api/src/main/java/cn/iocoder/yudao/module/crm/api/trace/dto/CrmSaleTraceRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,125 @@
package cn.iocoder.yudao.module.crm.api.trace.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
 * CRM é”€å”®æµç¨‹è¿½æº¯ Response DTO
 */
@Schema(description = "RPC æœåŠ¡ - CRM é”€å”®æµç¨‹è¿½æº¯ Response DTO")
@Data
public class CrmSaleTraceRespDTO {
    // ========== å•†æœºä¿¡æ¯ ==========
    @Schema(description = "商机编号")
    private Long businessId;
    @Schema(description = "商机名称")
    private String businessName;
    @Schema(description = "商机金额")
    private BigDecimal businessPrice;
    @Schema(description = "商机状态")
    private String businessStatus;
    @Schema(description = "商机创建时间")
    private LocalDateTime businessCreateTime;
    // ========== å®¢æˆ·ä¿¡æ¯ ==========
    @Schema(description = "客户编号")
    private Long customerId;
    @Schema(description = "客户名称")
    private String customerName;
    // ========== åˆåŒä¿¡æ¯ ==========
    @Schema(description = "合同编号")
    private Long contractId;
    @Schema(description = "合同名称")
    private String contractName;
    @Schema(description = "合同金额")
    private BigDecimal contractPrice;
    @Schema(description = "合同状态")
    private Integer contractStatus;
    @Schema(description = "合同创建时间")
    private LocalDateTime contractCreateTime;
    // ========== é”€å”®è®¢å•信息 ==========
    @Schema(description = "销售订单编号")
    private Long orderId;
    @Schema(description = "销售订单单号")
    private String orderNo;
    @Schema(description = "订单金额")
    private BigDecimal orderPrice;
    @Schema(description = "订单状态")
    private Integer orderStatus;
    @Schema(description = "订单创建时间")
    private LocalDateTime orderCreateTime;
    // ========== å‡ºåº“信息 ==========
    @Schema(description = "出库单编号")
    private Long outId;
    @Schema(description = "出库单号")
    private String outNo;
    @Schema(description = "出库金额")
    private BigDecimal outPrice;
    @Schema(description = "出库状态")
    private Integer outStatus;
    @Schema(description = "出库时间")
    private LocalDateTime outTime;
    // ========== å›žæ¬¾ä¿¡æ¯ ==========
    @Schema(description = "回款金额")
    private BigDecimal receivablePrice;
    @Schema(description = "回款记录列表")
    private List<ReceivableItem> receivableItems;
    // ========== é€€è´§ä¿¡æ¯ ==========
    @Schema(description = "退货金额")
    private BigDecimal returnPrice;
    /**
     * å›žæ¬¾è®°å½•项
     */
    @Data
    public static class ReceivableItem {
        @Schema(description = "回款编号")
        private Long receivableId;
        @Schema(description = "回款单号")
        private String receivableNo;
        @Schema(description = "回款金额")
        private BigDecimal price;
        @Schema(description = "回款时间")
        private LocalDateTime receivableTime;
    }
}
yudao-module-crm/pom.xml
@@ -18,6 +18,13 @@
    </description>
    <dependencies>
        <!-- CRM API æ¨¡å— -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-crm-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-system</artifactId>
@@ -34,6 +41,20 @@
            <version>${revision}</version>
        </dependency>
        <!-- MDM ä¸»æ•°æ®ç®¡ç† API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-mdm-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- ERP é”€å”®è®¢å• API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-erp-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- ä¸šåŠ¡ç»„ä»¶ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.crm.api.customer;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
 * CRM å®¢æˆ· API æŽ¥å£
 *
 * æä¾›ç»™å…¶å®ƒæ¨¡å—(如 ERP)调用
 *
 * @author èŠ‹é“æºç 
 */
public interface CrmCustomerApi {
    /**
     * èŽ·å¾—å®¢æˆ·ä¿¡æ¯
     *
     * @param id å®¢æˆ·ç¼–号
     * @return å®¢æˆ·ä¿¡æ¯
     */
    CrmCustomerRespDTO getCustomer(Long id);
    /**
     * èŽ·å¾—å®¢æˆ·ä¿¡æ¯åˆ—è¡¨
     *
     * @param ids å®¢æˆ·ç¼–号列表
     * @return å®¢æˆ·ä¿¡æ¯åˆ—表
     */
    List<CrmCustomerRespDTO> getCustomerList(Collection<Long> ids);
    /**
     * èŽ·å¾—å®¢æˆ·ä¿¡æ¯ Map
     *
     * @param ids å®¢æˆ·ç¼–号列表
     * @return å®¢æˆ·ä¿¡æ¯ Map
     */
    Map<Long, CrmCustomerRespDTO> getCustomerMap(Collection<Long> ids);
    /**
     * æ ¡éªŒå®¢æˆ·æ˜¯å¦å­˜åœ¨
     *
     * @param id å®¢æˆ·ç¼–号
     * @return å®¢æˆ·ä¿¡æ¯ï¼ˆæ ¡éªŒé€šè¿‡ï¼‰
     */
    CrmCustomerRespDTO validateCustomer(Long id);
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/CrmCustomerApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.crm.api.customer;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
 * CRM å®¢æˆ· API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
public class CrmCustomerApiImpl implements CrmCustomerApi {
    @Resource
    private CrmCustomerService customerService;
    @Override
    public CrmCustomerRespDTO getCustomer(Long id) {
        CrmCustomerDO customer = customerService.getCustomer(id);
        return BeanUtils.toBean(customer, CrmCustomerRespDTO.class);
    }
    @Override
    public List<CrmCustomerRespDTO> getCustomerList(Collection<Long> ids) {
        List<CrmCustomerDO> customers = customerService.getCustomerList(ids);
        return BeanUtils.toBean(customers, CrmCustomerRespDTO.class);
    }
    @Override
    public Map<Long, CrmCustomerRespDTO> getCustomerMap(Collection<Long> ids) {
        List<CrmCustomerDO> customers = customerService.getCustomerList(ids);
        return customers.stream()
                .collect(java.util.stream.Collectors.toMap(
                        CrmCustomerDO::getId,
                        customer -> BeanUtils.toBean(customer, CrmCustomerRespDTO.class)));
    }
    @Override
    public CrmCustomerRespDTO validateCustomer(Long id) {
        customerService.validateCustomer(id);
        return getCustomer(id);
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/customer/dto/CrmCustomerRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,93 @@
package cn.iocoder.yudao.module.crm.api.customer.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * CRM å®¢æˆ· Response DTO
 *
 * æä¾›ç»™å…¶å®ƒæ¨¡å—(如 ERP)使用
 *
 * @author èŠ‹é“æºç 
 */
@Data
public class CrmCustomerRespDTO {
    /**
     * ç¼–号
     */
    private Long id;
    /**
     * å®¢æˆ·åç§°
     */
    private String name;
    /**
     * æ‰‹æœº
     */
    private String mobile;
    /**
     * ç”µè¯
     */
    private String telephone;
    /**
     * QQ
     */
    private String qq;
    /**
     * wechat
     */
    private String wechat;
    /**
     * email
     */
    private String email;
    /**
     * æ‰€åœ¨åœ°
     */
    private Integer areaId;
    /**
     * è¯¦ç»†åœ°å€
     */
    private String detailAddress;
    /**
     * æ‰€å±žè¡Œä¸š
     */
    private Integer industryId;
    /**
     * å®¢æˆ·ç­‰çº§
     */
    private Integer level;
    /**
     * å®¢æˆ·æ¥æº
     */
    private Integer source;
    /**
     * è´Ÿè´£äººçš„用户编号
     */
    private Long ownerUserId;
    /**
     * æˆä¸ºè´Ÿè´£äººçš„æ—¶é—´
     */
    private LocalDateTime ownerTime;
    /**
     * é”å®šçŠ¶æ€
     */
    private Boolean lockStatus;
    /**
     * æˆäº¤çŠ¶æ€
     */
    private Boolean dealStatus;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/report/CrmReportApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,244 @@
package cn.iocoder.yudao.module.crm.api.report;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.crm.api.report.dto.CrmBusinessAnalyzeRespDTO;
import cn.iocoder.yudao.module.crm.api.report.dto.CrmContractAnalyzeRespDTO;
import cn.iocoder.yudao.module.crm.api.report.dto.CrmCustomerAnalyzeRespDTO;
import cn.iocoder.yudao.module.crm.api.report.dto.CrmSalesFunnelRespDTO;
import cn.iocoder.yudao.module.crm.api.report.dto.CrmSalesPerformanceRespDTO;
import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerMapper;
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
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 jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * CRM è¥é”€æŠ¥è¡¨ API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@RequestMapping(CrmReportApi.PREFIX)
@Validated
public class CrmReportApiImpl implements CrmReportApi {
    @Resource
    private CrmCustomerMapper customerMapper;
    @Resource
    private CrmBusinessMapper businessMapper;
    @Resource
    private CrmContractMapper contractMapper;
    @Resource
    private CrmReceivableService receivableService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @Override
    public CommonResult<CrmCustomerAnalyzeRespDTO> getCustomerAnalyze(LocalDateTime startTime, LocalDateTime endTime) {
        CrmCustomerAnalyzeRespDTO result = new CrmCustomerAnalyzeRespDTO();
        // å®¢æˆ·æ€»æ•°
        result.setTotalCount(customerMapper.selectCount());
        // ä»Šæ—¥æ–°å¢ž
        result.setTodayCount(customerMapper.selectTodayCount());
        // æˆäº¤å®¢æˆ·æ•°
        result.setDealCount(customerMapper.selectDealCount());
        // æˆäº¤çއ
        if (result.getTotalCount() > 0) {
            result.setDealRate(BigDecimal.valueOf(result.getDealCount())
                    .divide(BigDecimal.valueOf(result.getTotalCount()), 4, RoundingMode.HALF_UP)
                    .multiply(BigDecimal.valueOf(100)));
        } else {
            result.setDealRate(BigDecimal.ZERO);
        }
        // é”å®šå®¢æˆ·æ•°
        result.setLockCount(customerMapper.selectLockCount());
        // å…¬æµ·å®¢æˆ·æ•°
        result.setPoolCount(customerMapper.selectPoolCount());
        return success(result);
    }
    @Override
    public CommonResult<CrmBusinessAnalyzeRespDTO> getBusinessAnalyze(LocalDateTime startTime, LocalDateTime endTime) {
        CrmBusinessAnalyzeRespDTO result = new CrmBusinessAnalyzeRespDTO();
        // å•†æœºæ€»æ•°
        result.setTotalCount(businessMapper.selectCount());
        // ä»Šæ—¥æ–°å¢ž
        result.setTodayCount(businessMapper.selectTodayCount());
        // å•†æœºæ€»é‡‘额
        result.setTotalPrice(businessMapper.selectTotalPriceSum());
        // æˆäº¤å•†æœºæ•°
        result.setSuccessCount(businessMapper.selectSuccessCount());
        // æˆäº¤å•†æœºé‡‘额
        result.setSuccessPrice(businessMapper.selectSuccessPriceSum());
        // å•†æœºè½¬åŒ–率
        if (result.getTotalCount() > 0) {
            result.setSuccessRate(BigDecimal.valueOf(result.getSuccessCount())
                    .divide(BigDecimal.valueOf(result.getTotalCount()), 4, RoundingMode.HALF_UP)
                    .multiply(BigDecimal.valueOf(100)));
        } else {
            result.setSuccessRate(BigDecimal.ZERO);
        }
        // è¿›è¡Œä¸­å•†æœºæ•°
        result.setProcessingCount(businessMapper.selectProcessingCount());
        // å¤±è´¥å•†æœºæ•°
        result.setFailCount(businessMapper.selectFailCount());
        return success(result);
    }
    @Override
    public CommonResult<CrmContractAnalyzeRespDTO> getContractAnalyze(LocalDateTime startTime, LocalDateTime endTime) {
        CrmContractAnalyzeRespDTO result = new CrmContractAnalyzeRespDTO();
        // åˆåŒæ€»æ•°
        result.setTotalCount(contractMapper.selectCount());
        // ä»Šæ—¥æ–°å¢ž
        result.setTodayCount(contractMapper.selectTodayCount());
        // åˆåŒæ€»é‡‘额
        result.setTotalPrice(contractMapper.selectTotalPriceSum());
        // å®¡æ‰¹é€šè¿‡åˆåŒæ•°
        result.setApproveCount(contractMapper.selectApproveCount());
        // å®¡æ‰¹é€šè¿‡åˆåŒé‡‘额
        result.setApprovePrice(contractMapper.selectApprovePriceSum());
        // å®¡æ‰¹ä¸­åˆåŒæ•°
        result.setProcessCount(contractMapper.selectProcessCount());
        // å·²å›žæ¬¾é‡‘额
        BigDecimal receivablePrice = receivableService.getReceivablePriceSum();
        result.setReceivablePrice(receivablePrice != null ? receivablePrice : BigDecimal.ZERO);
        // å›žæ¬¾çއ
        if (result.getApprovePrice() != null && result.getApprovePrice().compareTo(BigDecimal.ZERO) > 0) {
            result.setReceivableRate(result.getReceivablePrice()
                    .divide(result.getApprovePrice(), 4, RoundingMode.HALF_UP)
                    .multiply(BigDecimal.valueOf(100)));
        } else {
            result.setReceivableRate(BigDecimal.ZERO);
        }
        return success(result);
    }
    @Override
    public CommonResult<CrmSalesFunnelRespDTO> getSalesFunnel() {
        CrmSalesFunnelRespDTO result = new CrmSalesFunnelRespDTO();
        List<CrmSalesFunnelRespDTO.FunnelStage> stages = new ArrayList<>();
        // é˜¶æ®µ1:线索/客户
        Long customerCount = customerMapper.selectCount();
        stages.add(createStage("客户", customerCount, BigDecimal.ZERO));
        // é˜¶æ®µ2:商机
        Long businessCount = businessMapper.selectCount();
        BigDecimal businessPrice = businessMapper.selectTotalPriceSum();
        stages.add(createStage("商机", businessCount, businessPrice));
        // é˜¶æ®µ3:合同
        Long contractCount = contractMapper.selectApproveCount();
        BigDecimal contractPrice = contractMapper.selectApprovePriceSum();
        stages.add(createStage("合同", contractCount, contractPrice));
        // é˜¶æ®µ4:回款
        BigDecimal receivablePrice = receivableService.getReceivablePriceSum();
        stages.add(createStage("回款", null, receivablePrice));
        // è®¡ç®—转化率
        for (int i = 0; i < stages.size(); i++) {
            if (i == 0) {
                stages.get(i).setConversionRate(BigDecimal.valueOf(100));
            } else if (customerCount > 0 && stages.get(i).getCount() != null) {
                stages.get(i).setConversionRate(BigDecimal.valueOf(stages.get(i).getCount())
                        .divide(BigDecimal.valueOf(customerCount), 4, RoundingMode.HALF_UP)
                        .multiply(BigDecimal.valueOf(100)));
            } else {
                stages.get(i).setConversionRate(BigDecimal.ZERO);
            }
        }
        result.setStages(stages);
        return success(result);
    }
    @Override
    public CommonResult<CrmSalesPerformanceRespDTO> getSalesPerformance(LocalDateTime startTime, LocalDateTime endTime) {
        CrmSalesPerformanceRespDTO result = new CrmSalesPerformanceRespDTO();
        List<CrmSalesPerformanceRespDTO.SalesItem> items = new ArrayList<>();
        // èŽ·å–æ‰€æœ‰é”€å”®äººå‘˜ï¼ˆæœ‰åˆåŒæˆ–å•†æœºçš„è´Ÿè´£äººï¼‰
        List<Long> userIds = contractMapper.selectOwnerUserIds();
        userIds.addAll(businessMapper.selectOwnerUserIds());
        userIds = userIds.stream().distinct().collect(Collectors.toList());
        if (CollUtil.isEmpty(userIds)) {
            result.setItems(items);
            return success(result);
        }
        // èŽ·å–ç”¨æˆ·ä¿¡æ¯
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
        // èŽ·å–éƒ¨é—¨ä¿¡æ¯
        List<Long> deptIds = userMap.values().stream()
                .map(AdminUserRespDTO::getDeptId)
                .filter(id -> id != null)
                .distinct()
                .collect(Collectors.toList());
        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(deptIds);
        for (Long userId : userIds) {
            AdminUserRespDTO user = userMap.get(userId);
            if (user == null) {
                continue;
            }
            CrmSalesPerformanceRespDTO.SalesItem item = new CrmSalesPerformanceRespDTO.SalesItem();
            item.setUserId(userId);
            item.setUserName(user.getNickname());
            // èŽ·å–éƒ¨é—¨åç§°
            if (user.getDeptId() != null) {
                DeptRespDTO dept = deptMap.get(user.getDeptId());
                item.setDeptName(dept != null ? dept.getName() : "");
            }
            // åˆåŒæ•°é‡å’Œé‡‘额
            item.setContractCount(contractMapper.selectCountByOwnerUserId(userId));
            item.setContractPrice(contractMapper.selectPriceSumByOwnerUserId(userId));
            // å›žæ¬¾é‡‘额
            item.setReceivablePrice(receivableService.getReceivablePriceByOwnerUserId(userId));
            // å•†æœºæ•°é‡å’Œé‡‘额
            item.setBusinessCount(businessMapper.selectCountByOwnerUserId(userId));
            item.setBusinessPrice(businessMapper.selectPriceSumByOwnerUserId(userId));
            items.add(item);
        }
        result.setItems(items);
        return success(result);
    }
    private CrmSalesFunnelRespDTO.FunnelStage createStage(String name, Long count, BigDecimal price) {
        CrmSalesFunnelRespDTO.FunnelStage stage = new CrmSalesFunnelRespDTO.FunnelStage();
        stage.setName(name);
        stage.setCount(count);
        stage.setPrice(price != null ? price : BigDecimal.ZERO);
        return stage;
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/api/trace/CrmSaleTraceApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,190 @@
package cn.iocoder.yudao.module.crm.api.trace;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.crm.api.trace.dto.CrmSaleTraceRespDTO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.customer.CrmCustomerDO;
import cn.iocoder.yudao.module.crm.dal.mysql.business.CrmBusinessMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractMapper;
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * CRM é”€å”®è¿½æº¯ API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@RequestMapping(CrmSaleTraceApi.PREFIX)
@Validated
public class CrmSaleTraceApiImpl implements CrmSaleTraceApi {
    @Resource
    private CrmBusinessMapper businessMapper;
    @Resource
    private CrmContractMapper contractMapper;
    @Resource
    private CrmCustomerService customerService;
    @Resource
    private CrmReceivableService receivableService;
    @Resource
    private ErpSaleOrderApi saleOrderApi;
    @Override
    public CommonResult<CrmSaleTraceRespDTO> getTraceByBusinessId(Long businessId) {
        CrmBusinessDO business = businessMapper.selectById(businessId);
        if (business == null) {
            return success(null);
        }
        CrmSaleTraceRespDTO trace = buildBaseTrace(business.getCustomerId());
        // å¡«å……商机信息
        trace.setBusinessId(business.getId());
        trace.setBusinessName(business.getName());
        trace.setBusinessPrice(business.getTotalPrice());
        trace.setBusinessCreateTime(business.getCreateTime());
        // æŸ¥è¯¢å…³è”合同
        CrmContractDO contract = contractMapper.selectByBusinessId(businessId);
        if (contract != null) {
            fillContractInfo(trace, contract);
            // æŸ¥è¯¢å…³è”订单
            if (contract.getOrderId() != null) {
                fillOrderInfo(trace, contract.getOrderId());
            }
        }
        return success(trace);
    }
    @Override
    public CommonResult<CrmSaleTraceRespDTO> getTraceByContractId(Long contractId) {
        CrmContractDO contract = contractMapper.selectById(contractId);
        if (contract == null) {
            return success(null);
        }
        CrmSaleTraceRespDTO trace = buildBaseTrace(contract.getCustomerId());
        // å¡«å……合同信息
        fillContractInfo(trace, contract);
        // å¡«å……商机信息
        if (contract.getBusinessId() != null) {
            CrmBusinessDO business = businessMapper.selectById(contract.getBusinessId());
            if (business != null) {
                trace.setBusinessId(business.getId());
                trace.setBusinessName(business.getName());
                trace.setBusinessPrice(business.getTotalPrice());
                trace.setBusinessCreateTime(business.getCreateTime());
            }
        }
        // æŸ¥è¯¢å…³è”订单
        if (contract.getOrderId() != null) {
            fillOrderInfo(trace, contract.getOrderId());
        }
        return success(trace);
    }
    @Override
    public CommonResult<List<CrmSaleTraceRespDTO>> getTraceListByCustomerId(Long customerId) {
        CrmCustomerDO customer = customerService.getCustomer(customerId);
        if (customer == null) {
            return success(new ArrayList<>());
        }
        // æŸ¥è¯¢å®¢æˆ·æ‰€æœ‰åˆåŒ
        List<CrmContractDO> contracts = contractMapper.selectListByCustomerId(customerId);
        if (CollUtil.isEmpty(contracts)) {
            return success(new ArrayList<>());
        }
        List<CrmSaleTraceRespDTO> traces = new ArrayList<>();
        for (CrmContractDO contract : contracts) {
            CrmSaleTraceRespDTO trace = new CrmSaleTraceRespDTO();
            trace.setCustomerId(customerId);
            trace.setCustomerName(customer.getName());
            fillContractInfo(trace, contract);
            // å¡«å……商机信息
            if (contract.getBusinessId() != null) {
                CrmBusinessDO business = businessMapper.selectById(contract.getBusinessId());
                if (business != null) {
                    trace.setBusinessId(business.getId());
                    trace.setBusinessName(business.getName());
                    trace.setBusinessPrice(business.getTotalPrice());
                    trace.setBusinessCreateTime(business.getCreateTime());
                }
            }
            // æŸ¥è¯¢å…³è”订单
            if (contract.getOrderId() != null) {
                fillOrderInfo(trace, contract.getOrderId());
            }
            traces.add(trace);
        }
        return success(traces);
    }
    /**
     * æž„建基础追溯信息
     */
    private CrmSaleTraceRespDTO buildBaseTrace(Long customerId) {
        CrmSaleTraceRespDTO trace = new CrmSaleTraceRespDTO();
        if (customerId != null) {
            CrmCustomerDO customer = customerService.getCustomer(customerId);
            if (customer != null) {
                trace.setCustomerId(customerId);
                trace.setCustomerName(customer.getName());
            }
        }
        return trace;
    }
    /**
     * å¡«å……合同信息
     */
    private void fillContractInfo(CrmSaleTraceRespDTO trace, CrmContractDO contract) {
        trace.setContractId(contract.getId());
        trace.setContractName(contract.getName());
        trace.setContractPrice(contract.getTotalPrice());
        trace.setContractStatus(contract.getAuditStatus());
        trace.setContractCreateTime(contract.getCreateTime());
        // è®¡ç®—回款金额
        BigDecimal receivablePrice = receivableService.getReceivablePriceByContractId(contract.getId());
        trace.setReceivablePrice(receivablePrice != null ? receivablePrice : BigDecimal.ZERO);
    }
    /**
     * å¡«å……订单信息
     */
    private void fillOrderInfo(CrmSaleTraceRespDTO trace, Long orderId) {
        ErpSaleOrderRespDTO order = saleOrderApi.getSaleOrder(orderId).getCheckedData();
        if (order != null) {
            trace.setOrderId(orderId);
            trace.setOrderNo(order.getNo());
            trace.setOrderPrice(order.getTotalPrice());
            trace.setOrderStatus(order.getStatus());
        }
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java
@@ -87,6 +87,12 @@
    @ExcelProperty("商机总金额")
    private BigDecimal totalPrice;
    @Schema(description = "关联 ERP é”€å”®è®¢å•编号", example = "12345")
    private Long orderId;
    @Schema(description = "ERP é”€å”®è®¢å•单号", example = "SO001")
    @ExcelProperty("销售订单单号")
    private String orderNo;
    @Schema(description = "备注", example = "随便")
    @ExcelProperty("备注")
    private String remark;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/CrmContractController.java
@@ -253,4 +253,12 @@
                .setTotalReceivablePrice(receivablePriceMap.getOrDefault(contract.getId(), BigDecimal.ZERO))));
    }
    @PostMapping("/generate-sale-order")
    @Operation(summary = "根据合同生成销售订单")
    @Parameter(name = "id", description = "合同编号", required = true)
    @PreAuthorize("@ss.hasPermission('crm:contract:update')")
    public CommonResult<Long> generateSaleOrder(@RequestParam("id") Long id) {
        return success(contractService.generateSaleOrder(id, getLoginUserId()));
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java
@@ -92,6 +92,12 @@
    @ExcelProperty("已回款金额")
    private BigDecimal totalReceivablePrice;
    @Schema(description = "关联 ERP é”€å”®è®¢å•编号", example = "12345")
    private Long orderId;
    @Schema(description = "ERP é”€å”®è®¢å•单号", example = "SO001")
    @ExcelProperty("销售订单单号")
    private String orderNo;
    @Schema(description = "客户签约人编号", example = "18546")
    private Long signContactId;
    @Schema(description = "客户签约人", example = "小豆")
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/business/CrmBusinessDO.java
@@ -104,6 +104,14 @@
     */
    private BigDecimal totalPrice;
    /**
     * å…³è” ERP é”€å”®è®¢å•编号
     */
    private Long orderId;
    /**
     * ERP é”€å”®è®¢å•单号
     */
    private String orderNo;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/contract/CrmContractDO.java
@@ -104,6 +104,14 @@
     */
    private BigDecimal totalPrice;
    /**
     * å…³è” ERP é”€å”®è®¢å•编号
     */
    private Long orderId;
    /**
     * ERP é”€å”®è®¢å•单号
     */
    private String orderNo;
    /**
     * å®¢æˆ·ç­¾çº¦äººï¼Œéžå¿…é¡»
     *
     * å…³è” {@link CrmContactDO#getId()}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/business/CrmBusinessMapper.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.crm.dal.mysql.business;
import cn.hutool.core.date.LocalDateTimeUtil;
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;
@@ -7,11 +8,14 @@
import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessPageReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.statistics.vo.funnel.CrmStatisticsFunnelReqVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.business.CrmBusinessDO;
import cn.iocoder.yudao.module.crm.enums.business.CrmBusinessEndStatusEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
@@ -75,4 +79,70 @@
                .betweenIfPresent(CrmBusinessDO::getCreateTime, pageVO.getTimes()));
    }
    // ========== ç»Ÿè®¡æ–¹æ³• ==========
    default Long selectTodayCount() {
        LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
        LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now());
        return selectCount(new LambdaQueryWrapperX<CrmBusinessDO>()
                .between(CrmBusinessDO::getCreateTime, beginOfToday, endOfToday));
    }
    default BigDecimal selectTotalPriceSum() {
        LambdaQueryWrapperX<CrmBusinessDO> query = new LambdaQueryWrapperX<>();
        query.select(CrmBusinessDO::getTotalPrice);
        List<CrmBusinessDO> list = selectList(query);
        return list.stream()
                .map(CrmBusinessDO::getTotalPrice)
                .filter(p -> p != null)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    default Long selectSuccessCount() {
        return selectCount(new LambdaQueryWrapperX<CrmBusinessDO>()
                .eq(CrmBusinessDO::getEndStatus, CrmBusinessEndStatusEnum.WIN.getStatus()));
    }
    default BigDecimal selectSuccessPriceSum() {
        LambdaQueryWrapperX<CrmBusinessDO> query = new LambdaQueryWrapperX<>();
        query.eq(CrmBusinessDO::getEndStatus, CrmBusinessEndStatusEnum.WIN.getStatus())
                .select(CrmBusinessDO::getTotalPrice);
        List<CrmBusinessDO> list = selectList(query);
        return list.stream()
                .map(CrmBusinessDO::getTotalPrice)
                .filter(p -> p != null)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    default Long selectProcessingCount() {
        return selectCount(new LambdaQueryWrapperX<CrmBusinessDO>()
                .isNull(CrmBusinessDO::getEndStatus));
    }
    default Long selectFailCount() {
        return selectCount(new LambdaQueryWrapperX<CrmBusinessDO>()
                .eq(CrmBusinessDO::getEndStatus, CrmBusinessEndStatusEnum.LOSE.getStatus()));
    }
    default List<Long> selectOwnerUserIds() {
        LambdaQueryWrapperX<CrmBusinessDO> query = new LambdaQueryWrapperX<>();
        query.select(CrmBusinessDO::getOwnerUserId).isNotNull(CrmBusinessDO::getOwnerUserId);
        List<CrmBusinessDO> list = selectList(query);
        return list.stream().map(CrmBusinessDO::getOwnerUserId).distinct().toList();
    }
    default Long selectCountByOwnerUserId(Long ownerUserId) {
        return selectCount(CrmBusinessDO::getOwnerUserId, ownerUserId);
    }
    default BigDecimal selectPriceSumByOwnerUserId(Long ownerUserId) {
        LambdaQueryWrapperX<CrmBusinessDO> query = new LambdaQueryWrapperX<>();
        query.eq(CrmBusinessDO::getOwnerUserId, ownerUserId).select(CrmBusinessDO::getTotalPrice);
        List<CrmBusinessDO> list = selectList(query);
        return list.stream()
                .map(CrmBusinessDO::getTotalPrice)
                .filter(p -> p != null)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/contract/CrmContractMapper.java
@@ -14,6 +14,7 @@
import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
import org.apache.ibatis.annotations.Mapper;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@@ -113,4 +114,71 @@
                .eq(CrmContractDO::getOwnerUserId, ownerUserId));
    }
    default CrmContractDO selectByBusinessId(Long businessId) {
        return selectOne(CrmContractDO::getBusinessId, businessId);
    }
    default List<CrmContractDO> selectListByCustomerId(Long customerId) {
        return selectList(CrmContractDO::getCustomerId, customerId);
    }
    // ========== ç»Ÿè®¡æ–¹æ³• ==========
    default Long selectTodayCount() {
        LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
        LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now());
        return selectCount(new LambdaQueryWrapperX<CrmContractDO>()
                .between(CrmContractDO::getCreateTime, beginOfToday, endOfToday));
    }
    default BigDecimal selectTotalPriceSum() {
        LambdaQueryWrapperX<CrmContractDO> query = new LambdaQueryWrapperX<>();
        query.select(CrmContractDO::getTotalPrice);
        List<CrmContractDO> list = selectList(query);
        return list.stream()
                .map(CrmContractDO::getTotalPrice)
                .filter(p -> p != null)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    default Long selectApproveCount() {
        return selectCount(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.APPROVE.getStatus());
    }
    default BigDecimal selectApprovePriceSum() {
        LambdaQueryWrapperX<CrmContractDO> query = new LambdaQueryWrapperX<>();
        query.eq(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.APPROVE.getStatus())
                .select(CrmContractDO::getTotalPrice);
        List<CrmContractDO> list = selectList(query);
        return list.stream()
                .map(CrmContractDO::getTotalPrice)
                .filter(p -> p != null)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    default Long selectProcessCount() {
        return selectCount(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.PROCESS.getStatus());
    }
    default List<Long> selectOwnerUserIds() {
        LambdaQueryWrapperX<CrmContractDO> query = new LambdaQueryWrapperX<>();
        query.select(CrmContractDO::getOwnerUserId).isNotNull(CrmContractDO::getOwnerUserId);
        List<CrmContractDO> list = selectList(query);
        return list.stream().map(CrmContractDO::getOwnerUserId).distinct().toList();
    }
    default Long selectCountByOwnerUserId(Long ownerUserId) {
        return selectCount(CrmContractDO::getOwnerUserId, ownerUserId);
    }
    default BigDecimal selectPriceSumByOwnerUserId(Long ownerUserId) {
        LambdaQueryWrapperX<CrmContractDO> query = new LambdaQueryWrapperX<>();
        query.eq(CrmContractDO::getOwnerUserId, ownerUserId).select(CrmContractDO::getTotalPrice);
        List<CrmContractDO> list = selectList(query);
        return list.stream()
                .map(CrmContractDO::getTotalPrice)
                .filter(p -> p != null)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/customer/CrmCustomerMapper.java
@@ -180,4 +180,26 @@
        return selectCount(query);
    }
    // ========== ç»Ÿè®¡æ–¹æ³• ==========
    default Long selectTodayCount() {
        LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
        LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now());
        return selectCount(new LambdaQueryWrapperX<CrmCustomerDO>()
                .between(CrmCustomerDO::getCreateTime, beginOfToday, endOfToday));
    }
    default Long selectDealCount() {
        return selectCount(CrmCustomerDO::getDealStatus, true);
    }
    default Long selectLockCount() {
        return selectCount(CrmCustomerDO::getLockStatus, true);
    }
    default Long selectPoolCount() {
        return selectCount(new LambdaQueryWrapperX<CrmCustomerDO>()
                .isNull(CrmCustomerDO::getOwnerUserId));
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/mysql/receivable/CrmReceivableMapper.java
@@ -94,4 +94,35 @@
        return selectCount(CrmReceivableDO::getContractId, contractId);
    }
    // ========== ç»Ÿè®¡æ–¹æ³• ==========
    default BigDecimal selectReceivablePriceSum() {
        LambdaQueryWrapperX<CrmReceivableDO> query = new LambdaQueryWrapperX<>();
        query.in(CrmReceivableDO::getAuditStatus,
                CrmAuditStatusEnum.DRAFT.getStatus(),
                CrmAuditStatusEnum.PROCESS.getStatus(),
                CrmAuditStatusEnum.APPROVE.getStatus())
                .select(CrmReceivableDO::getPrice);
        List<CrmReceivableDO> list = selectList(query);
        return list.stream()
                .map(CrmReceivableDO::getPrice)
                .filter(p -> p != null)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    default BigDecimal selectReceivablePriceSumByOwnerUserId(Long ownerUserId) {
        LambdaQueryWrapperX<CrmReceivableDO> query = new LambdaQueryWrapperX<>();
        query.eq(CrmReceivableDO::getOwnerUserId, ownerUserId)
                .in(CrmReceivableDO::getAuditStatus,
                        CrmAuditStatusEnum.DRAFT.getStatus(),
                        CrmAuditStatusEnum.PROCESS.getStatus(),
                        CrmAuditStatusEnum.APPROVE.getStatus())
                .select(CrmReceivableDO::getPrice);
        List<CrmReceivableDO> list = selectList(query);
        return list.stream()
                .map(CrmReceivableDO::getPrice)
                .filter(p -> p != null)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/enums/ErrorCodeConstants.java
@@ -16,6 +16,9 @@
    ErrorCode CONTRACT_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_020_000_003, "更新合同审核状态失败,原因:合同不是审核中状态");
    ErrorCode CONTRACT_NO_EXISTS = new ErrorCode(1_020_000_004, "生成合同序列号重复,请重试");
    ErrorCode CONTRACT_DELETE_FAIL = new ErrorCode(1_020_000_005, "删除合同失败,原因:有被回款所使用");
    ErrorCode CONTRACT_GENERATE_ORDER_FAIL_NOT_APPROVE = new ErrorCode(1_020_000_006, "生成销售订单失败,原因:合同未审批通过");
    ErrorCode CONTRACT_GENERATE_ORDER_FAIL_EXISTS_ORDER = new ErrorCode(1_020_000_007, "生成销售订单失败,原因:已关联销售订单【{}】");
    ErrorCode CONTRACT_GENERATE_ORDER_FAIL_NO_PRODUCT = new ErrorCode(1_020_000_008, "生成销售订单失败,原因:合同没有产品信息");
    // ========== çº¿ç´¢ç®¡ç† 1-020-001-000 ==========
    ErrorCode CLUE_NOT_EXISTS = new ErrorCode(1_020_001_000, "线索不存在");
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractService.java
@@ -202,4 +202,13 @@
     */
    List<CrmContractDO> getContractListByCustomerIdOwnerUserId(Long customerId, Long ownerUserId);
    /**
     * æ ¹æ®åˆåŒç”Ÿæˆé”€å”®è®¢å•
     *
     * @param id     åˆåŒç¼–号
     * @param userId ç”¨æˆ·ç¼–号
     * @return é”€å”®è®¢å•编号
     */
    Long generateSaleOrder(Long id, Long userId);
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
@@ -31,6 +31,9 @@
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionTransferReqBO;
import cn.iocoder.yudao.module.crm.service.product.CrmProductService;
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderCreateReqDTO;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
@@ -95,6 +98,8 @@
    private AdminUserApi adminUserApi;
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Resource
    private ErpSaleOrderApi saleOrderApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -413,4 +418,66 @@
        return contractMapper.selectListByCustomerIdOwnerUserId(customerId, ownerUserId);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = CRM_CONTRACT_TYPE, subType = "生成销售订单", bizNo = "{{#id}}",
            success = "合同【{{#contractName}}】生成销售订单【{{#orderNo}}】成功")
    @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
    public Long generateSaleOrder(Long id, Long userId) {
        // 1. æ ¡éªŒåˆåŒå­˜åœ¨ä¸”已审批通过
        CrmContractDO contract = validateContractExists(id);
        if (!CrmAuditStatusEnum.APPROVE.getStatus().equals(contract.getAuditStatus())) {
            throw exception(CONTRACT_GENERATE_ORDER_FAIL_NOT_APPROVE);
        }
        // 2. æ ¡éªŒæ˜¯å¦å·²ç”Ÿæˆé”€å”®è®¢å•
        ErpSaleOrderRespDTO existOrder = saleOrderApi.getSaleOrderByContractId(id).getCheckedData();
        if (existOrder != null) {
            throw exception(CONTRACT_GENERATE_ORDER_FAIL_EXISTS_ORDER, existOrder.getNo());
        }
        // 3. èŽ·å–åˆåŒäº§å“åˆ—è¡¨
        List<CrmContractProductDO> contractProducts = contractProductMapper.selectListByContractId(id);
        if (CollUtil.isEmpty(contractProducts)) {
            throw exception(CONTRACT_GENERATE_ORDER_FAIL_NO_PRODUCT);
        }
        // 4. æž„建销售订单创建请求
        ErpSaleOrderCreateReqDTO createReqDTO = new ErpSaleOrderCreateReqDTO();
        createReqDTO.setCustomerId(contract.getCustomerId());
        createReqDTO.setContractId(contract.getId());
        createReqDTO.setContractNo(contract.getNo());
        createReqDTO.setSaleUserId(contract.getOwnerUserId());
        createReqDTO.setOrderTime(LocalDateTime.now().toString());
        createReqDTO.setDiscountPercent(contract.getDiscountPercent());
        createReqDTO.setRemark("由合同【" + contract.getNo() + "】自动生成");
        // 5. æž„建订单项
        List<ErpSaleOrderCreateReqDTO.Item> items = contractProducts.stream()
                .map(cp -> {
                    ErpSaleOrderCreateReqDTO.Item item = new ErpSaleOrderCreateReqDTO.Item();
                    item.setProductId(cp.getProductId());
                    item.setProductPrice(cp.getContractPrice());
                    item.setCount(cp.getCount());
                    return item;
                })
                .toList();
        createReqDTO.setItems(items);
        // 6. åˆ›å»ºé”€å”®è®¢å•
        Long orderId = saleOrderApi.createSaleOrder(createReqDTO).getCheckedData();
        ErpSaleOrderRespDTO order = saleOrderApi.getSaleOrder(orderId).getCheckedData();
        // 7. æ›´æ–°åˆåŒå…³è”订单信息
        contractMapper.updateById(new CrmContractDO()
                .setId(id)
                .setOrderId(orderId)
                .setOrderNo(order.getNo()));
        // 8. è®°å½•操作日志
        LogRecordContext.putVariable("contractName", contract.getName());
        LogRecordContext.putVariable("orderNo", order.getNo());
        return orderId;
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/product/CrmProductServiceImpl.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.crm.service.product;
import cn.hutool.core.collection.CollUtil;
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.crm.controller.admin.product.vo.product.CrmProductPageReqVO;
@@ -14,6 +15,11 @@
import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemCreateReqDTO;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemUpdateReqDTO;
import cn.iocoder.yudao.module.mdm.enums.MdmItemTypeEnum;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
@@ -23,10 +29,8 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
@@ -36,6 +40,11 @@
/**
 * CRM äº§å“ Service å®žçŽ°ç±»
 *
 * æ”¹é€ è¯´æ˜Žï¼šäº§å“æ•°æ®ç»Ÿä¸€ä½¿ç”¨ MDM ä¸»æ•°æ®ç®¡ç†ï¼ˆitemType=2 äº§å“ç±»åž‹ï¼‰
 * - åˆ›å»ºã€æ›´æ–°æ“ä½œé€šè¿‡ MDM API è¿›è¡Œ
 * - æŸ¥è¯¢æ“ä½œä»Ž MDM èŽ·å–æ•°æ®
 * - CRM äº§å“è¡¨ä¿ç•™ç”¨äºŽæƒé™ç®¡ç†
 *
 * @author ZanGe丶
 */
@@ -54,6 +63,9 @@
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MdmItemApi mdmItemApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = CRM_PRODUCT_TYPE, subType = CRM_PRODUCT_CREATE_SUB_TYPE, bizNo = "{{#productId}}",
@@ -64,16 +76,38 @@
        validateProductNoDuplicate(null, createReqVO.getNo());
        validateProductCategoryExists(createReqVO.getCategoryId());
        // 2. æ’入产品
        CrmProductDO product = BeanUtils.toBean(createReqVO, CrmProductDO.class);
        // 2. é€šè¿‡ MDM API åˆ›å»ºäº§å“
        MdmItemCreateReqDTO createReqDTO = new MdmItemCreateReqDTO();
        createReqDTO.setCode(createReqVO.getNo());
        createReqDTO.setName(createReqVO.getName());
        createReqDTO.setBarCode(createReqVO.getNo()); // ä½¿ç”¨ç¼–码作为条码
        createReqDTO.setCategoryId(createReqVO.getCategoryId());
        createReqDTO.setUnitMeasureId(createReqVO.getUnit() != null ? createReqVO.getUnit().longValue() : null);
        createReqDTO.setItemType(MdmItemTypeEnum.PRODUCT.getType()); // äº§å“ç±»åž‹
        createReqDTO.setStatus(createReqVO.getStatus() != null ? createReqVO.getStatus() : CommonStatusEnum.ENABLE.getStatus());
        createReqDTO.setSalesPrice(createReqVO.getPrice());
        createReqDTO.setRemark(createReqVO.getDescription());
        Long itemId = mdmItemApi.createItem(createReqDTO).getCheckedData();
        // 3. æ’入本地产品记录(用于权限管理)
        CrmProductDO product = new CrmProductDO();
        product.setId(itemId);
        product.setName(createReqVO.getName());
        product.setNo(createReqVO.getNo());
        product.setUnit(createReqVO.getUnit());
        product.setPrice(createReqVO.getPrice());
        product.setStatus(createReqVO.getStatus());
        product.setCategoryId(createReqVO.getCategoryId());
        product.setDescription(createReqVO.getDescription());
        product.setOwnerUserId(createReqVO.getOwnerUserId());
        productMapper.insert(product);
        // 3. æ’入数据权限
        // 4. æ’入数据权限
        permissionService.createPermission(new CrmPermissionCreateReqBO().setUserId(product.getOwnerUserId())
                .setBizType(CrmBizTypeEnum.CRM_PRODUCT.getType()).setBizId(product.getId())
                .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
        // 4. è®°å½•操作日志上下文
        // 5. è®°å½•操作日志上下文
        LogRecordContext.putVariable("productId", product.getId());
        return product.getId();
    }
@@ -89,26 +123,47 @@
        validateProductNoDuplicate(updateReqVO.getId(), updateReqVO.getNo());
        validateProductCategoryExists(updateReqVO.getCategoryId());
        // 2. æ›´æ–°äº§å“
        // 2. é€šè¿‡ MDM API æ›´æ–°äº§å“
        MdmItemUpdateReqDTO updateReqDTO = new MdmItemUpdateReqDTO();
        updateReqDTO.setId(updateReqVO.getId());
        updateReqDTO.setCode(updateReqVO.getNo());
        updateReqDTO.setName(updateReqVO.getName());
        updateReqDTO.setBarCode(updateReqVO.getNo());
        updateReqDTO.setCategoryId(updateReqVO.getCategoryId());
        updateReqDTO.setUnitMeasureId(updateReqVO.getUnit() != null ? updateReqVO.getUnit().longValue() : null);
        updateReqDTO.setItemType(MdmItemTypeEnum.PRODUCT.getType());
        updateReqDTO.setStatus(updateReqVO.getStatus());
        updateReqDTO.setSalesPrice(updateReqVO.getPrice());
        updateReqDTO.setRemark(updateReqVO.getDescription());
        mdmItemApi.updateItem(updateReqDTO);
        // 3. æ›´æ–°æœ¬åœ°äº§å“è®°å½•
        CrmProductDO updateObj = BeanUtils.toBean(updateReqVO, CrmProductDO.class);
        productMapper.updateById(updateObj);
        // 3. è®°å½•操作日志上下文
        // 4. è®°å½•操作日志上下文
        LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(crmProductDO, CrmProductSaveReqVO.class));
    }
    private CrmProductDO validateProductExists(Long id) {
        // ä»Ž MDM æ ¡éªŒ
        MdmItemRespDTO item = mdmItemApi.getItem(id).getCheckedData();
        if (item == null) {
            throw exception(PRODUCT_NOT_EXISTS);
        }
        // ä»Žæœ¬åœ°èŽ·å– CRM äº§å“è®°å½•
        CrmProductDO product = productMapper.selectById(id);
        if (product == null) {
            throw exception(PRODUCT_NOT_EXISTS);
            // å¦‚果本地不存在,创建一个同步记录
            product = convertToCrmProduct(item);
            productMapper.insert(product);
        }
        return product;
    }
    private void validateProductNoDuplicate(Long id, String no) {
        CrmProductDO product = productMapper.selectByNo(no);
        if (product == null
                || product.getId().equals(id)) {
        MdmItemRespDTO item = mdmItemApi.getItemByCode(no).getCheckedData();
        if (item == null || item.getId().equals(id)) {
            return;
        }
        throw exception(PRODUCT_NO_EXISTS);
@@ -128,29 +183,65 @@
    public void deleteProduct(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateProductExists(id);
        // åˆ é™¤
        // é€šè¿‡ MDM API åˆ é™¤ï¼ˆæ›´æ–°çŠ¶æ€ä¸ºç¦ç”¨ï¼‰
        mdmItemApi.updateItemStatus(id, CommonStatusEnum.DISABLE.getStatus());
        // åˆ é™¤æœ¬åœ°è®°å½•
        productMapper.deleteById(id);
    }
    @Override
    @CrmPermission(bizType = CrmBizTypeEnum.CRM_PRODUCT, bizId = "#id", level = CrmPermissionLevelEnum.READ)
    public CrmProductDO getProduct(Long id) {
        return productMapper.selectById(id);
        // ä»Ž MDM èŽ·å–äº§å“æ•°æ®
        MdmItemRespDTO item = mdmItemApi.getItem(id).getCheckedData();
        if (item == null || !MdmItemTypeEnum.PRODUCT.getType().equals(item.getItemType())) {
            return null;
        }
        // èŽ·å–æœ¬åœ° CRM äº§å“è®°å½•(用于权限)
        CrmProductDO product = productMapper.selectById(id);
        if (product == null) {
            // åŒæ­¥åˆ›å»ºæœ¬åœ°è®°å½•
            product = convertToCrmProduct(item);
            productMapper.insert(product);
        } else {
            // æ›´æ–°æœ¬åœ°è®°å½•的数据
            product.setName(item.getName());
            product.setNo(item.getCode());
            product.setPrice(item.getSalesPrice());
            product.setStatus(item.getStatus());
            product.setCategoryId(item.getCategoryId());
            product.setDescription(item.getRemark());
        }
        return product;
    }
    @Override
    public PageResult<CrmProductDO> getProductPage(CrmProductPageReqVO pageReqVO) {
        return productMapper.selectPage(pageReqVO);
        // ä»Ž MDM èŽ·å–äº§å“åˆ†é¡µæ•°æ®
        cn.iocoder.yudao.framework.common.pojo.PageResult<MdmItemRespDTO> mdmPageResult =
                mdmItemApi.getItemPage(MdmItemTypeEnum.PRODUCT.getType(), null,
                        pageReqVO.getPageNo(), pageReqVO.getPageSize()).getCheckedData();
        // è½¬æ¢ä¸º CRM äº§å“
        List<CrmProductDO> list = mdmPageResult.getList().stream()
                .map(this::convertToCrmProduct)
                .collect(Collectors.toList());
        return new PageResult<>(list, mdmPageResult.getTotal());
    }
    @Override
    public Long getProductByCategoryId(Long categoryId) {
        return productMapper.selectCountByCategoryId(categoryId);
        return mdmItemApi.getItemCountByCategoryId(categoryId).getCheckedData();
    }
    @Override
    public List<CrmProductDO> getProductListByStatus(Integer status) {
        return productMapper.selectListByStatus(status);
        // ä»Ž MDM èŽ·å–äº§å“æ•°æ®
        List<MdmItemRespDTO> items = mdmItemApi.getItemListByStatus(status).getCheckedData();
        // è¿‡æ»¤äº§å“ç±»åž‹å¹¶è½¬æ¢
        return items.stream()
                .filter(item -> MdmItemTypeEnum.PRODUCT.getType().equals(item.getItemType()))
                .map(this::convertToCrmProduct)
                .collect(Collectors.toList());
    }
    @Override
@@ -158,18 +249,20 @@
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        List<CrmProductDO> list = productMapper.selectByIds(ids);
        Map<Long, CrmProductDO> productMap = convertMap(list, CrmProductDO::getId);
        List<MdmItemRespDTO> items = mdmItemApi.getItemList(ids).getCheckedData();
        Map<Long, MdmItemRespDTO> itemMap = convertMap(items, MdmItemRespDTO::getId);
        List<CrmProductDO> result = new ArrayList<>();
        for (Long id : ids) {
            CrmProductDO product = productMap.get(id);
            if (productMap.get(id) == null) {
            MdmItemRespDTO item = itemMap.get(id);
            if (item == null) {
                throw exception(PRODUCT_NOT_EXISTS);
            }
            if (CrmProductStatusEnum.isDisable(product.getStatus())) {
                throw exception(PRODUCT_NOT_ENABLE, product.getName());
            if (CrmProductStatusEnum.isDisable(item.getStatus())) {
                throw exception(PRODUCT_NOT_ENABLE, item.getName());
            }
            result.add(convertToCrmProduct(item));
        }
        return list;
        return result;
    }
    @Override
@@ -177,7 +270,27 @@
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return productMapper.selectByIds(ids);
        List<MdmItemRespDTO> items = mdmItemApi.getItemList(ids).getCheckedData();
        return items.stream()
                .filter(item -> MdmItemTypeEnum.PRODUCT.getType().equals(item.getItemType()))
                .map(this::convertToCrmProduct)
                .collect(Collectors.toList());
    }
    /**
     * å°† MDM ç‰©æ–™è½¬æ¢ä¸º CRM äº§å“
     */
    private CrmProductDO convertToCrmProduct(MdmItemRespDTO item) {
        CrmProductDO product = new CrmProductDO();
        product.setId(item.getId());
        product.setName(item.getName());
        product.setNo(item.getCode());
        product.setUnit(item.getUnitMeasureId() != null ? item.getUnitMeasureId().intValue() : null);
        product.setPrice(item.getSalesPrice());
        product.setStatus(item.getStatus());
        product.setCategoryId(item.getCategoryId());
        product.setDescription(item.getRemark());
        return product;
    }
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableService.java
@@ -130,4 +130,27 @@
     */
    Long getReceivableCountByContractId(Long contractId);
    /**
     * æ ¹æ®åˆåŒç¼–号查询已回款金额
     *
     * @param contractId åˆåŒç¼–号
     * @return å·²å›žæ¬¾é‡‘额
     */
    BigDecimal getReceivablePriceByContractId(Long contractId);
    /**
     * èŽ·å–æ€»å›žæ¬¾é‡‘é¢
     *
     * @return æ€»å›žæ¬¾é‡‘额
     */
    BigDecimal getReceivablePriceSum();
    /**
     * æ ¹æ®è´Ÿè´£äººæŸ¥è¯¢å›žæ¬¾é‡‘额
     *
     * @param ownerUserId è´Ÿè´£äººç¼–号
     * @return å›žæ¬¾é‡‘额
     */
    BigDecimal getReceivablePriceByOwnerUserId(Long ownerUserId);
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java
@@ -307,4 +307,21 @@
        return receivableMapper.selectCountByContractId(contractId);
    }
    @Override
    public BigDecimal getReceivablePriceByContractId(Long contractId) {
        Map<Long, BigDecimal> priceMap = receivableMapper.selectReceivablePriceMapByContractId(
                java.util.Collections.singleton(contractId));
        return priceMap.getOrDefault(contractId, BigDecimal.ZERO);
    }
    @Override
    public BigDecimal getReceivablePriceSum() {
        return receivableMapper.selectReceivablePriceSum();
    }
    @Override
    public BigDecimal getReceivablePriceByOwnerUserId(Long ownerUserId) {
        return receivableMapper.selectReceivablePriceSumByOwnerUserId(ownerUserId);
    }
}
yudao-module-erp-api/pom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>yudao-module-erp-api</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        erp æ¨¡å—下,企业资源管理的 API æŽ¥å£å®šä¹‰ï¼Œä¾›å…¶ä»–模块引用
    </description>
    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-common</artifactId>
        </dependency>
        <!-- Swagger æ³¨è§£ -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.erp.api.sale;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderCreateReqDTO;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
/**
 * ERP é”€å”®è®¢å• API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface ErpSaleOrderApi {
    String PREFIX = "/rpc-api/erp/sale-order";
    /**
     * åˆ›å»ºé”€å”®è®¢å•
     *
     * @param createReqDTO åˆ›å»ºä¿¡æ¯
     * @return è®¢å•编号
     */
    CommonResult<Long> createSaleOrder(ErpSaleOrderCreateReqDTO createReqDTO);
    /**
     * èŽ·å–é”€å”®è®¢å•
     *
     * @param id è®¢å•编号
     * @return é”€å”®è®¢å•
     */
    CommonResult<ErpSaleOrderRespDTO> getSaleOrder(Long id);
    /**
     * æ ¹æ®åˆåŒç¼–号获取销售订单
     *
     * @param contractId åˆåŒç¼–号
     * @return é”€å”®è®¢å•
     */
    CommonResult<ErpSaleOrderRespDTO> getSaleOrderByContractId(Long contractId);
}
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/dto/ErpSaleOrderCreateReqDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
package cn.iocoder.yudao.module.erp.api.sale.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - ERP é”€å”®è®¢å•创建 Request DTO")
@Data
public class ErpSaleOrderCreateReqDTO {
    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long customerId;
    @Schema(description = "关联合同编号", example = "1")
    private Long contractId;
    @Schema(description = "合同单号", example = "HT001")
    private String contractNo;
    @Schema(description = "销售员编号", example = "1")
    private Long saleUserId;
    @Schema(description = "结算账户编号", example = "1")
    private Long accountId;
    @Schema(description = "下单时间")
    private String orderTime;
    @Schema(description = "优惠率,百分比", example = "10")
    private BigDecimal discountPercent;
    @Schema(description = "定金金额,单位:元", example = "100")
    private BigDecimal depositPrice;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
    @Schema(description = "订单项列表")
    private java.util.List<Item> items;
    @Schema(description = "订单项")
    @Data
    public static class Item {
        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
        private Long productId;
        @Schema(description = "产品单位编号", example = "1")
        private Long productUnitId;
        @Schema(description = "产品单价,单位:元", example = "100")
        private BigDecimal productPrice;
        @Schema(description = "数量", example = "10")
        private BigDecimal count;
        @Schema(description = "税率,百分比", example = "10")
        private BigDecimal taxPercent;
        @Schema(description = "备注", example = "备注")
        private String remark;
    }
}
yudao-module-erp-api/src/main/java/cn/iocoder/yudao/module/erp/api/sale/dto/ErpSaleOrderRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.erp.api.sale.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "RPC æœåŠ¡ - ERP é”€å”®è®¢å• Response DTO")
@Data
public class ErpSaleOrderRespDTO {
    @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "销售订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "SO001")
    private String no;
    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long customerId;
    @Schema(description = "关联合同编号", example = "1")
    private Long contractId;
    @Schema(description = "合同单号", example = "HT001")
    private String contractNo;
    @Schema(description = "订单状态", example = "0")
    private Integer status;
    @Schema(description = "合计金额,单位:元", example = "1000")
    private java.math.BigDecimal totalPrice;
}
yudao-module-erp/pom.xml
@@ -24,6 +24,34 @@
            <version>${revision}</version>
        </dependency>
        <!-- BPM å·¥ä½œæµ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-bpm</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- CRM å®¢æˆ· API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-crm-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- ERP é”€å”®è®¢å• API(本模块 API) -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-erp-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- MDM ä¸»æ•°æ®ç®¡ç† API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-mdm-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- ä¸šåŠ¡ç»„ä»¶ -->
        <!-- Web ç›¸å…³ -->
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/api/sale/ErpSaleOrderApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.erp.api.sale;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderCreateReqDTO;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
import cn.iocoder.yudao.module.erp.dal.mysql.sale.ErpSaleOrderMapper;
import cn.iocoder.yudao.module.erp.service.sale.ErpSaleOrderService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * ERP é”€å”®è®¢å• API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@RequestMapping(ErpSaleOrderApi.PREFIX)
@Validated
public class ErpSaleOrderApiImpl implements ErpSaleOrderApi {
    @Resource
    private ErpSaleOrderService saleOrderService;
    @Resource
    private ErpSaleOrderMapper saleOrderMapper;
    @Override
    public CommonResult<Long> createSaleOrder(ErpSaleOrderCreateReqDTO createReqDTO) {
        // è½¬æ¢ä¸º SaveReqVO
        ErpSaleOrderSaveReqVO saveReqVO = BeanUtils.toBean(createReqDTO, ErpSaleOrderSaveReqVO.class);
        // è½¬æ¢è®¢å•项
        if (createReqDTO.getItems() != null) {
            saveReqVO.setItems(createReqDTO.getItems().stream()
                    .map(item -> BeanUtils.toBean(item, ErpSaleOrderSaveReqVO.Item.class))
                    .toList());
        }
        return success(saleOrderService.createSaleOrder(saveReqVO));
    }
    @Override
    public CommonResult<ErpSaleOrderRespDTO> getSaleOrder(Long id) {
        ErpSaleOrderDO order = saleOrderService.getSaleOrder(id);
        return success(BeanUtils.toBean(order, ErpSaleOrderRespDTO.class));
    }
    @Override
    public CommonResult<ErpSaleOrderRespDTO> getSaleOrderByContractId(Long contractId) {
        ErpSaleOrderDO order = saleOrderMapper.selectByContractId(contractId);
        return success(BeanUtils.toBean(order, ErpSaleOrderRespDTO.class));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/ErpProductController.java
@@ -67,8 +67,8 @@
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('erp:product:query')")
    public CommonResult<ErpProductRespVO> getProduct(@RequestParam("id") Long id) {
        ErpProductDO product = productService.getProduct(id);
        return success(BeanUtils.toBean(product, ErpProductRespVO.class));
        ErpProductRespVO product = productService.getProductVO(id);
        return success(product);
    }
    @GetMapping("/page")
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseRequestController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,210 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase;
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.erp.controller.admin.product.vo.product.ErpProductRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpSupplierDO;
import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseRequestService;
import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
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.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.convertMultiMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserDeptId;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - ERP é‡‡è´­ç”³è¯·")
@RestController
@RequestMapping("/erp/purchase-request")
@Validated
public class ErpPurchaseRequestController {
    @Resource
    private ErpPurchaseRequestService purchaseRequestService;
    @Resource
    private ErpProductService productService;
    @Resource
    private ErpSupplierService supplierService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @PostMapping("/create")
    @Operation(summary = "创建采购申请")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:create')")
    public CommonResult<Long> createPurchaseRequest(@Valid @RequestBody ErpPurchaseRequestSaveReqVO createReqVO) {
        return success(purchaseRequestService.createPurchaseRequest(createReqVO, getLoginUserId(), getLoginUserDeptId()));
    }
    @PutMapping("/update")
    @Operation(summary = "更新采购申请")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:update')")
    public CommonResult<Boolean> updatePurchaseRequest(@Valid @RequestBody ErpPurchaseRequestSaveReqVO updateReqVO) {
        purchaseRequestService.updatePurchaseRequest(updateReqVO);
        return success(true);
    }
    @PutMapping("/submit")
    @Operation(summary = "提交采购申请审批")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:submit')")
    public CommonResult<Boolean> submitPurchaseRequest(@RequestParam("id") Long id,
                                                        @RequestParam("processDefinitionKey") String processDefinitionKey) {
        purchaseRequestService.submitPurchaseRequest(id, processDefinitionKey, getLoginUserId());
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除采购申请")
    @Parameter(name = "ids", description = "编号数组", required = true)
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:delete')")
    public CommonResult<Boolean> deletePurchaseRequest(@RequestParam("ids") List<Long> ids) {
        purchaseRequestService.deletePurchaseRequest(ids);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得采购申请")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:query')")
    public CommonResult<ErpPurchaseRequestRespVO> getPurchaseRequest(@RequestParam("id") Long id) {
        ErpPurchaseRequestDO purchaseRequest = purchaseRequestService.getPurchaseRequest(id);
        if (purchaseRequest == null) {
            return success(null);
        }
        List<ErpPurchaseRequestItemDO> purchaseRequestItemList = purchaseRequestService.getPurchaseRequestItemListByRequestId(id);
        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
                convertSet(purchaseRequestItemList, ErpPurchaseRequestItemDO::getProductId));
        return success(BeanUtils.toBean(purchaseRequest, ErpPurchaseRequestRespVO.class, vo -> {
            vo.setItems(BeanUtils.toBean(purchaseRequestItemList, ErpPurchaseRequestRespVO.Item.class, item ->
                    MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
            // å¡«å……关联字段
            fillPurchaseRequestRelatedFields(vo);
        }));
    }
    @GetMapping("/page")
    @Operation(summary = "获得采购申请分页")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:query')")
    public CommonResult<PageResult<ErpPurchaseRequestRespVO>> getPurchaseRequestPage(@Valid ErpPurchaseRequestPageReqVO pageReqVO) {
        PageResult<ErpPurchaseRequestDO> pageResult = purchaseRequestService.getPurchaseRequestPage(pageReqVO);
        return success(buildPurchaseRequestVOPageResult(pageResult));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出采购申请 Excel")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportPurchaseRequestExcel(@Valid ErpPurchaseRequestPageReqVO pageReqVO,
                                           HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<ErpPurchaseRequestRespVO> list = buildPurchaseRequestVOPageResult(
                purchaseRequestService.getPurchaseRequestPage(pageReqVO)).getList();
        ExcelUtils.write(response, "采购申请.xls", "数据", ErpPurchaseRequestRespVO.class, list);
    }
    @PostMapping("/generate-order")
    @Operation(summary = "生成采购订单")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:generate-order')")
    public CommonResult<Long> generatePurchaseOrder(@RequestParam("id") Long id,
                                                    @RequestParam("supplierId") Long supplierId) {
        return success(purchaseRequestService.generatePurchaseOrder(id, supplierId, getLoginUserId()));
    }
    @GetMapping("/approve-process-list")
    @Operation(summary = "获取采购申请审批流程列表")
    @PreAuthorize("@ss.hasPermission('erp:purchase-request:query')")
    public CommonResult<List<Map<String, Object>>> getPurchaseRequestApproveProcessList() {
        return success(purchaseRequestService.getPurchaseRequestApproveProcessDefinitionList());
    }
    private PageResult<ErpPurchaseRequestRespVO> buildPurchaseRequestVOPageResult(PageResult<ErpPurchaseRequestDO> pageResult) {
        if (CollUtil.isEmpty(pageResult.getList())) {
            return PageResult.empty(pageResult.getTotal());
        }
        // 1.1 ç”³è¯·æ˜Žç»†
        List<ErpPurchaseRequestItemDO> purchaseRequestItemList = purchaseRequestService.getPurchaseRequestItemListByRequestIds(
                convertSet(pageResult.getList(), ErpPurchaseRequestDO::getId));
        Map<Long, List<ErpPurchaseRequestItemDO>> purchaseRequestItemMap = convertMultiMap(purchaseRequestItemList, ErpPurchaseRequestItemDO::getRequestId);
        // 1.2 äº§å“ä¿¡æ¯
        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
                convertSet(purchaseRequestItemList, ErpPurchaseRequestItemDO::getProductId));
        // 1.3 ä¾›åº”商信息
        Map<Long, ErpSupplierDO> supplierMap = supplierService.getSupplierMap(
                convertSet(pageResult.getList(), ErpPurchaseRequestDO::getSupplierId));
        // 1.4 ç”¨æˆ·ä¿¡æ¯
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
                convertSet(pageResult.getList(), ErpPurchaseRequestDO::getRequestUserId));
        // 1.5 éƒ¨é—¨ä¿¡æ¯
        Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
                convertSet(pageResult.getList(), ErpPurchaseRequestDO::getRequestDeptId));
        // 2. å¼€å§‹æ‹¼æŽ¥
        return BeanUtils.toBean(pageResult, ErpPurchaseRequestRespVO.class, vo -> {
            vo.setItems(BeanUtils.toBean(purchaseRequestItemMap.get(vo.getId()), ErpPurchaseRequestRespVO.Item.class,
                    item -> MapUtils.findAndThen(productMap, item.getProductId(), product -> item.setProductName(product.getName())
                            .setProductBarCode(product.getBarCode()).setProductUnitName(product.getUnitName()))));
            vo.setProductNames(CollUtil.join(vo.getItems(), ",", ErpPurchaseRequestRespVO.Item::getProductName));
            MapUtils.findAndThen(supplierMap, vo.getSupplierId(), supplier -> vo.setSupplierName(supplier.getName()));
            MapUtils.findAndThen(userMap, vo.getRequestUserId(), user -> vo.setRequestUserName(user.getNickname()));
            MapUtils.findAndThen(deptMap, vo.getRequestDeptId(), dept -> vo.setRequestDeptName(dept.getName()));
        });
    }
    private void fillPurchaseRequestRelatedFields(ErpPurchaseRequestRespVO vo) {
        // å¡«å……申请人名称
        if (vo.getRequestUserId() != null) {
            AdminUserRespDTO user = adminUserApi.getUser(vo.getRequestUserId());
            if (user != null) {
                vo.setRequestUserName(user.getNickname());
            }
        }
        // å¡«å……部门名称
        if (vo.getRequestDeptId() != null) {
            DeptRespDTO dept = deptApi.getDept(vo.getRequestDeptId());
            if (dept != null) {
                vo.setRequestDeptName(dept.getName());
            }
        }
        // å¡«å……供应商名称
        if (vo.getSupplierId() != null) {
            ErpSupplierDO supplier = supplierService.getSupplier(vo.getSupplierId());
            if (supplier != null) {
                vo.setSupplierName(supplier.getName());
            }
        }
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request;
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 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 = "管理后台 - ERP é‡‡è´­ç”³è¯·åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class ErpPurchaseRequestPageReqVO extends PageParam {
    @Schema(description = "采购申请单号", example = "CGSQ001")
    private String no;
    @Schema(description = "申请人编号", example = "1")
    private Long requestUserId;
    @Schema(description = "申请部门编号", example = "1")
    private Long requestDeptId;
    @Schema(description = "申请时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime[] requestTime;
    @Schema(description = "审批状态", example = "20")
    private Integer status;
    @Schema(description = "申请理由", example = "生产需要")
    private String requestReason;
    @Schema(description = "产品编号", example = "1")
    private Long productId;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,156 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request;
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.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - ERP é‡‡è´­ç”³è¯· Response VO")
@Data
@ExcelIgnoreUnannotated
public class ErpPurchaseRequestRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
    @ExcelProperty("编号")
    private Long id;
    @Schema(description = "采购申请单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "CGSQ001")
    @ExcelProperty("采购申请单号")
    private String no;
    @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
    @ExcelProperty("审批状态")
    private Integer status;
    @Schema(description = "申请人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long requestUserId;
    @Schema(description = "申请人名称", example = "张三")
    @ExcelProperty("申请人")
    private String requestUserName;
    @Schema(description = "申请部门编号", example = "1")
    private Long requestDeptId;
    @Schema(description = "申请部门名称", example = "生产部")
    @ExcelProperty("申请部门")
    private String requestDeptName;
    @Schema(description = "申请时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("申请时间")
    private LocalDateTime requestTime;
    @Schema(description = "申请理由", example = "生产需要")
    @ExcelProperty("申请理由")
    private String requestReason;
    @Schema(description = "供应商编号", example = "1724")
    private Long supplierId;
    @Schema(description = "供应商名称", example = "供应商A")
    @ExcelProperty("供应商名称")
    private String supplierName;
    @Schema(description = "合计数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
    @ExcelProperty("合计数量")
    private BigDecimal totalCount;
    @Schema(description = "合计产品价格", example = "10000.00")
    private BigDecimal totalProductPrice;
    @Schema(description = "合计税额", example = "1300.00")
    private BigDecimal totalTaxPrice;
    @Schema(description = "最终合计价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "11300.00")
    @ExcelProperty("最终合计价格")
    private BigDecimal totalPrice;
    @Schema(description = "优惠率,百分比", example = "10.00")
    private BigDecimal discountPercent;
    @Schema(description = "优惠金额", example = "1130.00")
    private BigDecimal discountPrice;
    @Schema(description = "工作流编号", example = "xxx-xxx")
    private String processInstanceId;
    @Schema(description = "关联采购订单编号", example = "100")
    private Long orderId;
    @Schema(description = "关联采购订单单号", example = "CGDD001")
    @ExcelProperty("采购订单单号")
    private String orderNo;
    @Schema(description = "生成采购订单时间")
    private LocalDateTime orderTime;
    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
    @ExcelProperty("附件地址")
    private String fileUrl;
    @Schema(description = "备注", example = "你猜")
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "创建人", example = "1")
    private String creator;
    @Schema(description = "创建人名称", example = "芋道")
    private String creatorName;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
    @Schema(description = "申请明细列表")
    private List<Item> items;
    @Schema(description = "产品信息")
    @ExcelProperty("产品信息")
    private String productNames;
    @Data
    public static class Item {
        @Schema(description = "申请项编号", example = "11756")
        private Long id;
        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
        private Long productId;
        @Schema(description = "产品单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
        private Long productUnitId;
        @Schema(description = "产品单价(参考价)", example = "100.00")
        private BigDecimal productPrice;
        @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
        private BigDecimal count;
        @Schema(description = "总价", example = "10000.00")
        private BigDecimal totalPrice;
        @Schema(description = "税率,百分比", example = "13.00")
        private BigDecimal taxPercent;
        @Schema(description = "税额", example = "1300.00")
        private BigDecimal taxPrice;
        @Schema(description = "需求日期", example = "2024-01-01")
        private LocalDate demandTime;
        @Schema(description = "备注", example = "随便")
        private String remark;
        // ========== å…³è”字段 ==========
        @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "巧克力")
        private String productName;
        @Schema(description = "产品条码", requiredMode = Schema.RequiredMode.REQUIRED, example = "A9985")
        private String productBarCode;
        @Schema(description = "产品单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "盒")
        private String productUnitName;
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - ERP é‡‡è´­ç”³è¯·æ–°å¢ž/修改 Request VO")
@Data
public class ErpPurchaseRequestSaveReqVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17386")
    private Long id;
    @Schema(description = "申请时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "申请时间不能为空")
    private LocalDateTime requestTime;
    @Schema(description = "申请理由", example = "生产需要")
    private String requestReason;
    @Schema(description = "供应商编号(可选)", example = "1724")
    private Long supplierId;
    @Schema(description = "优惠率,百分比", example = "99.88")
    private BigDecimal discountPercent;
    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
    private String fileUrl;
    @Schema(description = "备注", example = "你猜")
    private String remark;
    @Schema(description = "申请明细列表")
    @NotEmpty(message = "申请明细列表不能为空")
    @Valid
    private List<Item> items;
    @Data
    public static class Item {
        @Schema(description = "申请项编号", example = "11756")
        private Long id;
        @Schema(description = "产品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
        @NotNull(message = "产品编号不能为空")
        private Long productId;
        @Schema(description = "产品单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3113")
        @NotNull(message = "产品单位编号不能为空")
        private Long productUnitId;
        @Schema(description = "产品单价(参考价)", example = "100.00")
        private BigDecimal productPrice;
        @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
        @NotNull(message = "数量不能为空")
        private BigDecimal count;
        @Schema(description = "税率,百分比", example = "13.00")
        private BigDecimal taxPercent;
        @Schema(description = "需求日期", example = "2024-01-01")
        private LocalDate demandTime;
        @Schema(description = "备注", example = "随便")
        private String remark;
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -8,15 +8,15 @@
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.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpCustomerDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.sale.ErpSaleOrderItemDO;
import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
import cn.iocoder.yudao.module.erp.service.sale.ErpCustomerService;
import cn.iocoder.yudao.module.erp.service.sale.ErpSaleOrderService;
import cn.iocoder.yudao.module.erp.service.stock.ErpStockService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -54,7 +54,7 @@
    @Resource
    private ErpProductService productService;
    @Resource
    private ErpCustomerService customerService;
    private CrmCustomerApi customerApi;
    @Resource
    private AdminUserApi adminUserApi;
@@ -144,8 +144,8 @@
        // 1.2 äº§å“ä¿¡æ¯
        Map<Long, ErpProductRespVO> productMap = productService.getProductVOMap(
                convertSet(saleOrderItemList, ErpSaleOrderItemDO::getProductId));
        // 1.3 å®¢æˆ·ä¿¡æ¯
        Map<Long, ErpCustomerDO> customerMap = customerService.getCustomerMap(
        // 1.3 å®¢æˆ·ä¿¡æ¯ï¼ˆä½¿ç”¨ CRM å®¢æˆ· API)
        Map<Long, CrmCustomerRespDTO> customerMap = customerApi.getCustomerMap(
                convertSet(pageResult.getList(), ErpSaleOrderDO::getCustomerId));
        // 1.4 ç®¡ç†å‘˜ä¿¡æ¯
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java
@@ -19,6 +19,12 @@
    @NotNull(message = "客户编号不能为空")
    private Long customerId;
    @Schema(description = "关联合同编号", example = "1")
    private Long contractId;
    @Schema(description = "合同单号", example = "HT001")
    private String contractNo;
    @Schema(description = "下单时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "下单时间不能为空")
    private LocalDateTime orderTime;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/finance/ErpFinanceReceiptDO.java
@@ -53,7 +53,7 @@
    /**
     * å®¢æˆ·ç¼–号
     *
     * å…³è” {@link ErpCustomerDO#getId()}
     * å…³è” CRM å®¢æˆ· {@link cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO#getId()}
     */
    private Long customerId;
    /**
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,122 @@
package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpAccountDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * ERP é‡‡è´­ç”³è¯· DO
 *
 * @author èŠ‹é“æºç 
 */
@TableName(value = "erp_purchase_request")
@KeySequence("erp_purchase_request_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErpPurchaseRequestDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * é‡‡è´­ç”³è¯·å•号
     */
    private String no;
    /**
     * å®¡æ‰¹çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.erp.enums.ErpPurchaseRequestStatusEnum}
     */
    private Integer status;
    /**
     * ç”³è¯·äººç¼–号
     */
    private Long requestUserId;
    /**
     * ç”³è¯·éƒ¨é—¨ç¼–号
     */
    private Long requestDeptId;
    /**
     * ç”³è¯·æ—¶é—´
     */
    private LocalDateTime requestTime;
    /**
     * ç”³è¯·ç†ç”±
     */
    private String requestReason;
    /**
     * ä¾›åº”商编号
     *
     * å…³è” {@link ErpSupplierDO#getId()}
     */
    private Long supplierId;
    /**
     * åˆè®¡æ•°é‡
     */
    private BigDecimal totalCount;
    /**
     * åˆè®¡äº§å“ä»·æ ¼ï¼Œå•位:元
     */
    private BigDecimal totalProductPrice;
    /**
     * åˆè®¡ç¨Žé¢ï¼Œå•位:元
     */
    private BigDecimal totalTaxPrice;
    /**
     * æœ€ç»ˆåˆè®¡ä»·æ ¼ï¼Œå•位:元
     *
     * totalPrice = totalProductPrice + totalTaxPrice - discountPrice
     */
    private BigDecimal totalPrice;
    /**
     * ä¼˜æƒ çŽ‡ï¼Œç™¾åˆ†æ¯”
     */
    private BigDecimal discountPercent;
    /**
     * ä¼˜æƒ é‡‘额,单位:元
     *
     * discountPrice = (totalProductPrice + totalTaxPrice) * discountPercent
     */
    private BigDecimal discountPrice;
    /**
     * å·¥ä½œæµç¼–号
     */
    private String processInstanceId;
    /**
     * å…³è”采购订单编号(审批通过后生成)
     *
     * å…³è” {@link ErpPurchaseOrderDO#getId()}
     */
    private Long orderId;
    /**
     * ç”Ÿæˆé‡‡è´­è®¢å•æ—¶é—´
     */
    private LocalDateTime orderTime;
    /**
     * é™„件地址
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestItemDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * ERP é‡‡è´­ç”³è¯·æ˜Žç»† DO
 *
 * @author èŠ‹é“æºç 
 */
@TableName("erp_purchase_request_items")
@KeySequence("erp_purchase_request_items_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErpPurchaseRequestItemDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * é‡‡è´­ç”³è¯·ç¼–号
     *
     * å…³è” {@link ErpPurchaseRequestDO#getId()}
     */
    private Long requestId;
    /**
     * äº§å“ç¼–号
     *
     * å…³è” {@link ErpProductDO#getId()}
     */
    private Long productId;
    /**
     * äº§å“å•位编号
     *
     * å†—ä½™ {@link ErpProductDO#getUnitId()}
     */
    private Long productUnitId;
    /**
     * äº§å“å•价(参考价),单位:元
     */
    private BigDecimal productPrice;
    /**
     * æ•°é‡
     */
    private BigDecimal count;
    /**
     * æ€»ä»·ï¼Œå•位:元
     *
     * totalPrice = productPrice * count
     */
    private BigDecimal totalPrice;
    /**
     * ç¨ŽçŽ‡ï¼Œç™¾åˆ†æ¯”
     */
    private BigDecimal taxPercent;
    /**
     * ç¨Žé¢ï¼Œå•位:元
     *
     * taxPrice = totalPrice * taxPercent
     */
    private BigDecimal taxPrice;
    /**
     * éœ€æ±‚日期
     */
    private LocalDate demandTime;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java
@@ -43,10 +43,18 @@
    /**
     * å®¢æˆ·ç¼–号
     *
     * å…³è” {@link ErpCustomerDO#getId()}
     * å…³è” CRM å®¢æˆ· {@link cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO#getId()}
     */
    private Long customerId;
    /**
     * å…³è” CRM åˆåŒç¼–号
     */
    private Long contractId;
    /**
     * CRM åˆåŒå•号
     */
    private String contractNo;
    /**
     * ç»“算账户编号
     *
     * å…³è” {@link ErpAccountDO#getId()}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOutDO.java
@@ -43,7 +43,7 @@
    /**
     * å®¢æˆ·ç¼–号
     *
     * å…³è” {@link ErpCustomerDO#getId()}
     * å…³è” CRM å®¢æˆ· {@link cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO#getId()}
     */
    private Long customerId;
    /**
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleReturnDO.java
@@ -43,7 +43,7 @@
    /**
     * å®¢æˆ·ç¼–号
     *
     * å…³è” {@link ErpCustomerDO#getId()}
     * å…³è” CRM å®¢æˆ· {@link cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO#getId()}
     */
    private Long customerId;
    /**
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestItemMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.erp.dal.mysql.purchase;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * ERP é‡‡è´­ç”³è¯·æ˜Žç»† Mapper
 *
 * @author èŠ‹é“æºç 
 */
@Mapper
public interface ErpPurchaseRequestItemMapper extends BaseMapperX<ErpPurchaseRequestItemDO> {
    default List<ErpPurchaseRequestItemDO> selectListByRequestId(Long requestId) {
        return selectList(ErpPurchaseRequestItemDO::getRequestId, requestId);
    }
    default List<ErpPurchaseRequestItemDO> selectListByRequestIds(Collection<Long> requestIds) {
        return selectList(ErpPurchaseRequestItemDO::getRequestId, requestIds);
    }
    default int deleteByRequestId(Long requestId) {
        return delete(ErpPurchaseRequestItemDO::getRequestId, requestId);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseRequestMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.erp.dal.mysql.purchase;
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.MPJLambdaWrapperX;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
/**
 * ERP é‡‡è´­ç”³è¯· Mapper
 *
 * @author èŠ‹é“æºç 
 */
@Mapper
public interface ErpPurchaseRequestMapper extends BaseMapperX<ErpPurchaseRequestDO> {
    default PageResult<ErpPurchaseRequestDO> selectPage(ErpPurchaseRequestPageReqVO reqVO) {
        MPJLambdaWrapperX<ErpPurchaseRequestDO> query = new MPJLambdaWrapperX<ErpPurchaseRequestDO>()
                .likeIfPresent(ErpPurchaseRequestDO::getNo, reqVO.getNo())
                .eqIfPresent(ErpPurchaseRequestDO::getRequestUserId, reqVO.getRequestUserId())
                .eqIfPresent(ErpPurchaseRequestDO::getRequestDeptId, reqVO.getRequestDeptId())
                .betweenIfPresent(ErpPurchaseRequestDO::getRequestTime, reqVO.getRequestTime())
                .eqIfPresent(ErpPurchaseRequestDO::getStatus, reqVO.getStatus())
                .likeIfPresent(ErpPurchaseRequestDO::getRequestReason, reqVO.getRequestReason())
                .orderByDesc(ErpPurchaseRequestDO::getId);
        if (reqVO.getProductId() != null) {
            query.leftJoin(ErpPurchaseRequestItemDO.class, ErpPurchaseRequestItemDO::getRequestId, ErpPurchaseRequestDO::getId)
                    .eq(ErpPurchaseRequestItemDO::getProductId, reqVO.getProductId())
                    .groupBy(ErpPurchaseRequestDO::getId);
        }
        return selectJoinPage(reqVO, ErpPurchaseRequestDO.class, query);
    }
    default int updateByIdAndStatus(Long id, Integer status, ErpPurchaseRequestDO updateObj) {
        return update(updateObj, new LambdaUpdateWrapper<ErpPurchaseRequestDO>()
                .eq(ErpPurchaseRequestDO::getId, id).eq(ErpPurchaseRequestDO::getStatus, status));
    }
    default ErpPurchaseRequestDO selectByNo(String no) {
        return selectOne(ErpPurchaseRequestDO::getNo, no);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/sale/ErpSaleOrderMapper.java
@@ -73,4 +73,8 @@
        return selectOne(ErpSaleOrderDO::getNo, no);
    }
    default ErpSaleOrderDO selectByContractId(Long contractId) {
        return selectOne(ErpSaleOrderDO::getContractId, contractId);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/redis/no/ErpNoRedisDAO.java
@@ -56,6 +56,10 @@
     */
    public static final String PURCHASE_ORDER_NO_PREFIX = "CGDD";
    /**
     * é‡‡è´­ç”³è¯· {@link cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO}
     */
    public static final String PURCHASE_REQUEST_NO_PREFIX = "CGSQ";
    /**
     * é‡‡è´­å…¥åº“ {@link cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInDO}
     */
    public static final String PURCHASE_IN_NO_PREFIX = "CGRK";
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseRequestStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.erp.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
 * ERP é‡‡è´­ç”³è¯·å®¡æ‰¹çŠ¶æ€æžšä¸¾
 *
 * @author èŠ‹é“æºç 
 */
@RequiredArgsConstructor
@Getter
public enum ErpPurchaseRequestStatusEnum implements ArrayValuable<Integer> {
    DRAFT(0, "未提交"),
    PROCESS(10, "审批中"),
    APPROVE(20, "审核通过"),
    REJECT(30, "审核不通过"),
    CANCEL(40, "已取消");
    private final Integer status;
    private final String name;
    public static final Integer[] ARRAYS = Arrays.stream(values())
            .map(ErpPurchaseRequestStatusEnum::getStatus).toArray(Integer[]::new);
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
@@ -25,6 +25,8 @@
    ErrorCode PURCHASE_ORDER_PROCESS_FAIL_EXISTS_IN = new ErrorCode(1_030_101_008, "反审核失败,已存在对应的采购入库单");
ErrorCode PURCHASE_ORDER_ITEM_RETURN_FAIL_IN_EXCEED = new ErrorCode(1_030_101_009, "采购订单项({})超过最大允许退货数量({})");
    ErrorCode PURCHASE_ORDER_PROCESS_FAIL_EXISTS_RETURN = new ErrorCode(1_030_101_010, "反审核失败,已存在对应的采购退货单");
    ErrorCode PURCHASE_ORDER_BPM_CATEGORY_NOT_EXISTS = new ErrorCode(1_030_101_011, "采购审核分类不存在,请先在BPM模块创建采购审核流程分类");
    ErrorCode PURCHASE_ORDER_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_101_012, "采购审核分类下没有可用的流程模型,请先创建并发布流程");
    // ========== ERP é‡‡è´­å…¥åº“(1-030-102-000) ==========
    ErrorCode PURCHASE_IN_NOT_EXISTS = new ErrorCode(1_030_102_000, "采购入库单不存在");
@@ -36,6 +38,18 @@
    ErrorCode PURCHASE_IN_NOT_APPROVE = new ErrorCode(1_030_102_006, "采购入库单未审核,无法操作");
    ErrorCode PURCHASE_IN_FAIL_PAYMENT_PRICE_EXCEED = new ErrorCode(1_030_102_007, "付款金额({})超过采购入库单总金额({})");
    ErrorCode PURCHASE_IN_PROCESS_FAIL_EXISTS_PAYMENT = new ErrorCode(1_030_102_008, "反审核失败,已存在对应的付款单");
    // ========== ERP é‡‡è´­ç”³è¯·ï¼ˆ1-030-104-000) ==========
    ErrorCode PURCHASE_REQUEST_NOT_EXISTS = new ErrorCode(1_030_104_000, "采购申请单不存在");
    ErrorCode PURCHASE_REQUEST_DELETE_FAIL_NOT_DRAFT = new ErrorCode(1_030_104_001, "采购申请单({})不是草稿状态,无法删除");
    ErrorCode PURCHASE_REQUEST_UPDATE_FAIL_NOT_DRAFT = new ErrorCode(1_030_104_002, "采购申请单({})不是草稿状态,无法修改");
    ErrorCode PURCHASE_REQUEST_SUBMIT_FAIL_NOT_DRAFT = new ErrorCode(1_030_104_003, "提交失败,只有草稿状态的申请单才能提交");
    ErrorCode PURCHASE_REQUEST_NO_EXISTS = new ErrorCode(1_030_104_004, "生成申请单号失败,请重新提交");
    ErrorCode PURCHASE_REQUEST_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_030_104_005, "更新审批状态失败,只有审批中的申请单才能更新");
    ErrorCode PURCHASE_REQUEST_NOT_APPROVE = new ErrorCode(1_030_104_006, "采购申请单未审核通过,无法操作");
    ErrorCode PURCHASE_REQUEST_ORDER_EXISTS = new ErrorCode(1_030_104_007, "采购申请单已生成采购订单,请勿重复操作");
    ErrorCode PURCHASE_REQUEST_BPM_CATEGORY_NOT_EXISTS = new ErrorCode(1_030_104_008, "采购申请审批分类不存在,请先在BPM模块创建采购申请审批流程分类");
    ErrorCode PURCHASE_REQUEST_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_104_009, "采购申请审批分类下没有可用的流程模型,请先创建并发布流程");
    // ========== ERP é‡‡è´­é€€è´§ï¼ˆ1-030-103-000) ==========
    ErrorCode PURCHASE_RETURN_NOT_EXISTS = new ErrorCode(1_030_103_000, "采购退货单不存在");
@@ -130,6 +144,7 @@
    // ========== ERP äº§å“ 1-030-500-000 ==========
    ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_030_500_000, "产品不存在");
    ErrorCode PRODUCT_NOT_ENABLE = new ErrorCode(1_030_500_001, "产品({})未启用");
    ErrorCode PRODUCT_BAR_CODE_EXISTS = new ErrorCode(1_030_500_002, "产品条码已存在");
    // ========== ERP äº§å“åˆ†ç±» 1-030-501-000 ==========
    ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_030_501_000, "产品分类不存在");
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinanceReceiptServiceImpl.java
@@ -6,6 +6,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.receipt.ErpFinanceReceiptPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.finance.vo.receipt.ErpFinanceReceiptSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.finance.ErpFinanceReceiptDO;
@@ -17,7 +18,6 @@
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.common.ErpBizTypeEnum;
import cn.iocoder.yudao.module.erp.service.sale.ErpCustomerService;
import cn.iocoder.yudao.module.erp.service.sale.ErpSaleOutService;
import cn.iocoder.yudao.module.erp.service.sale.ErpSaleReturnService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -55,7 +55,7 @@
    private ErpNoRedisDAO noRedisDAO;
    @Resource
    private ErpCustomerService customerService;
    private CrmCustomerApi customerApi;
    @Resource
    private ErpAccountService accountService;
    @Resource
@@ -72,8 +72,8 @@
        // 1.1 æ ¡éªŒè®¢å•项的有效性
        List<ErpFinanceReceiptItemDO> receiptItems = validateFinanceReceiptItems(
                createReqVO.getCustomerId(), createReqVO.getItems());
        // 1.2 æ ¡éªŒå®¢æˆ·
        customerService.validateCustomer(createReqVO.getCustomerId());
        // 1.2 æ ¡éªŒå®¢æˆ·ï¼ˆä½¿ç”¨ CRM å®¢æˆ· API)
        customerApi.validateCustomer(createReqVO.getCustomerId());
        // 1.3 æ ¡éªŒç»“算账户
        if (createReqVO.getAccountId() != null) {
            accountService.validateAccount(createReqVO.getAccountId());
@@ -110,8 +110,8 @@
        if (ErpAuditStatus.APPROVE.getStatus().equals(receipt.getStatus())) {
            throw exception(FINANCE_RECEIPT_UPDATE_FAIL_APPROVE, receipt.getNo());
        }
        // 1.2 æ ¡éªŒå®¢æˆ·
        customerService.validateCustomer(updateReqVO.getCustomerId());
        // 1.2 æ ¡éªŒå®¢æˆ·ï¼ˆä½¿ç”¨ CRM å®¢æˆ· API)
        customerApi.validateCustomer(updateReqVO.getCustomerId());
        // 1.3 æ ¡éªŒç»“算账户
        if (updateReqVO.getAccountId() != null) {
            accountService.validateAccount(updateReqVO.getAccountId());
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductService.java
@@ -59,6 +59,14 @@
    ErpProductDO getProduct(Long id);
    /**
     * èŽ·å¾—äº§å“ VO
     *
     * @param id ç¼–号
     * @return äº§å“ VO
     */
    ErpProductRespVO getProductVO(Long id);
    /**
     * èŽ·å¾—æŒ‡å®šçŠ¶æ€çš„äº§å“ VO åˆ—表
     *
     * @param status çŠ¶æ€
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/product/ErpProductServiceImpl.java
@@ -4,14 +4,16 @@
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
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.module.erp.controller.admin.product.vo.product.ErpProductPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ErpProductRespVO;
import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ProductSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductCategoryDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductUnitDO;
import cn.iocoder.yudao.module.erp.dal.mysql.product.ErpProductMapper;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemCreateReqDTO;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemUpdateReqDTO;
import cn.iocoder.yudao.module.mdm.enums.MdmItemTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@@ -25,6 +27,10 @@
/**
 * ERP äº§å“ Service å®žçŽ°ç±»
 *
 * æ”¹é€ è¯´æ˜Žï¼šäº§å“æ•°æ®ç»Ÿä¸€ä½¿ç”¨ MDM ä¸»æ•°æ®ç®¡ç†ï¼ˆitemType=2 äº§å“ç±»åž‹ï¼‰
 * - åˆ›å»ºã€æ›´æ–°ã€åˆ é™¤æ“ä½œé€šè¿‡ MDM API è¿›è¡Œ
 * - æŸ¥è¯¢æ“ä½œä»Ž MDM èŽ·å–æ•°æ®
 *
 * @author èŠ‹é“æºç 
 */
@Service
@@ -35,36 +41,77 @@
    private ErpProductMapper productMapper;
    @Resource
    private ErpProductCategoryService productCategoryService;
    @Resource
    private ErpProductUnitService productUnitService;
    private MdmItemApi mdmItemApi;
    @Override
    public Long createProduct(ProductSaveReqVO createReqVO) {
        // TODO èŠ‹è‰¿ï¼šæ ¡éªŒåˆ†ç±»
        // æ’å…¥
        ErpProductDO product = BeanUtils.toBean(createReqVO, ErpProductDO.class);
        productMapper.insert(product);
        // è¿”回
        return product.getId();
        // 1. æ ¡éªŒæ¡ç æ˜¯å¦é‡å¤
        MdmItemRespDTO existItem = mdmItemApi.getItemByBarCode(createReqVO.getBarCode()).getCheckedData();
        if (existItem != null) {
            throw exception(PRODUCT_BAR_CODE_EXISTS);
        }
        // 2. é€šè¿‡ MDM API åˆ›å»ºäº§å“
        MdmItemCreateReqDTO createReqDTO = new MdmItemCreateReqDTO();
        createReqDTO.setCode(createReqVO.getBarCode()); // ä½¿ç”¨æ¡ç ä½œä¸ºç¼–码
        createReqDTO.setName(createReqVO.getName());
        createReqDTO.setBarCode(createReqVO.getBarCode());
        createReqDTO.setSpecification(createReqVO.getStandard());
        createReqDTO.setCategoryId(createReqVO.getCategoryId());
        createReqDTO.setUnitMeasureId(createReqVO.getUnitId());
        createReqDTO.setItemType(MdmItemTypeEnum.PRODUCT.getType()); // äº§å“ç±»åž‹
        createReqDTO.setStatus(createReqVO.getStatus() != null ? createReqVO.getStatus() : CommonStatusEnum.ENABLE.getStatus());
        createReqDTO.setPurchasePrice(createReqVO.getPurchasePrice());
        createReqDTO.setSalesPrice(createReqVO.getSalePrice());
        createReqDTO.setCostPrice(createReqVO.getMinPrice());
        createReqDTO.setExpiryDay(createReqVO.getExpiryDay());
        createReqDTO.setWeight(createReqVO.getWeight());
        createReqDTO.setRemark(createReqVO.getRemark());
        return mdmItemApi.createItem(createReqDTO).getCheckedData();
    }
    @Override
    public void updateProduct(ProductSaveReqVO updateReqVO) {
        // TODO èŠ‹è‰¿ï¼šæ ¡éªŒåˆ†ç±»
        // æ ¡éªŒå­˜åœ¨
        validateProductExists(updateReqVO.getId());
        // æ›´æ–°
        ErpProductDO updateObj = BeanUtils.toBean(updateReqVO, ErpProductDO.class);
        productMapper.updateById(updateObj);
        // 1. æ ¡éªŒå­˜åœ¨
        MdmItemRespDTO existItem = mdmItemApi.getItem(updateReqVO.getId()).getCheckedData();
        if (existItem == null) {
            throw exception(PRODUCT_NOT_EXISTS);
        }
        // 2. æ ¡éªŒæ¡ç æ˜¯å¦é‡å¤
        if (!existItem.getBarCode().equals(updateReqVO.getBarCode())) {
            MdmItemRespDTO barCodeItem = mdmItemApi.getItemByBarCode(updateReqVO.getBarCode()).getCheckedData();
            if (barCodeItem != null) {
                throw exception(PRODUCT_BAR_CODE_EXISTS);
            }
        }
        // 3. é€šè¿‡ MDM API æ›´æ–°äº§å“
        MdmItemUpdateReqDTO updateReqDTO = new MdmItemUpdateReqDTO();
        updateReqDTO.setId(updateReqVO.getId());
        updateReqDTO.setCode(updateReqVO.getBarCode());
        updateReqDTO.setName(updateReqVO.getName());
        updateReqDTO.setBarCode(updateReqVO.getBarCode());
        updateReqDTO.setSpecification(updateReqVO.getStandard());
        updateReqDTO.setCategoryId(updateReqVO.getCategoryId());
        updateReqDTO.setUnitMeasureId(updateReqVO.getUnitId());
        updateReqDTO.setItemType(MdmItemTypeEnum.PRODUCT.getType());
        updateReqDTO.setStatus(updateReqVO.getStatus());
        updateReqDTO.setPurchasePrice(updateReqVO.getPurchasePrice());
        updateReqDTO.setSalesPrice(updateReqVO.getSalePrice());
        updateReqDTO.setCostPrice(updateReqVO.getMinPrice());
        updateReqDTO.setExpiryDay(updateReqVO.getExpiryDay());
        updateReqDTO.setWeight(updateReqVO.getWeight());
        updateReqDTO.setRemark(updateReqVO.getRemark());
        mdmItemApi.updateItem(updateReqDTO);
    }
    @Override
    public void deleteProduct(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateProductExists(id);
        // åˆ é™¤
        productMapper.deleteById(id);
        MdmItemRespDTO item = mdmItemApi.getItem(id).getCheckedData();
        if (item == null) {
            throw exception(PRODUCT_NOT_EXISTS);
        }
        // é€šè¿‡ MDM API åˆ é™¤ï¼ˆæ›´æ–°çŠ¶æ€ä¸ºç¦ç”¨ï¼‰
        mdmItemApi.updateItemStatus(id, CommonStatusEnum.DISABLE.getStatus());
    }
    @Override
@@ -72,35 +119,90 @@
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        List<ErpProductDO> list = productMapper.selectByIds(ids);
        Map<Long, ErpProductDO> productMap = convertMap(list, ErpProductDO::getId);
        // ä»Ž MDM èŽ·å–äº§å“æ•°æ®
        List<MdmItemRespDTO> items = mdmItemApi.getItemList(ids).getCheckedData();
        Map<Long, MdmItemRespDTO> itemMap = convertMap(items, MdmItemRespDTO::getId);
        List<ErpProductDO> result = new ArrayList<>();
        for (Long id : ids) {
            ErpProductDO product = productMap.get(id);
            if (productMap.get(id) == null) {
            MdmItemRespDTO item = itemMap.get(id);
            if (item == null) {
                throw exception(PRODUCT_NOT_EXISTS);
            }
            if (CommonStatusEnum.isDisable(product.getStatus())) {
                throw exception(PRODUCT_NOT_ENABLE, product.getName());
            if (CommonStatusEnum.isDisable(item.getStatus())) {
                throw exception(PRODUCT_NOT_ENABLE, item.getName());
            }
            // è½¬æ¢ä¸º ErpProductDO
            result.add(convertToErpProduct(item));
        }
        return list;
        return result;
    }
    private void validateProductExists(Long id) {
        if (productMapper.selectById(id) == null) {
        // ä»Ž MDM æ ¡éªŒ
        MdmItemRespDTO item = mdmItemApi.getItem(id).getCheckedData();
        if (item == null) {
            throw exception(PRODUCT_NOT_EXISTS);
        }
    }
    @Override
    public ErpProductDO getProduct(Long id) {
        return productMapper.selectById(id);
        // ä»Ž MDM èŽ·å–äº§å“æ•°æ®
        MdmItemRespDTO item = mdmItemApi.getItem(id).getCheckedData();
        if (item == null) {
            return null;
        }
        // æ£€æŸ¥æ˜¯å¦ä¸ºäº§å“ç±»åž‹
        if (!MdmItemTypeEnum.PRODUCT.getType().equals(item.getItemType())) {
            return null;
        }
        return convertToErpProduct(item);
    }
    @Override
    public ErpProductRespVO getProductVO(Long id) {
        // ä»Ž MDM èŽ·å–äº§å“æ•°æ®
        MdmItemRespDTO item = mdmItemApi.getItem(id).getCheckedData();
        if (item == null) {
            return null;
        }
        // æ£€æŸ¥æ˜¯å¦ä¸ºäº§å“ç±»åž‹
        if (!MdmItemTypeEnum.PRODUCT.getType().equals(item.getItemType())) {
            return null;
        }
        return convertToErpProductVO(item);
    }
    /**
     * å°† MDM ç‰©æ–™è½¬æ¢ä¸º ERP äº§å“ VO
     */
    private ErpProductRespVO convertToErpProductVO(MdmItemRespDTO item) {
        ErpProductRespVO vo = new ErpProductRespVO();
        vo.setId(item.getId());
        vo.setName(item.getName());
        vo.setBarCode(item.getBarCode());
        vo.setStandard(item.getSpecification());
        vo.setCategoryId(item.getCategoryId());
        vo.setUnitId(item.getUnitMeasureId());
        vo.setStatus(item.getStatus());
        vo.setExpiryDay(item.getExpiryDay());
        vo.setWeight(item.getWeight());
        vo.setPurchasePrice(item.getPurchasePrice());
        vo.setSalePrice(item.getSalesPrice());
        vo.setMinPrice(item.getCostPrice());
        vo.setRemark(item.getRemark());
        return vo;
    }
    @Override
    public List<ErpProductRespVO> getProductVOListByStatus(Integer status) {
        List<ErpProductDO> list = productMapper.selectListByStatus(status);
        return buildProductVOList(list);
        // ä»Ž MDM èŽ·å–äº§å“æ•°æ®ï¼ˆitemType=2)
        List<MdmItemRespDTO> items = mdmItemApi.getItemListByStatus(status).getCheckedData();
        // è¿‡æ»¤äº§å“ç±»åž‹
        items = items.stream()
                .filter(item -> MdmItemTypeEnum.PRODUCT.getType().equals(item.getItemType()))
                .toList();
        return buildProductVOListFromMdm(items);
    }
    @Override
@@ -108,40 +210,78 @@
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        List<ErpProductDO> list = productMapper.selectByIds(ids);
        return buildProductVOList(list);
        // ä»Ž MDM èŽ·å–äº§å“æ•°æ®
        List<MdmItemRespDTO> items = mdmItemApi.getItemList(ids).getCheckedData();
        return buildProductVOListFromMdm(items);
    }
    @Override
    public PageResult<ErpProductRespVO> getProductVOPage(ErpProductPageReqVO pageReqVO) {
        PageResult<ErpProductDO> pageResult = productMapper.selectPage(pageReqVO);
        return new PageResult<>(buildProductVOList(pageResult.getList()), pageResult.getTotal());
        // ä»Ž MDM èŽ·å–äº§å“åˆ†é¡µæ•°æ®
        cn.iocoder.yudao.framework.common.pojo.PageResult<MdmItemRespDTO> mdmPageResult =
                mdmItemApi.getItemPage(MdmItemTypeEnum.PRODUCT.getType(), null,
                        pageReqVO.getPageNo(), pageReqVO.getPageSize()).getCheckedData();
        return new PageResult<>(buildProductVOListFromMdm(mdmPageResult.getList()), mdmPageResult.getTotal());
    }
    private List<ErpProductRespVO> buildProductVOList(List<ErpProductDO> list) {
        if (CollUtil.isEmpty(list)) {
    /**
     * ä»Ž MDM æ•°æ®æž„建 ERP äº§å“ VO åˆ—表
     */
    private List<ErpProductRespVO> buildProductVOListFromMdm(List<MdmItemRespDTO> items) {
        if (CollUtil.isEmpty(items)) {
            return Collections.emptyList();
        }
        Map<Long, ErpProductCategoryDO> categoryMap = productCategoryService.getProductCategoryMap(
                convertSet(list, ErpProductDO::getCategoryId));
        Map<Long, ErpProductUnitDO> unitMap = productUnitService.getProductUnitMap(
                convertSet(list, ErpProductDO::getUnitId));
        return BeanUtils.toBean(list, ErpProductRespVO.class, product -> {
            MapUtils.findAndThen(categoryMap, product.getCategoryId(),
                    category -> product.setCategoryName(category.getName()));
            MapUtils.findAndThen(unitMap, product.getUnitId(),
                    unit -> product.setUnitName(unit.getName()));
        });
        List<ErpProductRespVO> result = new ArrayList<>();
        for (MdmItemRespDTO item : items) {
            ErpProductRespVO vo = new ErpProductRespVO();
            vo.setId(item.getId());
            vo.setName(item.getName());
            vo.setBarCode(item.getBarCode());
            vo.setStandard(item.getSpecification());
            vo.setCategoryId(item.getCategoryId());
            vo.setUnitId(item.getUnitMeasureId());
            vo.setStatus(item.getStatus());
            vo.setExpiryDay(item.getExpiryDay());
            vo.setWeight(item.getWeight());
            vo.setPurchasePrice(item.getPurchasePrice());
            vo.setSalePrice(item.getSalesPrice());
            vo.setMinPrice(item.getCostPrice()); // ä½¿ç”¨æˆæœ¬ä»·ä½œä¸ºæœ€ä½Žä»·
            vo.setRemark(item.getRemark());
            // åˆ†ç±»åç§°å’Œå•位名称需要额外查询(简化处理,后续可优化)
            result.add(vo);
        }
        return result;
    }
    /**
     * å°† MDM ç‰©æ–™è½¬æ¢ä¸º ERP äº§å“ DO
     */
    private ErpProductDO convertToErpProduct(MdmItemRespDTO item) {
        ErpProductDO product = new ErpProductDO();
        product.setId(item.getId());
        product.setName(item.getName());
        product.setBarCode(item.getBarCode());
        product.setStandard(item.getSpecification());
        product.setCategoryId(item.getCategoryId());
        product.setUnitId(item.getUnitMeasureId());
        product.setStatus(item.getStatus());
        product.setExpiryDay(item.getExpiryDay());
        product.setWeight(item.getWeight());
        product.setPurchasePrice(item.getPurchasePrice());
        product.setSalePrice(item.getSalesPrice());
        product.setMinPrice(item.getCostPrice());
        product.setRemark(item.getRemark());
        return product;
    }
    @Override
    public Long getProductCountByCategoryId(Long categoryId) {
        return productMapper.selectCountByCategoryId(categoryId);
        return mdmItemApi.getItemCountByCategoryId(categoryId).getCheckedData();
    }
    @Override
    public Long getProductCountByUnitId(Long unitId) {
        return productMapper.selectCountByUnitId(unitId);
        return mdmItemApi.getItemCountByUnitMeasureId(unitId).getCheckedData();
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseOrderServiceImpl.java
@@ -22,19 +22,17 @@
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
// TODO èŠ‹è‰¿ï¼šè®°å½•æ“ä½œæ—¥å¿—
/**
 * ERP é‡‡è´­è®¢å• Service å®žçŽ°ç±»
 *
 * æ³¨æ„ï¼šé‡‡è´­è®¢å•不需要 BPM å®¡æ‰¹ï¼Œå®¡æ‰¹æµç¨‹åœ¨é‡‡è´­ç”³è¯·é˜¶æ®µå®Œæˆã€‚
 * é‡‡è´­è®¢å•由采购申请生成,或直接创建(状态为已审核)。
 *
 * @author èŠ‹é“æºç 
 */
@@ -74,9 +72,9 @@
            throw exception(PURCHASE_ORDER_NO_EXISTS);
        }
        // 2.1 æ’入订单
        // 2.1 æ’入订单(采购订单不需要审批,直接为已审核状态)
        ErpPurchaseOrderDO purchaseOrder = BeanUtils.toBean(createReqVO, ErpPurchaseOrderDO.class, in -> in
                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus()));
                .setNo(no).setStatus(ErpAuditStatus.APPROVE.getStatus()));
        calculateTotalPrice(purchaseOrder, purchaseOrderItems);
        purchaseOrderMapper.insert(purchaseOrder);
        // 2.2 æ’入订单项
@@ -90,6 +88,7 @@
    public void updatePurchaseOrder(ErpPurchaseOrderSaveReqVO updateReqVO) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(updateReqVO.getId());
        // é‡‡è´­è®¢å•不允许修改(已审核状态)
        if (ErpAuditStatus.APPROVE.getStatus().equals(purchaseOrder.getStatus())) {
            throw exception(PURCHASE_ORDER_UPDATE_FAIL_APPROVE, purchaseOrder.getNo());
        }
@@ -126,6 +125,8 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updatePurchaseOrderStatus(Long id, Integer status) {
        // é‡‡è´­è®¢å•不再支持状态流转(不需要审批)
        // æ­¤æ–¹æ³•保留用于特殊情况(如反审核后重新审核)
        boolean approve = ErpAuditStatus.APPROVE.getStatus().equals(status);
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpPurchaseOrderDO purchaseOrder = validatePurchaseOrderExists(id);
@@ -133,12 +134,14 @@
        if (purchaseOrder.getStatus().equals(status)) {
            throw exception(approve ? PURCHASE_ORDER_APPROVE_FAIL : PURCHASE_ORDER_PROCESS_FAIL);
        }
        // 1.3 å­˜åœ¨é‡‡è´­å…¥å•,无法反审核
        if (!approve && purchaseOrder.getInCount().compareTo(BigDecimal.ZERO) > 0) {
        // 1.3 å­˜åœ¨é‡‡è´­å…¥åº“单,无法反审核
        BigDecimal inCount = purchaseOrder.getInCount();
        if (!approve && inCount != null && inCount.compareTo(BigDecimal.ZERO) > 0) {
            throw exception(PURCHASE_ORDER_PROCESS_FAIL_EXISTS_IN);
        }
        // 1.4 å­˜åœ¨é‡‡è´­é€€è´§å•,无法反审核
        if (!approve && purchaseOrder.getReturnCount().compareTo(BigDecimal.ZERO) > 0) {
        BigDecimal returnCount = purchaseOrder.getReturnCount();
        if (!approve && returnCount != null && returnCount.compareTo(BigDecimal.ZERO) > 0) {
            throw exception(PURCHASE_ORDER_PROCESS_FAIL_EXISTS_RETURN);
        }
@@ -171,7 +174,7 @@
    private void updatePurchaseOrderItemList(Long id, List<ErpPurchaseOrderItemDO> newList) {
        // ç¬¬ä¸€æ­¥ï¼Œå¯¹æ¯”新老数据,获得添加、修改、删除的列表
        List<ErpPurchaseOrderItemDO> oldList = purchaseOrderItemMapper.selectListByOrderId(id);
        List<List<ErpPurchaseOrderItemDO>> diffList = diffList(oldList, newList, // id ä¸åŒï¼Œå°±è®¤ä¸ºæ˜¯ä¸åŒçš„记录
        List<List<ErpPurchaseOrderItemDO>> diffList = diffList(oldList, newList,
                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
        // ç¬¬äºŒæ­¥ï¼Œæ‰¹é‡æ·»åŠ ã€ä¿®æ”¹ã€åˆ é™¤
@@ -241,11 +244,9 @@
            }
        });
        // 2. éåŽ†åˆ é™¤ï¼Œå¹¶è®°å½•æ“ä½œæ—¥å¿—
        // 2. éåŽ†åˆ é™¤
        purchaseOrders.forEach(purchaseOrder -> {
            // 2.1 åˆ é™¤è®¢å•
            purchaseOrderMapper.deleteById(purchaseOrder.getId());
            // 2.2 åˆ é™¤è®¢å•项
            purchaseOrderItemMapper.deleteByOrderId(purchaseOrder.getId());
        });
    }
@@ -292,4 +293,4 @@
        return purchaseOrderItemMapper.selectListByOrderIds(orderIds);
    }
}
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,110 @@
package cn.iocoder.yudao.module.erp.service.purchase;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
 * ERP é‡‡è´­ç”³è¯· Service æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface ErpPurchaseRequestService {
    /**
     * åˆ›å»ºé‡‡è´­ç”³è¯·
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @param userId      ç”¨æˆ·ç¼–号(申请人)
     * @param deptId      éƒ¨é—¨ç¼–号(申请部门)
     * @return ç¼–号
     */
    Long createPurchaseRequest(@Valid ErpPurchaseRequestSaveReqVO createReqVO, Long userId, Long deptId);
    /**
     * æ›´æ–°é‡‡è´­ç”³è¯·
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updatePurchaseRequest(@Valid ErpPurchaseRequestSaveReqVO updateReqVO);
    /**
     * åˆ é™¤é‡‡è´­ç”³è¯·
     *
     * @param ids ç¼–号数组
     */
    void deletePurchaseRequest(List<Long> ids);
    /**
     * èŽ·å¾—é‡‡è´­ç”³è¯·
     *
     * @param id ç¼–号
     * @return é‡‡è´­ç”³è¯·
     */
    ErpPurchaseRequestDO getPurchaseRequest(Long id);
    /**
     * èŽ·å¾—é‡‡è´­ç”³è¯·åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return é‡‡è´­ç”³è¯·åˆ†é¡µ
     */
    PageResult<ErpPurchaseRequestDO> getPurchaseRequestPage(ErpPurchaseRequestPageReqVO pageReqVO);
    /**
     * èŽ·å¾—é‡‡è´­ç”³è¯·æ˜Žç»†åˆ—è¡¨
     *
     * @param requestId é‡‡è´­ç”³è¯·ç¼–号
     * @return é‡‡è´­ç”³è¯·æ˜Žç»†åˆ—表
     */
    List<ErpPurchaseRequestItemDO> getPurchaseRequestItemListByRequestId(Long requestId);
    /**
     * èŽ·å¾—é‡‡è´­ç”³è¯·æ˜Žç»†åˆ—è¡¨
     *
     * @param requestIds é‡‡è´­ç”³è¯·ç¼–号数组
     * @return é‡‡è´­ç”³è¯·æ˜Žç»†åˆ—表
     */
    List<ErpPurchaseRequestItemDO> getPurchaseRequestItemListByRequestIds(Collection<Long> requestIds);
    /**
     * æäº¤é‡‡è´­ç”³è¯·å®¡æ‰¹
     *
     * @param id                   ç¼–号
     * @param processDefinitionKey æµç¨‹å®šä¹‰ Key(用户选择)
     * @param userId               ç”¨æˆ·ç¼–号
     */
    void submitPurchaseRequest(Long id, String processDefinitionKey, Long userId);
    /**
     * æ›´æ–°é‡‡è´­ç”³è¯·å®¡æ‰¹çŠ¶æ€
     *
     * @param id        ç¼–号
     * @param bpmResult BPM å®¡æ‰¹ç»“æžœ
     */
    void updatePurchaseRequestAuditStatus(Long id, Integer bpmResult);
    /**
     * æ ¹æ®é‡‡è´­ç”³è¯·ç”Ÿæˆé‡‡è´­è®¢å•
     *
     * @param id         é‡‡è´­ç”³è¯·ç¼–号
     * @param supplierId ä¾›åº”商编号
     * @param userId     ç”¨æˆ·ç¼–号
     * @return é‡‡è´­è®¢å•编号
     */
    Long generatePurchaseOrder(Long id, Long supplierId, Long userId);
    /**
     * èŽ·å–é‡‡è´­ç”³è¯·å®¡æ‰¹æµç¨‹åˆ—è¡¨
     *
     * @return æµç¨‹å®šä¹‰åˆ—表
     */
    List<java.util.Map<String, Object>> getPurchaseRequestApproveProcessDefinitionList();
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseRequestServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,403 @@
package cn.iocoder.yudao.module.erp.service.purchase;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
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.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.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.request.ErpPurchaseRequestSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestItemDO;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestItemMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseRequestStatusEnum;
import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.erp.enums.ErrorCodeConstants.*;
/**
 * ERP é‡‡è´­ç”³è¯· Service å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
@Slf4j
public class ErpPurchaseRequestServiceImpl implements ErpPurchaseRequestService {
    /**
     * é‡‡è´­ç”³è¯· BPM åˆ†ç±»ç¼–码
     */
    private static final String PURCHASE_REQUEST_APPROVE_CATEGORY_CODE = "purchase_request_approve";
    @Resource
    private ErpPurchaseRequestMapper purchaseRequestMapper;
    @Resource
    private ErpPurchaseRequestItemMapper purchaseRequestItemMapper;
    @Resource
    private ErpNoRedisDAO noRedisDAO;
    @Resource
    private ErpProductService productService;
    @Resource
    private ErpSupplierService supplierService;
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Resource
    private ErpPurchaseOrderService purchaseOrderService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createPurchaseRequest(ErpPurchaseRequestSaveReqVO createReqVO, Long userId, Long deptId) {
        // 1.1 æ ¡éªŒè®¢å•项的有效性
        List<ErpPurchaseRequestItemDO> purchaseRequestItems = validatePurchaseRequestItems(createReqVO.getItems());
        // 1.2 ç”Ÿæˆç”³è¯·å•号,并校验唯一性
        String no = noRedisDAO.generate(ErpNoRedisDAO.PURCHASE_REQUEST_NO_PREFIX);
        if (purchaseRequestMapper.selectByNo(no) != null) {
            throw exception(PURCHASE_REQUEST_NO_EXISTS);
        }
        // æ—¥å¿—:检查部门和用户信息
        log.info("[createPurchaseRequest] åˆ›å»ºé‡‡è´­ç”³è¯·ï¼ŒuserId={}, deptId={}", userId, deptId);
        // 2.1 æ’入申请单
        ErpPurchaseRequestDO purchaseRequest = BeanUtils.toBean(createReqVO, ErpPurchaseRequestDO.class, req -> req
                .setNo(no)
                .setStatus(ErpPurchaseRequestStatusEnum.DRAFT.getStatus())
                .setRequestUserId(userId)
                .setRequestDeptId(deptId));
        calculateTotalPrice(purchaseRequest, purchaseRequestItems);
        purchaseRequestMapper.insert(purchaseRequest);
        // 2.2 æ’入申请明细
        purchaseRequestItems.forEach(o -> o.setRequestId(purchaseRequest.getId()));
        purchaseRequestItemMapper.insertBatch(purchaseRequestItems);
        return purchaseRequest.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updatePurchaseRequest(ErpPurchaseRequestSaveReqVO updateReqVO) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpPurchaseRequestDO purchaseRequest = validatePurchaseRequestExists(updateReqVO.getId());
        // 1.2 æ ¡éªŒçŠ¶æ€ï¼Œåªæœ‰è‰ç¨¿çŠ¶æ€æ‰èƒ½ä¿®æ”¹
        if (!ErpPurchaseRequestStatusEnum.DRAFT.getStatus().equals(purchaseRequest.getStatus())) {
            throw exception(PURCHASE_REQUEST_UPDATE_FAIL_NOT_DRAFT, purchaseRequest.getNo());
        }
        // 1.3 æ ¡éªŒè®¢å•项的有效性
        List<ErpPurchaseRequestItemDO> purchaseRequestItems = validatePurchaseRequestItems(updateReqVO.getItems());
        // 2.1 æ›´æ–°ç”³è¯·å•
        ErpPurchaseRequestDO updateObj = BeanUtils.toBean(updateReqVO, ErpPurchaseRequestDO.class);
        calculateTotalPrice(updateObj, purchaseRequestItems);
        purchaseRequestMapper.updateById(updateObj);
        // 2.2 æ›´æ–°ç”³è¯·æ˜Žç»†
        updatePurchaseRequestItemList(updateReqVO.getId(), purchaseRequestItems);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deletePurchaseRequest(List<Long> ids) {
        // 1. æ ¡éªŒä¸å¤„于非草稿状态
        List<ErpPurchaseRequestDO> purchaseRequests = purchaseRequestMapper.selectByIds(ids);
        if (CollUtil.isEmpty(purchaseRequests)) {
            return;
        }
        purchaseRequests.forEach(purchaseRequest -> {
            if (!ErpPurchaseRequestStatusEnum.DRAFT.getStatus().equals(purchaseRequest.getStatus())) {
                throw exception(PURCHASE_REQUEST_DELETE_FAIL_NOT_DRAFT, purchaseRequest.getNo());
            }
        });
        // 2. éåŽ†åˆ é™¤
        purchaseRequests.forEach(purchaseRequest -> {
            // 2.1 åˆ é™¤ç”³è¯·å•
            purchaseRequestMapper.deleteById(purchaseRequest.getId());
            // 2.2 åˆ é™¤ç”³è¯·æ˜Žç»†
            purchaseRequestItemMapper.deleteByRequestId(purchaseRequest.getId());
        });
    }
    private ErpPurchaseRequestDO validatePurchaseRequestExists(Long id) {
        ErpPurchaseRequestDO purchaseRequest = purchaseRequestMapper.selectById(id);
        if (purchaseRequest == null) {
            throw exception(PURCHASE_REQUEST_NOT_EXISTS);
        }
        return purchaseRequest;
    }
    private List<ErpPurchaseRequestItemDO> validatePurchaseRequestItems(List<ErpPurchaseRequestSaveReqVO.Item> list) {
        // 1. æ ¡éªŒäº§å“å­˜åœ¨
        List<ErpProductDO> productList = productService.validProductList(
                convertSet(list, ErpPurchaseRequestSaveReqVO.Item::getProductId));
        Map<Long, ErpProductDO> productMap = convertMap(productList, ErpProductDO::getId);
        // 2. è½¬åŒ–为 ErpPurchaseRequestItemDO åˆ—表
        return convertList(list, o -> BeanUtils.toBean(o, ErpPurchaseRequestItemDO.class, item -> {
            item.setProductUnitId(productMap.get(item.getProductId()).getUnitId());
            if (item.getProductPrice() != null && item.getCount() != null) {
                item.setTotalPrice(MoneyUtils.priceMultiply(item.getProductPrice(), item.getCount()));
                if (item.getTaxPercent() != null) {
                    item.setTaxPrice(MoneyUtils.priceMultiplyPercent(item.getTotalPrice(), item.getTaxPercent()));
                }
            }
        }));
    }
    private void updatePurchaseRequestItemList(Long id, List<ErpPurchaseRequestItemDO> newList) {
        // ç¬¬ä¸€æ­¥ï¼Œå¯¹æ¯”新老数据,获得添加、修改、删除的列表
        List<ErpPurchaseRequestItemDO> oldList = purchaseRequestItemMapper.selectListByRequestId(id);
        List<List<ErpPurchaseRequestItemDO>> diffList = diffList(oldList, newList,
                (oldVal, newVal) -> oldVal.getId().equals(newVal.getId()));
        // ç¬¬äºŒæ­¥ï¼Œæ‰¹é‡æ·»åŠ ã€ä¿®æ”¹ã€åˆ é™¤
        if (CollUtil.isNotEmpty(diffList.get(0))) {
            diffList.get(0).forEach(o -> o.setRequestId(id));
            purchaseRequestItemMapper.insertBatch(diffList.get(0));
        }
        if (CollUtil.isNotEmpty(diffList.get(1))) {
            purchaseRequestItemMapper.updateBatch(diffList.get(1));
        }
        if (CollUtil.isNotEmpty(diffList.get(2))) {
            purchaseRequestItemMapper.deleteByIds(convertList(diffList.get(2), ErpPurchaseRequestItemDO::getId));
        }
    }
    private void calculateTotalPrice(ErpPurchaseRequestDO purchaseRequest, List<ErpPurchaseRequestItemDO> items) {
        purchaseRequest.setTotalCount(getSumValue(items, ErpPurchaseRequestItemDO::getCount, BigDecimal::add));
        purchaseRequest.setTotalProductPrice(getSumValue(items, ErpPurchaseRequestItemDO::getTotalPrice, BigDecimal::add, BigDecimal.ZERO));
        purchaseRequest.setTotalTaxPrice(getSumValue(items, ErpPurchaseRequestItemDO::getTaxPrice, BigDecimal::add, BigDecimal.ZERO));
        purchaseRequest.setTotalPrice(purchaseRequest.getTotalProductPrice().add(purchaseRequest.getTotalTaxPrice()));
        // è®¡ç®—优惠价格
        if (purchaseRequest.getDiscountPercent() == null) {
            purchaseRequest.setDiscountPercent(BigDecimal.ZERO);
        }
        purchaseRequest.setDiscountPrice(MoneyUtils.priceMultiplyPercent(purchaseRequest.getTotalPrice(), purchaseRequest.getDiscountPercent()));
        purchaseRequest.setTotalPrice(purchaseRequest.getTotalPrice().subtract(purchaseRequest.getDiscountPrice()));
    }
    @Override
    public ErpPurchaseRequestDO getPurchaseRequest(Long id) {
        return purchaseRequestMapper.selectById(id);
    }
    @Override
    public PageResult<ErpPurchaseRequestDO> getPurchaseRequestPage(ErpPurchaseRequestPageReqVO pageReqVO) {
        return purchaseRequestMapper.selectPage(pageReqVO);
    }
    @Override
    public List<ErpPurchaseRequestItemDO> getPurchaseRequestItemListByRequestId(Long requestId) {
        return purchaseRequestItemMapper.selectListByRequestId(requestId);
    }
    @Override
    public List<ErpPurchaseRequestItemDO> getPurchaseRequestItemListByRequestIds(Collection<Long> requestIds) {
        if (CollUtil.isEmpty(requestIds)) {
            return Collections.emptyList();
        }
        return purchaseRequestItemMapper.selectListByRequestIds(requestIds);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitPurchaseRequest(Long id, String processDefinitionKey, Long userId) {
        // 1. æ ¡éªŒç”³è¯·å•存在且为草稿状态
        ErpPurchaseRequestDO request = validatePurchaseRequestExists(id);
        if (!ErpPurchaseRequestStatusEnum.DRAFT.getStatus().equals(request.getStatus())) {
            throw exception(PURCHASE_REQUEST_SUBMIT_FAIL_NOT_DRAFT);
        }
        // 2. æ ¡éªŒåˆ†ç±»å’Œæµç¨‹å®šä¹‰æ˜¯å¦å­˜åœ¨
        validatePurchaseRequestApproveCategoryAndProcessDefinition();
        // 3. æ ¡éªŒä¼ å…¥çš„æµç¨‹å®šä¹‰ Key æ˜¯å¦å±žäºŽè¯¥åˆ†ç±»ä¸”处于激活状态
        ProcessDefinition processDefinition = bpmProcessDefinitionService.getActiveProcessDefinition(processDefinitionKey);
        if (processDefinition == null) {
            throw exception(PURCHASE_REQUEST_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        // 4. åˆ›å»ºæµç¨‹å®žä¾‹ï¼ˆä½¿ç”¨ç”¨æˆ·é€‰æ‹©çš„æµç¨‹å®šä¹‰ Key)
        String processInstanceId = bpmProcessInstanceApi.createProcessInstance(userId,
                new BpmProcessInstanceCreateReqDTO()
                        .setProcessDefinitionKey(processDefinitionKey)
                        .setBusinessKey(String.valueOf(id)));
        // 5. æ›´æ–°ç”³è¯·å•状态
        purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
                .setId(id)
                .setProcessInstanceId(processInstanceId)
                .setStatus(ErpPurchaseRequestStatusEnum.PROCESS.getStatus()));
    }
    /**
     * æ ¡éªŒ BPM é‡‡è´­ç”³è¯·å®¡æ‰¹åˆ†ç±»å’Œæµç¨‹å®šä¹‰æ˜¯å¦å­˜åœ¨
     */
    private void validatePurchaseRequestApproveCategoryAndProcessDefinition() {
        // 1. æ ¡éªŒåˆ†ç±»æ˜¯å¦å­˜åœ¨
        List<String> codes = Collections.singletonList(PURCHASE_REQUEST_APPROVE_CATEGORY_CODE);
        Map<String, ?> categoryMap = bpmCategoryService.getCategoryMap(codes);
        if (!categoryMap.containsKey(PURCHASE_REQUEST_APPROVE_CATEGORY_CODE)) {
            throw exception(PURCHASE_REQUEST_BPM_CATEGORY_NOT_EXISTS);
        }
        // 2. æ ¡éªŒåˆ†ç±»ä¸‹æ˜¯å¦æœ‰å¯ç”¨çš„æµç¨‹å®šä¹‰
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(PURCHASE_REQUEST_APPROVE_CATEGORY_CODE);
        if (CollUtil.isEmpty(definitionInfoList)) {
            throw exception(PURCHASE_REQUEST_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
        // 3. æ ¡éªŒæ˜¯å¦æœ‰æ¿€æ´»çŠ¶æ€çš„æµç¨‹å®šä¹‰
        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
        List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
        boolean hasActiveDefinition = processDefinitions.stream()
                .anyMatch(pd -> !pd.isSuspended());
        if (!hasActiveDefinition) {
            throw exception(PURCHASE_REQUEST_BPM_PROCESS_DEFINITION_NOT_EXISTS);
        }
    }
    @Override
    public void updatePurchaseRequestAuditStatus(Long id, Integer bpmResult) {
        // 1. æ ¡éªŒç”³è¯·å•存在且为审批中状态
        ErpPurchaseRequestDO request = validatePurchaseRequestExists(id);
        if (!ErpPurchaseRequestStatusEnum.PROCESS.getStatus().equals(request.getStatus())) {
            log.error("[updatePurchaseRequestAuditStatus] ç”³è¯·å•({}) ä¸å¤„于审批中", id);
            throw exception(PURCHASE_REQUEST_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS);
        }
        // 2. è½¬æ¢å®¡æ‰¹ç»“æžœ
        Integer auditStatus = convertBpmResultToAuditStatus(bpmResult);
        purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
                .setId(id)
                .setStatus(auditStatus));
    }
    /**
     * BPM å®¡æ‰¹ç»“果转换
     */
    private Integer convertBpmResultToAuditStatus(Integer bpmResult) {
        Integer auditStatus = BpmTaskStatusEnum.APPROVE.getStatus().equals(bpmResult)
                ? ErpPurchaseRequestStatusEnum.APPROVE.getStatus()
                : BpmTaskStatusEnum.REJECT.getStatus().equals(bpmResult)
                ? ErpPurchaseRequestStatusEnum.REJECT.getStatus()
                : BpmTaskStatusEnum.CANCEL.getStatus().equals(bpmResult)
                ? ErpPurchaseRequestStatusEnum.CANCEL.getStatus() : null;
        Assert.notNull(auditStatus, "BPM å®¡æ‰¹ç»“æžœ({}) è½¬æ¢å¤±è´¥", bpmResult);
        return auditStatus;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long generatePurchaseOrder(Long id, Long supplierId, Long userId) {
        // 1. æ ¡éªŒç”³è¯·å•存在且已审批通过
        ErpPurchaseRequestDO request = validatePurchaseRequestExists(id);
        if (!ErpPurchaseRequestStatusEnum.APPROVE.getStatus().equals(request.getStatus())) {
            throw exception(PURCHASE_REQUEST_NOT_APPROVE);
        }
        // æ ¡éªŒæ˜¯å¦å·²ç”Ÿæˆé‡‡è´­è®¢å•
        if (request.getOrderId() != null) {
            throw exception(PURCHASE_REQUEST_ORDER_EXISTS);
        }
        // 2. æ ¡éªŒä¾›åº”商
        supplierService.validateSupplier(supplierId);
        // 3. èŽ·å–ç”³è¯·æ˜Žç»†
        List<ErpPurchaseRequestItemDO> requestItems = purchaseRequestItemMapper.selectListByRequestId(id);
        // 4. æž„建采购订单 VO
        ErpPurchaseOrderSaveReqVO orderVO = new ErpPurchaseOrderSaveReqVO();
        orderVO.setSupplierId(supplierId);
        orderVO.setOrderTime(java.time.LocalDateTime.now());
        orderVO.setRemark("由采购申请单[" + request.getNo() + "]生成");
        // è½¬æ¢æ˜Žç»†é¡¹
        orderVO.setItems(convertList(requestItems, item -> {
            ErpPurchaseOrderSaveReqVO.Item orderItem = new ErpPurchaseOrderSaveReqVO.Item();
            orderItem.setProductId(item.getProductId());
            orderItem.setProductUnitId(item.getProductUnitId());
            orderItem.setProductPrice(item.getProductPrice());
            orderItem.setCount(item.getCount());
            orderItem.setTaxPercent(item.getTaxPercent());
            orderItem.setRemark(item.getRemark());
            return orderItem;
        }));
        // 5. åˆ›å»ºé‡‡è´­è®¢å•
        Long orderId = purchaseOrderService.createPurchaseOrder(orderVO);
        // 6. æ›´æ–°ç”³è¯·å•关联信息
        purchaseRequestMapper.updateById(new ErpPurchaseRequestDO()
                .setId(id)
                .setOrderId(orderId)
                .setOrderTime(java.time.LocalDateTime.now()));
        return orderId;
    }
    @Override
    public List<Map<String, Object>> getPurchaseRequestApproveProcessDefinitionList() {
        // 1. æ ¡éªŒåˆ†ç±»å’Œæµç¨‹å®šä¹‰æ˜¯å¦å­˜åœ¨
        validatePurchaseRequestApproveCategoryAndProcessDefinition();
        // 2. èŽ·å–åˆ†ç±»ä¸‹çš„æµç¨‹å®šä¹‰ä¿¡æ¯
        List<BpmProcessDefinitionInfoDO> definitionInfoList = bpmProcessDefinitionService
                .getProcessDefinitionInfoListByCategory(PURCHASE_REQUEST_APPROVE_CATEGORY_CODE);
        // 3. èŽ·å–æµç¨‹å®šä¹‰è¯¦æƒ…
        Set<String> processDefinitionIds = convertSet(definitionInfoList, BpmProcessDefinitionInfoDO::getProcessDefinitionId);
        List<ProcessDefinition> processDefinitions = bpmProcessDefinitionService.getProcessDefinitionList(processDefinitionIds);
        // 4. è¿‡æ»¤ï¼šåªä¿ç•™æ¿€æ´»çŠ¶æ€ï¼Œä¸”ç›¸åŒ key åªä¿ç•™æœ€æ–°ç‰ˆæœ¬
        Map<String, ProcessDefinition> latestVersionMap = new HashMap<>();
        for (ProcessDefinition pd : processDefinitions) {
            if (pd.isSuspended()) {
                continue; // è·³è¿‡æŒ‚起的流程
            }
            ProcessDefinition existing = latestVersionMap.get(pd.getKey());
            if (existing == null || pd.getVersion() > existing.getVersion()) {
                latestVersionMap.put(pd.getKey(), pd);
            }
        }
        // 5. è¿”回流程定义列表
        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());
            item.put("version", pd.getVersion());
            result.add(item);
        }
        return result;
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/listener/ErpPurchaseRequestStatusListener.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.erp.service.purchase.listener;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseRequestDO;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseRequestMapper;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseRequestStatusEnum;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseRequestService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
 * é‡‡è´­ç”³è¯·å®¡æ‰¹çš„结果监听器实现类
 *
 * é€šè¿‡ businessKey(采购申请ID)匹配,而不是固定的流程定义 Key,
 * è¿™æ ·å¯ä»¥æ”¯æŒåˆ†ç±»ä¸‹ä»»æ„ Key çš„æµç¨‹æ¨¡åž‹ã€‚
 *
 * @author èŠ‹é“æºç 
 */
@Component
@Slf4j
public class ErpPurchaseRequestStatusListener implements ApplicationListener<BpmProcessInstanceStatusEvent> {
    @Resource
    private ErpPurchaseRequestService purchaseRequestService;
    @Resource
    private ErpPurchaseRequestMapper purchaseRequestMapper;
    @Override
    public void onApplicationEvent(BpmProcessInstanceStatusEvent event) {
        // é€šè¿‡ businessKey æŸ¥è¯¢é‡‡è´­ç”³è¯·å•
        Long requestId;
        try {
            requestId = Long.parseLong(event.getBusinessKey());
        } catch (NumberFormatException e) {
            return; // businessKey ä¸æ˜¯æ•°å­—,忽略
        }
        ErpPurchaseRequestDO request = purchaseRequestMapper.selectById(requestId);
        if (request == null) {
            return; // ä¸æ˜¯é‡‡è´­ç”³è¯·ï¼Œå¿½ç•¥
        }
        // åªæœ‰å®¡æ‰¹ä¸­çŠ¶æ€çš„ç”³è¯·å•æ‰å¤„ç†
        if (!ErpPurchaseRequestStatusEnum.PROCESS.getStatus().equals(request.getStatus())) {
            log.warn("[onApplicationEvent] é‡‡è´­ç”³è¯·å•({}) ä¸å¤„于审批中状态,当前状态: {}", requestId, request.getStatus());
            return;
        }
        log.info("[onApplicationEvent] é‡‡è´­ç”³è¯·å•({}) å®¡æ‰¹çŠ¶æ€æ›´æ–°: {}", requestId, event.getStatus());
        // æ›´æ–°å®¡æ‰¹çŠ¶æ€
        purchaseRequestService.updatePurchaseRequestAuditStatus(requestId, event.getStatus());
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -5,6 +5,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderPageReqVO;
import cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order.ErpSaleOrderSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
@@ -54,7 +55,7 @@
    @Resource
    private ErpProductService productService;
    @Resource
    private ErpCustomerService customerService;
    private CrmCustomerApi customerApi;
    @Resource
    private ErpAccountService accountService;
@@ -66,8 +67,8 @@
    public Long createSaleOrder(ErpSaleOrderSaveReqVO createReqVO) {
        // 1.1 æ ¡éªŒè®¢å•项的有效性
        List<ErpSaleOrderItemDO> saleOrderItems = validateSaleOrderItems(createReqVO.getItems());
        // 1.2 æ ¡éªŒå®¢æˆ·
        customerService.validateCustomer(createReqVO.getCustomerId());
        // 1.2 æ ¡éªŒå®¢æˆ·ï¼ˆä½¿ç”¨ CRM å®¢æˆ· API)
        customerApi.validateCustomer(createReqVO.getCustomerId());
        // 1.3 æ ¡éªŒç»“算账户
        if (createReqVO.getAccountId() != null) {
            accountService.validateAccount(createReqVO.getAccountId());
@@ -101,8 +102,8 @@
        if (ErpAuditStatus.APPROVE.getStatus().equals(saleOrder.getStatus())) {
            throw exception(SALE_ORDER_UPDATE_FAIL_APPROVE, saleOrder.getNo());
        }
        // 1.2 æ ¡éªŒå®¢æˆ·
        customerService.validateCustomer(updateReqVO.getCustomerId());
        // 1.2 æ ¡éªŒå®¢æˆ·ï¼ˆä½¿ç”¨ CRM å®¢æˆ· API)
        customerApi.validateCustomer(updateReqVO.getCustomerId());
        // 1.3 æ ¡éªŒç»“算账户
        if (updateReqVO.getAccountId() != null) {
            accountService.validateAccount(updateReqVO.getAccountId());
yudao-module-mdm-api/pom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>yudao-module-mdm-api</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        mdm æ¨¡å—下,主数据管理的 API æŽ¥å£å®šä¹‰ï¼Œä¾›å…¶ä»–模块引用
    </description>
    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-common</artifactId>
        </dependency>
        <!-- Swagger æ³¨è§£ -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,143 @@
package cn.iocoder.yudao.module.mdm.api.item;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemCreateReqDTO;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemUpdateReqDTO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * MDM ç‰©æ–™ API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmItemApi {
    String PREFIX = "/rpc-api/mdm/item";
    /**
     * åˆ›å»ºç‰©æ–™
     *
     * @param createReqDTO åˆ›å»ºä¿¡æ¯
     * @return ç‰©æ–™ç¼–号
     */
    CommonResult<Long> createItem(MdmItemCreateReqDTO createReqDTO);
    /**
     * æ›´æ–°ç‰©æ–™
     *
     * @param updateReqDTO æ›´æ–°ä¿¡æ¯
     * @return æ˜¯å¦æˆåŠŸ
     */
    CommonResult<Boolean> updateItem(MdmItemUpdateReqDTO updateReqDTO);
    /**
     * èŽ·å–ç‰©æ–™
     *
     * @param id ç‰©æ–™ç¼–号
     * @return ç‰©æ–™
     */
    CommonResult<MdmItemRespDTO> getItem(Long id);
    /**
     * èŽ·å–ç‰©æ–™åˆ—è¡¨
     *
     * @param ids ç‰©æ–™ç¼–号列表
     * @return ç‰©æ–™åˆ—表
     */
    CommonResult<List<MdmItemRespDTO>> getItemList(Collection<Long> ids);
    /**
     * èŽ·å–ç‰©æ–™ Map
     *
     * @param ids ç‰©æ–™ç¼–号列表
     * @return ç‰©æ–™ Map
     */
    default Map<Long, MdmItemRespDTO> getItemMap(Collection<Long> ids) {
        List<MdmItemRespDTO> list = getItemList(ids).getCheckedData();
        return convertMap(list, MdmItemRespDTO::getId);
    }
    /**
     * èŽ·å–ç‰©æ–™åˆ†é¡µ
     *
     * @param itemType ç‰©æ–™ç±»åž‹
     * @param status   çŠ¶æ€
     * @param pageNo   é¡µç 
     * @param pageSize æ¯é¡µæ•°é‡
     * @return ç‰©æ–™åˆ†é¡µ
     */
    CommonResult<PageResult<MdmItemRespDTO>> getItemPage(Integer itemType, Integer status, Integer pageNo, Integer pageSize);
    /**
     * æ ¹æ®çŠ¶æ€èŽ·å–ç‰©æ–™åˆ—è¡¨
     *
     * @param status çŠ¶æ€
     * @return ç‰©æ–™åˆ—表
     */
    CommonResult<List<MdmItemRespDTO>> getItemListByStatus(Integer status);
    /**
     * æ ¡éªŒç‰©æ–™å­˜åœ¨
     *
     * @param id ç‰©æ–™ç¼–号
     * @return ç‰©æ–™
     */
    CommonResult<MdmItemRespDTO> validateItemExists(Long id);
    /**
     * æ ¡éªŒç‰©æ–™å­˜åœ¨ä¸”启用
     *
     * @param id ç‰©æ–™ç¼–号
     * @return ç‰©æ–™
     */
    CommonResult<MdmItemRespDTO> validateItemExistsAndEnable(Long id);
    /**
     * æ ¹æ®ç¼–码获取物料
     *
     * @param code ç‰©æ–™ç¼–码
     * @return ç‰©æ–™
     */
    CommonResult<MdmItemRespDTO> getItemByCode(String code);
    /**
     * æ ¹æ®æ¡ç èŽ·å–ç‰©æ–™
     *
     * @param barCode ç‰©æ–™æ¡ç 
     * @return ç‰©æ–™
     */
    CommonResult<MdmItemRespDTO> getItemByBarCode(String barCode);
    /**
     * æ ¹æ®åˆ†ç±»ç¼–号统计物料数量
     *
     * @param categoryId åˆ†ç±»ç¼–号
     * @return ç‰©æ–™æ•°é‡
     */
    CommonResult<Long> getItemCountByCategoryId(Long categoryId);
    /**
     * æ ¹æ®å•位编号统计物料数量
     *
     * @param unitMeasureId å•位编号
     * @return ç‰©æ–™æ•°é‡
     */
    CommonResult<Long> getItemCountByUnitMeasureId(Long unitMeasureId);
    /**
     * æ›´æ–°ç‰©æ–™çŠ¶æ€
     *
     * @param id     ç‰©æ–™ç¼–号
     * @param status çŠ¶æ€
     * @return æ˜¯å¦æˆåŠŸ
     */
    CommonResult<Boolean> updateItemStatus(Long id, Integer status);
}
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemCreateReqDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.mdm.api.item.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - MDM ç‰©æ–™åˆ›å»º Request DTO")
@Data
public class MdmItemCreateReqDTO {
    @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
    private String code;
    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "螺丝")
    private String name;
    @Schema(description = "物料条码", example = "12345678")
    private String barCode;
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long categoryId;
    @Schema(description = "计量单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long unitMeasureId;
    @Schema(description = "品牌编号", example = "1")
    private Long brandId;
    @Schema(description = "物料类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer itemType;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
    @Schema(description = "成本价格", example = "8.00")
    private BigDecimal costPrice;
    @Schema(description = "安全库存", example = "100")
    private BigDecimal safetyStock;
    @Schema(description = "保质期天数", example = "365")
    private Integer expiryDay;
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "重量(kg)", example = "0.5")
    private BigDecimal weight;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.mdm.api.item.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - MDM ç‰©æ–™ Response DTO")
@Data
public class MdmItemRespDTO {
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
    private String code;
    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "螺丝")
    private String name;
    @Schema(description = "物料条码", example = "12345678")
    private String barCode;
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long categoryId;
    @Schema(description = "计量单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long unitMeasureId;
    @Schema(description = "品牌编号", example = "1")
    private Long brandId;
    @Schema(description = "物料类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer itemType;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
    @Schema(description = "成本价格", example = "8.00")
    private BigDecimal costPrice;
    @Schema(description = "安全库存", example = "100")
    private BigDecimal safetyStock;
    @Schema(description = "最低库存", example = "50")
    private BigDecimal minStock;
    @Schema(description = "最高库存", example = "500")
    private BigDecimal maxStock;
    @Schema(description = "保质期天数", example = "365")
    private Integer expiryDay;
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "是否高值物料", example = "false")
    private Boolean highValue;
    @Schema(description = "重量", example = "0.5")
    private BigDecimal weight;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemUpdateReqDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package cn.iocoder.yudao.module.mdm.api.item.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - MDM ç‰©æ–™æ›´æ–° Request DTO")
@Data
public class MdmItemUpdateReqDTO {
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
    private String code;
    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "螺丝")
    private String name;
    @Schema(description = "物料条码", example = "12345678")
    private String barCode;
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long categoryId;
    @Schema(description = "计量单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long unitMeasureId;
    @Schema(description = "品牌编号", example = "1")
    private Long brandId;
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
    @Schema(description = "成本价格", example = "8.00")
    private BigDecimal costPrice;
    @Schema(description = "安全库存", example = "100")
    private BigDecimal safetyStock;
    @Schema(description = "保质期天数", example = "365")
    private Integer expiryDay;
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "重量(kg)", example = "0.5")
    private BigDecimal weight;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mdm-api/src/main/java/cn/iocoder/yudao/module/mdm/enums/MdmItemTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.mdm.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MDM ç‰©æ–™ç±»åž‹æžšä¸¾
 *
 * @author èŠ‹é“æºç 
 */
@Getter
@AllArgsConstructor
public enum MdmItemTypeEnum {
    MATERIAL(1, "物料"),
    PRODUCT(2, "产品"),
    SEMI_FINISHED(3, "半成品");
    /**
     * ç±»åž‹ç¼–码
     */
    private final Integer type;
    /**
     * ç±»åž‹åç§°
     */
    private final String name;
    /**
     * æ ¹æ®ç±»åž‹ç¼–码获取枚举
     */
    public static MdmItemTypeEnum fromType(Integer type) {
        for (MdmItemTypeEnum value : values()) {
            if (value.getType().equals(type)) {
                return value;
            }
        }
        return null;
    }
}
yudao-module-mdm/pom.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>cn.iocoder.boot</groupId>
        <artifactId>yudao</artifactId>
        <version>${revision}</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>yudao-module-mdm</artifactId>
    <packaging>jar</packaging>
    <name>${project.artifactId}</name>
    <description>
        mdm æ¨¡å—下,主数据管理(Master Data Management)。
        ä¾‹å¦‚说:物料、分类、单位、品牌、仓库等主数据管理
    </description>
    <dependencies>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-mdm-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-system</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- Web ç›¸å…³ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-security</artifactId>
        </dependency>
        <!-- DB ç›¸å…³ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-redis</artifactId>
        </dependency>
        <!-- å·¥å…·ç±»ç›¸å…³ -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-excel</artifactId>
        </dependency>
        <!-- Test æµ‹è¯•相关 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/brand/MdmBrandApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.mdm.api.brand;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mdm.api.brand.dto.MdmBrandRespDTO;
import java.util.Collection;
import java.util.List;
/**
 * MDM å“ç‰Œ API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmBrandApi {
    String PREFIX = "/rpc-api/mdm/brand";
    /**
     * èŽ·å–å“ç‰Œ
     *
     * @param id å“ç‰Œç¼–号
     * @return å“ç‰Œ
     */
    CommonResult<MdmBrandRespDTO> getBrand(Long id);
    /**
     * èŽ·å–å“ç‰Œåˆ—è¡¨
     *
     * @param ids å“ç‰Œç¼–号列表
     * @return å“ç‰Œåˆ—表
     */
    CommonResult<List<MdmBrandRespDTO>> getBrandList(Collection<Long> ids);
    /**
     * æ ¡éªŒå“ç‰Œå­˜åœ¨
     *
     * @param id å“ç‰Œç¼–号
     * @return å“ç‰Œ
     */
    CommonResult<MdmBrandRespDTO> validateBrandExists(Long id);
    /**
     * æ ¹æ®ç¼–码获取品牌
     *
     * @param code å“ç‰Œç¼–码
     * @return å“ç‰Œ
     */
    CommonResult<MdmBrandRespDTO> getBrandByCode(String code);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/brand/MdmBrandApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.mdm.api.brand;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.brand.dto.MdmBrandRespDTO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.brand.MdmBrandDO;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MDM å“ç‰Œ API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@RequestMapping(MdmBrandApi.PREFIX)
@Validated
public class MdmBrandApiImpl implements MdmBrandApi {
    @Resource
    private MdmBrandService brandService;
    @Override
    public CommonResult<MdmBrandRespDTO> getBrand(Long id) {
        MdmBrandDO brand = brandService.getBrand(id);
        return success(BeanUtils.toBean(brand, MdmBrandRespDTO.class));
    }
    @Override
    public CommonResult<List<MdmBrandRespDTO>> getBrandList(Collection<Long> ids) {
        List<MdmBrandDO> list = brandService.getBrandList(ids);
        return success(BeanUtils.toBean(list, MdmBrandRespDTO.class));
    }
    @Override
    public CommonResult<MdmBrandRespDTO> validateBrandExists(Long id) {
        MdmBrandDO brand = brandService.validateBrandExists(id);
        return success(BeanUtils.toBean(brand, MdmBrandRespDTO.class));
    }
    @Override
    public CommonResult<MdmBrandRespDTO> getBrandByCode(String code) {
        MdmBrandDO brand = brandService.getBrandByCode(code);
        return success(BeanUtils.toBean(brand, MdmBrandRespDTO.class));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/brand/dto/MdmBrandRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.mdm.api.brand.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "RPC æœåŠ¡ - MDM å“ç‰Œ Response DTO")
@Data
public class MdmBrandRespDTO {
    @Schema(description = "品牌编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "品牌编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "BRAND001")
    private String code;
    @Schema(description = "品牌名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "品牌A")
    private String name;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.mdm.api.category;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mdm.api.category.dto.MdmItemCategoryRespDTO;
import java.util.Collection;
import java.util.List;
/**
 * MDM ç‰©æ–™åˆ†ç±» API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmItemCategoryApi {
    String PREFIX = "/rpc-api/mdm/category";
    /**
     * èŽ·å–ç‰©æ–™åˆ†ç±»
     *
     * @param id åˆ†ç±»ç¼–号
     * @return ç‰©æ–™åˆ†ç±»
     */
    CommonResult<MdmItemCategoryRespDTO> getItemCategory(Long id);
    /**
     * èŽ·å–ç‰©æ–™åˆ†ç±»åˆ—è¡¨
     *
     * @param ids åˆ†ç±»ç¼–号列表
     * @return ç‰©æ–™åˆ†ç±»åˆ—表
     */
    CommonResult<List<MdmItemCategoryRespDTO>> getItemCategoryList(Collection<Long> ids);
    /**
     * æ ¡éªŒç‰©æ–™åˆ†ç±»å­˜åœ¨
     *
     * @param id åˆ†ç±»ç¼–号
     * @return ç‰©æ–™åˆ†ç±»
     */
    CommonResult<MdmItemCategoryRespDTO> validateItemCategoryExists(Long id);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/MdmItemCategoryApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.mdm.api.category;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.category.dto.MdmItemCategoryRespDTO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MDM ç‰©æ–™åˆ†ç±» API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@RequestMapping(MdmItemCategoryApi.PREFIX)
@Validated
public class MdmItemCategoryApiImpl implements MdmItemCategoryApi {
    @Resource
    private MdmItemCategoryService itemCategoryService;
    @Override
    public CommonResult<MdmItemCategoryRespDTO> getItemCategory(Long id) {
        MdmItemCategoryDO itemCategory = itemCategoryService.getItemCategory(id);
        return success(BeanUtils.toBean(itemCategory, MdmItemCategoryRespDTO.class));
    }
    @Override
    public CommonResult<List<MdmItemCategoryRespDTO>> getItemCategoryList(Collection<Long> ids) {
        List<MdmItemCategoryDO> list = itemCategoryService.getItemCategoryList(ids);
        return success(BeanUtils.toBean(list, MdmItemCategoryRespDTO.class));
    }
    @Override
    public CommonResult<MdmItemCategoryRespDTO> validateItemCategoryExists(Long id) {
        MdmItemCategoryDO itemCategory = itemCategoryService.validateItemCategoryExists(id);
        return success(BeanUtils.toBean(itemCategory, MdmItemCategoryRespDTO.class));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/category/dto/MdmItemCategoryRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mdm.api.category.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "RPC æœåŠ¡ - MDM ç‰©æ–™åˆ†ç±» Response DTO")
@Data
public class MdmItemCategoryRespDTO {
    @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "父分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long parentId;
    @Schema(description = "分类编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CAT001")
    private String code;
    @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "五金配件")
    private String name;
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,143 @@
package cn.iocoder.yudao.module.mdm.api.item;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemCreateReqDTO;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemUpdateReqDTO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * MDM ç‰©æ–™ API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmItemApi {
    String PREFIX = "/rpc-api/mdm/item";
    /**
     * åˆ›å»ºç‰©æ–™
     *
     * @param createReqDTO åˆ›å»ºä¿¡æ¯
     * @return ç‰©æ–™ç¼–号
     */
    CommonResult<Long> createItem(MdmItemCreateReqDTO createReqDTO);
    /**
     * æ›´æ–°ç‰©æ–™
     *
     * @param updateReqDTO æ›´æ–°ä¿¡æ¯
     * @return æ˜¯å¦æˆåŠŸ
     */
    CommonResult<Boolean> updateItem(MdmItemUpdateReqDTO updateReqDTO);
    /**
     * èŽ·å–ç‰©æ–™
     *
     * @param id ç‰©æ–™ç¼–号
     * @return ç‰©æ–™
     */
    CommonResult<MdmItemRespDTO> getItem(Long id);
    /**
     * èŽ·å–ç‰©æ–™åˆ—è¡¨
     *
     * @param ids ç‰©æ–™ç¼–号列表
     * @return ç‰©æ–™åˆ—表
     */
    CommonResult<List<MdmItemRespDTO>> getItemList(Collection<Long> ids);
    /**
     * èŽ·å–ç‰©æ–™ Map
     *
     * @param ids ç‰©æ–™ç¼–号列表
     * @return ç‰©æ–™ Map
     */
    default Map<Long, MdmItemRespDTO> getItemMap(Collection<Long> ids) {
        List<MdmItemRespDTO> list = getItemList(ids).getCheckedData();
        return convertMap(list, MdmItemRespDTO::getId);
    }
    /**
     * èŽ·å–ç‰©æ–™åˆ†é¡µ
     *
     * @param itemType ç‰©æ–™ç±»åž‹
     * @param status   çŠ¶æ€
     * @param pageNo   é¡µç 
     * @param pageSize æ¯é¡µæ•°é‡
     * @return ç‰©æ–™åˆ†é¡µ
     */
    CommonResult<PageResult<MdmItemRespDTO>> getItemPage(Integer itemType, Integer status, Integer pageNo, Integer pageSize);
    /**
     * æ ¹æ®çŠ¶æ€èŽ·å–ç‰©æ–™åˆ—è¡¨
     *
     * @param status çŠ¶æ€
     * @return ç‰©æ–™åˆ—表
     */
    CommonResult<List<MdmItemRespDTO>> getItemListByStatus(Integer status);
    /**
     * æ ¡éªŒç‰©æ–™å­˜åœ¨
     *
     * @param id ç‰©æ–™ç¼–号
     * @return ç‰©æ–™
     */
    CommonResult<MdmItemRespDTO> validateItemExists(Long id);
    /**
     * æ ¡éªŒç‰©æ–™å­˜åœ¨ä¸”启用
     *
     * @param id ç‰©æ–™ç¼–号
     * @return ç‰©æ–™
     */
    CommonResult<MdmItemRespDTO> validateItemExistsAndEnable(Long id);
    /**
     * æ ¹æ®ç¼–码获取物料
     *
     * @param code ç‰©æ–™ç¼–码
     * @return ç‰©æ–™
     */
    CommonResult<MdmItemRespDTO> getItemByCode(String code);
    /**
     * æ ¹æ®æ¡ç èŽ·å–ç‰©æ–™
     *
     * @param barCode ç‰©æ–™æ¡ç 
     * @return ç‰©æ–™
     */
    CommonResult<MdmItemRespDTO> getItemByBarCode(String barCode);
    /**
     * æ ¹æ®åˆ†ç±»ç¼–号统计物料数量
     *
     * @param categoryId åˆ†ç±»ç¼–号
     * @return ç‰©æ–™æ•°é‡
     */
    CommonResult<Long> getItemCountByCategoryId(Long categoryId);
    /**
     * æ ¹æ®å•位编号统计物料数量
     *
     * @param unitMeasureId å•位编号
     * @return ç‰©æ–™æ•°é‡
     */
    CommonResult<Long> getItemCountByUnitMeasureId(Long unitMeasureId);
    /**
     * æ›´æ–°ç‰©æ–™çŠ¶æ€
     *
     * @param id     ç‰©æ–™ç¼–号
     * @param status çŠ¶æ€
     * @return æ˜¯å¦æˆåŠŸ
     */
    CommonResult<Boolean> updateItemStatus(Long id, Integer status);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
package cn.iocoder.yudao.module.mdm.api.item;
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.mdm.api.item.dto.MdmItemCreateReqDTO;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemUpdateReqDTO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import cn.iocoder.yudao.module.mdm.service.item.MdmItemService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MDM ç‰©æ–™ API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@RequestMapping(MdmItemApi.PREFIX)
@Validated
public class MdmItemApiImpl implements MdmItemApi {
    @Resource
    private MdmItemService itemService;
    @Resource
    private MdmItemMapper itemMapper;
    @Override
    public CommonResult<Long> createItem(MdmItemCreateReqDTO createReqDTO) {
        MdmItemSaveReqVO saveReqVO = BeanUtils.toBean(createReqDTO, MdmItemSaveReqVO.class);
        // è®¾ç½®é»˜è®¤çŠ¶æ€
        if (saveReqVO.getStatus() == null) {
            saveReqVO.setStatus(0);
        }
        return success(itemService.createItem(saveReqVO));
    }
    @Override
    public CommonResult<Boolean> updateItem(MdmItemUpdateReqDTO updateReqDTO) {
        MdmItemSaveReqVO saveReqVO = BeanUtils.toBean(updateReqDTO, MdmItemSaveReqVO.class);
        itemService.updateItem(saveReqVO);
        return success(true);
    }
    @Override
    public CommonResult<MdmItemRespDTO> getItem(Long id) {
        MdmItemDO item = itemService.getItem(id);
        return success(BeanUtils.toBean(item, MdmItemRespDTO.class));
    }
    @Override
    public CommonResult<List<MdmItemRespDTO>> getItemList(Collection<Long> ids) {
        List<MdmItemDO> list = itemService.getItemList(ids);
        return success(BeanUtils.toBean(list, MdmItemRespDTO.class));
    }
    @Override
    public CommonResult<PageResult<MdmItemRespDTO>> getItemPage(Integer itemType, Integer status, Integer pageNo, Integer pageSize) {
        MdmItemPageReqVO pageReqVO = new MdmItemPageReqVO();
        pageReqVO.setItemType(itemType);
        pageReqVO.setStatus(status);
        pageReqVO.setPageNo(pageNo);
        pageReqVO.setPageSize(pageSize);
        PageResult<MdmItemDO> pageResult = itemService.getItemPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, MdmItemRespDTO.class));
    }
    @Override
    public CommonResult<List<MdmItemRespDTO>> getItemListByStatus(Integer status) {
        MdmItemPageReqVO pageReqVO = new MdmItemPageReqVO();
        pageReqVO.setStatus(status);
        pageReqVO.setPageNo(1);
        pageReqVO.setPageSize(Integer.MAX_VALUE);
        PageResult<MdmItemDO> pageResult = itemService.getItemPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult.getList(), MdmItemRespDTO.class));
    }
    @Override
    public CommonResult<MdmItemRespDTO> validateItemExists(Long id) {
        MdmItemDO item = itemService.validateItemExists(id);
        return success(BeanUtils.toBean(item, MdmItemRespDTO.class));
    }
    @Override
    public CommonResult<MdmItemRespDTO> validateItemExistsAndEnable(Long id) {
        MdmItemDO item = itemService.validateItemExistsAndEnable(id);
        return success(BeanUtils.toBean(item, MdmItemRespDTO.class));
    }
    @Override
    public CommonResult<MdmItemRespDTO> getItemByCode(String code) {
        MdmItemDO item = itemService.getItemByCode(code);
        return success(BeanUtils.toBean(item, MdmItemRespDTO.class));
    }
    @Override
    public CommonResult<MdmItemRespDTO> getItemByBarCode(String barCode) {
        MdmItemDO item = itemMapper.selectByBarCode(barCode);
        return success(BeanUtils.toBean(item, MdmItemRespDTO.class));
    }
    @Override
    public CommonResult<Long> getItemCountByCategoryId(Long categoryId) {
        return success(itemService.getItemCountByCategoryId(categoryId));
    }
    @Override
    public CommonResult<Long> getItemCountByUnitMeasureId(Long unitMeasureId) {
        return success(itemService.getItemCountByUnitMeasureId(unitMeasureId));
    }
    @Override
    public CommonResult<Boolean> updateItemStatus(Long id, Integer status) {
        itemService.updateItemStatus(id, status);
        return success(true);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemSkuApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
package cn.iocoder.yudao.module.mdm.api.item;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemSkuRespDTO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * MDM ç‰©æ–™ SKU API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmItemSkuApi {
    String PREFIX = "/rpc-api/mdm/item-sku";
    /**
     * èŽ·å– SKU
     *
     * @param id SKU ç¼–号
     * @return SKU
     */
    CommonResult<MdmItemSkuRespDTO> getItemSku(Long id);
    /**
     * èŽ·å– SKU åˆ—表
     *
     * @param ids SKU ç¼–号列表
     * @return SKU åˆ—表
     */
    CommonResult<List<MdmItemSkuRespDTO>> getItemSkuList(Collection<Long> ids);
    /**
     * èŽ·å– SKU Map
     *
     * @param ids SKU ç¼–号列表
     * @return SKU Map
     */
    default Map<Long, MdmItemSkuRespDTO> getItemSkuMap(Collection<Long> ids) {
        List<MdmItemSkuRespDTO> list = getItemSkuList(ids).getCheckedData();
        return convertMap(list, MdmItemSkuRespDTO::getId);
    }
    /**
     * èŽ·å–ç‰©æ–™çš„ SKU åˆ—表
     *
     * @param itemId ç‰©æ–™ç¼–号
     * @return SKU åˆ—表
     */
    CommonResult<List<MdmItemSkuRespDTO>> getItemSkuListByItemId(Long itemId);
    /**
     * æ ¡éªŒ SKU å­˜åœ¨
     *
     * @param id SKU ç¼–号
     * @return SKU
     */
    CommonResult<MdmItemSkuRespDTO> validateItemSkuExists(Long id);
    /**
     * æ ¡éªŒ SKU å­˜åœ¨ä¸”启用
     *
     * @param id SKU ç¼–号
     * @return SKU
     */
    CommonResult<MdmItemSkuRespDTO> validateItemSkuExistsAndEnable(Long id);
    /**
     * æ ¹æ®ç¼–码获取 SKU
     *
     * @param code SKU ç¼–码
     * @return SKU
     */
    CommonResult<MdmItemSkuRespDTO> getItemSkuByCode(String code);
    /**
     * æ ¹æ®æ¡ç èŽ·å– SKU
     *
     * @param barCode SKU æ¡ç 
     * @return SKU
     */
    CommonResult<MdmItemSkuRespDTO> getItemSkuByBarCode(String barCode);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/MdmItemSkuApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.mdm.api.item;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemSkuRespDTO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemSkuDO;
import cn.iocoder.yudao.module.mdm.service.item.MdmItemSkuService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MDM ç‰©æ–™ SKU API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@RequestMapping(MdmItemSkuApi.PREFIX)
@Validated
public class MdmItemSkuApiImpl implements MdmItemSkuApi {
    @Resource
    private MdmItemSkuService itemSkuService;
    @Override
    public CommonResult<MdmItemSkuRespDTO> getItemSku(Long id) {
        MdmItemSkuDO sku = itemSkuService.getItemSku(id);
        return success(BeanUtils.toBean(sku, MdmItemSkuRespDTO.class));
    }
    @Override
    public CommonResult<List<MdmItemSkuRespDTO>> getItemSkuList(Collection<Long> ids) {
        List<MdmItemSkuDO> list = itemSkuService.getItemSkuList(ids);
        return success(BeanUtils.toBean(list, MdmItemSkuRespDTO.class));
    }
    @Override
    public CommonResult<List<MdmItemSkuRespDTO>> getItemSkuListByItemId(Long itemId) {
        List<MdmItemSkuDO> list = itemSkuService.getItemSkuListByItemId(itemId);
        return success(BeanUtils.toBean(list, MdmItemSkuRespDTO.class));
    }
    @Override
    public CommonResult<MdmItemSkuRespDTO> validateItemSkuExists(Long id) {
        MdmItemSkuDO sku = itemSkuService.validateItemSkuExists(id);
        return success(BeanUtils.toBean(sku, MdmItemSkuRespDTO.class));
    }
    @Override
    public CommonResult<MdmItemSkuRespDTO> validateItemSkuExistsAndEnable(Long id) {
        MdmItemSkuDO sku = itemSkuService.validateItemSkuExistsAndEnable(id);
        return success(BeanUtils.toBean(sku, MdmItemSkuRespDTO.class));
    }
    @Override
    public CommonResult<MdmItemSkuRespDTO> getItemSkuByCode(String code) {
        MdmItemSkuDO sku = itemSkuService.getItemSkuByCode(code);
        return success(BeanUtils.toBean(sku, MdmItemSkuRespDTO.class));
    }
    @Override
    public CommonResult<MdmItemSkuRespDTO> getItemSkuByBarCode(String barCode) {
        MdmItemSkuDO sku = itemSkuService.getItemSkuByBarCode(barCode);
        return success(BeanUtils.toBean(sku, MdmItemSkuRespDTO.class));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemCreateReqDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.mdm.api.item.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - MDM ç‰©æ–™åˆ›å»º Request DTO")
@Data
public class MdmItemCreateReqDTO {
    @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
    private String code;
    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "螺丝")
    private String name;
    @Schema(description = "物料条码", example = "12345678")
    private String barCode;
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long categoryId;
    @Schema(description = "计量单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long unitMeasureId;
    @Schema(description = "品牌编号", example = "1")
    private Long brandId;
    @Schema(description = "物料类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer itemType;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
    @Schema(description = "成本价格", example = "8.00")
    private BigDecimal costPrice;
    @Schema(description = "安全库存", example = "100")
    private BigDecimal safetyStock;
    @Schema(description = "保质期天数", example = "365")
    private Integer expiryDay;
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "重量(kg)", example = "0.5")
    private BigDecimal weight;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
package cn.iocoder.yudao.module.mdm.api.item.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - MDM ç‰©æ–™ Response DTO")
@Data
public class MdmItemRespDTO {
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
    private String code;
    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "螺丝")
    private String name;
    @Schema(description = "物料条码", example = "12345678")
    private String barCode;
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long categoryId;
    @Schema(description = "计量单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long unitMeasureId;
    @Schema(description = "品牌编号", example = "1")
    private Long brandId;
    @Schema(description = "物料类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer itemType;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
    @Schema(description = "成本价格", example = "8.00")
    private BigDecimal costPrice;
    @Schema(description = "安全库存", example = "100")
    private BigDecimal safetyStock;
    @Schema(description = "最低库存", example = "50")
    private BigDecimal minStock;
    @Schema(description = "最高库存", example = "500")
    private BigDecimal maxStock;
    @Schema(description = "保质期天数", example = "365")
    private Integer expiryDay;
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "是否高值物料", example = "false")
    private Boolean highValue;
    @Schema(description = "重量", example = "0.5")
    private BigDecimal weight;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemSkuRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package cn.iocoder.yudao.module.mdm.api.item.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - MDM ç‰©æ–™ SKU Response DTO")
@Data
public class MdmItemSkuRespDTO {
    @Schema(description = "SKU ç¼–号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long itemId;
    @Schema(description = "SKU ç¼–码", requiredMode = Schema.RequiredMode.REQUIRED, example = "SKU001")
    private String code;
    @Schema(description = "SKU åç§°", example = "规格A")
    private String name;
    @Schema(description = "SKU æ¡ç ", example = "12345678")
    private String barCode;
    @Schema(description = "长度(cm)", example = "10")
    private BigDecimal length;
    @Schema(description = "宽度(cm)", example = "5")
    private BigDecimal width;
    @Schema(description = "高度(cm)", example = "2")
    private BigDecimal height;
    @Schema(description = "毛重(kg)", example = "0.5")
    private BigDecimal grossWeight;
    @Schema(description = "净重(kg)", example = "0.4")
    private BigDecimal netWeight;
    @Schema(description = "成本价", example = "8.00")
    private BigDecimal costPrice;
    @Schema(description = "销售价", example = "15.00")
    private BigDecimal salePrice;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/item/dto/MdmItemUpdateReqDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package cn.iocoder.yudao.module.mdm.api.item.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - MDM ç‰©æ–™æ›´æ–° Request DTO")
@Data
public class MdmItemUpdateReqDTO {
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
    private String code;
    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "螺丝")
    private String name;
    @Schema(description = "物料条码", example = "12345678")
    private String barCode;
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long categoryId;
    @Schema(description = "计量单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long unitMeasureId;
    @Schema(description = "品牌编号", example = "1")
    private Long brandId;
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
    @Schema(description = "成本价格", example = "8.00")
    private BigDecimal costPrice;
    @Schema(description = "安全库存", example = "100")
    private BigDecimal safetyStock;
    @Schema(description = "保质期天数", example = "365")
    private Integer expiryDay;
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "重量(kg)", example = "0.5")
    private BigDecimal weight;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/unit/MdmUnitMeasureApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.mdm.api.unit;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import java.util.Collection;
import java.util.List;
/**
 * MDM è®¡é‡å•位 API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmUnitMeasureApi {
    String PREFIX = "/rpc-api/mdm/unit";
    /**
     * èŽ·å–è®¡é‡å•ä½
     *
     * @param id å•位编号
     * @return è®¡é‡å•位
     */
    CommonResult<MdmUnitMeasureRespDTO> getUnitMeasure(Long id);
    /**
     * èŽ·å–è®¡é‡å•ä½åˆ—è¡¨
     *
     * @param ids å•位编号列表
     * @return è®¡é‡å•位列表
     */
    CommonResult<List<MdmUnitMeasureRespDTO>> getUnitMeasureList(Collection<Long> ids);
    /**
     * æ ¡éªŒè®¡é‡å•位存在
     *
     * @param id å•位编号
     * @return è®¡é‡å•位
     */
    CommonResult<MdmUnitMeasureRespDTO> validateUnitMeasureExists(Long id);
    /**
     * æ ¹æ®ç¼–码获取计量单位
     *
     * @param code å•位编码
     * @return è®¡é‡å•位
     */
    CommonResult<MdmUnitMeasureRespDTO> getUnitMeasureByCode(String code);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/unit/MdmUnitMeasureApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.mdm.api.unit;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MDM è®¡é‡å•位 API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@RequestMapping(MdmUnitMeasureApi.PREFIX)
@Validated
public class MdmUnitMeasureApiImpl implements MdmUnitMeasureApi {
    @Resource
    private MdmUnitMeasureService unitMeasureService;
    @Override
    public CommonResult<MdmUnitMeasureRespDTO> getUnitMeasure(Long id) {
        MdmUnitMeasureDO unitMeasure = unitMeasureService.getUnitMeasure(id);
        return success(BeanUtils.toBean(unitMeasure, MdmUnitMeasureRespDTO.class));
    }
    @Override
    public CommonResult<List<MdmUnitMeasureRespDTO>> getUnitMeasureList(Collection<Long> ids) {
        List<MdmUnitMeasureDO> list = unitMeasureService.getUnitMeasureList(ids);
        return success(BeanUtils.toBean(list, MdmUnitMeasureRespDTO.class));
    }
    @Override
    public CommonResult<MdmUnitMeasureRespDTO> validateUnitMeasureExists(Long id) {
        MdmUnitMeasureDO unitMeasure = unitMeasureService.validateUnitMeasureExists(id);
        return success(BeanUtils.toBean(unitMeasure, MdmUnitMeasureRespDTO.class));
    }
    @Override
    public CommonResult<MdmUnitMeasureRespDTO> getUnitMeasureByCode(String code) {
        MdmUnitMeasureDO unitMeasure = unitMeasureService.getUnitMeasureByCode(code);
        return success(BeanUtils.toBean(unitMeasure, MdmUnitMeasureRespDTO.class));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/unit/dto/MdmUnitMeasureRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.mdm.api.unit.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - MDM è®¡é‡å•位 Response DTO")
@Data
public class MdmUnitMeasureRespDTO {
    @Schema(description = "单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "单位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "PCS")
    private String code;
    @Schema(description = "单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "个")
    private String name;
    @Schema(description = "是否主单位", example = "true")
    private Boolean primaryFlag;
    @Schema(description = "主单位编号", example = "1")
    private Long primaryId;
    @Schema(description = "与主单位换算比例", example = "1.0")
    private BigDecimal changeRate;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/warehouse/MdmWarehouseApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.mdm.api.warehouse;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mdm.api.warehouse.dto.MdmWarehouseRespDTO;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * MDM ä»“库 API æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmWarehouseApi {
    String PREFIX = "/rpc-api/mdm/warehouse";
    /**
     * èŽ·å–ä»“åº“
     *
     * @param id ä»“库编号
     * @return ä»“库
     */
    CommonResult<MdmWarehouseRespDTO> getWarehouse(Long id);
    /**
     * èŽ·å–ä»“åº“åˆ—è¡¨
     *
     * @param ids ä»“库编号列表
     * @return ä»“库列表
     */
    CommonResult<List<MdmWarehouseRespDTO>> getWarehouseList(Collection<Long> ids);
    /**
     * èŽ·å–ä»“åº“ Map
     *
     * @param ids ä»“库编号列表
     * @return ä»“库 Map
     */
    default Map<Long, MdmWarehouseRespDTO> getWarehouseMap(Collection<Long> ids) {
        List<MdmWarehouseRespDTO> list = getWarehouseList(ids).getCheckedData();
        return convertMap(list, MdmWarehouseRespDTO::getId);
    }
    /**
     * æ ¡éªŒä»“库存在
     *
     * @param id ä»“库编号
     * @return ä»“库
     */
    CommonResult<MdmWarehouseRespDTO> validateWarehouseExists(Long id);
    /**
     * æ ¡éªŒä»“库存在且启用
     *
     * @param id ä»“库编号
     * @return ä»“库
     */
    CommonResult<MdmWarehouseRespDTO> validateWarehouseExistsAndEnable(Long id);
    /**
     * æ ¹æ®ç¼–码获取仓库
     *
     * @param code ä»“库编码
     * @return ä»“库
     */
    CommonResult<MdmWarehouseRespDTO> getWarehouseByCode(String code);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/warehouse/MdmWarehouseApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package cn.iocoder.yudao.module.mdm.api.warehouse;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.warehouse.dto.MdmWarehouseRespDTO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.warehouse.MdmWarehouseDO;
import cn.iocoder.yudao.module.mdm.service.warehouse.MdmWarehouseService;
import jakarta.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
 * MDM ä»“库 API å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@RestController
@RequestMapping(MdmWarehouseApi.PREFIX)
@Validated
public class MdmWarehouseApiImpl implements MdmWarehouseApi {
    @Resource
    private MdmWarehouseService warehouseService;
    @Override
    public CommonResult<MdmWarehouseRespDTO> getWarehouse(Long id) {
        MdmWarehouseDO warehouse = warehouseService.getWarehouse(id);
        return success(BeanUtils.toBean(warehouse, MdmWarehouseRespDTO.class));
    }
    @Override
    public CommonResult<List<MdmWarehouseRespDTO>> getWarehouseList(Collection<Long> ids) {
        List<MdmWarehouseDO> list = warehouseService.getWarehouseList(ids);
        return success(BeanUtils.toBean(list, MdmWarehouseRespDTO.class));
    }
    @Override
    public CommonResult<MdmWarehouseRespDTO> validateWarehouseExists(Long id) {
        MdmWarehouseDO warehouse = warehouseService.validateWarehouseExists(id);
        return success(BeanUtils.toBean(warehouse, MdmWarehouseRespDTO.class));
    }
    @Override
    public CommonResult<MdmWarehouseRespDTO> validateWarehouseExistsAndEnable(Long id) {
        MdmWarehouseDO warehouse = warehouseService.validateWarehouseExistsAndEnable(id);
        return success(BeanUtils.toBean(warehouse, MdmWarehouseRespDTO.class));
    }
    @Override
    public CommonResult<MdmWarehouseRespDTO> getWarehouseByCode(String code) {
        MdmWarehouseDO warehouse = warehouseService.getWarehouseByCode(code);
        return success(BeanUtils.toBean(warehouse, MdmWarehouseRespDTO.class));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/api/warehouse/dto/MdmWarehouseRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mdm.api.warehouse.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "RPC æœåŠ¡ - MDM ä»“库 Response DTO")
@Data
public class MdmWarehouseRespDTO {
    @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "仓库编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "WH001")
    private String code;
    @Schema(description = "仓库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "原料仓")
    private String name;
    @Schema(description = "仓库地址", example = "上海市浦东新区")
    private String address;
    @Schema(description = "面积(平方米)", example = "1000")
    private BigDecimal area;
    @Schema(description = "仓库类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer type;
    @Schema(description = "负责人用户ID", example = "1")
    private Long chargeUserId;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "是否冻结", example = "false")
    private Boolean frozen;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/brand/MdmBrandController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.mdm.controller.admin.brand;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.controller.admin.brand.vo.MdmBrandPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.brand.vo.MdmBrandSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.brand.MdmBrandDO;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
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 java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MDM å“ç‰Œ")
@RestController
@RequestMapping("/mdm/brand")
@Validated
public class MdmBrandController {
    @Resource
    private MdmBrandService brandService;
    @PostMapping("/create")
    @Operation(summary = "创建品牌")
    @PreAuthorize("@ss.hasPermission('mdm:brand:create')")
    public CommonResult<Long> createBrand(@Valid @RequestBody MdmBrandSaveReqVO createReqVO) {
        return success(brandService.createBrand(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新品牌")
    @PreAuthorize("@ss.hasPermission('mdm:brand:update')")
    public CommonResult<Boolean> updateBrand(@Valid @RequestBody MdmBrandSaveReqVO updateReqVO) {
        brandService.updateBrand(updateReqVO);
        return success(true);
    }
    @PutMapping("/update-status")
    @Operation(summary = "更新品牌状态")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @Parameter(name = "status", description = "状态", required = true, example = "0")
    @PreAuthorize("@ss.hasPermission('mdm:brand:update')")
    public CommonResult<Boolean> updateBrandStatus(@RequestParam("id") Long id, @RequestParam("status") Integer status) {
        brandService.updateBrandStatus(id, status);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除品牌")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mdm:brand:delete')")
    public CommonResult<Boolean> deleteBrand(@RequestParam("id") Long id) {
        brandService.deleteBrand(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得品牌")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mdm:brand:query')")
    public CommonResult<MdmBrandDO> getBrand(@RequestParam("id") Long id) {
        return success(brandService.getBrand(id));
    }
    @GetMapping("/page")
    @Operation(summary = "获得品牌分页")
    @PreAuthorize("@ss.hasPermission('mdm:brand:query')")
    public CommonResult<PageResult<MdmBrandDO>> getBrandPage(@Valid MdmBrandPageReqVO pageReqVO) {
        return success(brandService.getBrandPage(pageReqVO));
    }
    @GetMapping("/list")
    @Operation(summary = "获得品牌列表")
    @PreAuthorize("@ss.hasPermission('mdm:brand:query')")
    public CommonResult<List<MdmBrandDO>> getBrandList() {
        return success(brandService.getBrandList(null));
    }
    @GetMapping("/list-by-ids")
    @Operation(summary = "获得品牌列表")
    @Parameter(name = "ids", description = "编号列表", required = true, example = "1,2,3")
    @PreAuthorize("@ss.hasPermission('mdm:brand:query')")
    public CommonResult<List<MdmBrandDO>> getBrandListByIds(@RequestParam("ids") List<Long> ids) {
        return success(brandService.getBrandList(ids));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/brand/vo/MdmBrandPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mdm.controller.admin.brand.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.LocalDateTime;
@Schema(description = "管理后台 - MDM å“ç‰Œåˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MdmBrandPageReqVO extends PageParam {
    @Schema(description = "品牌编码", example = "BRAND001")
    private String code;
    @Schema(description = "品牌名称", example = "品牌A")
    private String name;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "创建时间")
    private LocalDateTime[] createTime;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/brand/vo/MdmBrandSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.mdm.controller.admin.brand.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - MDM å“ç‰Œæ–°å¢ž/修改 Request VO")
@Data
public class MdmBrandSaveReqVO {
    @Schema(description = "品牌编号", example = "1")
    private Long id;
    @Schema(description = "品牌编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "BRAND001")
    private String code;
    @Schema(description = "品牌名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "品牌A")
    private String name;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/MdmItemCategoryController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,99 @@
package cn.iocoder.yudao.module.mdm.controller.admin.category;
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.mdm.controller.admin.category.vo.MdmItemCategoryPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.category.vo.MdmItemCategoryRespVO;
import cn.iocoder.yudao.module.mdm.controller.admin.category.vo.MdmItemCategorySaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
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 java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MDM ç‰©æ–™åˆ†ç±»")
@RestController
@RequestMapping("/mdm/category")
@Validated
public class MdmItemCategoryController {
    @Resource
    private MdmItemCategoryService itemCategoryService;
    @PostMapping("/create")
    @Operation(summary = "创建物料分类")
    @PreAuthorize("@ss.hasPermission('mdm:item-category:create')")
    public CommonResult<Long> createItemCategory(@Valid @RequestBody MdmItemCategorySaveReqVO createReqVO) {
        return success(itemCategoryService.createItemCategory(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新物料分类")
    @PreAuthorize("@ss.hasPermission('mdm:item-category:update')")
    public CommonResult<Boolean> updateItemCategory(@Valid @RequestBody MdmItemCategorySaveReqVO updateReqVO) {
        itemCategoryService.updateItemCategory(updateReqVO);
        return success(true);
    }
    @PutMapping("/update-status")
    @Operation(summary = "更新物料分类状态")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @Parameter(name = "status", description = "状态", required = true, example = "0")
    @PreAuthorize("@ss.hasPermission('mdm:item-category:update')")
    public CommonResult<Boolean> updateItemCategoryStatus(@RequestParam("id") Long id, @RequestParam("status") Integer status) {
        itemCategoryService.updateItemCategoryStatus(id, status);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除物料分类")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mdm:item-category:delete')")
    public CommonResult<Boolean> deleteItemCategory(@RequestParam("id") Long id) {
        itemCategoryService.deleteItemCategory(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得物料分类")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mdm:item-category:query')")
    public CommonResult<MdmItemCategoryRespVO> getItemCategory(@RequestParam("id") Long id) {
        MdmItemCategoryDO itemCategory = itemCategoryService.getItemCategory(id);
        return success(BeanUtils.toBean(itemCategory, MdmItemCategoryRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获得物料分类分页")
    @PreAuthorize("@ss.hasPermission('mdm:item-category:query')")
    public CommonResult<PageResult<MdmItemCategoryRespVO>> getItemCategoryPage(@Valid MdmItemCategoryPageReqVO pageReqVO) {
        PageResult<MdmItemCategoryDO> pageResult = itemCategoryService.getItemCategoryPage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, MdmItemCategoryRespVO.class));
    }
    @GetMapping("/list")
    @Operation(summary = "获得物料分类列表")
    @PreAuthorize("@ss.hasPermission('mdm:item-category:query')")
    public CommonResult<List<MdmItemCategoryRespVO>> getItemCategoryList() {
        List<MdmItemCategoryDO> list = itemCategoryService.getItemCategoryList(null);
        return success(BeanUtils.toBean(list, MdmItemCategoryRespVO.class));
    }
    @GetMapping("/list-all-simple")
    @Operation(summary = "获得启用的分类列表")
    public CommonResult<List<MdmItemCategoryRespVO>> getSimpleItemList() {
        List<MdmItemCategoryDO> list = itemCategoryService.getSimpleItemList();
        return success(BeanUtils.toBean(list, MdmItemCategoryRespVO.class));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategoryPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mdm.controller.admin.category.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;
@Schema(description = "管理后台 - MDM ç‰©æ–™åˆ†ç±»åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MdmItemCategoryPageReqVO extends PageParam {
    @Schema(description = "分类编码", example = "CAT001")
    private String code;
    @Schema(description = "分类名称", example = "五金配件")
    private String name;
    @Schema(description = "父分类编号", example = "0")
    private Long parentId;
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
    @Schema(description = "状态", example = "0")
    private Integer status;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategoryRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mdm.controller.admin.category.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MDM ç‰©æ–™åˆ†ç±» Response VO")
@Data
public class MdmItemCategoryRespVO {
    @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "父分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Long parentId;
    @Schema(description = "分类编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CAT001")
    private String code;
    @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "五金配件")
    private String name;
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
    @Schema(description = "排序", example = "1")
    private Integer sort;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/category/vo/MdmItemCategorySaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.mdm.controller.admin.category.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - MDM ç‰©æ–™åˆ†ç±»æ–°å¢ž/修改 Request VO")
@Data
public class MdmItemCategorySaveReqVO {
    @Schema(description = "分类编号", example = "1")
    private Long id;
    @Schema(description = "父分类编号", example = "0")
    private Long parentId;
    @Schema(description = "分类编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "CAT001")
    private String code;
    @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "五金配件")
    private String name;
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
    @Schema(description = "排序", example = "1")
    private Integer sort;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,110 @@
package cn.iocoder.yudao.module.mdm.controller.admin.item;
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.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemRespVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemStatusUpdateReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.service.item.MdmItemService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
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 java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@Tag(name = "管理后台 - MDM ç‰©æ–™ä¸»æ•°æ®")
@RestController
@RequestMapping("/mdm/item")
@Validated
public class MdmItemController {
    @Resource
    private MdmItemService itemService;
    @Resource
    private MdmUnitMeasureService unitMeasureService;
    @PostMapping("/create")
    @Operation(summary = "创建物料")
    @PreAuthorize("@ss.hasPermission('mdm:item:create')")
    public CommonResult<Long> createItem(@Valid @RequestBody MdmItemSaveReqVO createReqVO) {
        return success(itemService.createItem(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新物料")
    @PreAuthorize("@ss.hasPermission('mdm:item:update')")
    public CommonResult<Boolean> updateItem(@Valid @RequestBody MdmItemSaveReqVO updateReqVO) {
        itemService.updateItem(updateReqVO);
        return success(true);
    }
    @PutMapping("/update-status")
    @Operation(summary = "更新物料状态")
    @PreAuthorize("@ss.hasPermission('mdm:item:update')")
    public CommonResult<Boolean> updateItemStatus(@RequestBody MdmItemStatusUpdateReqVO reqVO) {
        itemService.updateItemStatus(reqVO.getId(), reqVO.getStatus());
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除物料")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mdm:item:delete')")
    public CommonResult<Boolean> deleteItem(@RequestParam("id") Long id) {
        itemService.deleteItem(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得物料")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mdm:item:query')")
    public CommonResult<MdmItemRespVO> getItem(@RequestParam("id") Long id) {
        MdmItemDO item = itemService.getItem(id);
        return success(BeanUtils.toBean(item, MdmItemRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获得物料分页")
    @PreAuthorize("@ss.hasPermission('mdm:item:query')")
    public CommonResult<PageResult<MdmItemRespVO>> getItemPage(@Valid MdmItemPageReqVO pageReqVO) {
        PageResult<MdmItemDO> pageResult = itemService.getItemPage(pageReqVO);
        // å…³è”查询计量单位名称
        Map<Long, MdmUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                convertSet(pageResult.getList(), MdmItemDO::getUnitMeasureId));
        // è½¬æ¢
        List<MdmItemRespVO> respList = BeanUtils.toBean(pageResult.getList(), MdmItemRespVO.class);
        respList.forEach(resp -> {
            MdmUnitMeasureDO unit = unitMap.get(resp.getUnitMeasureId());
            if (unit != null) {
                resp.setUnitMeasureName(unit.getName());
            }
        });
        return success(new PageResult<>(respList, pageResult.getTotal()));
    }
    @GetMapping("/list-by-ids")
    @Operation(summary = "获得物料列表")
    @Parameter(name = "ids", description = "编号列表", required = true, example = "1,2,3")
    @PreAuthorize("@ss.hasPermission('mdm:item:query')")
    public CommonResult<List<MdmItemRespVO>> getItemList(@RequestParam("ids") List<Long> ids) {
        List<MdmItemDO> list = itemService.getItemList(ids);
        return success(BeanUtils.toBean(list, MdmItemRespVO.class));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.mdm.controller.admin.item.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.LocalDateTime;
@Schema(description = "管理后台 - MDM ç‰©æ–™åˆ†é¡µ Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MdmItemPageReqVO extends PageParam {
    @Schema(description = "物料编码", example = "ITEM001")
    private String code;
    @Schema(description = "物料名称", example = "螺丝")
    private String name;
    @Schema(description = "物料条码", example = "12345678")
    private String barCode;
    @Schema(description = "物料分类编号", example = "1")
    private Long categoryId;
    @Schema(description = "物料类型", example = "1")
    private Integer itemType;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "品牌编号", example = "1")
    private Long brandId;
    @Schema(description = "创建时间")
    private LocalDateTime[] createTime;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
package cn.iocoder.yudao.module.mdm.controller.admin.item.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MDM ç‰©æ–™ Response VO")
@Data
public class MdmItemRespVO {
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
    private String code;
    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "螺丝")
    private String name;
    @Schema(description = "物料条码", example = "12345678")
    private String barCode;
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long categoryId;
    @Schema(description = "物料分类名称", example = "五金配件")
    private String categoryName;
    @Schema(description = "计量单位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long unitMeasureId;
    @Schema(description = "计量单位名称", example = "个")
    private String unitMeasureName;
    @Schema(description = "品牌编号", example = "1")
    private Long brandId;
    @Schema(description = "品牌名称", example = "品牌A")
    private String brandName;
    @Schema(description = "物料类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer itemType;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
    @Schema(description = "成本价格", example = "8.00")
    private BigDecimal costPrice;
    @Schema(description = "安全库存", example = "100")
    private BigDecimal safetyStock;
    @Schema(description = "最低库存", example = "50")
    private BigDecimal minStock;
    @Schema(description = "最高库存", example = "500")
    private BigDecimal maxStock;
    @Schema(description = "保质期天数", example = "365")
    private Integer expiryDay;
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "是否高值物料", example = "false")
    private Boolean isHighValue;
    @Schema(description = "重量", example = "0.5")
    private BigDecimal weight;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.mdm.controller.admin.item.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - MDM ç‰©æ–™æ–°å¢ž/修改 Request VO")
@Data
public class MdmItemSaveReqVO {
    @Schema(description = "物料编号", example = "1")
    private Long id;
    @Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ITEM001")
    private String code;
    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "螺丝")
    private String name;
    @Schema(description = "物料条码", example = "12345678")
    private String barCode;
    @Schema(description = "规格型号", example = "M8*20")
    private String specification;
    @Schema(description = "物料分类编号", example = "1")
    private Long categoryId;
    @Schema(description = "计量单位编号", example = "1")
    private Long unitMeasureId;
    @Schema(description = "物料类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer itemType;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "采购价格", example = "10.00")
    private BigDecimal purchasePrice;
    @Schema(description = "销售价格", example = "15.00")
    private BigDecimal salesPrice;
    @Schema(description = "成本价格", example = "8.00")
    private BigDecimal costPrice;
    @Schema(description = "安全库存", example = "100")
    private BigDecimal safetyStock;
    @Schema(description = "最低库存", example = "50")
    private BigDecimal minStock;
    @Schema(description = "最高库存", example = "500")
    private BigDecimal maxStock;
    @Schema(description = "保质期天数", example = "365")
    private Integer expiryDay;
    @Schema(description = "是否批次管理", example = "false")
    private Boolean isBatchManaged;
    @Schema(description = "是否高值物料", example = "false")
    private Boolean isHighValue;
    @Schema(description = "重量", example = "0.5")
    private BigDecimal weight;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemStatusUpdateReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mdm.controller.admin.item.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotNull;
@Schema(description = "管理后台 - MDM ç‰©æ–™çŠ¶æ€æ›´æ–° Request VO")
@Data
public class MdmItemStatusUpdateReqVO {
    @Schema(description = "物料编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "物料编号不能为空")
    private Long id;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @NotNull(message = "状态不能为空")
    private Integer status;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/MdmUnitMeasureController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
package cn.iocoder.yudao.module.mdm.controller.admin.unit;
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.mdm.controller.admin.unit.vo.MdmUnitMeasurePageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
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 java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MDM è®¡é‡å•位")
@RestController
@RequestMapping("/mdm/unit")
@Validated
public class MdmUnitMeasureController {
    @Resource
    private MdmUnitMeasureService unitMeasureService;
    @PostMapping("/create")
    @Operation(summary = "创建计量单位")
    @PreAuthorize("@ss.hasPermission('mdm:unit-measure:create')")
    public CommonResult<Long> createUnitMeasure(@Valid @RequestBody MdmUnitMeasureSaveReqVO createReqVO) {
        return success(unitMeasureService.createUnitMeasure(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新计量单位")
    @PreAuthorize("@ss.hasPermission('mdm:unit-measure:update')")
    public CommonResult<Boolean> updateUnitMeasure(@Valid @RequestBody MdmUnitMeasureSaveReqVO updateReqVO) {
        unitMeasureService.updateUnitMeasure(updateReqVO);
        return success(true);
    }
    @PutMapping("/update-status")
    @Operation(summary = "更新计量单位状态")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @Parameter(name = "status", description = "状态", required = true, example = "0")
    @PreAuthorize("@ss.hasPermission('mdm:unit-measure:update')")
    public CommonResult<Boolean> updateUnitMeasureStatus(@RequestParam("id") Long id, @RequestParam("status") Integer status) {
        unitMeasureService.updateUnitMeasureStatus(id, status);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除计量单位")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mdm:unit-measure:delete')")
    public CommonResult<Boolean> deleteUnitMeasure(@RequestParam("id") Long id) {
        unitMeasureService.deleteUnitMeasure(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得计量单位")
    @Parameter(name = "id", description = "编号", required = true, example = "1")
    @PreAuthorize("@ss.hasPermission('mdm:unit-measure:query')")
    public CommonResult<MdmUnitMeasureDO> getUnitMeasure(@RequestParam("id") Long id) {
        return success(unitMeasureService.getUnitMeasure(id));
    }
    @GetMapping("/page")
    @Operation(summary = "获得计量单位分页")
    @PreAuthorize("@ss.hasPermission('mdm:unit-measure:query')")
    public CommonResult<PageResult<MdmUnitMeasureDO>> getUnitMeasurePage(@Valid MdmUnitMeasurePageReqVO pageReqVO) {
        return success(unitMeasureService.getUnitMeasurePage(pageReqVO));
    }
    @GetMapping("/list")
    @Operation(summary = "获得计量单位列表")
    @PreAuthorize("@ss.hasPermission('mdm:unit-measure:query')")
    public CommonResult<List<MdmUnitMeasureDO>> getUnitMeasureList() {
        return success(unitMeasureService.getUnitMeasureList(null));
    }
    @GetMapping("/list-by-ids")
    @Operation(summary = "获得计量单位列表")
    @Parameter(name = "ids", description = "编号列表", required = true, example = "1,2,3")
    @PreAuthorize("@ss.hasPermission('mdm:unit-measure:query')")
    public CommonResult<List<MdmUnitMeasureDO>> getUnitMeasureListByIds(@RequestParam("ids") List<Long> ids) {
        return success(unitMeasureService.getUnitMeasureList(ids));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasurePageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.mdm.controller.admin.unit.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.LocalDateTime;
@Schema(description = "管理后台 - MDM è®¡é‡å•位分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MdmUnitMeasurePageReqVO extends PageParam {
    @Schema(description = "单位编码", example = "PCS")
    private String code;
    @Schema(description = "单位名称", example = "个")
    private String name;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "创建时间")
    private LocalDateTime[] createTime;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/unit/vo/MdmUnitMeasureSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mdm.controller.admin.unit.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - MDM è®¡é‡å•位新增/修改 Request VO")
@Data
public class MdmUnitMeasureSaveReqVO {
    @Schema(description = "单位编号", example = "1")
    private Long id;
    @Schema(description = "单位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "PCS")
    private String code;
    @Schema(description = "单位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "个")
    private String name;
    @Schema(description = "是否主单位", example = "true")
    private Boolean primaryFlag;
    @Schema(description = "主单位编号", example = "1")
    private Long primaryId;
    @Schema(description = "与主单位换算比例", example = "1.0")
    private BigDecimal changeRate;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/warehouse/MdmWarehouseController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
package cn.iocoder.yudao.module.mdm.controller.admin.warehouse;
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.mdm.controller.admin.warehouse.vo.*;
import cn.iocoder.yudao.module.mdm.dal.dataobject.warehouse.MdmWarehouseDO;
import cn.iocoder.yudao.module.mdm.service.warehouse.MdmWarehouseService;
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 java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MDM ä»“库")
@RestController
@RequestMapping("/mdm/warehouse")
@Validated
public class MdmWarehouseController {
    @Resource
    private MdmWarehouseService warehouseService;
    @PostMapping("/create")
    @Operation(summary = "创建仓库")
    @PreAuthorize("@ss.hasPermission('mdm:warehouse:create')")
    public CommonResult<Long> createWarehouse(@Valid @RequestBody MdmWarehouseSaveReqVO createReqVO) {
        return success(warehouseService.createWarehouse(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新仓库")
    @PreAuthorize("@ss.hasPermission('mdm:warehouse:update')")
    public CommonResult<Boolean> updateWarehouse(@Valid @RequestBody MdmWarehouseSaveReqVO updateReqVO) {
        warehouseService.updateWarehouse(updateReqVO);
        return success(true);
    }
    @PutMapping("/update-status")
    @Operation(summary = "更新仓库状态")
    @PreAuthorize("@ss.hasPermission('mdm:warehouse:update')")
    public CommonResult<Boolean> updateWarehouseStatus(@RequestParam("id") Long id, @RequestParam("status") Integer status) {
        warehouseService.updateWarehouseStatus(id, status);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除仓库")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mdm:warehouse:delete')")
    public CommonResult<Boolean> deleteWarehouse(@RequestParam("id") Long id) {
        warehouseService.deleteWarehouse(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得仓库")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
    @PreAuthorize("@ss.hasPermission('mdm:warehouse:query')")
    public CommonResult<MdmWarehouseRespVO> getWarehouse(@RequestParam("id") Long id) {
        MdmWarehouseDO warehouse = warehouseService.getWarehouse(id);
        return success(BeanUtils.toBean(warehouse, MdmWarehouseRespVO.class));
    }
    @GetMapping("/page")
    @Operation(summary = "获得仓库分页")
    @PreAuthorize("@ss.hasPermission('mdm:warehouse:query')")
    public CommonResult<PageResult<MdmWarehouseRespVO>> getWarehousePage(@Valid MdmWarehousePageReqVO pageReqVO) {
        PageResult<MdmWarehouseDO> pageResult = warehouseService.getWarehousePage(pageReqVO);
        return success(BeanUtils.toBean(pageResult, MdmWarehouseRespVO.class));
    }
    @GetMapping("/list-by-ids")
    @Operation(summary = "获得仓库列表")
    @Parameter(name = "ids", description = "编号列表", required = true, example = "1,2,3")
    @PreAuthorize("@ss.hasPermission('mdm:warehouse:query')")
    public CommonResult<List<MdmWarehouseRespVO>> getWarehouseList(@RequestParam("ids") List<Long> ids) {
        List<MdmWarehouseDO> list = warehouseService.getWarehouseList(ids);
        return success(BeanUtils.toBean(list, MdmWarehouseRespVO.class));
    }
    @GetMapping("/list-all-simple")
    @Operation(summary = "获得全部启用的仓库列表")
    public CommonResult<List<MdmWarehouseRespVO>> getSimpleWarehouseList() {
        MdmWarehouseDO query = new MdmWarehouseDO();
        query.setStatus(0);
        List<MdmWarehouseDO> list = warehouseService.getWarehousePage(new MdmWarehousePageReqVO()).getList();
        return success(BeanUtils.toBean(list, MdmWarehouseRespVO.class));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/warehouse/vo/MdmWarehousePageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mdm.controller.admin.warehouse.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.LocalDateTime;
@Schema(description = "管理后台 - MDM ä»“库分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MdmWarehousePageReqVO extends PageParam {
    @Schema(description = "仓库编码", example = "WH001")
    private String code;
    @Schema(description = "仓库名称", example = "成品仓库")
    private String name;
    @Schema(description = "仓库类型", example = "1")
    private Integer type;
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "创建时间")
    private LocalDateTime[] createTime;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/warehouse/vo/MdmWarehouseRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.mdm.controller.admin.warehouse.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MDM ä»“库 Response VO")
@Data
public class MdmWarehouseRespVO {
    @Schema(description = "仓库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long id;
    @Schema(description = "仓库编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "WH001")
    private String code;
    @Schema(description = "仓库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "成品仓库")
    private String name;
    @Schema(description = "仓库地址", example = "上海市浦东新区")
    private String address;
    @Schema(description = "面积(平方米)", example = "1000.00")
    private BigDecimal area;
    @Schema(description = "仓库类型", example = "1")
    private Integer type;
    @Schema(description = "负责人用户ID", example = "1")
    private Long chargeUserId;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "是否冻结", example = "false")
    private Boolean frozen;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/warehouse/vo/MdmWarehouseSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.mdm.controller.admin.warehouse.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - MDM ä»“库新增/修改 Request VO")
@Data
public class MdmWarehouseSaveReqVO {
    @Schema(description = "仓库编号", example = "1")
    private Long id;
    @Schema(description = "仓库编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "WH001")
    private String code;
    @Schema(description = "仓库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "成品仓库")
    private String name;
    @Schema(description = "仓库地址", example = "上海市浦东新区")
    private String address;
    @Schema(description = "面积(平方米)", example = "1000.00")
    private BigDecimal area;
    @Schema(description = "仓库类型", example = "1")
    private Integer type;
    @Schema(description = "负责人用户ID", example = "1")
    private Long chargeUserId;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    private Integer status;
    @Schema(description = "是否冻结", example = "false")
    private Boolean frozen;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/brand/MdmBrandDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.mdm.dal.dataobject.brand;
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.*;
/**
 * MDM å“ç‰Œ DO
 *
 * @author èŠ‹é“æºç 
 */
@TableName("mdm_brand")
@KeySequence("mdm_brand_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MdmBrandDO extends BaseDO {
    /**
     * å“ç‰Œç¼–号
     */
    @TableId
    private Long id;
    /**
     * å“ç‰Œç¼–码
     */
    private String code;
    /**
     * å“ç‰Œåç§°
     */
    private String name;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/category/MdmItemCategoryDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.mdm.dal.dataobject.category;
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.*;
/**
 * MDM ç‰©æ–™åˆ†ç±» DO
 *
 * @author èŠ‹é“æºç 
 */
@TableName("mdm_item_category")
@KeySequence("mdm_item_category_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MdmItemCategoryDO extends BaseDO {
    /**
     * åˆ†ç±»ç¼–号
     */
    @TableId
    private Long id;
    /**
     * çˆ¶åˆ†ç±»ç¼–号(根节点=0)
     */
    private Long parentId;
    /**
     * åˆ†ç±»ç¼–码
     */
    private String code;
    /**
     * åˆ†ç±»åç§°
     */
    private String name;
    /**
     * é€‚用物料类型
     *
     * 1: ç‰©æ–™
     * 2: äº§å“
     * 3: åŠæˆå“
     * NULL: å…¨éƒ¨
     */
    private Integer itemType;
    /**
     * æŽ’序
     */
    private Integer sort;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/item/MdmItemBatchConfigDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
package cn.iocoder.yudao.module.mdm.dal.dataobject.item;
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.*;
/**
 * MDM ç‰©æ–™æ‰¹æ¬¡å±žæ€§é…ç½® DO
 *
 * @author èŠ‹é“æºç 
 */
@TableName("mdm_item_batch_config")
@KeySequence("mdm_item_batch_config_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MdmItemBatchConfigDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * ç‰©æ–™ç¼–号
     *
     * å…³è” {@link MdmItemDO#getId()}
     */
    private Long itemId;
    /**
     * ç”Ÿäº§æ—¥æœŸ
     */
    private Boolean produceDateFlag;
    /**
     * æœ‰æ•ˆæœŸ
     */
    private Boolean expireDateFlag;
    /**
     * å…¥åº“日期
     */
    private Boolean receiptDateFlag;
    /**
     * ä¾›åº”商
     */
    private Boolean vendorFlag;
    /**
     * å®¢æˆ·
     */
    private Boolean clientFlag;
    /**
     * é”€å”®è®¢å•编号
     */
    private Boolean salesOrderCodeFlag;
    /**
     * é‡‡è´­è®¢å•编号
     */
    private Boolean purchaseOrderCodeFlag;
    /**
     * ç”Ÿäº§å·¥å•
     */
    private Boolean workOrderFlag;
    /**
     * ç”Ÿäº§ä»»åŠ¡
     */
    private Boolean taskFlag;
    /**
     * å·¥ä½œç«™
     */
    private Boolean workstationFlag;
    /**
     * å·¥å…·
     */
    private Boolean toolFlag;
    /**
     * æ¨¡å…·
     */
    private Boolean moldFlag;
    /**
     * ç”Ÿäº§æ‰¹å·
     */
    private Boolean lotNumberFlag;
    /**
     * è´¨é‡çŠ¶æ€
     */
    private Boolean qualityStatusFlag;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/item/MdmItemDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,176 @@
package cn.iocoder.yudao.module.mdm.dal.dataobject.item;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.brand.MdmBrandDO;
import cn.iocoder.yudao.module.mdm.enums.MdmItemTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
 * MDM ç‰©æ–™ä¸»æ•°æ® DO
 *
 * ç»Ÿä¸€ç‰©æ–™æ•°æ®ï¼ŒERP/MES/WMS å…±ç”¨
 *
 * @author èŠ‹é“æºç 
 */
@TableName("mdm_item")
@KeySequence("mdm_item_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MdmItemDO extends BaseDO {
    /**
     * ç‰©æ–™ID(全局唯一)
     */
    @TableId
    private Long id;
    /**
     * ç‰©æ–™ç¼–码(全局唯一)
     */
    private String code;
    /**
     * ç‰©æ–™åç§°
     */
    private String name;
    /**
     * ç‰©æ–™æ¡ç 
     */
    private String barCode;
    /**
     * è§„格型号
     */
    private String specification;
    /**
     * ç‰©æ–™åˆ†ç±»ç¼–号
     *
     * å…³è” {@link MdmItemCategoryDO#getId()}
     */
    private Long categoryId;
    /**
     * ä¸»è®¡é‡å•位编号
     *
     * å…³è” {@link MdmUnitMeasureDO#getId()}
     */
    private Long unitMeasureId;
    /**
     * å“ç‰Œç¼–号
     *
     * å…³è” {@link MdmBrandDO#getId()}
     */
    private Long brandId;
    /**
     * ç‰©æ–™ç±»åž‹
     *
     * æžšä¸¾ {@link MdmItemTypeEnum}
     * 1: ç‰©æ–™
     * 2: äº§å“
     * 3: åŠæˆå“
     */
    private Integer itemType;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
     */
    private Integer status;
    /**
     * é‡‡è´­ä»·
     */
    private BigDecimal purchasePrice;
    /**
     * é”€å”®ä»·
     */
    private BigDecimal salesPrice;
    /**
     * æˆæœ¬ä»·
     */
    private BigDecimal costPrice;
    /**
     * æœ€ä½Žä»·
     */
    private BigDecimal minPrice;
    /**
     * å®‰å…¨åº“å­˜
     */
    private BigDecimal safetyStock;
    /**
     * æœ€ä½Žåº“存量
     */
    private BigDecimal minStock;
    /**
     * æœ€é«˜åº“存量
     */
    private BigDecimal maxStock;
    /**
     * æœ€å°è®¢è´§é‡
     */
    private BigDecimal minOrderQty;
    /**
     * é‡‡è´­æå‰æœŸï¼ˆå¤©ï¼‰
     */
    private Integer leadTime;
    /**
     * æ˜¯å¦æ‰¹æ¬¡ç®¡ç†
     */
    private Boolean isBatchManaged;
    /**
     * æ˜¯å¦åºåˆ—号管理
     */
    private Boolean isSerialManaged;
    /**
     * æ¡ç è§„则ID
     */
    private Long barcodeRuleId;
    /**
     * ä¿è´¨æœŸå¤©æ•°
     */
    private Integer expiryDay;
    /**
     * æ˜¯å¦é«˜å€¼ç‰©æ–™
     */
    private Boolean highValue;
    /**
     * é‡é‡(kg)
     */
    private BigDecimal weight;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/item/MdmItemSkuDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,99 @@
package cn.iocoder.yudao.module.mdm.dal.dataobject.item;
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.*;
import java.math.BigDecimal;
/**
 * MDM ç‰©æ–™ SKU DO
 *
 * @author èŠ‹é“æºç 
 */
@TableName("mdm_item_sku")
@KeySequence("mdm_item_sku_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MdmItemSkuDO extends BaseDO {
    /**
     * SKU ç¼–号
     */
    @TableId
    private Long id;
    /**
     * ç‰©æ–™ç¼–号
     */
    private Long itemId;
    /**
     * SKU ç¼–码
     */
    private String code;
    /**
     * SKU åç§°
     */
    private String name;
    /**
     * SKU æ¡ç 
     */
    private String barCode;
    /**
     * é•¿åº¦ï¼ˆcm)
     */
    private BigDecimal length;
    /**
     * å®½åº¦ï¼ˆcm)
     */
    private BigDecimal width;
    /**
     * é«˜åº¦ï¼ˆcm)
     */
    private BigDecimal height;
    /**
     * æ¯›é‡ï¼ˆkg)
     */
    private BigDecimal grossWeight;
    /**
     * å‡€é‡ï¼ˆkg)
     */
    private BigDecimal netWeight;
    /**
     * æˆæœ¬ä»·
     */
    private BigDecimal costPrice;
    /**
     * é”€å”®ä»·
     */
    private BigDecimal salePrice;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/unit/MdmUnitMeasureDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
package cn.iocoder.yudao.module.mdm.dal.dataobject.unit;
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.*;
import java.math.BigDecimal;
/**
 * MDM è®¡é‡å•位 DO
 *
 * @author èŠ‹é“æºç 
 */
@TableName("mdm_unit_measure")
@KeySequence("mdm_unit_measure_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MdmUnitMeasureDO extends BaseDO {
    /**
     * å•位编号
     */
    @TableId
    private Long id;
    /**
     * å•位编码
     */
    private String code;
    /**
     * å•位名称
     */
    private String name;
    /**
     * æ˜¯å¦ä¸»å•位
     */
    private Boolean primaryFlag;
    /**
     * ä¸»å•位编号
     */
    private Long primaryId;
    /**
     * ä¸Žä¸»å•位换算比例
     */
    private BigDecimal changeRate;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/dataobject/warehouse/MdmWarehouseDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.mdm.dal.dataobject.warehouse;
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.*;
import java.math.BigDecimal;
/**
 * MDM ä»“库主数据 DO
 *
 * @author èŠ‹é“æºç 
 */
@TableName("mdm_warehouse")
@KeySequence("mdm_warehouse_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MdmWarehouseDO extends BaseDO {
    /**
     * ä»“库ID
     */
    @TableId
    private Long id;
    /**
     * ä»“库编码
     */
    private String code;
    /**
     * ä»“库名称
     */
    private String name;
    /**
     * ä»“库地址
     */
    private String address;
    /**
     * é¢ç§¯ï¼ˆå¹³æ–¹ç±³ï¼‰
     */
    private BigDecimal area;
    /**
     * ä»“库类型
     *
     * 1: åŽŸæ–™ä»“
     * 2: æˆå“ä»“
     * 3: åŠæˆå“ä»“
     * 4: çº¿è¾¹ä»“
     */
    private Integer type;
    /**
     * è´Ÿè´£äººç”¨æˆ·ID
     */
    private Long chargeUserId;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
     */
    private Integer status;
    /**
     * æ˜¯å¦å†»ç»“
     */
    private Boolean frozen;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/brand/MdmBrandMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.mdm.dal.mysql.brand;
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.mdm.controller.admin.brand.vo.MdmBrandPageReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.brand.MdmBrandDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MDM å“ç‰Œ Mapper
 *
 * @author èŠ‹é“æºç 
 */
@Mapper
public interface MdmBrandMapper extends BaseMapperX<MdmBrandDO> {
    default PageResult<MdmBrandDO> selectPage(MdmBrandPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MdmBrandDO>()
                .likeIfPresent(MdmBrandDO::getCode, reqVO.getCode())
                .likeIfPresent(MdmBrandDO::getName, reqVO.getName())
                .eqIfPresent(MdmBrandDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MdmBrandDO::getCreateTime, reqVO.getCreateTime())
                .orderByDesc(MdmBrandDO::getId));
    }
    default List<MdmBrandDO> selectList(MdmBrandDO reqVO) {
        return selectList(new LambdaQueryWrapperX<MdmBrandDO>()
                .likeIfPresent(MdmBrandDO::getCode, reqVO.getCode())
                .likeIfPresent(MdmBrandDO::getName, reqVO.getName())
                .eqIfPresent(MdmBrandDO::getStatus, reqVO.getStatus())
                .orderByDesc(MdmBrandDO::getId));
    }
    default MdmBrandDO selectByCode(String code) {
        return selectOne(MdmBrandDO::getCode, code);
    }
    default MdmBrandDO selectByName(String name) {
        return selectOne(MdmBrandDO::getName, name);
    }
    default List<MdmBrandDO> selectListByIds(Collection<Long> ids) {
        return selectList(MdmBrandDO::getId, ids);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/category/MdmItemCategoryMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.mdm.dal.mysql.category;
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.mdm.controller.admin.category.vo.MdmItemCategoryPageReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MDM ç‰©æ–™åˆ†ç±» Mapper
 *
 * @author èŠ‹é“æºç 
 */
@Mapper
public interface MdmItemCategoryMapper extends BaseMapperX<MdmItemCategoryDO> {
    default PageResult<MdmItemCategoryDO> selectPage(MdmItemCategoryPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MdmItemCategoryDO>()
                .likeIfPresent(MdmItemCategoryDO::getCode, reqVO.getCode())
                .likeIfPresent(MdmItemCategoryDO::getName, reqVO.getName())
                .eqIfPresent(MdmItemCategoryDO::getParentId, reqVO.getParentId())
                .eqIfPresent(MdmItemCategoryDO::getItemType, reqVO.getItemType())
                .eqIfPresent(MdmItemCategoryDO::getStatus, reqVO.getStatus())
                .orderByAsc(MdmItemCategoryDO::getSort));
    }
    default List<MdmItemCategoryDO> selectList(MdmItemCategoryDO reqVO) {
        return selectList(new LambdaQueryWrapperX<MdmItemCategoryDO>()
                .likeIfPresent(MdmItemCategoryDO::getCode, reqVO.getCode())
                .likeIfPresent(MdmItemCategoryDO::getName, reqVO.getName())
                .eqIfPresent(MdmItemCategoryDO::getParentId, reqVO.getParentId())
                .eqIfPresent(MdmItemCategoryDO::getItemType, reqVO.getItemType())
                .eqIfPresent(MdmItemCategoryDO::getStatus, reqVO.getStatus())
                .orderByAsc(MdmItemCategoryDO::getSort));
    }
    default MdmItemCategoryDO selectByCode(String code) {
        return selectOne(MdmItemCategoryDO::getCode, code);
    }
    default MdmItemCategoryDO selectByParentIdAndName(Long parentId, String name) {
        return selectOne(new LambdaQueryWrapperX<MdmItemCategoryDO>()
                .eq(MdmItemCategoryDO::getParentId, parentId)
                .eq(MdmItemCategoryDO::getName, name));
    }
    default List<MdmItemCategoryDO> selectListByParentId(Long parentId) {
        return selectList(MdmItemCategoryDO::getParentId, parentId);
    }
    default Long selectCountByParentId(Long parentId) {
        return selectCount(MdmItemCategoryDO::getParentId, parentId);
    }
    default List<MdmItemCategoryDO> selectListByIds(Collection<Long> ids) {
        return selectList(MdmItemCategoryDO::getId, ids);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/item/MdmItemBatchConfigMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mdm.dal.mysql.item;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO;
import org.apache.ibatis.annotations.Mapper;
/**
 * MDM ç‰©æ–™æ‰¹æ¬¡å±žæ€§é…ç½® Mapper
 *
 * @author èŠ‹é“æºç 
 */
@Mapper
public interface MdmItemBatchConfigMapper extends BaseMapperX<MdmItemBatchConfigDO> {
    default MdmItemBatchConfigDO selectByItemId(Long itemId) {
        return selectOne(MdmItemBatchConfigDO::getItemId, itemId);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/item/MdmItemMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
package cn.iocoder.yudao.module.mdm.dal.mysql.item;
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.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MDM ç‰©æ–™ä¸»æ•°æ® Mapper
 *
 * @author èŠ‹é“æºç 
 */
@Mapper
public interface MdmItemMapper extends BaseMapperX<MdmItemDO> {
    default PageResult<MdmItemDO> selectPage(MdmItemPageReqVO reqVO, Collection<Long> categoryIds) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MdmItemDO>()
                .likeIfPresent(MdmItemDO::getCode, reqVO.getCode())
                .likeIfPresent(MdmItemDO::getName, reqVO.getName())
                .likeIfPresent(MdmItemDO::getBarCode, reqVO.getBarCode())
                .inIfPresent(MdmItemDO::getCategoryId, categoryIds)
                .eqIfPresent(MdmItemDO::getItemType, reqVO.getItemType())
                .eqIfPresent(MdmItemDO::getBrandId, reqVO.getBrandId())
                .eqIfPresent(MdmItemDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MdmItemDO::getCreateTime, reqVO.getCreateTime())
                .orderByDesc(MdmItemDO::getId));
    }
    default List<MdmItemDO> selectList(MdmItemPageReqVO reqVO, Collection<Long> categoryIds) {
        return selectList(new LambdaQueryWrapperX<MdmItemDO>()
                .likeIfPresent(MdmItemDO::getCode, reqVO.getCode())
                .likeIfPresent(MdmItemDO::getName, reqVO.getName())
                .inIfPresent(MdmItemDO::getCategoryId, categoryIds)
                .eqIfPresent(MdmItemDO::getItemType, reqVO.getItemType())
                .eqIfPresent(MdmItemDO::getStatus, reqVO.getStatus())
                .orderByDesc(MdmItemDO::getId));
    }
    default MdmItemDO selectByCode(String code) {
        return selectOne(MdmItemDO::getCode, code);
    }
    default MdmItemDO selectByBarCode(String barCode) {
        return selectOne(MdmItemDO::getBarCode, barCode);
    }
    default Long selectCountByCategoryId(Long categoryId) {
        return selectCount(MdmItemDO::getCategoryId, categoryId);
    }
    default Long selectCountByUnitMeasureId(Long unitMeasureId) {
        return selectCount(MdmItemDO::getUnitMeasureId, unitMeasureId);
    }
    default Long selectCountByBrandId(Long brandId) {
        return selectCount(MdmItemDO::getBrandId, brandId);
    }
    default List<MdmItemDO> selectListByIds(Collection<Long> ids) {
        return selectList(MdmItemDO::getId, ids);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/item/MdmItemSkuMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mdm.dal.mysql.item;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemSkuDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MDM ç‰©æ–™ SKU Mapper
 *
 * @author èŠ‹é“æºç 
 */
@Mapper
public interface MdmItemSkuMapper extends BaseMapperX<MdmItemSkuDO> {
    default MdmItemSkuDO selectByCode(String code) {
        return selectOne(MdmItemSkuDO::getCode, code);
    }
    default MdmItemSkuDO selectByBarCode(String barCode) {
        return selectOne(MdmItemSkuDO::getBarCode, barCode);
    }
    default List<MdmItemSkuDO> selectListByItemId(Long itemId) {
        return selectList(new LambdaQueryWrapperX<MdmItemSkuDO>()
                .eq(MdmItemSkuDO::getItemId, itemId)
                .orderByAsc(MdmItemSkuDO::getId));
    }
    default List<MdmItemSkuDO> selectListByItemIds(Collection<Long> itemIds) {
        return selectList(MdmItemSkuDO::getItemId, itemIds);
    }
    default int deleteByItemId(Long itemId) {
        return delete(MdmItemSkuDO::getItemId, itemId);
    }
    default long selectCountByItemId(Long itemId) {
        return selectCount(MdmItemSkuDO::getItemId, itemId);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/unit/MdmUnitMeasureMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.mdm.dal.mysql.unit;
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.mdm.controller.admin.unit.vo.MdmUnitMeasurePageReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MDM è®¡é‡å•位 Mapper
 *
 * @author èŠ‹é“æºç 
 */
@Mapper
public interface MdmUnitMeasureMapper extends BaseMapperX<MdmUnitMeasureDO> {
    default PageResult<MdmUnitMeasureDO> selectPage(MdmUnitMeasurePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MdmUnitMeasureDO>()
                .likeIfPresent(MdmUnitMeasureDO::getCode, reqVO.getCode())
                .likeIfPresent(MdmUnitMeasureDO::getName, reqVO.getName())
                .eqIfPresent(MdmUnitMeasureDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MdmUnitMeasureDO::getCreateTime, reqVO.getCreateTime())
                .orderByDesc(MdmUnitMeasureDO::getId));
    }
    default List<MdmUnitMeasureDO> selectList(MdmUnitMeasureDO reqVO) {
        return selectList(new LambdaQueryWrapperX<MdmUnitMeasureDO>()
                .likeIfPresent(MdmUnitMeasureDO::getCode, reqVO.getCode())
                .likeIfPresent(MdmUnitMeasureDO::getName, reqVO.getName())
                .eqIfPresent(MdmUnitMeasureDO::getStatus, reqVO.getStatus())
                .orderByDesc(MdmUnitMeasureDO::getId));
    }
    default MdmUnitMeasureDO selectByCode(String code) {
        return selectOne(MdmUnitMeasureDO::getCode, code);
    }
    default MdmUnitMeasureDO selectByName(String name) {
        return selectOne(MdmUnitMeasureDO::getName, name);
    }
    default List<MdmUnitMeasureDO> selectListByIds(Collection<Long> ids) {
        return selectList(MdmUnitMeasureDO::getId, ids);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/dal/mysql/warehouse/MdmWarehouseMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.mdm.dal.mysql.warehouse;
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.mdm.controller.admin.warehouse.vo.MdmWarehousePageReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.warehouse.MdmWarehouseDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
 * MDM ä»“库 Mapper
 *
 * @author èŠ‹é“æºç 
 */
@Mapper
public interface MdmWarehouseMapper extends BaseMapperX<MdmWarehouseDO> {
    default PageResult<MdmWarehouseDO> selectPage(MdmWarehousePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MdmWarehouseDO>()
                .likeIfPresent(MdmWarehouseDO::getCode, reqVO.getCode())
                .likeIfPresent(MdmWarehouseDO::getName, reqVO.getName())
                .eqIfPresent(MdmWarehouseDO::getType, reqVO.getType())
                .eqIfPresent(MdmWarehouseDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MdmWarehouseDO::getCreateTime, reqVO.getCreateTime())
                .orderByDesc(MdmWarehouseDO::getId));
    }
    default List<MdmWarehouseDO> selectList(MdmWarehouseDO reqVO) {
        return selectList(new LambdaQueryWrapperX<MdmWarehouseDO>()
                .likeIfPresent(MdmWarehouseDO::getCode, reqVO.getCode())
                .likeIfPresent(MdmWarehouseDO::getName, reqVO.getName())
                .eqIfPresent(MdmWarehouseDO::getType, reqVO.getType())
                .eqIfPresent(MdmWarehouseDO::getStatus, reqVO.getStatus())
                .orderByDesc(MdmWarehouseDO::getId));
    }
    default MdmWarehouseDO selectByCode(String code) {
        return selectOne(MdmWarehouseDO::getCode, code);
    }
    default MdmWarehouseDO selectByName(String name) {
        return selectOne(MdmWarehouseDO::getName, name);
    }
    default List<MdmWarehouseDO> selectListByIds(Collection<Long> ids) {
        return selectList(MdmWarehouseDO::getId, ids);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/enums/ErrorCodeConstants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.mdm.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
 * MDM é”™è¯¯ç æžšä¸¾ç±»
 *
 * mdm æ¨¡å—,使用 1-050-000 æ®µ
 */
public interface ErrorCodeConstants {
    // ========== ç‰©æ–™ç›¸å…³ 1-050-001 ~ 1-050-050 ==========
    ErrorCode MDM_ITEM_NOT_EXISTS = new ErrorCode(1_050_001, "物料不存在");
    ErrorCode MDM_ITEM_IS_DISABLE = new ErrorCode(1_050_002, "物料已禁用");
    ErrorCode MDM_ITEM_CODE_DUPLICATE = new ErrorCode(1_050_003, "物料编码已存在");
    ErrorCode MDM_ITEM_SKU_NOT_EXISTS = new ErrorCode(1_050_004, "物料SKU不存在");
    ErrorCode MDM_ITEM_SKU_IS_DISABLE = new ErrorCode(1_050_005, "物料SKU已禁用");
    ErrorCode MDM_ITEM_SKU_CODE_DUPLICATE = new ErrorCode(1_050_006, "SKU编码已存在");
    // ========== ç‰©æ–™åˆ†ç±»ç›¸å…³ 1-050-051 ~ 1-050-100 ==========
    ErrorCode MDM_ITEM_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_051, "物料分类不存在");
    ErrorCode MDM_ITEM_CATEGORY_CODE_DUPLICATE = new ErrorCode(1_050_052, "物料分类编码已存在");
    ErrorCode MDM_ITEM_CATEGORY_NAME_DUPLICATE = new ErrorCode(1_050_053, "物料分类名称已存在");
    ErrorCode MDM_ITEM_CATEGORY_PARENT_NOT_EXISTS = new ErrorCode(1_050_054, "父分类不存在");
    ErrorCode MDM_ITEM_CATEGORY_EXISTS_CHILDREN = new ErrorCode(1_050_055, "存在子分类,无法删除");
    ErrorCode MDM_ITEM_CATEGORY_EXISTS_ITEM = new ErrorCode(1_050_056, "分类下存在物料,无法删除");
    // ========== è®¡é‡å•位相关 1-050-101 ~ 1-050-150 ==========
    ErrorCode MDM_UNIT_MEASURE_NOT_EXISTS = new ErrorCode(1_050_101, "计量单位不存在");
    ErrorCode MDM_UNIT_MEASURE_CODE_DUPLICATE = new ErrorCode(1_050_102, "计量单位编码已存在");
    ErrorCode MDM_UNIT_MEASURE_NAME_DUPLICATE = new ErrorCode(1_050_103, "计量单位名称已存在");
    ErrorCode MDM_UNIT_MEASURE_EXISTS_ITEM = new ErrorCode(1_050_104, "计量单位被物料使用,无法删除");
    // ========== å“ç‰Œç›¸å…³ 1-050-151 ~ 1-050-200 ==========
    ErrorCode MDM_BRAND_NOT_EXISTS = new ErrorCode(1_050_151, "品牌不存在");
    ErrorCode MDM_BRAND_CODE_DUPLICATE = new ErrorCode(1_050_152, "品牌编码已存在");
    ErrorCode MDM_BRAND_NAME_DUPLICATE = new ErrorCode(1_050_153, "品牌名称已存在");
    ErrorCode MDM_BRAND_EXISTS_ITEM = new ErrorCode(1_050_154, "品牌被物料使用,无法删除");
    // ========== ä»“库相关 1-050-201 ~ 1-050-250 ==========
    ErrorCode MDM_WAREHOUSE_NOT_EXISTS = new ErrorCode(1_050_201, "仓库不存在");
    ErrorCode MDM_WAREHOUSE_CODE_DUPLICATE = new ErrorCode(1_050_202, "仓库编码已存在");
    ErrorCode MDM_WAREHOUSE_NAME_DUPLICATE = new ErrorCode(1_050_203, "仓库名称已存在");
    ErrorCode MDM_WAREHOUSE_IS_DISABLE = new ErrorCode(1_050_204, "仓库已禁用");
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/enums/MdmItemTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.mdm.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
 * MDM ç‰©æ–™ç±»åž‹æžšä¸¾
 *
 * @author èŠ‹é“æºç 
 */
@Getter
@AllArgsConstructor
public enum MdmItemTypeEnum {
    MATERIAL(1, "物料"),
    PRODUCT(2, "产品"),
    SEMI_FINISHED(3, "半成品");
    /**
     * ç±»åž‹ç¼–码
     */
    private final Integer type;
    /**
     * ç±»åž‹åç§°
     */
    private final String name;
    /**
     * æ ¹æ®ç±»åž‹ç¼–码获取枚举
     */
    public static MdmItemTypeEnum fromType(Integer type) {
        for (MdmItemTypeEnum value : values()) {
            if (value.getType().equals(type)) {
                return value;
            }
        }
        return null;
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/brand/MdmBrandService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package cn.iocoder.yudao.module.mdm.service.brand;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.controller.admin.brand.vo.MdmBrandPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.brand.vo.MdmBrandSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.brand.MdmBrandDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * MDM å“ç‰Œ Service æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmBrandService {
    /**
     * åˆ›å»ºå“ç‰Œ
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createBrand(@Valid MdmBrandSaveReqVO createReqVO);
    /**
     * æ›´æ–°å“ç‰Œ
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateBrand(@Valid MdmBrandSaveReqVO updateReqVO);
    /**
     * æ›´æ–°å“ç‰ŒçŠ¶æ€
     *
     * @param id     ç¼–号
     * @param status çŠ¶æ€
     */
    void updateBrandStatus(Long id, Integer status);
    /**
     * åˆ é™¤å“ç‰Œ
     *
     * @param id ç¼–号
     */
    void deleteBrand(Long id);
    /**
     * èŽ·å¾—å“ç‰Œ
     *
     * @param id ç¼–号
     * @return å“ç‰Œ
     */
    MdmBrandDO getBrand(Long id);
    /**
     * æ ¡éªŒå“ç‰Œå­˜åœ¨
     *
     * @param id ç¼–号
     * @return å“ç‰Œ
     */
    MdmBrandDO validateBrandExists(Long id);
    /**
     * èŽ·å¾—å“ç‰Œåˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return å“ç‰Œåˆ†é¡µ
     */
    PageResult<MdmBrandDO> getBrandPage(MdmBrandPageReqVO pageReqVO);
    /**
     * èŽ·å¾—å“ç‰Œåˆ—è¡¨
     *
     * @param ids ç¼–号数组
     * @return å“ç‰Œåˆ—表
     */
    List<MdmBrandDO> getBrandList(Collection<Long> ids);
    /**
     * èŽ·å¾—å“ç‰Œ Map
     *
     * @param ids ç¼–号数组
     * @return å“ç‰Œ Map
     */
    default Map<Long, MdmBrandDO> getBrandMap(Collection<Long> ids) {
        return convertMap(getBrandList(ids), MdmBrandDO::getId);
    }
    /**
     * æ ¹æ®ç¼–码获取品牌
     *
     * @param code å“ç‰Œç¼–码
     * @return å“ç‰Œ
     */
    MdmBrandDO getBrandByCode(String code);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/brand/MdmBrandServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
package cn.iocoder.yudao.module.mdm.service.brand;
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.mdm.controller.admin.brand.vo.MdmBrandPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.brand.vo.MdmBrandSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.brand.MdmBrandDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.brand.MdmBrandMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mdm.enums.ErrorCodeConstants.*;
/**
 * MDM å“ç‰Œ Service å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
public class MdmBrandServiceImpl implements MdmBrandService {
    @Resource
    private MdmBrandMapper brandMapper;
    @Resource
    private MdmItemMapper itemMapper;
    @Override
    public Long createBrand(MdmBrandSaveReqVO createReqVO) {
        // æ ¡éªŒæ•°æ®
        validateBrandSaveData(createReqVO);
        // æ’å…¥
        MdmBrandDO brand = BeanUtils.toBean(createReqVO, MdmBrandDO.class);
        brandMapper.insert(brand);
        return brand.getId();
    }
    @Override
    public void updateBrand(MdmBrandSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateBrandExists(updateReqVO.getId());
        // æ ¡éªŒæ•°æ®
        validateBrandSaveData(updateReqVO);
        // æ›´æ–°
        MdmBrandDO updateObj = BeanUtils.toBean(updateReqVO, MdmBrandDO.class);
        brandMapper.updateById(updateObj);
    }
    @Override
    public void updateBrandStatus(Long id, Integer status) {
        // æ ¡éªŒå­˜åœ¨
        validateBrandExists(id);
        // æ›´æ–°çŠ¶æ€
        brandMapper.updateById(new MdmBrandDO().setId(id).setStatus(status));
    }
    @Override
    public void deleteBrand(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateBrandExists(id);
        // æ ¡éªŒæ˜¯å¦è¢«ç‰©æ–™ä½¿ç”¨ï¼ˆç›´æŽ¥ä½¿ç”¨ Mapper é¿å…å¾ªçŽ¯ä¾èµ–ï¼‰
        if (itemMapper.selectCountByBrandId(id) > 0) {
            throw exception(MDM_BRAND_EXISTS_ITEM);
        }
        // åˆ é™¤
        brandMapper.deleteById(id);
    }
    @Override
    public MdmBrandDO validateBrandExists(Long id) {
        MdmBrandDO brand = brandMapper.selectById(id);
        if (brand == null) {
            throw exception(MDM_BRAND_NOT_EXISTS);
        }
        return brand;
    }
    private void validateBrandSaveData(MdmBrandSaveReqVO reqVO) {
        // æ ¡éªŒç¼–码唯一
        MdmBrandDO brand = brandMapper.selectByCode(reqVO.getCode());
        if (brand != null && ObjUtil.notEqual(brand.getId(), reqVO.getId())) {
            throw exception(MDM_BRAND_CODE_DUPLICATE);
        }
        // æ ¡éªŒåç§°å”¯ä¸€
        MdmBrandDO sameName = brandMapper.selectByName(reqVO.getName());
        if (sameName != null && ObjUtil.notEqual(sameName.getId(), reqVO.getId())) {
            throw exception(MDM_BRAND_NAME_DUPLICATE);
        }
    }
    @Override
    public MdmBrandDO getBrand(Long id) {
        return brandMapper.selectById(id);
    }
    @Override
    public PageResult<MdmBrandDO> getBrandPage(MdmBrandPageReqVO pageReqVO) {
        return brandMapper.selectPage(pageReqVO);
    }
    @Override
    public List<MdmBrandDO> getBrandList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return brandMapper.selectBatchIds(ids);
    }
    @Override
    public MdmBrandDO getBrandByCode(String code) {
        return brandMapper.selectByCode(code);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
package cn.iocoder.yudao.module.mdm.service.category;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.controller.admin.category.vo.MdmItemCategoryPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.category.vo.MdmItemCategorySaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * MDM ç‰©æ–™åˆ†ç±» Service æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmItemCategoryService {
    /**
     * åˆ›å»ºç‰©æ–™åˆ†ç±»
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createItemCategory(@Valid MdmItemCategorySaveReqVO createReqVO);
    /**
     * æ›´æ–°ç‰©æ–™åˆ†ç±»
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateItemCategory(@Valid MdmItemCategorySaveReqVO updateReqVO);
    /**
     * æ›´æ–°ç‰©æ–™åˆ†ç±»çŠ¶æ€
     *
     * @param id     ç¼–号
     * @param status çŠ¶æ€
     */
    void updateItemCategoryStatus(Long id, Integer status);
    /**
     * åˆ é™¤ç‰©æ–™åˆ†ç±»
     *
     * @param id ç¼–号
     */
    void deleteItemCategory(Long id);
    /**
     * èŽ·å¾—ç‰©æ–™åˆ†ç±»
     *
     * @param id ç¼–号
     * @return ç‰©æ–™åˆ†ç±»
     */
    MdmItemCategoryDO getItemCategory(Long id);
    /**
     * æ ¡éªŒç‰©æ–™åˆ†ç±»å­˜åœ¨
     *
     * @param id ç¼–号
     * @return ç‰©æ–™åˆ†ç±»
     */
    MdmItemCategoryDO validateItemCategoryExists(Long id);
    /**
     * èŽ·å¾—ç‰©æ–™åˆ†ç±»åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return ç‰©æ–™åˆ†ç±»åˆ†é¡µ
     */
    PageResult<MdmItemCategoryDO> getItemCategoryPage(MdmItemCategoryPageReqVO pageReqVO);
    /**
     * èŽ·å¾—ç‰©æ–™åˆ†ç±»åˆ—è¡¨
     *
     * @param ids ç¼–号数组
     * @return ç‰©æ–™åˆ†ç±»åˆ—表
     */
    List<MdmItemCategoryDO> getItemCategoryList(Collection<Long> ids);
    /**
     * èŽ·å¾—ç‰©æ–™åˆ†ç±» Map
     *
     * @param ids ç¼–号数组
     * @return ç‰©æ–™åˆ†ç±» Map
     */
    default Map<Long, MdmItemCategoryDO> getItemCategoryMap(Collection<Long> ids) {
        return convertMap(getItemCategoryList(ids), MdmItemCategoryDO::getId);
    }
    /**
     * èŽ·å¾—æŒ‡å®šåˆ†ç±»çš„å­åˆ†ç±»åˆ—è¡¨ï¼ˆåŒ…å«è‡ªå·±ï¼‰
     *
     * @param id åˆ†ç±»ç¼–号
     * @return å­åˆ†ç±»åˆ—表
     */
    List<MdmItemCategoryDO> getItemCategoryChildrenList(Long id);
    /**
     * èŽ·å¾—å¯ç”¨çš„åˆ†ç±»åˆ—è¡¨
     *
     * @return åˆ†ç±»åˆ—表
     */
    List<MdmItemCategoryDO> getSimpleItemList();
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/category/MdmItemCategoryServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,158 @@
package cn.iocoder.yudao.module.mdm.service.category;
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.mdm.controller.admin.category.vo.MdmItemCategoryPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.category.vo.MdmItemCategorySaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.category.MdmItemCategoryMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mdm.enums.ErrorCodeConstants.*;
/**
 * MDM ç‰©æ–™åˆ†ç±» Service å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
public class MdmItemCategoryServiceImpl implements MdmItemCategoryService {
    @Resource
    private MdmItemCategoryMapper itemCategoryMapper;
    @Resource
    private MdmItemMapper itemMapper;
    @Override
    public Long createItemCategory(MdmItemCategorySaveReqVO createReqVO) {
        // æ ¡éªŒæ•°æ®
        validateItemCategorySaveData(createReqVO);
        // æ’å…¥
        MdmItemCategoryDO itemCategory = BeanUtils.toBean(createReqVO, MdmItemCategoryDO.class);
        itemCategoryMapper.insert(itemCategory);
        return itemCategory.getId();
    }
    @Override
    public void updateItemCategory(MdmItemCategorySaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateItemCategoryExists(updateReqVO.getId());
        // æ ¡éªŒæ•°æ®
        validateItemCategorySaveData(updateReqVO);
        // æ›´æ–°
        MdmItemCategoryDO updateObj = BeanUtils.toBean(updateReqVO, MdmItemCategoryDO.class);
        itemCategoryMapper.updateById(updateObj);
    }
    @Override
    public void updateItemCategoryStatus(Long id, Integer status) {
        // æ ¡éªŒå­˜åœ¨
        validateItemCategoryExists(id);
        // æ›´æ–°çŠ¶æ€
        itemCategoryMapper.updateById(new MdmItemCategoryDO().setId(id).setStatus(status));
    }
    @Override
    public void deleteItemCategory(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateItemCategoryExists(id);
        // æ ¡éªŒæ˜¯å¦æœ‰å­åˆ†ç±»
        if (itemCategoryMapper.selectCountByParentId(id) > 0) {
            throw exception(MDM_ITEM_CATEGORY_EXISTS_CHILDREN);
        }
        // æ ¡éªŒåˆ†ç±»ä¸‹æ˜¯å¦æœ‰ç‰©æ–™ï¼ˆç›´æŽ¥ä½¿ç”¨ Mapper é¿å…å¾ªçŽ¯ä¾èµ–ï¼‰
        if (itemMapper.selectCountByCategoryId(id) > 0) {
            throw exception(MDM_ITEM_CATEGORY_EXISTS_ITEM);
        }
        // åˆ é™¤
        itemCategoryMapper.deleteById(id);
    }
    @Override
    public MdmItemCategoryDO validateItemCategoryExists(Long id) {
        MdmItemCategoryDO itemCategory = itemCategoryMapper.selectById(id);
        if (itemCategory == null) {
            throw exception(MDM_ITEM_CATEGORY_NOT_EXISTS);
        }
        return itemCategory;
    }
    private void validateItemCategorySaveData(MdmItemCategorySaveReqVO reqVO) {
        // æ ¡éªŒç¼–码唯一
        MdmItemCategoryDO category = itemCategoryMapper.selectByCode(reqVO.getCode());
        if (category != null && ObjUtil.notEqual(category.getId(), reqVO.getId())) {
            throw exception(MDM_ITEM_CATEGORY_CODE_DUPLICATE);
        }
        // æ ¡éªŒåŒä¸€çˆ¶çº§ä¸‹åç§°å”¯ä¸€
        MdmItemCategoryDO sameName = itemCategoryMapper.selectByParentIdAndName(
                reqVO.getParentId() != null ? reqVO.getParentId() : 0L, reqVO.getName());
        if (sameName != null && ObjUtil.notEqual(sameName.getId(), reqVO.getId())) {
            throw exception(MDM_ITEM_CATEGORY_NAME_DUPLICATE);
        }
        // æ ¡éªŒçˆ¶åˆ†ç±»å­˜åœ¨
        if (reqVO.getParentId() != null && reqVO.getParentId() > 0) {
            if (itemCategoryMapper.selectById(reqVO.getParentId()) == null) {
                throw exception(MDM_ITEM_CATEGORY_PARENT_NOT_EXISTS);
            }
        }
    }
    @Override
    public MdmItemCategoryDO getItemCategory(Long id) {
        return itemCategoryMapper.selectById(id);
    }
    @Override
    public PageResult<MdmItemCategoryDO> getItemCategoryPage(MdmItemCategoryPageReqVO pageReqVO) {
        return itemCategoryMapper.selectPage(pageReqVO);
    }
    @Override
    public List<MdmItemCategoryDO> getItemCategoryList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return itemCategoryMapper.selectBatchIds(ids);
    }
    @Override
    public List<MdmItemCategoryDO> getItemCategoryChildrenList(Long id) {
        List<MdmItemCategoryDO> result = new ArrayList<>();
        // æ·»åŠ è‡ªå·±
        MdmItemCategoryDO self = itemCategoryMapper.selectById(id);
        if (self != null) {
            result.add(self);
        }
        // é€’归获取所有子分类
        loadChildren(id, result);
        return result;
    }
    private void loadChildren(Long parentId, List<MdmItemCategoryDO> result) {
        List<MdmItemCategoryDO> children = itemCategoryMapper.selectListByParentId(parentId);
        if (CollUtil.isNotEmpty(children)) {
            result.addAll(children);
            for (MdmItemCategoryDO child : children) {
                loadChildren(child.getId(), result);
            }
        }
    }
    @Override
    public List<MdmItemCategoryDO> getSimpleItemList() {
        return itemCategoryMapper.selectList(new MdmItemCategoryDO().setStatus(0));
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
package cn.iocoder.yudao.module.mdm.service.item;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * MDM ç‰©æ–™ä¸»æ•°æ® Service æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmItemService {
    /**
     * åˆ›å»ºç‰©æ–™
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createItem(@Valid MdmItemSaveReqVO createReqVO);
    /**
     * æ›´æ–°ç‰©æ–™
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateItem(@Valid MdmItemSaveReqVO updateReqVO);
    /**
     * æ›´æ–°ç‰©æ–™çŠ¶æ€
     *
     * @param id     ç¼–号
     * @param status çŠ¶æ€
     */
    void updateItemStatus(Long id, Integer status);
    /**
     * åˆ é™¤ç‰©æ–™
     *
     * @param id ç¼–号
     */
    void deleteItem(Long id);
    /**
     * èŽ·å¾—ç‰©æ–™
     *
     * @param id ç¼–号
     * @return ç‰©æ–™
     */
    MdmItemDO getItem(Long id);
    /**
     * æ ¡éªŒç‰©æ–™å­˜åœ¨
     *
     * @param id ç¼–号
     * @return ç‰©æ–™
     */
    MdmItemDO validateItemExists(Long id);
    /**
     * æ ¡éªŒç‰©æ–™å­˜åœ¨ä¸”启用
     *
     * @param id ç¼–号
     * @return ç‰©æ–™
     */
    MdmItemDO validateItemExistsAndEnable(Long id);
    /**
     * èŽ·å¾—ç‰©æ–™åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return ç‰©æ–™åˆ†é¡µ
     */
    PageResult<MdmItemDO> getItemPage(MdmItemPageReqVO pageReqVO);
    /**
     * èŽ·å¾—ç‰©æ–™åˆ—è¡¨
     *
     * @param ids ç¼–号数组
     * @return ç‰©æ–™åˆ—表
     */
    List<MdmItemDO> getItemList(Collection<Long> ids);
    /**
     * èŽ·å¾—ç‰©æ–™ Map
     *
     * @param ids ç¼–号数组
     * @return ç‰©æ–™ Map
     */
    default Map<Long, MdmItemDO> getItemMap(Collection<Long> ids) {
        return convertMap(getItemList(ids), MdmItemDO::getId);
    }
    /**
     * åŸºäºŽç‰©æ–™åˆ†ç±»ç¼–号,获得物料数量
     *
     * @param categoryId ç‰©æ–™åˆ†ç±»ç¼–号
     * @return ç‰©æ–™æ•°é‡
     */
    Long getItemCountByCategoryId(Long categoryId);
    /**
     * åŸºäºŽè®¡é‡å•位编号,获得物料数量
     *
     * @param unitMeasureId è®¡é‡å•位编号
     * @return ç‰©æ–™æ•°é‡
     */
    Long getItemCountByUnitMeasureId(Long unitMeasureId);
    /**
     * åŸºäºŽå“ç‰Œç¼–号,获得物料数量
     *
     * @param brandId å“ç‰Œç¼–号
     * @return ç‰©æ–™æ•°é‡
     */
    Long getItemCountByBrandId(Long brandId);
    /**
     * æ ¹æ®ç¼–码获取物料
     *
     * @param code ç‰©æ–™ç¼–码
     * @return ç‰©æ–™
     */
    MdmItemDO getItemByCode(String code);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,181 @@
package cn.iocoder.yudao.module.mdm.service.item;
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.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mdm.enums.ErrorCodeConstants.*;
/**
 * MDM ç‰©æ–™ä¸»æ•°æ® Service å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
public class MdmItemServiceImpl implements MdmItemService {
    @Resource
    private MdmItemMapper itemMapper;
    @Resource
    private MdmItemCategoryService itemCategoryService;
    @Resource
    private MdmUnitMeasureService unitMeasureService;
    @Resource
    private MdmBrandService brandService;
    @Override
    public Long createItem(MdmItemSaveReqVO createReqVO) {
        // æ ¡éªŒæ•°æ®
        validateItemSaveData(createReqVO);
        // æ’å…¥
        MdmItemDO item = BeanUtils.toBean(createReqVO, MdmItemDO.class);
        itemMapper.insert(item);
        return item.getId();
    }
    @Override
    public void updateItem(MdmItemSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateItemExists(updateReqVO.getId());
        // æ ¡éªŒæ•°æ®
        validateItemSaveData(updateReqVO);
        // æ›´æ–°
        MdmItemDO updateObj = BeanUtils.toBean(updateReqVO, MdmItemDO.class);
        itemMapper.updateById(updateObj);
    }
    private void validateItemSaveData(MdmItemSaveReqVO reqVO) {
        // æ ¡éªŒç‰©æ–™ç¼–码的唯一性
        validateItemCodeUnique(reqVO.getId(), reqVO.getCode());
    }
    @Override
    public void updateItemStatus(Long id, Integer status) {
        // æ ¡éªŒå­˜åœ¨
        validateItemExists(id);
        // æ›´æ–°çŠ¶æ€
        itemMapper.updateById(new MdmItemDO().setId(id).setStatus(status));
    }
    @Override
    public void deleteItem(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateItemExists(id);
        // åˆ é™¤
        itemMapper.deleteById(id);
    }
    @Override
    public MdmItemDO validateItemExists(Long id) {
        MdmItemDO item = itemMapper.selectById(id);
        if (item == null) {
            throw exception(MDM_ITEM_NOT_EXISTS);
        }
        return item;
    }
    @Override
    public MdmItemDO validateItemExistsAndEnable(Long id) {
        MdmItemDO item = validateItemExists(id);
        if (ObjUtil.notEqual(cn.iocoder.yudao.framework.common.enums.CommonStatusEnum.ENABLE.getStatus(), item.getStatus())) {
            throw exception(MDM_ITEM_IS_DISABLE);
        }
        return item;
    }
    private void validateItemCodeUnique(Long id, String code) {
        MdmItemDO item = itemMapper.selectByCode(code);
        if (item == null) {
            return;
        }
        if (ObjUtil.notEqual(item.getId(), id)) {
            throw exception(MDM_ITEM_CODE_DUPLICATE);
        }
    }
    private void validateCategoryExists(Long categoryId) {
        if (itemCategoryService.getItemCategory(categoryId) == null) {
            throw exception(MDM_ITEM_CATEGORY_NOT_EXISTS);
        }
    }
    private void validateUnitMeasureExists(Long unitMeasureId) {
        if (unitMeasureService.getUnitMeasure(unitMeasureId) == null) {
            throw exception(MDM_UNIT_MEASURE_NOT_EXISTS);
        }
    }
    private void validateBrandExists(Long brandId) {
        if (brandService.getBrand(brandId) == null) {
            throw exception(MDM_BRAND_NOT_EXISTS);
        }
    }
    @Override
    public MdmItemDO getItem(Long id) {
        return itemMapper.selectById(id);
    }
    @Override
    public PageResult<MdmItemDO> getItemPage(MdmItemPageReqVO pageReqVO) {
        // æŸ¥æ‰¾æ—¶ï¼Œå¦‚果查找某个分类编号,则包含它的子分类
        Set<Long> categoryIds = null;
        if (pageReqVO.getCategoryId() != null) {
            categoryIds = new HashSet<>();
            categoryIds.add(pageReqVO.getCategoryId());
            List<cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO> children =
                    itemCategoryService.getItemCategoryChildrenList(pageReqVO.getCategoryId());
            categoryIds.addAll(convertSet(children, cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO::getId));
        }
        // åˆ†é¡µæŸ¥è¯¢
        return itemMapper.selectPage(pageReqVO, categoryIds);
    }
    @Override
    public List<MdmItemDO> getItemList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return itemMapper.selectBatchIds(ids);
    }
    @Override
    public Long getItemCountByCategoryId(Long categoryId) {
        return itemMapper.selectCountByCategoryId(categoryId);
    }
    @Override
    public Long getItemCountByUnitMeasureId(Long unitMeasureId) {
        return itemMapper.selectCountByUnitMeasureId(unitMeasureId);
    }
    @Override
    public Long getItemCountByBrandId(Long brandId) {
        return itemMapper.selectCountByBrandId(brandId);
    }
    @Override
    public MdmItemDO getItemByCode(String code) {
        return itemMapper.selectByCode(code);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemSkuService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,130 @@
package cn.iocoder.yudao.module.mdm.service.item;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemSkuDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * MDM ç‰©æ–™ SKU Service æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmItemSkuService {
    /**
     * åˆ›å»º SKU
     *
     * @param sku åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createItemSku(@Valid MdmItemSkuDO sku);
    /**
     * æ‰¹é‡åˆ›å»º SKU
     *
     * @param itemId   ç‰©æ–™ç¼–号
     * @param skuList SKU åˆ—表
     */
    void createItemSkuList(Long itemId, List<MdmItemSkuDO> skuList);
    /**
     * æ›´æ–° SKU
     *
     * @param sku æ›´æ–°ä¿¡æ¯
     */
    void updateItemSku(@Valid MdmItemSkuDO sku);
    /**
     * æ‰¹é‡æ›´æ–° SKU
     *
     * @param itemId   ç‰©æ–™ç¼–号
     * @param skuList SKU åˆ—表
     */
    void updateItemSkuList(Long itemId, List<MdmItemSkuDO> skuList);
    /**
     * åˆ é™¤ SKU
     *
     * @param id ç¼–号
     */
    void deleteItemSku(Long id);
    /**
     * åˆ é™¤ç‰©æ–™çš„æ‰€æœ‰ SKU
     *
     * @param itemId ç‰©æ–™ç¼–号
     */
    void deleteItemSkuListByItemId(Long itemId);
    /**
     * èŽ·å¾— SKU
     *
     * @param id ç¼–号
     * @return SKU
     */
    MdmItemSkuDO getItemSku(Long id);
    /**
     * æ ¡éªŒ SKU å­˜åœ¨
     *
     * @param id ç¼–号
     * @return SKU
     */
    MdmItemSkuDO validateItemSkuExists(Long id);
    /**
     * æ ¡éªŒ SKU å­˜åœ¨ä¸”启用
     *
     * @param id ç¼–号
     * @return SKU
     */
    MdmItemSkuDO validateItemSkuExistsAndEnable(Long id);
    /**
     * èŽ·å¾—ç‰©æ–™çš„ SKU åˆ—表
     *
     * @param itemId ç‰©æ–™ç¼–号
     * @return SKU åˆ—表
     */
    List<MdmItemSkuDO> getItemSkuListByItemId(Long itemId);
    /**
     * èŽ·å¾— SKU åˆ—表
     *
     * @param ids ç¼–号数组
     * @return SKU åˆ—表
     */
    List<MdmItemSkuDO> getItemSkuList(Collection<Long> ids);
    /**
     * èŽ·å¾— SKU Map
     *
     * @param ids ç¼–号数组
     * @return SKU Map
     */
    default Map<Long, MdmItemSkuDO> getItemSkuMap(Collection<Long> ids) {
        return convertMap(getItemSkuList(ids), MdmItemSkuDO::getId);
    }
    /**
     * æ ¹æ®ç¼–码获取 SKU
     *
     * @param code SKU ç¼–码
     * @return SKU
     */
    MdmItemSkuDO getItemSkuByCode(String code);
    /**
     * æ ¹æ®æ¡ç èŽ·å– SKU
     *
     * @param barCode SKU æ¡ç 
     * @return SKU
     */
    MdmItemSkuDO getItemSkuByBarCode(String barCode);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemSkuServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,151 @@
package cn.iocoder.yudao.module.mdm.service.item;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemSkuDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemSkuMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mdm.enums.ErrorCodeConstants.*;
/**
 * MDM ç‰©æ–™ SKU Service å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
public class MdmItemSkuServiceImpl implements MdmItemSkuService {
    @Resource
    private MdmItemSkuMapper itemSkuMapper;
    @Override
    public Long createItemSku(MdmItemSkuDO sku) {
        // æ ¡éªŒç¼–码唯一
        validateItemSkuCodeUnique(null, sku.getCode());
        // è®¾ç½®é»˜è®¤çŠ¶æ€
        if (sku.getStatus() == null) {
            sku.setStatus(CommonStatusEnum.ENABLE.getStatus());
        }
        itemSkuMapper.insert(sku);
        return sku.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createItemSkuList(Long itemId, List<MdmItemSkuDO> skuList) {
        if (CollUtil.isEmpty(skuList)) {
            return;
        }
        for (MdmItemSkuDO sku : skuList) {
            sku.setItemId(itemId);
            createItemSku(sku);
        }
    }
    @Override
    public void updateItemSku(MdmItemSkuDO sku) {
        // æ ¡éªŒå­˜åœ¨
        validateItemSkuExists(sku.getId());
        // æ ¡éªŒç¼–码唯一
        validateItemSkuCodeUnique(sku.getId(), sku.getCode());
        itemSkuMapper.updateById(sku);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateItemSkuList(Long itemId, List<MdmItemSkuDO> skuList) {
        // ç¬¬ä¸€æ­¥ï¼Œåˆ é™¤æ—§çš„ SKU
        itemSkuMapper.deleteByItemId(itemId);
        // ç¬¬äºŒæ­¥ï¼Œæ’入新的 SKU
        if (CollUtil.isEmpty(skuList)) {
            return;
        }
        for (MdmItemSkuDO sku : skuList) {
            sku.setItemId(itemId);
            sku.setId(null); // æ¸…空 ID,作为新插入
            if (sku.getStatus() == null) {
                sku.setStatus(CommonStatusEnum.ENABLE.getStatus());
            }
            itemSkuMapper.insert(sku);
        }
    }
    private void validateItemSkuCodeUnique(Long id, String code) {
        MdmItemSkuDO sku = itemSkuMapper.selectByCode(code);
        if (sku == null) {
            return;
        }
        if (id == null || ObjUtil.notEqual(sku.getId(), id)) {
            throw exception(MDM_ITEM_SKU_CODE_DUPLICATE);
        }
    }
    @Override
    public void deleteItemSku(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateItemSkuExists(id);
        // åˆ é™¤
        itemSkuMapper.deleteById(id);
    }
    @Override
    public void deleteItemSkuListByItemId(Long itemId) {
        itemSkuMapper.deleteByItemId(itemId);
    }
    @Override
    public MdmItemSkuDO validateItemSkuExists(Long id) {
        MdmItemSkuDO sku = itemSkuMapper.selectById(id);
        if (sku == null) {
            throw exception(MDM_ITEM_SKU_NOT_EXISTS);
        }
        return sku;
    }
    @Override
    public MdmItemSkuDO validateItemSkuExistsAndEnable(Long id) {
        MdmItemSkuDO sku = validateItemSkuExists(id);
        if (CommonStatusEnum.isDisable(sku.getStatus())) {
            throw exception(MDM_ITEM_SKU_IS_DISABLE);
        }
        return sku;
    }
    @Override
    public MdmItemSkuDO getItemSku(Long id) {
        return itemSkuMapper.selectById(id);
    }
    @Override
    public List<MdmItemSkuDO> getItemSkuListByItemId(Long itemId) {
        return itemSkuMapper.selectListByItemId(itemId);
    }
    @Override
    public List<MdmItemSkuDO> getItemSkuList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return itemSkuMapper.selectBatchIds(ids);
    }
    @Override
    public MdmItemSkuDO getItemSkuByCode(String code) {
        return itemSkuMapper.selectByCode(code);
    }
    @Override
    public MdmItemSkuDO getItemSkuByBarCode(String barCode) {
        return itemSkuMapper.selectByBarCode(barCode);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/unit/MdmUnitMeasureService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package cn.iocoder.yudao.module.mdm.service.unit;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasurePageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
 * MDM è®¡é‡å•位 Service æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmUnitMeasureService {
    /**
     * åˆ›å»ºè®¡é‡å•位
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createUnitMeasure(@Valid MdmUnitMeasureSaveReqVO createReqVO);
    /**
     * æ›´æ–°è®¡é‡å•位
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateUnitMeasure(@Valid MdmUnitMeasureSaveReqVO updateReqVO);
    /**
     * æ›´æ–°è®¡é‡å•位状态
     *
     * @param id     ç¼–号
     * @param status çŠ¶æ€
     */
    void updateUnitMeasureStatus(Long id, Integer status);
    /**
     * åˆ é™¤è®¡é‡å•位
     *
     * @param id ç¼–号
     */
    void deleteUnitMeasure(Long id);
    /**
     * èŽ·å¾—è®¡é‡å•ä½
     *
     * @param id ç¼–号
     * @return è®¡é‡å•位
     */
    MdmUnitMeasureDO getUnitMeasure(Long id);
    /**
     * æ ¡éªŒè®¡é‡å•位存在
     *
     * @param id ç¼–号
     * @return è®¡é‡å•位
     */
    MdmUnitMeasureDO validateUnitMeasureExists(Long id);
    /**
     * èŽ·å¾—è®¡é‡å•ä½åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return è®¡é‡å•位分页
     */
    PageResult<MdmUnitMeasureDO> getUnitMeasurePage(MdmUnitMeasurePageReqVO pageReqVO);
    /**
     * èŽ·å¾—è®¡é‡å•ä½åˆ—è¡¨
     *
     * @param ids ç¼–号数组
     * @return è®¡é‡å•位列表
     */
    List<MdmUnitMeasureDO> getUnitMeasureList(Collection<Long> ids);
    /**
     * èŽ·å¾—è®¡é‡å•ä½ Map
     *
     * @param ids ç¼–号数组
     * @return è®¡é‡å•位 Map
     */
    default Map<Long, MdmUnitMeasureDO> getUnitMeasureMap(Collection<Long> ids) {
        return convertMap(getUnitMeasureList(ids), MdmUnitMeasureDO::getId);
    }
    /**
     * æ ¹æ®ç¼–码获取计量单位
     *
     * @param code å•位编码
     * @return è®¡é‡å•位
     */
    MdmUnitMeasureDO getUnitMeasureByCode(String code);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/unit/MdmUnitMeasureServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
package cn.iocoder.yudao.module.mdm.service.unit;
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.mdm.controller.admin.unit.vo.MdmUnitMeasurePageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.unit.vo.MdmUnitMeasureSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.unit.MdmUnitMeasureMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mdm.enums.ErrorCodeConstants.*;
/**
 * MDM è®¡é‡å•位 Service å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
public class MdmUnitMeasureServiceImpl implements MdmUnitMeasureService {
    @Resource
    private MdmUnitMeasureMapper unitMeasureMapper;
    @Resource
    private MdmItemMapper itemMapper;
    @Override
    public Long createUnitMeasure(MdmUnitMeasureSaveReqVO createReqVO) {
        // æ ¡éªŒæ•°æ®
        validateUnitMeasureSaveData(createReqVO);
        // æ’å…¥
        MdmUnitMeasureDO unitMeasure = BeanUtils.toBean(createReqVO, MdmUnitMeasureDO.class);
        unitMeasureMapper.insert(unitMeasure);
        return unitMeasure.getId();
    }
    @Override
    public void updateUnitMeasure(MdmUnitMeasureSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateUnitMeasureExists(updateReqVO.getId());
        // æ ¡éªŒæ•°æ®
        validateUnitMeasureSaveData(updateReqVO);
        // æ›´æ–°
        MdmUnitMeasureDO updateObj = BeanUtils.toBean(updateReqVO, MdmUnitMeasureDO.class);
        unitMeasureMapper.updateById(updateObj);
    }
    @Override
    public void updateUnitMeasureStatus(Long id, Integer status) {
        // æ ¡éªŒå­˜åœ¨
        validateUnitMeasureExists(id);
        // æ›´æ–°çŠ¶æ€
        unitMeasureMapper.updateById(new MdmUnitMeasureDO().setId(id).setStatus(status));
    }
    @Override
    public void deleteUnitMeasure(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateUnitMeasureExists(id);
        // æ ¡éªŒæ˜¯å¦è¢«ç‰©æ–™ä½¿ç”¨ï¼ˆç›´æŽ¥ä½¿ç”¨ Mapper é¿å…å¾ªçŽ¯ä¾èµ–ï¼‰
        if (itemMapper.selectCountByUnitMeasureId(id) > 0) {
            throw exception(MDM_UNIT_MEASURE_EXISTS_ITEM);
        }
        // åˆ é™¤
        unitMeasureMapper.deleteById(id);
    }
    @Override
    public MdmUnitMeasureDO validateUnitMeasureExists(Long id) {
        MdmUnitMeasureDO unitMeasure = unitMeasureMapper.selectById(id);
        if (unitMeasure == null) {
            throw exception(MDM_UNIT_MEASURE_NOT_EXISTS);
        }
        return unitMeasure;
    }
    private void validateUnitMeasureSaveData(MdmUnitMeasureSaveReqVO reqVO) {
        // æ ¡éªŒç¼–码唯一
        MdmUnitMeasureDO unit = unitMeasureMapper.selectByCode(reqVO.getCode());
        if (unit != null && ObjUtil.notEqual(unit.getId(), reqVO.getId())) {
            throw exception(MDM_UNIT_MEASURE_CODE_DUPLICATE);
        }
        // æ ¡éªŒåç§°å”¯ä¸€
        MdmUnitMeasureDO sameName = unitMeasureMapper.selectByName(reqVO.getName());
        if (sameName != null && ObjUtil.notEqual(sameName.getId(), reqVO.getId())) {
            throw exception(MDM_UNIT_MEASURE_NAME_DUPLICATE);
        }
    }
    @Override
    public MdmUnitMeasureDO getUnitMeasure(Long id) {
        return unitMeasureMapper.selectById(id);
    }
    @Override
    public PageResult<MdmUnitMeasureDO> getUnitMeasurePage(MdmUnitMeasurePageReqVO pageReqVO) {
        return unitMeasureMapper.selectPage(pageReqVO);
    }
    @Override
    public List<MdmUnitMeasureDO> getUnitMeasureList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return unitMeasureMapper.selectBatchIds(ids);
    }
    @Override
    public MdmUnitMeasureDO getUnitMeasureByCode(String code) {
        return unitMeasureMapper.selectByCode(code);
    }
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/warehouse/MdmWarehouseService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
package cn.iocoder.yudao.module.mdm.service.warehouse;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mdm.controller.admin.warehouse.vo.MdmWarehousePageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.warehouse.vo.MdmWarehouseSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.warehouse.MdmWarehouseDO;
import jakarta.validation.Valid;
import java.util.Collection;
import java.util.List;
/**
 * MDM ä»“库 Service æŽ¥å£
 *
 * @author èŠ‹é“æºç 
 */
public interface MdmWarehouseService {
    /**
     * åˆ›å»ºä»“库
     *
     * @param createReqVO åˆ›å»ºä¿¡æ¯
     * @return ç¼–号
     */
    Long createWarehouse(@Valid MdmWarehouseSaveReqVO createReqVO);
    /**
     * æ›´æ–°ä»“库
     *
     * @param updateReqVO æ›´æ–°ä¿¡æ¯
     */
    void updateWarehouse(@Valid MdmWarehouseSaveReqVO updateReqVO);
    /**
     * æ›´æ–°ä»“库状态
     *
     * @param id     ç¼–号
     * @param status çŠ¶æ€
     */
    void updateWarehouseStatus(Long id, Integer status);
    /**
     * åˆ é™¤ä»“库
     *
     * @param id ç¼–号
     */
    void deleteWarehouse(Long id);
    /**
     * èŽ·å¾—ä»“åº“
     *
     * @param id ç¼–号
     * @return ä»“库
     */
    MdmWarehouseDO getWarehouse(Long id);
    /**
     * æ ¡éªŒä»“库存在
     *
     * @param id ç¼–号
     * @return ä»“库
     */
    MdmWarehouseDO validateWarehouseExists(Long id);
    /**
     * æ ¡éªŒä»“库存在且启用
     *
     * @param id ç¼–号
     * @return ä»“库
     */
    MdmWarehouseDO validateWarehouseExistsAndEnable(Long id);
    /**
     * èŽ·å¾—ä»“åº“åˆ†é¡µ
     *
     * @param pageReqVO åˆ†é¡µæŸ¥è¯¢
     * @return ä»“库分页
     */
    PageResult<MdmWarehouseDO> getWarehousePage(MdmWarehousePageReqVO pageReqVO);
    /**
     * èŽ·å¾—ä»“åº“åˆ—è¡¨
     *
     * @param ids ç¼–号数组
     * @return ä»“库列表
     */
    List<MdmWarehouseDO> getWarehouseList(Collection<Long> ids);
    /**
     * æ ¹æ®ç¼–码获取仓库
     *
     * @param code ä»“库编码
     * @return ä»“库
     */
    MdmWarehouseDO getWarehouseByCode(String code);
}
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/warehouse/MdmWarehouseServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,126 @@
package cn.iocoder.yudao.module.mdm.service.warehouse;
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.mdm.controller.admin.warehouse.vo.MdmWarehousePageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.warehouse.vo.MdmWarehouseSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.warehouse.MdmWarehouseDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.warehouse.MdmWarehouseMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mdm.enums.ErrorCodeConstants.*;
/**
 * MDM ä»“库 Service å®žçŽ°ç±»
 *
 * @author èŠ‹é“æºç 
 */
@Service
@Validated
public class MdmWarehouseServiceImpl implements MdmWarehouseService {
    @Resource
    private MdmWarehouseMapper warehouseMapper;
    @Override
    public Long createWarehouse(MdmWarehouseSaveReqVO createReqVO) {
        // æ ¡éªŒæ•°æ®
        validateWarehouseSaveData(createReqVO);
        // æ’å…¥
        MdmWarehouseDO warehouse = BeanUtils.toBean(createReqVO, MdmWarehouseDO.class);
        warehouseMapper.insert(warehouse);
        return warehouse.getId();
    }
    @Override
    public void updateWarehouse(MdmWarehouseSaveReqVO updateReqVO) {
        // æ ¡éªŒå­˜åœ¨
        validateWarehouseExists(updateReqVO.getId());
        // æ ¡éªŒæ•°æ®
        validateWarehouseSaveData(updateReqVO);
        // æ›´æ–°
        MdmWarehouseDO updateObj = BeanUtils.toBean(updateReqVO, MdmWarehouseDO.class);
        warehouseMapper.updateById(updateObj);
    }
    @Override
    public void updateWarehouseStatus(Long id, Integer status) {
        // æ ¡éªŒå­˜åœ¨
        validateWarehouseExists(id);
        // æ›´æ–°çŠ¶æ€
        warehouseMapper.updateById(new MdmWarehouseDO().setId(id).setStatus(status));
    }
    @Override
    public void deleteWarehouse(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateWarehouseExists(id);
        // TODO: æ ¡éªŒæ˜¯å¦è¢«åº“存使用
        // åˆ é™¤
        warehouseMapper.deleteById(id);
    }
    @Override
    public MdmWarehouseDO validateWarehouseExists(Long id) {
        MdmWarehouseDO warehouse = warehouseMapper.selectById(id);
        if (warehouse == null) {
            throw exception(MDM_WAREHOUSE_NOT_EXISTS);
        }
        return warehouse;
    }
    @Override
    public MdmWarehouseDO validateWarehouseExistsAndEnable(Long id) {
        MdmWarehouseDO warehouse = validateWarehouseExists(id);
        if (warehouse.getStatus() != null && warehouse.getStatus() != 0) {
            throw exception(MDM_WAREHOUSE_IS_DISABLE);
        }
        return warehouse;
    }
    private void validateWarehouseSaveData(MdmWarehouseSaveReqVO reqVO) {
        // æ ¡éªŒç¼–码唯一
        MdmWarehouseDO warehouse = warehouseMapper.selectByCode(reqVO.getCode());
        if (warehouse != null && ObjUtil.notEqual(warehouse.getId(), reqVO.getId())) {
            throw exception(MDM_WAREHOUSE_CODE_DUPLICATE);
        }
        // æ ¡éªŒåç§°å”¯ä¸€
        MdmWarehouseDO sameName = warehouseMapper.selectByName(reqVO.getName());
        if (sameName != null && ObjUtil.notEqual(sameName.getId(), reqVO.getId())) {
            throw exception(MDM_WAREHOUSE_NAME_DUPLICATE);
        }
    }
    @Override
    public MdmWarehouseDO getWarehouse(Long id) {
        return warehouseMapper.selectById(id);
    }
    @Override
    public PageResult<MdmWarehouseDO> getWarehousePage(MdmWarehousePageReqVO pageReqVO) {
        return warehouseMapper.selectPage(pageReqVO);
    }
    @Override
    public List<MdmWarehouseDO> getWarehouseList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return warehouseMapper.selectListByIds(ids);
    }
    @Override
    public MdmWarehouseDO getWarehouseByCode(String code) {
        return warehouseMapper.selectByCode(code);
    }
}
yudao-module-mes/pom.xml
@@ -24,6 +24,13 @@
            <version>${revision}</version>
        </dependency>
        <!-- MDM ä¸»æ•°æ®ç®¡ç† API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-mdm</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- ä¸šåŠ¡ç»„ä»¶ -->
        <!-- Web ç›¸å…³ -->
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -38,6 +38,8 @@
/**
 * MES ç‰©æ–™äº§å“ Service å®žçŽ°ç±»
 *
 * æ³¨æ„ï¼šå½“前保持使用 MES æœ¬åœ°è¡¨ï¼Œå¾… MDM æ•°æ®è¿ç§»å®ŒæˆåŽå†åˆ‡æ¢åˆ° MDM API
 *
 * @author èŠ‹é“æºç 
 */
@Service
@@ -362,4 +364,4 @@
        return respVO;
    }
}
}
yudao-module-wms/pom.xml
@@ -24,6 +24,13 @@
            <version>${revision}</version>
        </dependency>
        <!-- MDM ä¸»æ•°æ®ç®¡ç† API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-mdm</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- ä¸šåŠ¡ç»„ä»¶ -->
        <!-- Web ç›¸å…³ -->
yudao-module-wms/src/main/java/cn/iocoder/yudao/module/wms/service/md/item/WmsItemServiceImpl.java
@@ -4,6 +4,9 @@
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mdm.enums.MdmItemTypeEnum;
import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemListReqVO;
import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemPageReqVO;
import cn.iocoder.yudao.module.wms.controller.admin.md.item.vo.item.WmsItemSaveReqVO;
@@ -15,6 +18,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -25,6 +29,9 @@
/**
 * WMS å•†å“ Service å®žçŽ°ç±»
 *
 * æ”¹é€ è¯´æ˜Žï¼šå•†å“åŸºç¡€æ•°æ®ä»Ž MDM æ¨¡å—获取(itemType=1 ç‰©æ–™ç±»åž‹ï¼‰
 * WMS ç‰¹æœ‰åŠŸèƒ½ï¼ˆSKU、库存等)仍保留 WMS ç®¡ç†
 *
 * @author èŠ‹é“æºç 
 */
@Service
@@ -33,6 +40,10 @@
    @Resource
    private WmsItemMapper itemMapper;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    @Lazy // å»¶è¿ŸåŠ è½½ï¼Œé¿å…å¾ªçŽ¯ä¾èµ–
    private WmsItemCategoryService categoryService;
@@ -48,7 +59,7 @@
        // æ ¡éªŒæ•°æ®
        validateItemSaveData(createReqVO);
        // æ’入商品
        // æ’入商品(暂时保留原逻辑,后续可完全迁移到 MDM)
        WmsItemDO item = BeanUtils.toBean(createReqVO, WmsItemDO.class);
        itemMapper.insert(item);
        // æ’å…¥ SKU
@@ -64,7 +75,7 @@
        // æ ¡éªŒæ•°æ®
        validateItemSaveData(updateReqVO);
        // æ›´æ–°å•†å“
        // æ›´æ–°å•†å“ï¼ˆæš‚时保留原逻辑)
        WmsItemDO updateObj = BeanUtils.toBean(updateReqVO, WmsItemDO.class);
        itemMapper.updateById(updateObj);
        // æ›´æ–° SKU
@@ -83,11 +94,9 @@
    }
    private void validateItemCodeUnique(Long id, String code) {
        WmsItemDO item = itemMapper.selectByCode(code);
        if (item == null) {
            return;
        }
        if (id == null || ObjectUtil.notEqual(item.getId(), id)) {
        // æ£€æŸ¥ MDM ä¸­ç¼–码唯一性
        MdmItemRespDTO item = mdmItemApi.getItemByCode(code).getCheckedData();
        if (item != null && ObjectUtil.notEqual(item.getId(), id)) {
            throw exception(ITEM_CODE_DUPLICATE);
        }
    }
@@ -126,28 +135,58 @@
    @Override
    public WmsItemDO validateItemExists(Long id) {
        WmsItemDO item = itemMapper.selectById(id);
        // ä»Ž MDM æ ¡éªŒç‰©æ–™å­˜åœ¨
        MdmItemRespDTO item = mdmItemApi.validateItemExists(id).getCheckedData();
        if (item == null) {
            throw exception(ITEM_NOT_EXISTS);
            // å¦‚æžœ MDM ä¸­ä¸å­˜åœ¨ï¼Œæ£€æŸ¥æœ¬åœ°è¡¨
            WmsItemDO localItem = itemMapper.selectById(id);
            if (localItem == null) {
                throw exception(ITEM_NOT_EXISTS);
            }
            return localItem;
        }
        return item;
        return convertToWmsItem(item);
    }
    @Override
    public WmsItemDO getItem(Long id) {
        return itemMapper.selectById(id);
        // ä»Ž MDM èŽ·å–ç‰©æ–™æ•°æ®ï¼ˆæ³¨æ„ WMS ID åç§» 30000)
        MdmItemRespDTO item = mdmItemApi.getItem(id).getCheckedData();
        if (item == null) {
            // å¦‚æžœ MDM ä¸­ä¸å­˜åœ¨ï¼ŒæŸ¥è¯¢æœ¬åœ°è¡¨
            return itemMapper.selectById(id);
        }
        return convertToWmsItem(item);
    }
    @Override
    public PageResult<WmsItemDO> getItemPage(WmsItemPageReqVO pageReqVO) {
        return itemMapper.selectPage(pageReqVO,
                categoryService.getSelfAndChildItemCategoryIdList(pageReqVO.getCategoryId()));
        // ä»Ž MDM èŽ·å–ç‰©æ–™åˆ†é¡µæ•°æ®ï¼ˆitemType=1 ç‰©æ–™ç±»åž‹ï¼‰
        cn.iocoder.yudao.framework.common.pojo.PageResult<MdmItemRespDTO> mdmPageResult =
                mdmItemApi.getItemPage(MdmItemTypeEnum.MATERIAL.getType(), null,
                        pageReqVO.getPageNo(), pageReqVO.getPageSize()).getCheckedData();
        // è½¬æ¢ä¸º WMS å•†å“ DO
        List<WmsItemDO> list = new ArrayList<>();
        for (MdmItemRespDTO item : mdmPageResult.getList()) {
            list.add(convertToWmsItem(item));
        }
        return new PageResult<>(list, mdmPageResult.getTotal());
    }
    @Override
    public List<WmsItemDO> getItemList(WmsItemListReqVO listReqVO) {
        return itemMapper.selectList(listReqVO,
                categoryService.getSelfAndChildItemCategoryIdList(listReqVO.getCategoryId()));
        // ä»Ž MDM èŽ·å–ç‰©æ–™åˆ—è¡¨ï¼ˆitemType=1 ç‰©æ–™ç±»åž‹ï¼‰
        List<MdmItemRespDTO> items = mdmItemApi.getItemListByStatus(null).getCheckedData();
        List<WmsItemDO> result = new ArrayList<>();
        for (MdmItemRespDTO item : items) {
            // åªè¿”回物料类型
            if (MdmItemTypeEnum.MATERIAL.getType().equals(item.getItemType())) {
                result.add(convertToWmsItem(item));
            }
        }
        return result;
    }
    @Override
@@ -155,17 +194,42 @@
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return itemMapper.selectByIds(ids);
        // ä»Ž MDM èŽ·å–ç‰©æ–™åˆ—è¡¨
        List<MdmItemRespDTO> items = mdmItemApi.getItemList(ids).getCheckedData();
        List<WmsItemDO> result = new ArrayList<>();
        for (MdmItemRespDTO item : items) {
            result.add(convertToWmsItem(item));
        }
        return result;
    }
    /**
     * å°† MDM ç‰©æ–™è½¬æ¢ä¸º WMS å•†å“ DO
     */
    private WmsItemDO convertToWmsItem(MdmItemRespDTO item) {
        WmsItemDO wmsItem = new WmsItemDO();
        wmsItem.setId(item.getId());
        wmsItem.setCode(item.getCode());
        wmsItem.setName(item.getName());
        // MDM categoryId æ˜ å°„到 WMS categoryId(注意 ID åç§»ï¼‰
        wmsItem.setCategoryId(item.getCategoryId() != null ? item.getCategoryId() - 20000 : null);
        wmsItem.setBrandId(item.getBrandId());
        // å•位名称需要从 MDM å•位表获取,这里暂时留空
        wmsItem.setRemark(item.getRemark());
        return wmsItem;
    }
    @Override
    public long getItemCountByCategoryId(Long categoryId) {
        return itemMapper.selectCountByCategoryId(categoryId);
        // ä»Ž MDM èŽ·å–åˆ†ç±»ç‰©æ–™æ•°é‡ï¼ˆæ³¨æ„ ID åç§»ï¼‰
        return mdmItemApi.getItemCountByCategoryId(categoryId + 20000).getCheckedData();
    }
    @Override
    public long getItemCountByBrandId(Long brandId) {
        // ä»Ž MDM èŽ·å–å“ç‰Œç‰©æ–™æ•°é‡
        // æ³¨æ„ï¼šMDM ç›®å‰æ²¡æœ‰æŒ‰å“ç‰Œç»Ÿè®¡çš„ API,暂时返回本地数据
        return itemMapper.selectCountByBrandId(brandId);
    }
}
}
yudao-server/pom.xml
@@ -136,6 +136,13 @@
            <version>${revision}</version>
        </dependency>
        <!-- MDM ä¸»æ•°æ®ç®¡ç†æ¨¡å— -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-mdm</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- spring boot é…ç½®æ‰€éœ€ä¾èµ– -->
        <dependency>
            <groupId>org.springframework.boot</groupId>