# Batch 2:产销建模(Sales)迁移方案 > 本文档是 [S0迁移.md](S0迁移.md) Batch 2 的**细化版**,可直接交由 Cursor / AI 编程助手逐步执行。 --- ## 〇、与原计划的修正 对比 `S0迁移.md` Batch 2 部分,本方案做了以下修正: | # | 原计划描述 | 实际情况 | 修正 | |---|-----------|---------|------| | 1 | Batch 2 含 Bom / BomItem(6 个实体) | `S0.Domain.Sales/` 只有 Customer、Material、OrderPriorityRule 3 个实体,BOM 不在 Sales 域 | 移除 Bom/BomItem,实体数改为 **3** | | 2 | Customer 仅列 8 个字段 | 实际含 NameEn、CustomerType、ForbidStatus、Currency、IsTaxIncluded、PrimarySales、BackupSales、CustomerLevel 等 **20 字段** | 补全 | | 3 | Material 仅列 10 个字段 | 实际含 30+ 字段(库存、采购、质量、批次管理等参数) | 补全 | | 4 | BOM 主从页面 | 不属于 Sales 域 | 移除 | | 5 | S0 菜单根目录 ID = `1321001000000L` | 代码实际生成 `1321000001000L`(`1321000000000 + 1*1000`) | 修正所有 S0DirId 引用 | | 6 | Batch 1 需新建 4 实体 + 4 页面 | S0 Platform 与 Admin.NET 的 SysOrg/SysDictType/SysDictData 同构 | 改为复用平台 + 灌入种子数据,无需新建页面 | --- ## 一、源文件清单(只读) ### 1.1 后端 ``` /home/yy968/work/s0/s0-operating-modeling/backend/src/ ├─ S0.Domain/ │ ├─ Common/BaseEntity.cs ← 公共基类(Id, CompanyId, FactoryId, IsEnabled, IsDeleted, VersionNo, CreatedBy, CreatedAt, UpdatedBy, UpdatedAt) │ └─ Sales/ │ ├─ Customer.cs ← [Table("sales_customer")] │ ├─ Material.cs ← [Table("sales_material")] │ └─ OrderPriorityRule.cs ← [Table("sales_order_priority_rule")] ├─ S0.Application/Sales/ │ ├─ Common/SalesPagedQuery.cs ← 分页基类(Keyword, IsEnabled, PageNo, PageSize) │ ├─ Customers/ │ │ ├─ CustomerDtos.cs ← CustomerUpsertRequest + CustomerDto │ │ ├─ ICustomerService.cs │ │ └─ CustomerService.cs │ ├─ Materials/ │ │ ├─ MaterialDtos.cs ← MaterialQuery + MaterialUpsertRequest + MaterialDto │ │ ├─ IMaterialService.cs │ │ └─ MaterialService.cs │ └─ OrderPriorityRules/ │ ├─ OrderPriorityRuleDtos.cs ← OrderPriorityRuleQuery + UpsertRequest + Dto │ ├─ IOrderPriorityRuleService.cs │ └─ OrderPriorityRuleService.cs └─ S0.Api/Controllers/Sales/ ├─ CustomersController.cs ← api/sales/customers ├─ MaterialsController.cs ← api/sales/materials └─ OrderPriorityRulesController.cs ← api/sales/order-priority-rules ``` ### 1.2 前端 ``` /home/yy968/work/s0/s0-operating-modeling/src/ ├─ api/modules/sales.js ← customersApi / materialsApi / orderPriorityRulesApi └─ views/ ├─ CustomerManagement.vue ← 路由 /sales-modeling/customers ├─ MaterialManagement.vue ← 路由 /sales-modeling/materials └─ OrderPriorityConfig.vue ← 路由 /sales-modeling/order-priority ``` --- ## 二、目标文件清单 ### 2.1 后端新建 ``` server/Plugins/Admin.NET.Plugin.AiDOP/ ├─ Entity/S0/Sales/ │ ├─ AdoS0Customer.cs ← ado_s0_sales_customer │ ├─ AdoS0Material.cs ← ado_s0_sales_material │ └─ AdoS0OrderPriorityRule.cs ← ado_s0_sales_order_priority_rule ├─ Dto/S0/Sales/ │ └─ AdoS0SalesDtos.cs ← 3 组 QueryDto + UpsertDto └─ Controllers/S0/Sales/ ├─ AdoS0CustomersController.cs ├─ AdoS0MaterialsController.cs └─ AdoS0OrderPriorityRulesController.cs ``` ### 2.2 后端修改(追加,不替换) ``` server/Plugins/Admin.NET.Plugin.AiDOP/Startup.cs ← InitTables 追加 3 实体 + using server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs ← 追加 BuildS0SalesMenus()(注意是插件的 SeedData) ``` ### 2.3 前端新建 ``` Web/src/views/aidop/s0/ ├─ api/ │ └─ s0SalesApi.ts ← 3 组 API 函数 + TS 接口 └─ sales/ ├─ CustomerList.vue ← name="aidopS0SalesCustomer" ├─ MaterialList.vue ← name="aidopS0SalesMaterial" └─ OrderPriorityRuleList.vue ← name="aidopS0SalesOrderPriorityRule" ``` --- ## 三、实体字段完整映射 > 规则回顾:BaseEntity 字段内联(去掉 IsDeleted / VersionNo / CreatedBy / UpdatedBy);不继承基类;不迁移导航属性。 ### 3.1 AdoS0Customer → `ado_s0_sales_customer` ```csharp namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales; /// 客户主数据(S0 Sales / sales_customer) [SugarTable("ado_s0_sales_customer", "S0 客户主数据")] public class AdoS0Customer { [SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")] public long Id { get; set; } [SugarColumn(ColumnDescription = "关联公司 ID", ColumnDataType = "bigint")] public long CompanyRefId { get; set; } [SugarColumn(ColumnDescription = "关联工厂 ID", ColumnDataType = "bigint")] public long FactoryRefId { get; set; } [SugarColumn(ColumnDescription = "客户编码", Length = 100)] public string Code { get; set; } = string.Empty; [SugarColumn(ColumnDescription = "客户名称", Length = 200)] public string Name { get; set; } = string.Empty; [SugarColumn(ColumnDescription = "英文名称", Length = 200, IsNullable = true)] public string? NameEn { get; set; } [SugarColumn(ColumnDescription = "客户类型", Length = 100, IsNullable = true)] public string? CustomerType { get; set; } [SugarColumn(ColumnDescription = "联系人", Length = 100, IsNullable = true)] public string? ContactPerson { get; set; } [SugarColumn(ColumnDescription = "联系电话", Length = 50, IsNullable = true)] public string? ContactPhone { get; set; } [SugarColumn(ColumnDescription = "地址", Length = 500, IsNullable = true)] public string? Address { get; set; } [SugarColumn(ColumnDescription = "禁用状态", Length = 20)] public string ForbidStatus { get; set; } = "normal"; [SugarColumn(ColumnDescription = "币种", Length = 50, IsNullable = true)] public string? Currency { get; set; } [SugarColumn(ColumnDescription = "是否含税", ColumnDataType = "boolean")] public bool IsTaxIncluded { get; set; } = true; [SugarColumn(ColumnDescription = "主销售员", Length = 100, IsNullable = true)] public string? PrimarySales { get; set; } [SugarColumn(ColumnDescription = "备用销售员", Length = 100, IsNullable = true)] public string? BackupSales { get; set; } [SugarColumn(ColumnDescription = "客户等级")] public int CustomerLevel { get; set; } = 1; [SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)] public string? Remark { get; set; } [SugarColumn(ColumnDescription = "启用", ColumnDataType = "boolean")] public bool IsEnabled { get; set; } = true; [SugarColumn(ColumnDescription = "创建时间")] public DateTime CreatedAt { get; set; } = DateTime.Now; [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)] public DateTime? UpdatedAt { get; set; } } ``` **字段来源对照表**: | # | 目标字段 | C# 类型 | 长度/精度 | 默认值 | S0 原列名 | S0 来源 | |---|---------|---------|----------|--------|-----------|---------| | 1 | Id | long | — | PK 自增 | id | BaseEntity | | 2 | CompanyRefId | long | — | — | company_id | BaseEntity | | 3 | FactoryRefId | long | — | — | factory_id | BaseEntity | | 4 | Code | string | 100 | — | code | Customer | | 5 | Name | string | 200 | — | name | Customer | | 6 | NameEn | string? | 200 | — | name_en | Customer | | 7 | CustomerType | string? | 100 | — | customer_type | Customer | | 8 | ContactPerson | string? | 100 | — | contact_person | Customer | | 9 | ContactPhone | string? | 50 | — | contact_phone | Customer | | 10 | Address | string? | 500 | — | address | Customer | | 11 | ForbidStatus | string | 20 | "normal" | forbid_status | Customer | | 12 | Currency | string? | 50 | — | currency | Customer | | 13 | IsTaxIncluded | bool | — | true | is_tax_included | Customer | | 14 | PrimarySales | string? | 100 | — | primary_sales | Customer | | 15 | BackupSales | string? | 100 | — | backup_sales | Customer | | 16 | CustomerLevel | int | — | 1 | customer_level | Customer | | 17 | Remark | string? | 500 | — | remark | Customer | | 18 | IsEnabled | bool | — | true | is_enabled | BaseEntity | | 19 | CreatedAt | DateTime | — | Now | created_at | BaseEntity | | 20 | UpdatedAt | DateTime? | — | — | updated_at | BaseEntity | > **不迁移字段**:IsDeleted、VersionNo、CreatedBy、UpdatedBy(遵循总体原则) --- ### 3.2 AdoS0Material → `ado_s0_sales_material` ```csharp namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales; /// 物料主数据(S0 Sales / sales_material) [SugarTable("ado_s0_sales_material", "S0 物料主数据")] public class AdoS0Material { [SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")] public long Id { get; set; } [SugarColumn(ColumnDescription = "关联公司 ID", ColumnDataType = "bigint")] public long CompanyRefId { get; set; } [SugarColumn(ColumnDescription = "关联工厂 ID", ColumnDataType = "bigint")] public long FactoryRefId { get; set; } // ── 基本信息 ── [SugarColumn(ColumnDescription = "物料编码", Length = 100)] public string Code { get; set; } = string.Empty; [SugarColumn(ColumnDescription = "物料名称", Length = 200)] public string Name { get; set; } = string.Empty; [SugarColumn(ColumnDescription = "英文名称", Length = 200, IsNullable = true)] public string? NameEn { get; set; } [SugarColumn(ColumnDescription = "物料类型", Length = 100, IsNullable = true)] public string? MaterialType { get; set; } [SugarColumn(ColumnDescription = "单位", Length = 50, IsNullable = true)] public string? Unit { get; set; } [SugarColumn(ColumnDescription = "规格型号", Length = 200, IsNullable = true)] public string? Spec { get; set; } [SugarColumn(ColumnDescription = "计划类别", Length = 100, IsNullable = true)] public string? PlCategory { get; set; } [SugarColumn(ColumnDescription = "图纸编号", Length = 100, IsNullable = true)] public string? DrawingNo { get; set; } [SugarColumn(ColumnDescription = "语言版本", Length = 50, IsNullable = true)] public string? Language { get; set; } [SugarColumn(ColumnDescription = "业务版本", Length = 50, IsNullable = true)] public string? BizVersion { get; set; } [SugarColumn(ColumnDescription = "产品编码", Length = 100, IsNullable = true)] public string? ProductCode { get; set; } [SugarColumn(ColumnDescription = "物料属性", Length = 50, IsNullable = true)] public string? MaterialAttribute { get; set; } // ── 库存参数 ── [SugarColumn(ColumnDescription = "默认库位 ID", ColumnDataType = "bigint", IsNullable = true)] public long? DefaultLocationId { get; set; } [SugarColumn(ColumnDescription = "默认货架 ID", ColumnDataType = "bigint", IsNullable = true)] public long? DefaultRackId { get; set; } [SugarColumn(ColumnDescription = "库存类型编码", Length = 50, IsNullable = true)] public string? StockTypeCode { get; set; } [SugarColumn(ColumnDescription = "安全库存", ColumnDataType = "decimal(18,5)", IsNullable = true)] public decimal? SafetyStock { get; set; } [SugarColumn(ColumnDescription = "保质期(天)", IsNullable = true)] public int? ShelfLifeDays { get; set; } [SugarColumn(ColumnDescription = "到期预警天数", IsNullable = true)] public int? ExpireWarningDays { get; set; } // ── 采购参数 ── [SugarColumn(ColumnDescription = "采购提前期(天)", IsNullable = true)] public int? PurchaseLeadDays { get; set; } [SugarColumn(ColumnDescription = "最小订货量", ColumnDataType = "decimal(18,5)", IsNullable = true)] public decimal? MinOrderQty { get; set; } [SugarColumn(ColumnDescription = "最大订货量", ColumnDataType = "decimal(18,5)", IsNullable = true)] public decimal? MaxOrderQty { get; set; } [SugarColumn(ColumnDescription = "订货倍数", ColumnDataType = "decimal(18,5)", IsNullable = true)] public decimal? OrderMultiple { get; set; } [SugarColumn(ColumnDescription = "备料提前期(天)", IsNullable = true)] public int? PreparationLeadDays { get; set; } [SugarColumn(ColumnDescription = "按需采购", ColumnDataType = "boolean")] public bool IsOnDemand { get; set; } [SugarColumn(ColumnDescription = "特殊需求类型", Length = 50, IsNullable = true)] public string? SpecialReqType { get; set; } // ── 质量 / 管控 ── [SugarColumn(ColumnDescription = "需检验", ColumnDataType = "boolean")] public bool IsInspectionRequired { get; set; } [SugarColumn(ColumnDescription = "检验天数", IsNullable = true)] public int? InspectionDays { get; set; } [SugarColumn(ColumnDescription = "关键物料", ColumnDataType = "boolean")] public bool IsKeyMaterial { get; set; } [SugarColumn(ColumnDescription = "主要物料", ColumnDataType = "boolean")] public bool IsMainMaterial { get; set; } [SugarColumn(ColumnDescription = "需预处理", ColumnDataType = "boolean")] public bool IsPreprocess { get; set; } [SugarColumn(ColumnDescription = "自动批次", ColumnDataType = "boolean")] public bool IsAutoBatch { get; set; } [SugarColumn(ColumnDescription = "需打标签", ColumnDataType = "boolean")] public bool IsLabelRequired { get; set; } // ── 批次管理 ── [SugarColumn(ColumnDescription = "批次先进先出提醒", ColumnDataType = "boolean")] public bool IsBatchFifoReminder { get; set; } [SugarColumn(ColumnDescription = "批次先进先出严格", ColumnDataType = "boolean")] public bool IsBatchFifoStrict { get; set; } [SugarColumn(ColumnDescription = "库存周转率", ColumnDataType = "decimal(18,5)", IsNullable = true)] public decimal? InventoryTurnoverRate { get; set; } // ── 状态 ── [SugarColumn(ColumnDescription = "禁用状态", Length = 20)] public string ForbidStatus { get; set; } = "normal"; [SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)] public string? Remark { get; set; } [SugarColumn(ColumnDescription = "启用", ColumnDataType = "boolean")] public bool IsEnabled { get; set; } = true; [SugarColumn(ColumnDescription = "创建时间")] public DateTime CreatedAt { get; set; } = DateTime.Now; [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)] public DateTime? UpdatedAt { get; set; } } ``` **字段来源对照表**(共 39 字段): | # | 目标字段 | C# 类型 | S0 原列名 | S0 来源 | |---|---------|---------|-----------|---------| | 1 | Id | long | id | BaseEntity | | 2 | CompanyRefId | long | company_id | BaseEntity | | 3 | FactoryRefId | long | factory_id | BaseEntity | | 4 | Code | string(100) | code | Material | | 5 | Name | string(200) | name | Material | | 6 | NameEn | string?(200) | name_en | Material | | 7 | MaterialType | string?(100) | material_type | Material | | 8 | Unit | string?(50) | unit | Material | | 9 | Spec | string?(200) | spec | Material | | 10 | PlCategory | string?(100) | pl_category | Material | | 11 | DrawingNo | string?(100) | drawing_no | Material | | 12 | Language | string?(50) | language | Material | | 13 | BizVersion | string?(50) | biz_version | Material | | 14 | ProductCode | string?(100) | product_code | Material | | 15 | MaterialAttribute | string?(50) | material_attribute | Material | | 16 | DefaultLocationId | long? | default_location_id | Material | | 17 | DefaultRackId | long? | default_rack_id | Material | | 18 | StockTypeCode | string?(50) | stock_type_code | Material | | 19 | SafetyStock | decimal?(18,5) | safety_stock | Material | | 20 | ShelfLifeDays | int? | shelf_life_days | Material | | 21 | ExpireWarningDays | int? | expire_warning_days | Material | | 22 | PurchaseLeadDays | int? | purchase_lead_days | Material | | 23 | MinOrderQty | decimal?(18,5) | min_order_qty | Material | | 24 | MaxOrderQty | decimal?(18,5) | max_order_qty | Material | | 25 | OrderMultiple | decimal?(18,5) | order_multiple | Material | | 26 | PreparationLeadDays | int? | preparation_lead_days | Material | | 27 | IsOnDemand | bool | is_on_demand | Material | | 28 | SpecialReqType | string?(50) | special_req_type | Material | | 29 | IsInspectionRequired | bool | is_inspection_required | Material | | 30 | InspectionDays | int? | inspection_days | Material | | 31 | IsKeyMaterial | bool | is_key_material | Material | | 32 | IsMainMaterial | bool | is_main_material | Material | | 33 | IsPreprocess | bool | is_preprocess | Material | | 34 | IsAutoBatch | bool | is_auto_batch | Material | | 35 | IsLabelRequired | bool | is_label_required | Material | | 36 | IsBatchFifoReminder | bool | is_batch_fifo_reminder | Material | | 37 | IsBatchFifoStrict | bool | is_batch_fifo_strict | Material | | 38 | InventoryTurnoverRate | decimal?(18,5) | inventory_turnover_rate | Material | | 39 | ForbidStatus | string(20) | forbid_status | Material | | 40 | Remark | string?(500) | remark | Material | | 41 | IsEnabled | bool | is_enabled | BaseEntity | | 42 | CreatedAt | DateTime | created_at | BaseEntity | | 43 | UpdatedAt | DateTime? | updated_at | BaseEntity | > **跨 Batch 依赖**:DefaultLocationId / DefaultRackId 关联仓储 Location / Rack(Batch 4)。Batch 2 阶段前端用 `el-input-number` 输入 ID;Batch 4 完成后升级为仓储下拉。 --- ### 3.3 AdoS0OrderPriorityRule → `ado_s0_sales_order_priority_rule` ```csharp namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales; /// 订单优先规则(S0 Sales / sales_order_priority_rule) [SugarTable("ado_s0_sales_order_priority_rule", "S0 订单优先规则")] public class AdoS0OrderPriorityRule { [SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")] public long Id { get; set; } [SugarColumn(ColumnDescription = "关联公司 ID", ColumnDataType = "bigint")] public long CompanyRefId { get; set; } [SugarColumn(ColumnDescription = "关联工厂 ID", ColumnDataType = "bigint")] public long FactoryRefId { get; set; } [SugarColumn(ColumnDescription = "规则编码", Length = 100)] public string Code { get; set; } = string.Empty; [SugarColumn(ColumnDescription = "规则名称", Length = 200)] public string Name { get; set; } = string.Empty; [SugarColumn(ColumnDescription = "优先级别")] public int PriorityLevel { get; set; } [SugarColumn(ColumnDescription = "排序方向", Length = 10)] public string SortDirection { get; set; } = "ASC"; [SugarColumn(ColumnDescription = "来源实体", Length = 200, IsNullable = true)] public string? SourceEntity { get; set; } [SugarColumn(ColumnDescription = "来源字段", Length = 200, IsNullable = true)] public string? SourceField { get; set; } [SugarColumn(ColumnDescription = "来源字段类型", Length = 100, IsNullable = true)] public string? SourceFieldType { get; set; } [SugarColumn(ColumnDescription = "来源关联字段", Length = 200, IsNullable = true)] public string? SourceLinkField { get; set; } [SugarColumn(ColumnDescription = "工单字段", Length = 200, IsNullable = true)] public string? WorkOrderField { get; set; } [SugarColumn(ColumnDescription = "工单字段类型", Length = 100, IsNullable = true)] public string? WorkOrderFieldType { get; set; } [SugarColumn(ColumnDescription = "工单关联字段", Length = 200, IsNullable = true)] public string? WorkOrderLinkField { get; set; } [SugarColumn(ColumnDescription = "规则表达式", Length = 1000, IsNullable = true)] public string? RuleExpr { get; set; } [SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)] public string? Remark { get; set; } [SugarColumn(ColumnDescription = "启用", ColumnDataType = "boolean")] public bool IsEnabled { get; set; } = true; [SugarColumn(ColumnDescription = "创建时间")] public DateTime CreatedAt { get; set; } = DateTime.Now; [SugarColumn(ColumnDescription = "更新时间", IsNullable = true)] public DateTime? UpdatedAt { get; set; } } ``` **字段来源对照表**(共 19 字段): | # | 目标字段 | C# 类型 | S0 原列名 | |---|---------|---------|-----------| | 1 | Id | long | id | | 2 | CompanyRefId | long | company_id | | 3 | FactoryRefId | long | factory_id | | 4 | Code | string(100) | code | | 5 | Name | string(200) | name | | 6 | PriorityLevel | int | priority_level | | 7 | SortDirection | string(10) | sort_direction | | 8 | SourceEntity | string?(200) | source_entity | | 9 | SourceField | string?(200) | source_field | | 10 | SourceFieldType | string?(100) | source_field_type | | 11 | SourceLinkField | string?(200) | source_link_field | | 12 | WorkOrderField | string?(200) | work_order_field | | 13 | WorkOrderFieldType | string?(100) | work_order_field_type | | 14 | WorkOrderLinkField | string?(200) | work_order_link_field | | 15 | RuleExpr | string?(1000) | rule_expr | | 16 | Remark | string?(500) | remark | | 17 | IsEnabled | bool | is_enabled | | 18 | CreatedAt | DateTime | created_at | | 19 | UpdatedAt | DateTime? | updated_at | --- ## 四、DTO 设计 文件:`server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S0/Sales/AdoS0SalesDtos.cs` ```csharp namespace Admin.NET.Plugin.AiDOP.Dto.S0.Sales; // ══════════════════════════════════════ // Customer // ══════════════════════════════════════ public class AdoS0CustomerQueryDto { public string? Keyword { get; set; } // 模糊搜索 Code / Name public bool? IsEnabled { get; set; } public int Page { get; set; } = 1; public int PageSize { get; set; } = 20; } public class AdoS0CustomerUpsertDto { public long CompanyRefId { get; set; } public long FactoryRefId { get; set; } public string Code { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public string? NameEn { get; set; } public string? CustomerType { get; set; } public string? ContactPerson { get; set; } public string? ContactPhone { get; set; } public string? Address { get; set; } public string ForbidStatus { get; set; } = "normal"; public string? Currency { get; set; } public bool IsTaxIncluded { get; set; } = true; public string? PrimarySales { get; set; } public string? BackupSales { get; set; } public int CustomerLevel { get; set; } = 1; public string? Remark { get; set; } public bool IsEnabled { get; set; } = true; } // ══════════════════════════════════════ // Material // ══════════════════════════════════════ public class AdoS0MaterialQueryDto { public string? Keyword { get; set; } // 模糊搜索 Code / Name public string? Code { get; set; } public string? Name { get; set; } public string? Spec { get; set; } public string? DrawingNo { get; set; } public string? PlCategory { get; set; } public string? MaterialType { get; set; } public string? Language { get; set; } public bool? IsEnabled { get; set; } public int Page { get; set; } = 1; public int PageSize { get; set; } = 20; } public class AdoS0MaterialUpsertDto { public long CompanyRefId { get; set; } public long FactoryRefId { get; set; } // 基本信息 public string Code { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public string? NameEn { get; set; } public string? MaterialType { get; set; } public string? Unit { get; set; } public string? Spec { get; set; } public string? PlCategory { get; set; } public string? DrawingNo { get; set; } public string? Language { get; set; } public string? BizVersion { get; set; } public string? ProductCode { get; set; } public string? MaterialAttribute { get; set; } // 库存参数 public long? DefaultLocationId { get; set; } public long? DefaultRackId { get; set; } public string? StockTypeCode { get; set; } public decimal? SafetyStock { get; set; } public int? ShelfLifeDays { get; set; } public int? ExpireWarningDays { get; set; } // 采购参数 public int? PurchaseLeadDays { get; set; } public decimal? MinOrderQty { get; set; } public decimal? MaxOrderQty { get; set; } public decimal? OrderMultiple { get; set; } public int? PreparationLeadDays { get; set; } public bool IsOnDemand { get; set; } public string? SpecialReqType { get; set; } // 质量 / 管控 public bool IsInspectionRequired { get; set; } public int? InspectionDays { get; set; } public bool IsKeyMaterial { get; set; } public bool IsMainMaterial { get; set; } public bool IsPreprocess { get; set; } public bool IsAutoBatch { get; set; } public bool IsLabelRequired { get; set; } // 批次管理 public bool IsBatchFifoReminder { get; set; } public bool IsBatchFifoStrict { get; set; } public decimal? InventoryTurnoverRate { get; set; } // 状态 public string ForbidStatus { get; set; } = "normal"; public bool IsEnabled { get; set; } = true; public string? Remark { get; set; } } // ══════════════════════════════════════ // OrderPriorityRule // ══════════════════════════════════════ public class AdoS0OrderPriorityRuleQueryDto { public string? Keyword { get; set; } // 模糊搜索 Code / Name public string? SourceEntity { get; set; } public bool? IsEnabled { get; set; } public int Page { get; set; } = 1; public int PageSize { get; set; } = 20; } public class AdoS0OrderPriorityRuleUpsertDto { public long CompanyRefId { get; set; } public long FactoryRefId { get; set; } public string Code { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public int PriorityLevel { get; set; } public string SortDirection { get; set; } = "ASC"; public string? SourceEntity { get; set; } public string? SourceField { get; set; } public string? SourceFieldType { get; set; } public string? SourceLinkField { get; set; } public string? WorkOrderField { get; set; } public string? WorkOrderFieldType { get; set; } public string? WorkOrderLinkField { get; set; } public string? RuleExpr { get; set; } public string? Remark { get; set; } public bool IsEnabled { get; set; } = true; } ``` --- ## 五、控制器路由 ### 5.1 AdoS0CustomersController 路由 `api/s0/sales/customers`,风格对标 `OrderController.cs`: ``` GET /api/s0/sales/customers ← 分页(keyword, isEnabled, page, pageSize) GET /api/s0/sales/customers/{id:long} POST /api/s0/sales/customers PUT /api/s0/sales/customers/{id:long} DELETE /api/s0/sales/customers/{id:long} ``` 查询过滤逻辑: ```csharp .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => x.Code.Contains(q.Keyword!) || x.Name.Contains(q.Keyword!)) .WhereIF(q.IsEnabled.HasValue, x => x.IsEnabled == q.IsEnabled!.Value) ``` ### 5.2 AdoS0MaterialsController 路由 `api/s0/sales/materials`: ``` GET /api/s0/sales/materials ← 分页(多条件过滤) GET /api/s0/sales/materials/{id:long} POST /api/s0/sales/materials PUT /api/s0/sales/materials/{id:long} DELETE /api/s0/sales/materials/{id:long} ``` 查询过滤逻辑: ```csharp .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => x.Code.Contains(q.Keyword!) || x.Name.Contains(q.Keyword!)) .WhereIF(!string.IsNullOrWhiteSpace(q.Code), x => x.Code.Contains(q.Code!)) .WhereIF(!string.IsNullOrWhiteSpace(q.Name), x => x.Name.Contains(q.Name!)) .WhereIF(!string.IsNullOrWhiteSpace(q.Spec), x => x.Spec!.Contains(q.Spec!)) .WhereIF(!string.IsNullOrWhiteSpace(q.DrawingNo), x => x.DrawingNo!.Contains(q.DrawingNo!)) .WhereIF(!string.IsNullOrWhiteSpace(q.PlCategory), x => x.PlCategory == q.PlCategory) .WhereIF(!string.IsNullOrWhiteSpace(q.MaterialType), x => x.MaterialType == q.MaterialType) .WhereIF(!string.IsNullOrWhiteSpace(q.Language), x => x.Language == q.Language) .WhereIF(q.IsEnabled.HasValue, x => x.IsEnabled == q.IsEnabled!.Value) ``` ### 5.3 AdoS0OrderPriorityRulesController 路由 `api/s0/sales/order-priority-rules`: ``` GET /api/s0/sales/order-priority-rules ← 分页(keyword, sourceEntity, isEnabled) GET /api/s0/sales/order-priority-rules/{id:long} POST /api/s0/sales/order-priority-rules PUT /api/s0/sales/order-priority-rules/{id:long} DELETE /api/s0/sales/order-priority-rules/{id:long} ``` 查询过滤逻辑: ```csharp .WhereIF(!string.IsNullOrWhiteSpace(q.Keyword), x => x.Code.Contains(q.Keyword!) || x.Name.Contains(q.Keyword!)) .WhereIF(!string.IsNullOrWhiteSpace(q.SourceEntity), x => x.SourceEntity == q.SourceEntity) .WhereIF(q.IsEnabled.HasValue, x => x.IsEnabled == q.IsEnabled!.Value) ``` --- ## 六、Startup.cs 修改 在 `server/Plugins/Admin.NET.Plugin.AiDOP/Startup.cs` 中: **顶部追加 using**: ```csharp using Admin.NET.Plugin.AiDOP.Entity.S0.Sales; ``` **InitTables 追加**: ```csharp typeof(AdoS0Customer), typeof(AdoS0Material), typeof(AdoS0OrderPriorityRule) ``` --- ## 七、前端 API(s0SalesApi.ts) 文件:`Web/src/views/aidop/s0/api/s0SalesApi.ts` **要点**: - `import service from '/@/utils/request'` - 3 组 API 对象:`s0CustomersApi`、`s0MaterialsApi`、`s0OrderPriorityRulesApi` - 每组提供 `list`、`get`、`create`、`update`、`delete` 方法 - TS 接口:`S0CustomerRow` / `S0CustomerUpsert`、`S0MaterialRow` / `S0MaterialUpsert`、`S0OrderPriorityRuleRow` / `S0OrderPriorityRuleUpsert` - 公共分页接口 `Paged { total, page, pageSize, list }` --- ## 八、前端页面设计 ### 8.1 CustomerList.vue(name="aidopS0SalesCustomer") - **搜索栏**:编码/名称关键字(el-input) - **表格列**:code、name、nameEn、customerType、contactPerson、contactPhone、forbidStatus(tag)、isEnabled(tag) - **操作**:编辑、删除 - **弹窗表单**: - companyRefId(下拉 → `SysOrgApi` 按 Type=201)、factoryRefId(下拉 → `SysOrgApi` 按 Type=501 + Pid 联动) - code(必填)、name(必填)、nameEn - customerType(下拉 → `SysDictDataApi` code=`s0_customer_type`) - contactPerson、contactPhone、address - forbidStatus(下拉 → `SysDictDataApi` code=`s0_forbid_status`) - currency(下拉 → `SysDictDataApi` code=`s0_currency`) - isTaxIncluded(switch) - primarySales、backupSales - customerLevel(el-input-number, min=1) - isEnabled(switch)、remark(textarea) - **组件引用**:`import AidopDemoShell from '../../components/AidopDemoShell.vue'` ### 8.2 MaterialList.vue(name="aidopS0SalesMaterial") - **搜索栏**:编码、名称、规格、图纸编号、计划类别(select)、物料类型(select)、语言(select) - **表格列**(精简展示):code、name、spec、unit、materialType、plCategory、isEnabled(tag) - **弹窗表单**(使用 `el-tabs` 分组): - **Tab 0 归属**:companyRefId(下拉 → `SysOrgApi` Type=201)、factoryRefId(下拉 → `SysOrgApi` Type=501 联动) - **Tab 1 基本信息**:code(必填)、name(必填)、nameEn、materialType(下拉 → `s0_material_type`)、unit、spec、plCategory(下拉 → `s0_pl_category`)、drawingNo、language、bizVersion、productCode、materialAttribute(下拉 → `s0_material_attribute`) - **Tab 2 库存参数**:stockTypeCode(下拉 → `s0_stock_type`)、safetyStock、shelfLifeDays、expireWarningDays、defaultLocationId(输入框,Batch 4 后升级)、defaultRackId(输入框,Batch 4 后升级) - **Tab 3 采购参数**:purchaseLeadDays、minOrderQty、maxOrderQty、orderMultiple、preparationLeadDays、isOnDemand(switch)、specialReqType(下拉 → `s0_special_req_type`) - **Tab 4 质量/管控**:isInspectionRequired(switch)、inspectionDays、isKeyMaterial(switch)、isMainMaterial(switch)、isPreprocess(switch)、isAutoBatch(switch)、isLabelRequired(switch) - **Tab 5 批次管理**:isBatchFifoReminder(switch)、isBatchFifoStrict(switch)、inventoryTurnoverRate - **Tab 6 状态**:forbidStatus(下拉 → `s0_forbid_status`)、isEnabled(switch)、remark(textarea) > 下拉选项统一从平台字典 API 获取(`SysDictDataApi.apiSysDictDataDataListCodeGet`), > 不再硬编码。S0 原 `MaterialManagement.vue` 中的 fallback 选项仅作为种子数据参考。 ### 8.3 OrderPriorityRuleList.vue(name="aidopS0SalesOrderPriorityRule") - **搜索栏**:编码/名称关键字、sourceEntity(select/input) - **表格列**:code、name、priorityLevel、sortDirection、sourceEntity、sourceField、isEnabled(tag) - **弹窗表单**: - code(必填)、name(必填) - priorityLevel(el-input-number) - sortDirection(select: ASC/DESC) - sourceEntity、sourceField、sourceFieldType、sourceLinkField - workOrderField、workOrderFieldType、workOrderLinkField - ruleExpr(el-input type="textarea" :rows="4") - isEnabled(switch)、remark(textarea) --- ## 九、菜单 SeedData 追加 在 `server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/SysMenuSeedData.cs` 的 `HasData()` 末尾(`BuildAidopSmartOpsSeedMenus` 之后)追加: ```csharp foreach (var m in BuildS0SalesMenus(ct)) list.Add(m); ``` 私有方法: ```csharp private static IEnumerable BuildS0SalesMenus(DateTime ct) { const long S0DirId = 1321000001000L; // 已存在的 S0 根目录(ModuleDefinitions 第 1 项:1321000000000 + 1*1000) const long SubDirId = 1329002000000L; // 产销建模 子目录 yield return new SysMenu { Id = SubDirId, Pid = S0DirId, Title = "产销建模", Path = "/aidop/s0/sales", Name = "aidopS0Sales", Component = "Layout", Icon = "ele-ShoppingCart", Type = MenuTypeEnum.Dir, CreateTime = ct, OrderNo = 20 }; yield return new SysMenu { Id = SubDirId + 1, Pid = SubDirId, Title = "客户管理", Path = "/aidop/s0/sales/customer", Name = "aidopS0SalesCustomer", Component = "/aidop/s0/sales/CustomerList", Icon = "ele-User", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 1 }; yield return new SysMenu { Id = SubDirId + 2, Pid = SubDirId, Title = "物料管理", Path = "/aidop/s0/sales/material", Name = "aidopS0SalesMaterial", Component = "/aidop/s0/sales/MaterialList", Icon = "ele-Box", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 2 }; yield return new SysMenu { Id = SubDirId + 3, Pid = SubDirId, Title = "订单优先规则", Path = "/aidop/s0/sales/order-priority-rule", Name = "aidopS0SalesOrderPriorityRule", Component = "/aidop/s0/sales/OrderPriorityRuleList", Icon = "ele-Sort", Type = MenuTypeEnum.Menu, CreateTime = ct, OrderNo = 3 }; } ``` --- ## 十、执行步骤 ``` 前置 确认 Batch 1 种子已就绪 → S0DictSeedData.cs 已创建,平台字典和组织节点已入库 Step 1 阅读本文档 + S0 源文件 → 理解字段定义 Step 2 新建 Entity/S0/Sales/ 下 3 个实体文件 → [SugarTable] + [SugarColumn] 格式 Step 3 新建 Dto/S0/Sales/AdoS0SalesDtos.cs → 3 组 QueryDto + UpsertDto Step 4 新建 Controllers/S0/Sales/ 下 3 个控制器 → [AllowAnonymous][NonUnify] 插件风格 Step 5 修改 Startup.cs → InitTables 追加 3 实体 + using Step 6 后端编译验证 → dotnet build 无报错 Step 7 新建 Web/src/views/aidop/s0/api/s0SalesApi.ts Step 8 新建 Web/src/views/aidop/s0/sales/ 下 3 个 Vue 页面 (下拉调用 SysOrgApi + SysDictDataApi,不硬编码选项) Step 9 修改插件 SysMenuSeedData.cs → 追加 BuildS0SalesMenus()(S0DirId=1321000001000L) Step 10 前端编译验证 → pnpm dev 无 TS 错误 Step 11 集成验收 → 按第十一节标准逐项检查 ``` --- ## 十一、验收标准 - [ ] 后端 `dotnet build` 无报错 - [ ] 开发环境启动后自动创建 3 张表:`ado_s0_sales_customer`、`ado_s0_sales_material`、`ado_s0_sales_order_priority_rule` - [ ] Swagger 中可见 `/api/s0/sales/*` 共 15 个接口(每实体 5 个 CRUD) - [ ] 前端 `pnpm dev` 无 TypeScript 类型错误 - [ ] 侧栏菜单 "Ai-DOP > S0 运营建模 > 产销建模" 可见,含 3 个子菜单 - [ ] 客户管理页:增删改查正常,表单含全部 17 个业务字段 - [ ] 物料管理页:分 Tab 表单正常保存,全部 39 个字段入库 - [ ] 订单优先规则页:增删改查正常,规则表达式可保存长文本 --- ## 十二、前置依赖与后续关联 ### 前置依赖(Batch 1 已改为平台复用方案) Batch 1 不再新建独立实体/页面,改为向 `SysOrg` + `SysDictType`/`SysDictData` 灌入种子数据。 产销建模页面中的下拉选择器直接调用平台标准 API: | 下拉项 | 数据源 API | 字典 Code / 过滤条件 | |--------|-----------|-------------------| | CompanyRefId(公司) | `SysOrgApi` 获取列表后按 `Type="201"` 过滤 | — | | FactoryRefId(工厂) | `SysOrgApi` 获取列表后按 `Type="501"` + `Pid=公司Id` 过滤 | — | | MaterialType(物料类型) | `SysDictDataApi.apiSysDictDataDataListCodeGet` | `s0_material_type` | | PlCategory(计划类别) | 同上 | `s0_pl_category` | | StockTypeCode(库存类型) | 同上 | `s0_stock_type` | | SpecialReqType(特殊需求) | 同上 | `s0_special_req_type` | | MaterialAttribute(物料属性) | 同上 | `s0_material_attribute` | | CustomerType(客户类型) | 同上 | `s0_customer_type` | | Currency(币种) | 同上 | `s0_currency` | | ForbidStatus(禁用状态) | 同上 | `s0_forbid_status` | ### 后续关联 | 后续 Batch | 关联点 | 动作 | |-----------|--------|------| | Batch 4(仓储) | Material.DefaultLocationId / DefaultRackId | 前端升级为仓储 Location/Rack 下拉 | | Batch 3(制造) | 制造实体引用 sales_material(物料 FK) | Material 表需先于制造迁移完成 |