Parcourir la source

fix(s0): resolve barcode rule customer/supplier display semantics

条码规则页面「客户/供应商编码」与「供应商简称」语义不一致收口。
采用 legacy field compatibility:不改 schema、不引入 OwnerType、
不重构 BarCodeNbr.Customer 字段。

背景:BarCodeNbr.Customer 的列描述本就是「客户/供应商编码」,同一字段
既可存客户编码也可存供应商编码;但 controller 的补显只查 SuppMaster,
而当前两条规则的编码只存在于 CustMaster,导致名称列恒为空。

- 实体:新增展示字段 PartyName(不落库);SuppName 保留兼容并标注废弃语义
- 控制器:ApplySuppNameAsync → ApplyPartyNameAsync,CustMaster 与 SuppMaster
  各一次批量查询建字典(无 N+1、均显式限定当前租户),按
  客户 → 供应商 → 留空 依次解析;两边同时命中时客户优先,并注明当前无
  Type 层 owner 语义证据、不做静默猜测
- 前端:列头「供应商简称」→「客户/供应商名称」(绑 partyName);
  表单标签与搜索占位同步为客户/供应商口径;
  Customer 下拉升级为客户+供应商合并主体选择器(label 标注来源,
  value 仍只是业务编码字符串,sourceType 不落库)

前置校验:本租户 CustMaster.Cust 与 SuppMaster.Supp 编码交集 = 0,
故合并选择器可安全以业务编码作为 value,未引入任何编码前缀。

chore: bump version server 1.0.411 / Web 2.4.332
YY968XX il y a 1 mois
Parent
commit
0d6caaa2bc

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.331",
+	"version": "2.4.332",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 3 - 0
Web/src/views/aidop/s0/api/s0WarehouseApi.ts

