Batch2-产销建模迁移方案.md 39 KB

Batch 2:产销建模(Sales)迁移方案

本文档是 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 代码实际生成 1321000001000L1321000000000 + 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

namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;

/// <summary>客户主数据(S0 Sales / sales_customer)</summary>
[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

namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;

/// <summary>物料主数据(S0 Sales / sales_material)</summary>
[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

namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;

/// <summary>订单优先规则(S0 Sales / sales_order_priority_rule)</summary>
[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

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}

查询过滤逻辑:

.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}

查询过滤逻辑:

.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}

查询过滤逻辑:

.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

using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;

InitTables 追加

typeof(AdoS0Customer), typeof(AdoS0Material), typeof(AdoS0OrderPriorityRule)

七、前端 API(s0SalesApi.ts)

文件:Web/src/views/aidop/s0/api/s0SalesApi.ts

要点

  • import service from '/@/utils/request'
  • 3 组 API 对象:s0CustomersApis0MaterialsApis0OrderPriorityRulesApi
  • 每组提供 listgetcreateupdatedelete 方法
  • TS 接口:S0CustomerRow / S0CustomerUpsertS0MaterialRow / S0MaterialUpsertS0OrderPriorityRuleRow / S0OrderPriorityRuleUpsert
  • 公共分页接口 Paged<T> { 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.csHasData() 末尾(BuildAidopSmartOpsSeedMenus 之后)追加:

foreach (var m in BuildS0SalesMenus(ct)) list.Add(m);

私有方法:

private static IEnumerable<SysMenu> 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_customerado_s0_sales_materialado_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 表需先于制造迁移完成