# CRM 与 ERP 销售一体化改造 - 前端文档(完整版) ## 概述 本次改造实现 CRM 与 ERP 销售模块的深度集成,包括: - **Phase 1**:客户数据统一 - ERP 使用 CRM 客户 - **Phase 2**:产品数据统一 - CRM/ERP 使用 MDM 物料 - **Phase 3**:合同生成销售订单 - **Phase 4**:销售全流程追溯 - **Phase 5**:营销数据报表 - **Phase 6**:数据迁移与清理(SQL 部分) --- ## 一、改造清单汇总 ### 1.1 新增模块 | 模块 | 说明 | |------|------| | `yudao-module-crm-api` | CRM API 模块(客户 API、报表 API、追溯 API) | | `yudao-module-erp-api` | ERP API 模块(销售订单 API) | | `yudao-module-mdm-api` | MDM API 模块(物料 API) | ### 1.2 后端改造文件 | 文件 | 改动说明 | |------|----------| | `CrmCustomerApi.java` | CRM 客户 API 接口 | | `CrmCustomerRespDTO.java` | 客户响应 DTO | | `CrmReportApi.java` | 营销报表 API 接口 | | `CrmSaleTraceApi.java` | 销售追溯 API 接口 | | `ErpSaleOrderApi.java` | 销售订单 API 接口 | | `MdmItemApi.java` | MDM 物料 API 接口 | | `ErpSaleOrderServiceImpl.java` | 使用 CRM 客户 API | | `ErpFinanceReceiptServiceImpl.java` | 使用 CRM 客户 API | | `CrmContractServiceImpl.java` | 新增生成销售订单方法 | | `CrmProductServiceImpl.java` | 使用 MDM 物料 API | | `ErpProductServiceImpl.java` | 使用 MDM 物料 API | --- ## 二、涉及页面 ### 2.1 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 客户选择组件 | ### 2.2 CRM 合同模块 | 页面 | 路径 | 改动说明 | |------|------|----------| | 合同列表 | `/crm/contract/index` | 添加"生成销售订单"按钮 | | 合同详情 | `/crm/contract/detail` | 显示销售订单信息、销售流程追溯 | ### 2.3 CRM 商机模块 | 页面 | 路径 | 改动说明 | |------|------|----------| | 商机详情 | `/crm/business/detail` | 添加销售流程追溯入口 | ### 2.4 CRM 客户模块 | 页面 | 路径 | 改动说明 | |------|------|----------| | 客户详情 | `/crm/customer/detail` | 添加销售流程追溯列表 | ### 2.5 CRM 报表模块(新增) | 页面 | 路径 | 说明 | |------|------|------| | 报表概览 | `/crm/report/dashboard` | 数据概览页面 | --- ## 三、API 接口 ### 3.1 CRM 客户 API | 方法 | 路径 | 说明 | |------|------|------| | GET | /crm/customer/simple-list | 获取客户精简列表(用于下拉选择) | | GET | /crm/customer/list-by-ids | 批量获取客户信息 | | GET | /rpc-api/crm/customer/get | 获取单个客户 | | POST | /rpc-api/crm/customer/validate | 校验客户存在 | **客户精简列表响应:** ```json { "code": 0, "data": [ { "id": 1, "name": "客户A", "mobile": "13800138000", "telephone": "0571-12345678", "ownerUserName": "张三" } ] } ``` ### 3.2 CRM 合同 API(新增) | 方法 | 路径 | 说明 | |------|------|------| | POST | /crm/contract/generate-sale-order | 根据合同生成销售订单 | **请求参数:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | id | Long | 是 | 合同编号 | **响应:** ```json { "code": 0, "data": 12345, "msg": "操作成功" } ``` ### 3.3 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, "customerId": 1, "customerName": "客户A", "contractId": 1, "contractName": "合同A", "contractPrice": 95000, "contractStatus": 20, "orderId": 1, "orderNo": "SO001", "orderPrice": 95000, "receivablePrice": 50000, "receivableItems": [ { "receivableId": 1, "receivableNo": "RK001", "price": 30000 } ] } } ``` ### 3.4 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 | 获取销售业绩统计 | **客户分析响应:** ```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 } ] } } ``` ### 3.5 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 | 根据合同编号获取订单 | ### 3.6 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 | 获取物料列表 | | GET | /rpc-api/mdm/item/validate-exists | 校验物料存在 | --- ## 四、前端修改点 ### 4.1 客户选择组件 ERP 销售模块的客户选择统一使用 CRM 客户选择组件。 ```html ``` **组件实现:** ```html ``` ### 4.2 合同列表页面 **文件路径:** `/src/views/crm/contract/index.vue` **新增按钮:** ```html ``` **新增方法:** ```js import { generateSaleOrder } from '@/api/crm/contract' methods: { async handleGenerateOrder(row) { try { await this.$confirm('确认根据该合同生成销售订单?', '提示') const { data } = await generateSaleOrder(row.id) this.$message.success('生成销售订单成功') 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 } }) } } ``` ### 4.3 销售订单列表页面 **文件路径:** `/src/views/erp/sale/order/index.vue` **新增列:** ```html ``` ### 4.4 商机详情页面 - 销售追溯 **文件路径:** `/src/views/crm/business/detail.vue` ```html