@@ -276,7 +276,10 @@ export interface S0BarcodeRuleRow {
 	factoryRefId?: string;
 	domainCode: string;
 	customer?: string | null;
+	/** @deprecated legacy 展示字段(仅供应商语义);新代码用 partyName。 */
 	suppName?: string | null;
+	/** 列表展示:客户/供应商名称。Customer 为 legacy 主体编码字段,按 CustMaster → SuppMaster 解析。 */
+	partyName?: string | null;
 	type?: string | null;
 	waterRules?: string | null;
 	waterLen?: number | null;

+ 17 - 1
Web/src/views/aidop/s0/composables/useS0BusinessLookups.ts

@@ -10,6 +10,7 @@
  *    不是内部自增主键或雪花 id —— 保存回后端的仍是原业务编码,落库语义不变。
  */
 import { s0CustomersApi, s0MaterialsApi } from '../api/s0SalesApi';
+import { s0SuppliersApi } from '../api/s0SupplyApi';
 import { s0EmployeesApi, s0LabelTypesApi, s0LocationsApi } from '../api/s0WarehouseApi';
 import { s0MfgPersonSkillsApi, s0MfgProductionLinesApi, s0MfgWorkCentersApi } from '../api/s0ManufacturingApi';
 
@@ -18,7 +19,7 @@ export interface S0LookupOption {
 	label: string;
 }
 
-export type S0LookupKey = 'item' | 'line' | 'personSkill' | 'workCenter' | 'location' | 'employee' | 'customer' | 'barcodeType';
+export type S0LookupKey = 'item' | 'line' | 'personSkill' | 'workCenter' | 'location' | 'employee' | 'customer' | 'supplier' | 'party' | 'barcodeType';
 
 /** 下拉一次性取数上限:S0 主数据为配置级数据量,单页取全量即可,避免分页拼装。 */
 const PAGE_SIZE = 500;
@@ -70,6 +71,21 @@ const loaders: Record<S0LookupKey, () => Promise<S0LookupOption[]>> = {
 		const d = await s0CustomersApi.list({ page: 1, pageSize: PAGE_SIZE });
 		return dedupe((d.list ?? []).map((x: any) => norm(x.cust, x.sortName ?? x.custFullName)));
 	},
+	supplier: async () => {
+		const d = await s0SuppliersApi.list({ page: 1, pageSize: PAGE_SIZE });
+		return dedupe((d.list ?? []).map((x: any) => norm(x.supp, x.sortName)));
+	},
+	/**
+	 * 客户 + 供应商合并主体。
+	 * BarCodeNbr.Customer 是 legacy 字段(列描述即「客户/供应商编码」),同一字段两种主体共用,
+	 * 故下拉需同时提供两类;label 标注来源,value 仍只是业务编码字符串(sourceType 不落库)。
+	 * 前置条件:CustMaster.Cust 与 SuppMaster.Supp 在本租户内无编码交集(本批实测 = 0)。
+	 */
+	party: async () => {
+		const [cust, supp] = await Promise.all([loadS0Lookup('customer'), loadS0Lookup('supplier')]);
+		const tag = (o: S0LookupOption, t: string) => ({ value: o.value, label: `${o.label}(${t})` });
+		return dedupe([...cust.map((o) => tag(o, '客户')), ...supp.map((o) => tag(o, '供应商'))]);
+	},
 	barcodeType: async () => {
 		const d = await s0LabelTypesApi.list({ page: 1, pageSize: PAGE_SIZE });
 		return dedupe((d.list ?? []).map((x: any) => norm(x.barType, x.class)));

+ 6 - 6
Web/src/views/aidop/s0/warehouse/BarcodeRuleList.vue

@@ -2,7 +2,7 @@
 	<AidopDemoShell :title="pageTitle" subtitle="S0 / 仓储建模 / 条码规则">
 		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
 			<el-form-item label="关键字">
-				<el-input v-model="query.keyword" placeholder="客户编码/类型" clearable style="width: 200px" />
+				<el-input v-model="query.keyword" placeholder="客户/供应商编码或条码类型" clearable style="width: 200px" />
 			</el-form-item>
 			<el-form-item>
 				<el-button type="primary" @click="loadList">查询</el-button>
@@ -13,7 +13,7 @@
 
 		<el-table :data="rows" v-loading="loading" border stripe style="width: 100%" max-height="calc(100vh - 260px)">
 			<el-table-column prop="customer" label="客户/供应商编码" width="160" show-overflow-tooltip />
-			<el-table-column prop="suppName" label="供应商简称" width="150" show-overflow-tooltip />
+			<el-table-column prop="partyName" label="客户/供应商名称" width="170" show-overflow-tooltip />
 			<el-table-column prop="type" label="条码类型" width="120" show-overflow-tooltip />
 			<el-table-column prop="waterRules" label="流水规则" width="120" show-overflow-tooltip />
 			<el-table-column prop="waterLen" label="流水长度" width="90" show-overflow-tooltip />
@@ -51,8 +51,8 @@
 							</el-select>
 						</el-form-item>
 					</el-col>
-					<el-col :span="12"><el-form-item label="客户编码" prop="customer"><el-select v-model="form.customer" filterable clearable allow-create default-first-option placeholder="请选择客户编码" style="width: 100%">
-								<el-option v-for="o in lookup.customer" :key="o.value" :label="o.label" :value="o.value" />
+					<el-col :span="12"><el-form-item label="客户/供应商编码" prop="customer"><el-select v-model="form.customer" filterable clearable allow-create default-first-option placeholder="请选择客户或供应商" style="width: 100%">
+								<el-option v-for="o in lookup.party" :key="o.value" :label="o.label" :value="o.value" />
 							</el-select></el-form-item></el-col>
 					<el-col :span="12"><el-form-item label="条码类型" prop="type"><el-select v-model="form.type" filterable clearable allow-create default-first-option placeholder="请选择条码类型" style="width: 100%">
 								<el-option v-for="o in lookup.barcodeType" :key="o.value" :label="o.label" :value="o.value" />
@@ -85,9 +85,9 @@ import { reactive as __reactive_lk } from 'vue';
 import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
 // 关系字段下拉:复用既有列表/options 接口,模块级缓存,每 key 每页仅取一次(无 N+1)。
 // 选项 value 为业务编码,保存语义不变;allow-create 保留手工录入历史值能力。
-const lookup = __reactive_lk<Record<string, S0LookupOption[]>>({ barcodeType: [], customer: [] });
+const lookup = __reactive_lk<Record<string, S0LookupOption[]>>({ barcodeType: [], party: [] });
 void loadS0Lookup('barcodeType').then((r) => (lookup.barcodeType = r));
-void loadS0Lookup('customer').then((r) => (lookup.customer = r));
+void loadS0Lookup('party').then((r) => (lookup.party = r));
 
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';

+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.410</AssemblyVersion>
-    <FileVersion>1.0.410</FileVersion>
-    <Version>1.0.410</Version>
+    <AssemblyVersion>1.0.411</AssemblyVersion>
+    <FileVersion>1.0.411</FileVersion>
+    <Version>1.0.411</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 34 - 7
server/Plugins/Admin.NET.Plugin.AiDOP/Controllers/S0/Warehouse/AdoS0BarcodeRulesController.cs

@@ -1,4 +1,5 @@
 using Admin.NET.Plugin.AiDOP.Dto.S0.Warehouse;
+using Admin.NET.Plugin.AiDOP.Entity.S0.Sales;
 using Admin.NET.Plugin.AiDOP.Entity.S0.Supply;
 using Admin.NET.Plugin.AiDOP.Entity.S0.Warehouse;
 using Admin.NET.Plugin.AiDOP.Infrastructure;
@@ -16,13 +17,16 @@ public class AdoS0BarcodeRulesController : ControllerBase
 {
     private readonly SqlSugarRepository<AdoS0BarCodeNbr> _rep;
     private readonly SqlSugarRepository<AdoS0SuppMaster> _suppRep;
+    private readonly SqlSugarRepository<AdoS0CustMaster> _custRep;
 
     public AdoS0BarcodeRulesController(
         SqlSugarRepository<AdoS0BarCodeNbr> rep,
-        SqlSugarRepository<AdoS0SuppMaster> suppRep)
+        SqlSugarRepository<AdoS0SuppMaster> suppRep,
+        SqlSugarRepository<AdoS0CustMaster> custRep)
     {
         _rep = rep;
         _suppRep = suppRep;
+        _custRep = custRep;
     }
 
     [HttpGet]
@@ -48,7 +52,7 @@ public class AdoS0BarcodeRulesController : ControllerBase
             .Take(q.PageSize)
             .ToListAsync();
 
-        await ApplySuppNameAsync(list, tenantId);
+        await ApplyPartyNameAsync(list, tenantId);
 
         return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
     }
@@ -59,7 +63,7 @@ public class AdoS0BarcodeRulesController : ControllerBase
         if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
         var item = await _rep.AsQueryable().Where(x => x.Id == id && x.TenantId == tenantId).FirstAsync();
         if (item == null) return NotFound();
-        await ApplySuppNameAsync(new List<AdoS0BarCodeNbr> { item }, tenantId);
+        await ApplyPartyNameAsync(new List<AdoS0BarCodeNbr> { item }, tenantId);
         return Ok(item);
     }
 
