|
|
@@ -4,6 +4,22 @@
|
|
|
|
|
|
---
|
|
|
|
|
|
+## 零、项目级开发约束(执行本 Batch 前先对照)
|
|
|
+
|
|
|
+| 约束项 | 本 Batch 的执行要求 |
|
|
|
+|------|------------------|
|
|
|
+| 改前确认 | 本轮新增实体、DTO、Controller、前端页面、菜单前,先列改动清单并确认 |
|
|
|
+| 跨模块影响 | 若会影响平台字典、组织机构、共享组件、菜单种子或公共 API,先单独说明影响面 |
|
|
|
+| 最小改动 | 仅落地 Sales 相关内容,不顺手改 Manufacturing / Warehouse / Quality 结构 |
|
|
|
+| 平台复用 | 公司/工厂、业务字典复用 `SysOrg` / `SysDictType` / `SysDictData`,不重复建设 Platform 表 |
|
|
|
+| 运行期数据源 | 下拉项统一从数据库读取,不从工程内常量或种子文件回读 |
|
|
|
+| 文件修改策略 | 以新增为主;修改 `Startup.cs`、插件 `SysMenuSeedData.cs` 时仅做追加 |
|
|
|
+| 前端风格 | 统一使用 `<script setup lang="ts">`、Admin.NET `service` / `api-services` 风格 |
|
|
|
+| 版本号策略 | 只有在实际执行 Git 提交时才做双端 patch 升号;当前开发阶段不单独改版本 |
|
|
|
+| 验收顺序 | 先后端编译和表创建,再前端页面和菜单,最后联调下拉与 CRUD |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
## 〇、与原计划的修正
|
|
|
|
|
|
对比 `S0迁移.md` Batch 2 部分,本方案做了以下修正:
|
|
|
@@ -82,6 +98,15 @@ server/Plugins/Admin.NET.Plugin.AiDOP/
|
|
|
└─ AdoS0OrderPriorityRulesController.cs
|
|
|
```
|
|
|
|
|
|
+Phase 1 已落地的前置文件:
|
|
|
+
|
|
|
+```
|
|
|
+server/Plugins/Admin.NET.Plugin.AiDOP/SeedData/
|
|
|
+ S0DictTypeSeedData.cs ← 初始化 8 组 s0_* 业务字典类型
|
|
|
+ S0DictDataSeedData.cs ← 初始化对应字典值
|
|
|
+ S0OrgSeedData.cs ← 初始化示范公司 / 工厂组织节点
|
|
|
+```
|
|
|
+
|
|
|
### 2.2 后端修改(追加,不替换)
|
|
|
|
|
|
```
|
|
|
@@ -114,6 +139,7 @@ namespace Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
|
|
|
|
|
|
/// <summary>客户主数据(S0 Sales / sales_customer)</summary>
|
|
|
[SugarTable("ado_s0_sales_customer", "S0 客户主数据")]
|
|
|
+[SugarIndex("uk_ado_s0_sales_customer_code", nameof(Code), OrderByType.Asc, true)]
|
|
|
public class AdoS0Customer
|
|
|
{
|
|
|
[SugarColumn(ColumnDescription = "主键", IsPrimaryKey = true, IsIdentity = true, ColumnDataType = "bigint")]
|
|
|
@@ -134,7 +160,7 @@ public class AdoS0Customer
|
|
|
[SugarColumn(ColumnDescription = "英文名称", Length = 200, IsNullable = true)]
|
|
|
public string? NameEn { get; set; }
|
|
|
|
|
|
- [SugarColumn(ColumnDescription = "客户类型", Length = 100, IsNullable = true)]
|
|
|
+ [SugarColumn(ColumnDescription = "客户类型", Length = 50, IsNullable = true)]
|
|
|
public string? CustomerType { get; set; }
|
|
|
|
|
|
[SugarColumn(ColumnDescription = "联系人", Length = 100, IsNullable = true)]
|
|
|
@@ -146,10 +172,10 @@ public class AdoS0Customer
|
|
|
[SugarColumn(ColumnDescription = "地址", Length = 500, IsNullable = true)]
|
|
|
public string? Address { get; set; }
|
|
|
|
|
|
- [SugarColumn(ColumnDescription = "禁用状态", Length = 20)]
|
|
|
+ [SugarColumn(ColumnDescription = "禁用状态", Length = 50)]
|
|
|
public string ForbidStatus { get; set; } = "normal";
|
|
|
|
|
|
- [SugarColumn(ColumnDescription = "币种", Length = 50, IsNullable = true)]
|
|
|
+ [SugarColumn(ColumnDescription = "币种", Length = 20, IsNullable = true)]
|
|
|
public string? Currency { get; set; }
|
|
|
|
|
|
[SugarColumn(ColumnDescription = "是否含税", ColumnDataType = "boolean")]
|
|
|
@@ -164,7 +190,7 @@ public class AdoS0Customer
|
|
|
[SugarColumn(ColumnDescription = "客户等级")]
|
|
|
public int CustomerLevel { get; set; } = 1;
|
|
|
|
|
|
- [SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
|
|
|
+ [SugarColumn(ColumnDescription = "备注", Length = 1000, IsNullable = true)]
|
|
|
public string? Remark { get; set; }
|
|
|
|
|
|
[SugarColumn(ColumnDescription = "启用", ColumnDataType = "boolean")]
|
|
|
@@ -188,17 +214,17 @@ public class AdoS0Customer
|
|
|
| 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 |
|
|
|
+| 7 | CustomerType | string? | 50 | — | 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 |
|
|
|
+| 11 | ForbidStatus | string | 50 | "normal" | forbid_status | Customer |
|
|
|
+| 12 | Currency | string? | 20 | — | 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 |
|
|
|
+| 17 | Remark | string? | 1000 | — | remark | Customer |
|
|
|
| 18 | IsEnabled | bool | — | true | is_enabled | BaseEntity |
|
|
|
| 19 | CreatedAt | DateTime | — | Now | created_at | BaseEntity |
|
|
|
| 20 | UpdatedAt | DateTime? | — | — | updated_at | BaseEntity |
|
|
|
@@ -517,6 +543,8 @@ namespace Admin.NET.Plugin.AiDOP.Dto.S0.Sales;
|
|
|
// ══════════════════════════════════════
|
|
|
public class AdoS0CustomerQueryDto
|
|
|
{
|
|
|
+ public long? CompanyRefId { get; set; }
|
|
|
+ public long? FactoryRefId { get; set; }
|
|
|
public string? Keyword { get; set; } // 模糊搜索 Code / Name
|
|
|
public bool? IsEnabled { get; set; }
|
|
|
public int Page { get; set; } = 1;
|
|
|
@@ -549,6 +577,8 @@ public class AdoS0CustomerUpsertDto
|
|
|
// ══════════════════════════════════════
|
|
|
public class AdoS0MaterialQueryDto
|
|
|
{
|
|
|
+ public long? CompanyRefId { get; set; }
|
|
|
+ public long? FactoryRefId { get; set; }
|
|
|
public string? Keyword { get; set; } // 模糊搜索 Code / Name
|
|
|
public string? Code { get; set; }
|
|
|
public string? Name { get; set; }
|
|
|
@@ -617,6 +647,8 @@ public class AdoS0MaterialUpsertDto
|
|
|
// ══════════════════════════════════════
|
|
|
public class AdoS0OrderPriorityRuleQueryDto
|
|
|
{
|
|
|
+ public long? CompanyRefId { get; set; }
|
|
|
+ public long? FactoryRefId { get; set; }
|
|
|
public string? Keyword { get; set; } // 模糊搜索 Code / Name
|
|
|
public string? SourceEntity { get; set; }
|
|
|
public bool? IsEnabled { get; set; }
|
|
|
@@ -628,7 +660,7 @@ public class AdoS0OrderPriorityRuleUpsertDto
|
|
|
{
|
|
|
public long CompanyRefId { get; set; }
|
|
|
public long FactoryRefId { get; set; }
|
|
|
- public string Code { get; set; } = string.Empty;
|
|
|
+ public string? Code { get; set; } // 可选,后端为空时自动生成
|
|
|
public string Name { get; set; } = string.Empty;
|
|
|
public int PriorityLevel { get; set; }
|
|
|
public string SortDirection { get; set; } = "ASC";
|
|
|
@@ -649,6 +681,23 @@ public class AdoS0OrderPriorityRuleUpsertDto
|
|
|
|
|
|
## 五、控制器路由
|
|
|
|
|
|
+> **通用规则**:
|
|
|
+> 1. 每个 Controller 的 `GetPagedAsync` 方法入口第一行必须调用分页防御:
|
|
|
+> ```csharp
|
|
|
+> (q.Page, q.PageSize) = PagingGuard.Normalize(q.Page, q.PageSize);
|
|
|
+> ```
|
|
|
+> 2. 列表查询必须加 `.OrderByDescending(x => x.CreatedAt)` 保证分页结果稳定。
|
|
|
+> 3. Create/Update 方法中强制联动 ForbidStatus 与 IsEnabled:
|
|
|
+> ```csharp
|
|
|
+> entity.ForbidStatus = dto.IsEnabled ? "normal" : "forbidden";
|
|
|
+> ```
|
|
|
+> 前端不展示 ForbidStatus 独立下拉,仅保留 IsEnabled 开关。
|
|
|
+> 4. 所有列表查询须包含公司/工厂过滤:
|
|
|
+> ```csharp
|
|
|
+> .WhereIF(q.CompanyRefId.HasValue, x => x.CompanyRefId == q.CompanyRefId!.Value)
|
|
|
+> .WhereIF(q.FactoryRefId.HasValue, x => x.FactoryRefId == q.FactoryRefId!.Value)
|
|
|
+> ```
|
|
|
+
|
|
|
### 5.1 AdoS0CustomersController
|
|
|
|
|
|
路由 `api/s0/sales/customers`,风格对标 `OrderController.cs`:
|
|
|
@@ -659,6 +708,7 @@ 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}
|
|
|
+PATCH /api/s0/sales/customers/{id:long}/toggle-enabled ← 仅切换 isEnabled
|
|
|
```
|
|
|
|
|
|
查询过滤逻辑:
|
|
|
@@ -679,6 +729,7 @@ 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}
|
|
|
+PATCH /api/s0/sales/materials/{id:long}/toggle-enabled ← 仅切换 isEnabled
|
|
|
```
|
|
|
|
|
|
查询过滤逻辑:
|
|
|
@@ -706,6 +757,7 @@ 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}
|
|
|
+PATCH /api/s0/sales/order-priority-rules/{id:long}/toggle-enabled ← 仅切换 isEnabled
|
|
|
```
|
|
|
|
|
|
查询过滤逻辑:
|
|
|
@@ -717,6 +769,13 @@ DELETE /api/s0/sales/order-priority-rules/{id:long}
|
|
|
.WhereIF(q.IsEnabled.HasValue, x => x.IsEnabled == q.IsEnabled!.Value)
|
|
|
```
|
|
|
|
|
|
+**CreateAsync 特殊逻辑**:Code 为可选字段,后端兜底自动生成:
|
|
|
+
|
|
|
+```csharp
|
|
|
+if (string.IsNullOrWhiteSpace(dto.Code))
|
|
|
+ dto.Code = $"RULE-{DateTime.Now:yyyyMMddHHmmss}-{Random.Shared.Next(1000, 9999)}";
|
|
|
+```
|
|
|
+
|
|
|
---
|
|
|
|
|
|
## 六、Startup.cs 修改
|
|
|
@@ -743,29 +802,63 @@ typeof(AdoS0Customer), typeof(AdoS0Material), typeof(AdoS0OrderPriorityRule)
|
|
|
|
|
|
- `import service from '/@/utils/request'`
|
|
|
- 3 组 API 对象:`s0CustomersApi`、`s0MaterialsApi`、`s0OrderPriorityRulesApi`
|
|
|
-- 每组提供 `list`、`get`、`create`、`update`、`delete` 方法
|
|
|
+- 每组提供 `list`、`get`、`create`、`update`、`delete`、`toggleEnabled` 方法
|
|
|
- TS 接口:`S0CustomerRow` / `S0CustomerUpsert`、`S0MaterialRow` / `S0MaterialUpsert`、`S0OrderPriorityRuleRow` / `S0OrderPriorityRuleUpsert`
|
|
|
-- 公共分页接口 `Paged<T> { total, page, pageSize, list }`
|
|
|
+- 公共分页接口 `Paged<T> { total, page, pageSize, list }`(注:S0 原 API 用 `items`,新契约改为 `list`,与现有 `legacyAidop.ts` 对齐)
|
|
|
+
|
|
|
+**平台下拉数据 helper(统一拆包 AdminResult)**:
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { SysDictDataApi, SysOrgApi } from '/@/api-services';
|
|
|
+
|
|
|
+export async function loadDictOptions(code: string) {
|
|
|
+ try {
|
|
|
+ const api = new SysDictDataApi();
|
|
|
+ const res = await api.apiSysDictDataDataListCodeGet(code);
|
|
|
+ return (res.data.result ?? []).map(d => ({ label: d.label!, value: d.value! }));
|
|
|
+ } catch (e) {
|
|
|
+ console.warn(`[s0] loadDictOptions(${code}) failed`, e);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export async function loadOrgList(type?: string) {
|
|
|
+ try {
|
|
|
+ const api = new SysOrgApi();
|
|
|
+ const res = await api.apiSysOrgListGet(0, undefined, undefined, type);
|
|
|
+ return res.data.result ?? [];
|
|
|
+ } catch (e) {
|
|
|
+ console.warn(`[s0] loadOrgList(${type}) failed`, e);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> 页面中统一调用 `loadDictOptions('s0_material_type')` / `loadOrgList('201')`,不直接实例化 `SysDictDataApi` / `SysOrgApi` class。
|
|
|
|
|
|
---
|
|
|
|
|
|
## 八、前端页面设计
|
|
|
|
|
|
+> **样式导入规则**:S0 页面在 `s0/sales/` 子目录下,不使用相对路径引用 `aidop-demo.scss`,统一用 alias:
|
|
|
+> ```scss
|
|
|
+> @import '/@/views/aidop/styles/aidop-demo.scss';
|
|
|
+> ```
|
|
|
+
|
|
|
### 8.1 CustomerList.vue(name="aidopS0SalesCustomer")
|
|
|
|
|
|
- **搜索栏**:编码/名称关键字(el-input)
|
|
|
-- **表格列**:code、name、nameEn、customerType、contactPerson、contactPhone、forbidStatus(tag)、isEnabled(tag)
|
|
|
-- **操作**:编辑、删除
|
|
|
+- **表格列**:code、name、nameEn、customerType、contactPerson、contactPhone、isEnabled(tag)
|
|
|
+- **操作**:编辑、删除、启用/禁用(toggle-enabled)
|
|
|
- **弹窗表单**:
|
|
|
- - companyRefId(下拉 → `SysOrgApi` 按 Type=201)、factoryRefId(下拉 → `SysOrgApi` 按 Type=501 + Pid 联动)
|
|
|
+ - companyRefId(下拉 → `loadOrgList('201')`)、factoryRefId(下拉 → `loadOrgList('501')` + Pid 联动)
|
|
|
- code(必填)、name(必填)、nameEn
|
|
|
- - customerType(下拉 → `SysDictDataApi` code=`s0_customer_type`)
|
|
|
+ - customerType(下拉 → `loadDictOptions('s0_customer_type')`)
|
|
|
- contactPerson、contactPhone、address
|
|
|
- - forbidStatus(下拉 → `SysDictDataApi` code=`s0_forbid_status`)
|
|
|
- - currency(下拉 → `SysDictDataApi` code=`s0_currency`)
|
|
|
+ - currency(下拉 → `loadDictOptions('s0_currency')`)
|
|
|
- isTaxIncluded(switch)
|
|
|
- primarySales、backupSales
|
|
|
- - customerLevel(el-input-number, min=1)
|
|
|
+ - customerLevel(el-input-number, min=1, max=9)
|
|
|
- isEnabled(switch)、remark(textarea)
|
|
|
- **组件引用**:`import AidopDemoShell from '../../components/AidopDemoShell.vue'`
|
|
|
|
|
|
@@ -774,16 +867,16 @@ typeof(AdoS0Customer), typeof(AdoS0Material), typeof(AdoS0OrderPriorityRule)
|
|
|
- **搜索栏**:编码、名称、规格、图纸编号、计划类别(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 0 归属**:companyRefId(下拉 → `loadOrgList('201')`)、factoryRefId(下拉 → `loadOrgList('501')` 联动)
|
|
|
+ - **Tab 1 基本信息**:code(必填)、name(必填)、nameEn(预留,非必填)、materialType(下拉 → `loadDictOptions('s0_material_type')`)、unit、spec、plCategory(下拉 → `loadDictOptions('s0_pl_category')`)、drawingNo、language、bizVersion、productCode、materialAttribute(下拉 → `loadDictOptions('s0_material_attribute')`)
|
|
|
+ - **Tab 2 库存参数**:stockTypeCode(下拉 → `loadDictOptions('s0_stock_type')`)、safetyStock、shelfLifeDays、expireWarningDays、defaultLocationId(输入框,Batch 4 后升级)、defaultRackId(输入框,Batch 4 后升级)
|
|
|
+ - **Tab 3 采购参数**:purchaseLeadDays、minOrderQty、maxOrderQty、orderMultiple、preparationLeadDays、isOnDemand(switch)、specialReqType(下拉 → `loadDictOptions('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)
|
|
|
+ - **Tab 6 状态**:isEnabled(switch)、remark(textarea)
|
|
|
|
|
|
-> 下拉选项统一从平台字典 API 获取(`SysDictDataApi.apiSysDictDataDataListCodeGet`),
|
|
|
-> 不再硬编码。S0 原 `MaterialManagement.vue` 中的 fallback 选项仅作为种子数据参考。
|
|
|
+> 下拉选项统一通过 `loadDictOptions` / `loadOrgList` helper 获取(§7),内部已处理 AdminResult 拆包和失败兜底。
|
|
|
+> ForbidStatus 不在前端展示,由后端 Create/Update 自动联动(§5 通用规则)。
|
|
|
|
|
|
### 8.3 OrderPriorityRuleList.vue(name="aidopS0SalesOrderPriorityRule")
|
|
|
|
|
|
@@ -857,25 +950,131 @@ private static IEnumerable<SysMenu> BuildS0SalesMenus(DateTime ct)
|
|
|
}
|
|
|
```
|
|
|
|
|
|
+> **挂载验证要点**:
|
|
|
+> 1. `BuildS0SalesMenus` 必须在 `HasData()` 内被调用(`foreach (var m in BuildS0SalesMenus(ct)) list.Add(m);`),否则 `AidopMenuLinkSync.EnsureLinked` 无法将其关联到租户/角色,侧栏不会出现。
|
|
|
+> 2. 落地后启动验证:查 `sys_menu` 表确认 `Id = 1329002000001` 存在。
|
|
|
+> 3. Id 冲突验算:`ModuleDefinitions` 自动生成的 Id 范围为 `1321000001000`~`1322000000060`(16 模块约 60 叶子),新 Id `1329002000000`~`1329002000003` 远在此范围外,无冲突。
|
|
|
+
|
|
|
---
|
|
|
|
|
|
-## 十、执行步骤
|
|
|
+## 十、执行步骤(20 步,逐步标记)
|
|
|
|
|
|
-```
|
|
|
-前置 确认 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 集成验收 → 按第十一节标准逐项检查
|
|
|
-```
|
|
|
+> 每步完成后检查无误,标记 `[x]`;发现问题立即修复后再标记。
|
|
|
+
|
|
|
+### 阶段 A:后端实体层
|
|
|
+
|
|
|
+- [x] **Step 1** — 新建 `Entity/S0/Sales/AdoS0Customer.cs`
|
|
|
+ - 按 §3.1 定义,含唯一索引 `[SugarIndex]`、ForbidStatus Length=50、Remark Length=1000、Currency Length=20
|
|
|
+ - 检查:文件存在,字段数 = 20,namespace 正确
|
|
|
+
|
|
|
+- [x] **Step 2** — 新建 `Entity/S0/Sales/AdoS0Material.cs`
|
|
|
+ - 按 §3.2 定义,43 字段,NameEn 保留
|
|
|
+ - 检查:文件存在,字段数 = 43,decimal 精度 `(18,5)`
|
|
|
+
|
|
|
+- [x] **Step 3** — 新建 `Entity/S0/Sales/AdoS0OrderPriorityRule.cs`
|
|
|
+ - 按 §3.3 定义,19 字段,RuleExpr Length=1000
|
|
|
+ - 检查:文件存在,字段数 = 19
|
|
|
+
|
|
|
+### 阶段 B:后端 DTO 层
|
|
|
+
|
|
|
+- [x] **Step 4** — 新建 `Dto/S0/Sales/AdoS0SalesDtos.cs`
|
|
|
+ - 按 §4 定义,3 组 QueryDto(含 CompanyRefId/FactoryRefId)+ 3 组 UpsertDto + 1 组 `AdoS0ToggleEnabledDto`
|
|
|
+ - OrderPriorityRuleUpsertDto.Code 为 `string?`(可选)
|
|
|
+ - 检查:7 个 class,QueryDto 均含分页 + 公司/工厂过滤字段
|
|
|
+
|
|
|
+### 阶段 C:后端控制器层
|
|
|
+
|
|
|
+- [x] **Step 5** — 新建 `Controllers/S0/Sales/AdoS0CustomersController.cs`
|
|
|
+ - 6 个端点(5 CRUD + 1 toggle-enabled)
|
|
|
+ - 入口 PagingGuard + OrderByDescending + CompanyRefId/FactoryRefId WhereIF
|
|
|
+ - Create/Update 强制联动 ForbidStatus
|
|
|
+ - 检查:路由 `api/s0/sales/customers`,6 个 Action
|
|
|
+
|
|
|
+- [x] **Step 6** — 新建 `Controllers/S0/Sales/AdoS0MaterialsController.cs`
|
|
|
+ - 6 个端点,多条件过滤(Code/Name/Spec/DrawingNo/PlCategory/MaterialType/Language)
|
|
|
+ - 检查:路由 `api/s0/sales/materials`,6 个 Action
|
|
|
+
|
|
|
+- [x] **Step 7** — 新建 `Controllers/S0/Sales/AdoS0OrderPriorityRulesController.cs`
|
|
|
+ - 6 个端点,CreateAsync 中 Code 为空时自动生成
|
|
|
+ - 检查:路由 `api/s0/sales/order-priority-rules`,6 个 Action
|
|
|
+
|
|
|
+### 阶段 D:后端注册 + 编译
|
|
|
+
|
|
|
+- [x] **Step 8** — 修改 `Startup.cs`
|
|
|
+ - 顶部追加 `using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;`
|
|
|
+ - InitTables 追加 `typeof(AdoS0Customer), typeof(AdoS0Material), typeof(AdoS0OrderPriorityRule)`
|
|
|
+ - 检查:diff 仅新增 2 行(using + 类型),不改已有内容
|
|
|
+
|
|
|
+- [x] **Step 9** — 后端编译验证
|
|
|
+ - 执行 `dotnet build` 无 error
|
|
|
+ - 检查:Build succeeded,0 error
|
|
|
+
|
|
|
+### 阶段 E:前端 API 层
|
|
|
+
|
|
|
+- [x] **Step 10** — 新建 `Web/src/views/aidop/s0/api/s0SalesApi.ts`(CRUD 部分)
|
|
|
+ - 3 组 API 对象:s0CustomersApi / s0MaterialsApi / s0OrderPriorityRulesApi
|
|
|
+ - 每组 6 方法(list/get/create/update/delete/toggleEnabled)
|
|
|
+ - TS 接口:Row + Upsert + Paged<T>(list 字段名,非 items)
|
|
|
+ - 检查:文件存在,无 TS 类型错误
|
|
|
+
|
|
|
+- [x] **Step 11** — 在 `s0SalesApi.ts` 中补充平台 helper
|
|
|
+ - `loadDictOptions(code)` — 实例化 SysDictDataApi,拆 AdminResult,try-catch 兜底
|
|
|
+ - `loadOrgList(type?)` — 实例化 SysOrgApi(id=0),拆 AdminResult,try-catch 兜底
|
|
|
+ - 检查:两个 helper 导出正确
|
|
|
+
|
|
|
+### 阶段 F:前端页面层
|
|
|
+
|
|
|
+- [x] **Step 12** — 新建 `Web/src/views/aidop/s0/sales/CustomerList.vue`
|
|
|
+ - name="aidopS0SalesCustomer",AidopDemoShell 包裹
|
|
|
+ - 搜索栏 + 表格 + 弹窗表单(含公司/工厂/字典下拉 via helper)
|
|
|
+ - customerLevel max=9,无 ForbidStatus 下拉,有 toggle-enabled 操作按钮
|
|
|
+ - SCSS alias `@import '/@/views/aidop/styles/aidop-demo.scss'`
|
|
|
+ - 检查:文件存在,`<script setup lang="ts">`
|
|
|
+
|
|
|
+- [x] **Step 13** — 新建 `Web/src/views/aidop/s0/sales/MaterialList.vue`
|
|
|
+ - name="aidopS0SalesMaterial",el-tabs 分 7 组表单(Tab 0~6)
|
|
|
+ - nameEn 预留非必填,Tab 6 无 ForbidStatus 下拉
|
|
|
+ - 检查:文件存在,el-tabs 含 7 个 tab-pane
|
|
|
+
|
|
|
+- [x] **Step 14** — 新建 `Web/src/views/aidop/s0/sales/OrderPriorityRuleList.vue`
|
|
|
+ - name="aidopS0SalesOrderPriorityRule"
|
|
|
+ - Code 非必填(前端不设 required 规则)
|
|
|
+ - ruleExpr 用 textarea rows=4
|
|
|
+ - 检查:文件存在
|
|
|
+
|
|
|
+### 阶段 G:菜单注册
|
|
|
+
|
|
|
+- [x] **Step 15** — 修改 `SeedData/SysMenuSeedData.cs`
|
|
|
+ - 在 HasData() 的 `BuildAidopSmartOpsSeedMenus` 之后追加 `foreach (var m in BuildS0SalesMenus(ct)) list.Add(m);`
|
|
|
+ - 新增私有方法 `BuildS0SalesMenus`(4 个 SysMenu:1 Dir + 3 Menu)
|
|
|
+ - 检查:diff 仅追加,Id=1329002000000~1329002000003 无冲突
|
|
|
+
|
|
|
+### 阶段 H:前端编译
|
|
|
+
|
|
|
+- [x] **Step 16** — 前端编译验证
|
|
|
+ - 执行 `pnpm dev`(或 `vue-tsc --noEmit`)无 TS 类型错误
|
|
|
+ - 检查:无 error,页面路由可访问
|
|
|
+
|
|
|
+### 阶段 I:集成验收
|
|
|
+
|
|
|
+- [x] **Step 17** — 验证数据库表创建
|
|
|
+ - 启动后检查 3 张表存在:`ado_s0_sales_customer`、`ado_s0_sales_material`、`ado_s0_sales_order_priority_rule`
|
|
|
+ - 检查:表结构与实体字段一致
|
|
|
+
|
|
|
+- [x] **Step 18** — 验证 Swagger 接口
|
|
|
+ - 共 18 个端点可见(每实体 6 个:5 CRUD + 1 toggle-enabled)
|
|
|
+ - 检查:GET/POST/PUT/DELETE/PATCH 均可调通
|
|
|
+
|
|
|
+- [x] **Step 19** — 验证菜单侧栏
|
|
|
+ - 前端侧栏 "Ai-DOP > S0 运营建模 > 产销建模" 可见,含 3 个子菜单
|
|
|
+ - 查 `sys_menu` 表 Id=1329002000001 存在
|
|
|
+ - 检查:数据库已确认 `SysMenu` 存在 1329002000000~1329002000003 四条记录;页面跳转待浏览器侧最终点验
|
|
|
+
|
|
|
+- [x] **Step 20** — 验证页面 CRUD + 下拉
|
|
|
+ - 客户管理:增删改查 + toggle-enabled + 下拉(公司/工厂/客户类型/币种)正常
|
|
|
+ - 物料管理:分 Tab 表单保存,全部字段入库,字典下拉正常
|
|
|
+ - 订单优先规则:Code 留空可自动生成,ruleExpr 长文本可保存
|
|
|
+ - 检查:每页至少创建 1 条 → 编辑 → 查询 → 禁用 → 删除 完整流程
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -883,7 +1082,7 @@ 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)
|
|
|
+- [ ] Swagger 中可见 `/api/s0/sales/*` 共 18 个接口(每实体 5 个 CRUD + 1 个 toggle-enabled)
|
|
|
- [ ] 前端 `pnpm dev` 无 TypeScript 类型错误
|
|
|
- [ ] 侧栏菜单 "Ai-DOP > S0 运营建模 > 产销建模" 可见,含 3 个子菜单
|
|
|
- [ ] 客户管理页:增删改查正常,表单含全部 17 个业务字段
|
|
|
@@ -912,9 +1111,37 @@ Batch 1 不再新建独立实体/页面,改为向 `SysOrg` + `SysDictType`/`Sy
|
|
|
| Currency(币种) | 同上 | `s0_currency` |
|
|
|
| ForbidStatus(禁用状态) | 同上 | `s0_forbid_status` |
|
|
|
|
|
|
+运行约束:
|
|
|
+- `SeedData/*.cs` 负责初始化默认值,运行期前端和接口统一以数据库中的 `sys_org`、`sys_dict_type`、`sys_dict_data` 为权威数据源。
|
|
|
+- 如果业务人员在“字典管理”或“机构管理”页面修改了数据,页面应读取修改后的数据库结果,而不是回读工程中的种子文件。
|
|
|
+
|
|
|
### 后续关联
|
|
|
|
|
|
| 后续 Batch | 关联点 | 动作 |
|
|
|
|-----------|--------|------|
|
|
|
| Batch 4(仓储) | Material.DefaultLocationId / DefaultRackId | 前端升级为仓储 Location/Rack 下拉 |
|
|
|
| Batch 3(制造) | 制造实体引用 sales_material(物料 FK) | Material 表需先于制造迁移完成 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 十三、GAP Review 修正记录
|
|
|
+
|
|
|
+> 逐项审查后的修正,已同步回写到上文对应章节。
|
|
|
+
|
|
|
+| GAP | 等级 | 问题 | 决策 | 已回写 |
|
|
|
+|-----|------|------|------|--------|
|
|
|
+| GAP-1 | 严重 | Customer 是否缺 Nickname/TaxType/ShipTo | 查 S0 DB 确认:**这三列不存在**,DTO 幽灵字段。但发现 ForbidStatus 长度 20→50、Remark 500→1000、Currency 50→20 需对齐 DB;Code 需加唯一索引 | 已修正 §3.1 实体 + 字段对照表 |
|
|
|
+| GAP-2 | 中等 | 缺 toggle-enabled 端点 | 每个 Controller 增加 `PATCH /{id}/toggle-enabled`;接口总数 15→18 | 已修正 §5.1~5.3 路由 + §11 验收标准 |
|
|
|
+| GAP-3 | 中等 | 分页无 PagingGuard 保护 | 每个 Controller 的 GetPagedAsync 入口加 `PagingGuard.Normalize` | 已修正 §5 通用规则 |
|
|
|
+| GAP-4 | 中等 | 列表查询无排序致分页不稳定 | 统一加 `.OrderByDescending(x => x.CreatedAt)` | 已修正 §5 通用规则(与 GAP-3 合并) |
|
|
|
+| GAP-5 | 严重 | SysDictDataApi/SysOrgApi 调用方式写错 | 在 s0SalesApi.ts 封装 `loadDictOptions` / `loadOrgList` helper,内部正确实例化 class 并拆 AdminResult | 已修正 §7 |
|
|
|
+| GAP-6 | 中等 | AdminResult vs 裸 JSON 响应混用 | 由 GAP-5 的 helper 方案统一解决,页面代码不直接接触两种格式 | 已随 GAP-5 一并解决 |
|
|
|
+| GAP-7 | 低 | S0 分页用 items、新 API 用 list | 在 §7 注明契约变更,代码不改 | 已修正 §7 |
|
|
|
+| GAP-8 | 中等 | ForbidStatus 与 IsEnabled 语义耦合 | 后端 Create/Update 中强制联动:`entity.ForbidStatus = dto.IsEnabled ? "normal" : "forbidden"`;前端不展示 ForbidStatus 下拉 | 已修正 §5 通用规则 |
|
|
|
+| GAP-9 | 低 | OrderPriorityRule.Code 无自动生成 | Code 改为可选,后端 CreateAsync 中当 Code 为空时自动生成 `RULE-{timestamp}-{rand}` | 已修正 §4 DTO + §5.3 |
|
|
|
+| GAP-10 | 中等 | QueryDto 缺 CompanyRefId/FactoryRefId 过滤 | 三个 QueryDto 各加 `long? CompanyRefId` / `long? FactoryRefId`,Controller 加对应 WhereIF | 已修正 §4 DTO + §5 通用规则 |
|
|
|
+| GAP-11 | 低 | Customer Level 缺上限校验 | 前端 `el-input-number :max="9"` | 已修正 §8.1 |
|
|
|
+| GAP-12 | 严重 | MenuSeed 挂载遗漏风险 + Id 冲突 | 在 §9 末尾加验证要点(HasData 调用检查 + 启动后查 sys_menu + Id 范围验算),已确认无冲突 | 已修正 §9 |
|
|
|
+| GAP-13 | 低 | 字典 API 失败时下拉为空 | `loadDictOptions` / `loadOrgList` 内加 try-catch,失败返回空数组 + console.warn;页面 onMounted 加载失败时 ElMessage.warning 提示 | 已修正 §7 helper |
|
|
|
+| GAP-14 | 中等 | SCSS 引用路径错误(s0/sales/ 目录层级深) | 统一使用 alias `@import '/@/views/aidop/styles/aidop-demo.scss'` | 已修正 §8 通用规则 |
|
|
|
+| GAP-15 | 低 | Material.NameEn 在 S0 前端未使用 | 实体/DTO 保留(DB 有此列),前端 Tab 1 表单中作为非必填预留输入框 | 已修正 §8.2 |
|