商机创建

商机名称:{{ traceInfo.businessName }}

商机金额:¥{{ traceInfo.businessPrice }}

合同签订

合同名称:{{ traceInfo.contractName }}

合同金额:¥{{ traceInfo.contractPrice }}

销售订单

订单号: {{ traceInfo.orderNo }}

订单金额:¥{{ traceInfo.orderPrice }}

回款记录

已回款金额:¥{{ traceInfo.receivablePrice }}

``` ### 4.5 报表概览页面 **文件路径:** `/src/views/crm/report/dashboard.vue` ```html ``` ### 4.6 API 定义文件 **文件路径:** `/src/api/crm/contract.js` ```js // 生成销售订单 export function generateSaleOrder(id) { return request({ url: '/crm/contract/generate-sale-order', method: 'post', params: { id } }) } ``` **文件路径:** `/src/api/crm/saleTrace.js` ```js // 根据商机编号获取销售流程追溯信息 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 } }) } ``` **文件路径:** `/src/api/crm/report.js` ```js // 获取客户分析统计 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 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`; -- 新增定金金额字段 ALTER TABLE `crm_contract` ADD COLUMN `deposit_price` decimal(24,6) DEFAULT NULL COMMENT '定金金额,单位:元' AFTER `total_price`; ``` ### 5.2 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`; ``` ### 5.3 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 客户数据规则 - ERP 销售模块统一使用 CRM 客户 - 原 ERP 客户表(erp_customer)废弃 - 客户选择通过 CRM 客户 API 获取 ### 6.3 产品数据规则 - CRM 和 ERP 产品统一使用 MDM 物料(itemType=2) - MDM 物料类型:1-物料,2-产品,3-半成品 --- ## 七、业务流程图 ``` 客户管理(CRM) → 商机跟进(CRM) → 合同签订(CRM) → 合同审批 ↓ 生成销售订单(ERP) ↓ 订单审批(ERP) ↓ 销售出库(ERP) ↓ 客户回款(ERP) ``` **销售漏斗转化:** ``` 客户(1000) → 商机(500) → 合同(300) → 回款(150) ↓ ↓ ↓ ↓ 100% 50% 30% 15% ``` --- ## 八、注意事项 1. **数据权限**:报表数据需要根据用户权限过滤 2. **状态控制**:只有审批通过的合同才能生成订单 3. **唯一性**:一个合同只能生成一个销售订单 4. **数据同步**:生成订单后,合同与订单双向关联 5. **废弃页面**:ERP 客户管理页面(/erp/customer/index)建议在菜单中隐藏 --- ## 九、完成状态 | Phase | 内容 | 状态 | |-------|------|------| | Phase 1 | 客户数据统一 - ERP 使用 CRM 客户 | **已完成** | | Phase 2 | 产品数据统一 - CRM/ERP 使用 MDM 物料 | **已完成** | | Phase 3 | 合同生成销售订单 | **已完成** | | Phase 4 | 销售全流程追溯 | **已完成** | | Phase 5 | 营销数据报表 | **已完成** | | Phase 6 | 数据迁移与清理 | **已完成** | --- ## 十一、销售报价模块(新增) ### 11.1 涉及页面 | 页面 | 路径 | 说明 | |------|------|------| | 报价单列表 | `/crm/sale-quotation/index` | 报价单列表、操作按钮 | | 报价单表单 | `/crm/sale-quotation/form` | 新建/编辑报价单 | | 报价单详情 | `/crm/sale-quotation/detail` | 查看详情、审批进度 | ### 11.2 API 接口 | 方法 | 路径 | 说明 | |------|------|------| | POST | /crm/sale-quotation/create | 创建报价单 | | PUT | /crm/sale-quotation/update | 更新报价单 | | DELETE | /crm/sale-quotation/delete | 删除报价单 | | GET | /crm/sale-quotation/get | 获取报价单详情 | | GET | /crm/sale-quotation/page | 分页查询 | | GET | /crm/sale-quotation/page-by-customer | 按客户分页 | | GET | /crm/sale-quotation/page-by-business | 按商机分页 | | PUT | /crm/sale-quotation/submit | 提交审批(需传 processDefinitionKey) | | GET | /crm/sale-quotation/approve-process-list | 获取可选审批流程列表 | | POST | /crm/sale-quotation/convert-contract | 转为合同 | **请求参数示例**(创建报价单): | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | name | String | 是 | 报价单名称 | | customerId | Long | 是 | 客户编号 | | businessId | Long | 否 | 商机编号 | | contactId | Long | 否 | 联系人编号 | | quotationTime | LocalDateTime | 否 | 报价日期 | | validUntil | LocalDateTime | 否 | 有效期至 | | discountPercent | BigDecimal | 否 | 整单折扣 | | taxRate | BigDecimal | 否 | 税率 | | fileUrl | String | 否 | 附件地址 | | remark | String | 否 | 备注 | | items | List | 是 | 物料明细列表 | **物料明细 Item**: | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | itemId | Long | 是 | MDM 物料编号 | | quotationPrice | BigDecimal | 是 | 报价单价 | | count | BigDecimal | 是 | 数量 | | taxPercent | BigDecimal | 否 | 税率 | | remark | String | 否 | 备注 | ### 11.3 前端修改点 #### 报价单列表页面 **文件路径:** `/src/views/crm/sale-quotation/index.vue` ```html ``` **提交审批弹窗**: ```html ``` **JS 方法**: ```js import { getSaleQuotationApproveProcessList, submitSaleQuotation, convertSaleQuotationToContract } from '@/api/crm/saleQuotation' methods: { async handleSubmit(row) { const { data } = await getSaleQuotationApproveProcessList() this.processList = data this.currentRow = row this.submitDialogVisible = true }, async confirmSubmit() { if (!this.selectedProcessKey) { this.$message.warning('请选择审批流程') return } this.submitLoading = true try { await submitSaleQuotation(this.currentRow.id, this.selectedProcessKey) this.$message.success('提交成功') this.submitDialogVisible = false this.getList() } finally { this.submitLoading = false } }, async handleConvertContract(row) { try { await this.$confirm('确认将该报价单转为合同?', '提示') const { data } = await convertSaleQuotationToContract(row.id) this.$message.success('转合同成功') this.$router.push({ path: '/crm/contract/detail', query: { id: data } }) } catch (e) { if (e !== 'cancel') this.$message.error(e.message || '转合同失败') } } } ``` #### 报价单表单页面 - MDM 物料选择 **文件路径:** `/src/views/crm/sale-quotation/form.vue` 由于报价单使用 MDM 物料,需要使用 MDM 物料选择组件: ```html ``` **API 定义文件**:`/src/api/crm/saleQuotation.js` ```js import request from '@/utils/request' // 创建报价单 export function createSaleQuotation(data) { return request({ url: '/crm/sale-quotation/create', method: 'post', data }) } // 更新报价单 export function updateSaleQuotation(data) { return request({ url: '/crm/sale-quotation/update', method: 'put', data }) } // 删除报价单 export function deleteSaleQuotation(ids) { return request({ url: '/crm/sale-quotation/delete', method: 'delete', params: { ids } }) } // 获取报价单详情 export function getSaleQuotation(id) { return request({ url: '/crm/sale-quotation/get', method: 'get', params: { id } }) } // 分页查询 export function getSaleQuotationPage(params) { return request({ url: '/crm/sale-quotation/page', method: 'get', params }) } // 提交审批 export function submitSaleQuotation(id, processDefinitionKey) { return request({ url: '/crm/sale-quotation/submit', method: 'put', params: { id, processDefinitionKey } }) } // 获取审批流程列表 export function getSaleQuotationApproveProcessList() { return request({ url: '/crm/sale-quotation/approve-process-list', method: 'get' }) } // 转合同 export function convertSaleQuotationToContract(id) { return request({ url: '/crm/sale-quotation/convert-contract', method: 'post', params: { id } }) } ``` ### 11.4 业务流程 ``` 商机(可选) → 销售报价(草稿) → 提交审批 → 审批中 → 审批通过 → 转合同 ↓ 审批拒绝 → 重新提交或修改 ``` ### 11.5 注意事项 1. 报价单和合同统一使用 **MDM 物料**(`MdmItemApi`),与 ERP 销售订单一致 2. BPM 分类编码 `sale_quotation_approve` 需要在 BPM 管理后台创建 3. 报价单转合同时,物料明细直接复制到合同产品表(字段一致) 4. 报价单号前缀为 `QT`,格式:QT + yyyyMMdd + 6位自增序号 5. 商机关联为可选,报价单可以独立创建不依赖商机 ### 11.6 合同产品字段变更说明 合同产品表 `crm_contract_product` 已改为使用 MDM 物料: | 原字段 | 新字段 | 说明 | |--------|--------|------| | `product_id` | `item_id` | MDM 物料编号 | | `product_price` | `item_price` | 物料原价 | | - | `item_unit_id` | 计量单位编号 | 前端表单需要使用 MDM 物料选择组件替代 CRM 产品选择组件。 --- ## 十、产出文档清单 | 文档 | 说明 | |------|------| | `docs/crm_erp_integration_frontend.md` | 本文档 | | `docs/crm_erp_integration_sql.sql` | SQL 变更脚本 | --- ## 十二、销售订单作废功能(新增) ### 12.1 功能说明 销售订单作废功能用于标记已审核的订单为"已作废"状态,实现订单全流程可追溯。 **业务规则:** - 只有已审核的订单才能作废 - 已出库或已退货的订单不能作废 - 作废后订单状态变为"已作废" - 作废后同步更新关联合同状态为"已作废" - 管理员可以重新启用已作废的订单 ### 12.2 状态枚举 | 状态值 | 枚举 | 说明 | |--------|------|------| | 10 | PROCESS | 未审核 | | 20 | APPROVE | 已审核 | | 30 | CANCEL | 已作废 | ### 12.3 合同状态同步 当销售订单作废/启用时,会自动同步关联合同的状态: | 操作 | 订单状态变化 | 合同状态变化 | |------|--------------|--------------| | 作废订单 | 已审核 → 已作废 | 审核通过 → 已作废 | | 启用订单 | 已作废 → 已审核 | 已作废 → 审核通过 | **合同状态枚举:** | 状态值 | 枚举 | 说明 | |--------|------|------| | 0 | DRAFT | 未提交 | | 10 | PROCESS | 审批中 | | 20 | APPROVE | 审核通过 | | 30 | REJECT | 审核不通过 | | 40 | CANCEL | 已取消 | | 50 | INVALID | 已作废 | ### 12.4 API 接口 | 方法 | 路径 | 说明 | |------|------|------| | PUT | /erp/sale-order/cancel | 作废销售订单 | | PUT | /erp/sale-order/enable | 重新启用销售订单 | **请求参数:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | id | Long | 是 | 销售订单编号 | **响应:** ```json { "code": 0, "msg": "操作成功" } ``` **错误码:** | 错误码 | 说明 | |--------|------| | 1_020_201_011 | 作废失败,订单未审核 | | 1_020_201_012 | 作废失败,已存在对应的销售出库单 | | 1_020_201_013 | 作废失败,已存在对应的销售退货单 | | 1_020_201_014 | 作废失败,请刷新页面后重试 | | 1_020_201_015 | 启用失败,订单未作废 | | 1_020_201_016 | 启用失败,请刷新页面后重试 | ### 12.5 前端修改点 **文件路径:** `/src/views/erp/sale/order/index.vue` **操作列按钮:** ```html ``` **状态标签:** ```html ``` **关联合同列(作废时显示红色):** ```html ``` **JS 方法:** ```js import { cancelSaleOrder, enableSaleOrder } from '@/api/erp/saleOrder' methods: { async handleCancel(row) { try { await this.$confirm('确认作废该销售订单?作废后关联合同也将标记为已作废!', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) await cancelSaleOrder(row.id) this.$message.success('作废成功') this.getList() } catch (e) { if (e !== 'cancel') { this.$message.error(e.message || '作废失败') } } }, async handleEnable(row) { try { await this.$confirm('确认重新启用该销售订单?启用后关联合同将恢复为审核通过状态!', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'info' }) await enableSaleOrder(row.id) this.$message.success('启用成功') this.getList() } catch (e) { if (e !== 'cancel') { this.$message.error(e.message || '启用失败') } } } } ``` **API 定义文件:** `/src/api/erp/saleOrder.js` ```js // 作废销售订单 export function cancelSaleOrder(id) { return request({ url: '/erp/sale-order/cancel', method: 'put', params: { id } }) } // 重新启用销售订单 export function enableSaleOrder(id) { return request({ url: '/erp/sale-order/enable', method: 'put', params: { id } }) } ``` ### 12.6 注意事项 1. 作废操作会同步更新关联合同状态为"已作废" 2. 启用操作会同步更新关联合同状态为"审核通过" 3. 已出库/已退货的订单不能作废,需先处理相关业务单据 4. 作废后订单号显示为红色,提醒用户该订单已失效 5. 启用功能需要管理员权限(`erp:sale-order:enable`)