Przeglądaj źródła

fix(s8): close order-chain filtered aggregation | Web 2.4.337 / server 1.0.417

- final assembly collaboration: count exceptions by related_object_code instead of
  source_object_type, matching the order-execution fix; the three golden exceptions that
  hit the candidate types carry SHIPMENT or no source type, so the panel always read
  "0 orders / --"
- aggregate orderCodes: the query DTO was the only order-filtered endpoint still typed
  List<string>, so "orderCodes=A,B" bound as the single element "A,B" and the filter
  silently returned an empty aggregate; switch to a CSV string parsed by the existing
  ParseOrderCodesCsv helper, in line with the four pivot endpoints
- stage aggregation: exclude not-yet-reached stages from the on-time denominator and round
  the actual average away from zero, so the cached baseline snapshot and the live
  aggregate agree field by field
- chain page: load the filtered collaboration summary with the same scope and orderCodes as
  the pivot, so a filtered view no longer shows another order's risk
- chain overview list: derive the single-order stage label through computeNodeStatus, so a
  started but unfinished stage reads as in progress rather than not reached
YY968XX 5 godzin temu
rodzic
commit
e02204488d

+ 1 - 1
Web/package.json

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

+ 8 - 1
Web/src/views/aidop/s8/api/s8OrderFlowDomainApi.ts

