# 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 表需先于制造迁移完成 |