@@ -155,28 +159,51 @@ public class AdoS0BarcodeRulesController : ControllerBase
         e.FirmString12 = dto.FirmString12; e.FirmStringLen12 = dto.FirmStringLen12; e.FirmString12Note = dto.FirmString12Note;
     }
 
-    private async Task ApplySuppNameAsync(List<AdoS0BarCodeNbr> rules, long tenantId)
+    /// <summary>
+    /// 补显「客户/供应商名称」。
+    ///
+    /// legacy 字段兼容:BarCodeNbr.Customer 的列描述即「客户/供应商编码」,
+    /// 同一字段既可能存客户编码、也可能存供应商编码,本批不改 schema、不引入 OwnerType。
+    /// 因此按 CustMaster → SuppMaster 依次解析:
+    ///   仅命中客户 → 客户简称;仅命中供应商 → 供应商简称;
+    ///   两边都命中 → 客户优先(当前无 Type 层面的 owner 语义证据,不做静默猜测);
+    ///   两边都不命中 → 留空,不造值。
+    ///
+    /// 两次批量查询(各一次),按 code 建字典,无 N+1;均显式限定当前租户。
+    /// PartyName 为新语义字段;SuppName 保留旧值以兼容既有引用。
+    /// </summary>
+    private async Task ApplyPartyNameAsync(List<AdoS0BarCodeNbr> rules, long tenantId)
     {
         if (rules.Count == 0) return;
 
-        var customerCodes = rules
+        var partyCodes = rules
             .Select(r => r.Customer)
             .Where(c => !string.IsNullOrWhiteSpace(c))
+            .Select(c => c!)
             .Distinct()
             .ToList();
 
-        if (customerCodes.Count == 0) return;
+        if (partyCodes.Count == 0) return;
+
+        // 客户关联限定当前租户
+        var custs = await _custRep.AsQueryable()
+            .Where(c => c.TenantId == tenantId && partyCodes.Contains(c.Cust))
+            .ToListAsync();
 
         // 供应商关联限定当前租户,避免同编码供应商跨租户补错简称
         var supps = await _suppRep.AsQueryable()
-            .Where(s => s.TenantId == tenantId && customerCodes.Contains(s.Supp))
+            .Where(s => s.TenantId == tenantId && partyCodes.Contains(s.Supp))
             .ToListAsync();
 
         foreach (var rule in rules)
         {
             if (string.IsNullOrWhiteSpace(rule.Customer)) continue;
+
+            var cust = custs.Find(c => string.Equals(c.Cust, rule.Customer, StringComparison.OrdinalIgnoreCase));
             var supp = supps.Find(s => string.Equals(s.Supp, rule.Customer, StringComparison.OrdinalIgnoreCase));
+
             rule.SuppName = supp?.SortName;
+            rule.PartyName = cust?.SortName ?? supp?.SortName;
         }
     }
 }

+ 13 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Entity/S0/Warehouse/AdoS0BarCodeNbr.cs

@@ -109,7 +109,19 @@ public class AdoS0BarCodeNbr : ITenantIdFilter
     [SugarColumn(ColumnName = "tenant_id", IsNullable = true)]
     public long? TenantId { get; set; }
 
-    /// <summary>列表展示:供应商简称(不落库,来自 SuppMaster 关联)</summary>
+    /// <summary>
+    /// 列表展示:供应商简称(不落库)。
+    /// 保留以兼容既有引用;Customer 字段实际可存客户或供应商编码,
+    /// 新代码请改用 <see cref="PartyName"/>。
+    /// </summary>
     [SugarColumn(IsIgnore = true)]
     public string? SuppName { get; set; }
+
+    /// <summary>
+    /// 列表展示:客户/供应商名称(不落库)。
+    /// legacy 字段 Customer 存的是"客户或供应商"的业务编码(列描述即「客户/供应商编码」),
+    /// 故名称需按 CustMaster → SuppMaster 依次解析,字段名不再单指供应商。
+    /// </summary>
+    [SugarColumn(IsIgnore = true)]
+    public string? PartyName { get; set; }
 }