@@ -97,7 +97,14 @@ export interface OrderFlowChain {
 export interface OrderFlowAggregateQuery {
 	/** BASELINE_PPT | CURRENT_FILTERED */
 	scope: string;
-	orderCodes?: string[];
+	/**
+	 * S8-STEP5B-ORDER-CHAIN-CLOSURE-1:订单编码 **CSV 字符串**,与 procurement / manufacturing /
+	 * final-assembly / drawings 四个 pivot API 的 orderCodes 口径统一。
+	 * 原类型是 `string[]`:axios 会序列化成 `orderCodes[]=X`,后端 `List<string>` **绑定不上**,
+	 * 过滤被静默忽略并退回全量(实测 `orderCodes[]=SO-2026-019` 返回 totalOrders=5)。
+	 * 该数组形态此前无调用方,故属类型层面的既有陷阱,非运行期回归。
+	 */
+	orderCodes?: string;
 }
 
 export interface OrderFlowStageAggregate {

+ 24 - 2
Web/src/views/aidop/s8/monitoring/OrderChainOverviewPage.vue

@@ -37,7 +37,10 @@ import {
 	getOrderFlowProcurementPivot,
 	getOrderFlowManufacturingPivot,
 	getOrderFlowFinalAssemblyPivot,
+	// S8-STEP5B-ORDER-CHAIN-CLOSURE-1(P-2):筛选态 collab summary 需按 orderCodes 重取。
+	getOrderFlowAggregate,
 } from '/@/views/aidop/s8/api/s8OrderFlowDomainApi';
+import { mapFinalAssemblyCollabSummary } from '/@/views/aidop/s8/monitoring/data/order-execution/domainMapper';
 // ORDER-FLOW-CHAIN-PAGE2-ORIGINAL-LOGIC-RESTORE-1:baseline (isUnfiltered) 态下 L2/L3 使用 PPT 常量。
 import {
 	PPT_REVIEW_SUBSTEPS,
@@ -375,7 +378,17 @@ const showFinalAssemblyCollab = computed(() => activeStageKey.value === 'final_a
 const finalStageSnapshot = computed(() =>
 	aggregateSnapshot.value?.stageSnapshots.find((s) => s.stageKey === 'final_assembly_shipping') ?? null,
 );
-const finalCollabSummary = computed(() => aggregateSnapshot.value?.finalAssemblyCollabSummary ?? null);
+// S8-STEP5B-ORDER-CHAIN-CLOSURE-1(P-2):筛选态下不得再用 baseline 的 collab summary。
+// aggregateSnapshot 只在挂载/刷新时以 scope=BASELINE_PPT 取一次、筛选变化不重取;
+// 而 finalAssemblyApiDetail 已按 orderCodes 过滤,两者混在同一张表里会串单——
+// 实测:筛选到 SO-2026-019 时「影响订单/主要风险」仍显示 SO-2026-020 的异常。
+// 修法:筛选态改用与 pivot 同 scope、同 orderCodes 取回的 collab summary(见 loadFinalAssemblyPivot)。
+const filteredCollabSummary = shallowRef<ReturnType<typeof mapFinalAssemblyCollabSummary>>(null);
+const finalCollabSummary = computed(() =>
+	isUnfiltered.value
+		? (aggregateSnapshot.value?.finalAssemblyCollabSummary ?? null)
+		: filteredCollabSummary.value,
+);
 
 function onRetry() {
 	// t3i:重试走 domain 流。
@@ -512,13 +525,22 @@ async function loadFinalAssemblyPivot() {
 	finalAssemblyApiLoading.value = true;
 	finalAssemblyApiError.value = null;
 	try {
-		const pivot = await getOrderFlowFinalAssemblyPivot({ scope, orderCodes });
+		// S8-STEP5B-ORDER-CHAIN-CLOSURE-1(P-2):筛选态同步取同 scope / 同 orderCodes 的 collab summary,
+		// 让「影响订单 / 风险订单 / 主要风险」与 pivot 落在同一批订单上;共用 mySeq 防旧响应覆盖。
+		const [pivot, agg] = await Promise.all([
+			getOrderFlowFinalAssemblyPivot({ scope, orderCodes }),
+			isBaseline ? Promise.resolve(null) : getOrderFlowAggregate({ scope, orderCodes }),
+		]);
 		if (mySeq !== finalAssemblyRequestSeq) return;
 		finalAssemblyApiDetail.value = adaptFinalAssemblyPivotFromApi(pivot);
+		filteredCollabSummary.value = isBaseline
+			? null
+			: mapFinalAssemblyCollabSummary(agg?.finalAssemblyCollabSummary ?? null);
 	} catch (e) {
 		if (mySeq !== finalAssemblyRequestSeq) return;
 		finalAssemblyApiError.value = '总装发货数据加载失败,请稍后重试';
 		finalAssemblyApiDetail.value = null;
+		filteredCollabSummary.value = null;
 	} finally {
 		if (mySeq === finalAssemblyRequestSeq) {
 			finalAssemblyApiLoading.value = false;

+ 8 - 2
Web/src/views/aidop/s8/monitoring/components/order-execution/ChainOverviewList.vue

@@ -8,7 +8,9 @@ import type {
 	StageSnapshot,
 } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
 import { ORDER_CHAIN_STAGE_ORDER } from '/@/views/aidop/s8/monitoring/data/order-execution/stage-meta';
-import { STAGE_STATUS_LABEL } from './statusMapping';
+// S8-STEP5B-ORDER-CHAIN-CLOSURE-1(E):单订单态达标文案改走 computeNodeStatus;
+// STAGE_STATUS_LABEL 仅保留给「该 stage 不存在」的兜底(此时确实是「未到达」,无起工时点可判)。
+import { STAGE_STATUS_LABEL, computeNodeStatus } from './statusMapping';
 
 interface Props {
 	orders: SalesOrderExecution[];
@@ -171,7 +173,11 @@ function buildSingleOrderColumn(
 		achievementDaysText: stage.actualDays != null ? `${stage.actualDays.toFixed(1)} 天` : '--',
 		achievementRateText: '--',
 		statusTone: tone,
-		statusLabel: STAGE_STATUS_LABEL[tone],
+		// S8-STEP5B-ORDER-CHAIN-CLOSURE-1(E):改走 computeNodeStatus,与 ORDER_EXEC 的
+		// 「节点状态」行 / 项目经理工作台同源。原写法直接索引 STAGE_STATUS_LABEL[tone],
+		// 会把已开工(actual_start_at 非空、actual_end_at 为空)的 pending 阶段标成「未到达」。
+		// tone 仍取自 stage.status,配色不变。
+		statusLabel: computeNodeStatus(stage).label,
 		isSingleOrder: true,
 	};
 }

+ 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.416</AssemblyVersion>
-    <FileVersion>1.0.416</FileVersion>
-    <Version>1.0.416</Version>
+    <AssemblyVersion>1.0.417</AssemblyVersion>
+    <FileVersion>1.0.417</FileVersion>
+    <Version>1.0.417</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 10 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/OrderFlow/AdoS8OrderFlowDtos.cs

@@ -115,7 +115,16 @@ public class AdoS8OrderFlowChainDto
 public class AdoS8OrderFlowAggregateQueryDto
 {
     public string Scope { get; set; } = string.Empty;
-    public List<string>? OrderCodes { get; set; }
+
+    /// <summary>
+    /// S8-STEP5B-P2-FILTER-SUBSET-CERT-1:订单编码 CSV,与 procurement / manufacturing /
+    /// final-assembly / drawings 四个 pivot DTO 的 OrderCodes 口径统一(服务侧走 ParseOrderCodesCsv)。
+    /// 原类型是 <c>List&lt;string&gt;?</c>——aggregate 是唯一没跟上该约定的端点。实测:
+    /// <c>orderCodes=SO-001,SO-002</c> 被默认集合绑定器绑成 **单元素** <c>["SO-001,SO-002"]</c>,
+    /// 与任何 order_code 都不相等 → totalOrders=0(静默返回空聚合,而非报错);
+    /// <c>orderCodes[]=X</c> 则整体绑不上 → 静默退回全量。两种失败都不会暴露为 4xx/5xx。
+    /// </summary>
+    public string? OrderCodes { get; set; }
 }
 
 public class AdoS8OrderFlowAggregateDto

+ 22 - 5
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/OrderFlow/S8OrderFlowService.cs

@@ -432,7 +432,9 @@ public class S8OrderFlowService : ITransient
             return await BuildBaselineAggregateAsync(tenantId, factoryId, scope);
 
         if (string.Equals(scope, ScopeCurrentFiltered, StringComparison.OrdinalIgnoreCase))
-            return await BuildCurrentAggregateAsync(tenantId, factoryId, scope, query.OrderCodes);
+            // S8-STEP5B-P2-FILTER-SUBSET-CERT-1:与 4 个 pivot 端点统一走 CSV 拆分,
+            // 不再依赖默认集合绑定(多订单会被绑成单元素、过滤静默失效)。
+            return await BuildCurrentAggregateAsync(tenantId, factoryId, scope, ParseOrderCodesCsv(query.OrderCodes));
 
         return EmptyAggregate(scope);
     }
@@ -515,9 +517,16 @@ public class S8OrderFlowService : ITransient
             .Distinct()
             .ToList();
 
+        // S8-STEP5B-ORDER-CHAIN-CLOSURE-1(B):移除 source_object_type == "SALES_ORDER" 过滤,
+        // 与 ORDER_EXEC F-1 同源修复。本方法的统计口径由 BuildObjectStat 定义为
+        // 「命中候选异常类型的**去重 RelatedObjectCode** 数」——即「有多少张销售订单存在该类异常」,
+        // 关联键就是 related_object_code;source_object_type 描述的是**触发对象类型**
+        // (SHIPMENT / PURCHASE_ORDER / WORK_ORDER / NULL …),与订单归属正交。
+        // 实测:命中候选类型的 3 条 Golden 异常(GEX-020-FAW / -PEND / -SHIP)无一为 SALES_ORDER,
+        // 原过滤令末端协同面板恒为「影响订单 0 单 / 主要风险 --」。
+        // 作用域安全性不变:tenant / factory / orderCodes 三重边界保留。
         var rows = await _exceptionRep.AsQueryable()
             .Where(e => e.TenantId == tenantId && e.FactoryId == factoryId && !e.IsDeleted)
-            .Where(e => e.SourceObjectType == ExceptionSourceObjectType)
             .Where(e => e.ExceptionTypeCode != null && allTypes.Contains(e.ExceptionTypeCode!))
             .WhereIF(orderCodes != null && orderCodes.Count > 0,
                 e => e.RelatedObjectCode != null && orderCodes!.Contains(e.RelatedObjectCode!))
@@ -595,13 +604,21 @@ public class S8OrderFlowService : ITransient
         var total = rows.Count;
 
         dto.KpiAvgDays = AvgOrZero(plannedList);
-        dto.ActualAvgDays = AvgOrZero(actualList);
+        // S8-STEP5B-ORDER-CHAIN-CLOSURE-1:与 BASELINE_PPT snapshot 的存储精度对齐(1 位小数)。
+        // 必须显式 AwayFromZero:Math.Round 默认银行家舍入(ToEven)会把 BODY_PRODUCTION 的 5.25 舍成 5.2,
+        // 而 snapshot 存的是 5.3,两个 scope 又会不一致。
+        dto.ActualAvgDays = Math.Round(AvgOrZero(actualList), 1, MidpointRounding.AwayFromZero);
         dto.Green = green;
         dto.Yellow = yellow;
         dto.Red = red;
         dto.Pending = pending;
-        // 整数百分比;total 不含 0 因为上面已 short-circuit。 percent 因子 100 是公式系数,非业务真值。
-        dto.OnTimeRate = total > 0 ? (int)Math.Round(green * 100.0 / total) : 0;
+        // S8-STEP5B-ORDER-CHAIN-CLOSURE-1:按期达成率**排除 pending**(尚未到达的阶段既非按期也非延期,
+        // 计入分母会系统性低估)。原实现用 green/total(含 pending),与 BASELINE_PPT snapshot 的
+        // green/(total-pending) 口径不一致——实测 BODY_PRODUCTION 100 vs 80、FINAL_ASSEMBLY 100 vs 60。
+        // 本项目既有约定同样排除未完成项:ORDER_FLOW_CYCLE_RATIO 只统计 actual_days 非空的阶段,
+        // ORDER_DELIVERY_RATE 只统计已完工订单。故此处对齐 snapshot 与既有约定,而非改写 snapshot 数据。
+        var settled = total - pending;
+        dto.OnTimeRate = settled > 0 ? (int)Math.Round(green * 100.0 / settled) : 0;
         return dto;
     }