Преглед на файлове

fix(s8): complete final assembly collaboration dependencies

补齐 148aeeb8d 中 OrderChainOverviewPage.vue 引用但未提交的 FinalAssemblyCollab 依赖。

- 新增 FinalAssemblyCollabPanel.vue 末端协同面板(gates + parallels 视图)
- 新增 final-assembly-collab.ts 类型 + GATES/PARALLELS 常量
- s8OrderFlowDomainApi.ts 扩展 OrderFlowAggregate.finalAssemblyCollabSummary + 类型
- domainMapper.ts 增 mapFinalAssemblyCollabSummary(后端 Record → 前端 Map)
- AdoS8OrderFlowDtos.cs 增 AdoS8FinalAssemblyCollabSummaryDto / ObjectStat DTO
- S8OrderFlowService.cs 增 BuildFinalAssemblyCollabSummaryAsync(仅查 ado_s8_exception
  现有数据,不新增表 / 不新增异常类型 / 不读 S0|S4 业务表)
- 版本号 Web 2.4.156 / server 1.0.120
YY968XX преди 1 месец
родител
ревизия
71750c733d

+ 14 - 0
Web/src/views/aidop/s8/api/s8OrderFlowDomainApi.ts

@@ -124,6 +124,20 @@ export interface OrderFlowAggregate {
 	avgProcessingMinutes: number;
 	avgLossMinutes: number;
 	stageAggregates: OrderFlowStageAggregate[];
+	/** ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:FINAL_ASSEMBLY_DELIVERY 末端协同摘要;缺省为 null。 */
+	finalAssemblyCollabSummary?: OrderFlowFinalAssemblyCollabSummary | null;
+}
+
+export interface OrderFlowFinalAssemblyObjectStat {
+	impactedOrderCount: number | null;
+	riskOrderCount: number | null;
+	topRiskTitle: string | null;
+	topRiskSeverity: string | null;
+}
+
+export interface OrderFlowFinalAssemblyCollabSummary {
+	gates: Record<string, OrderFlowFinalAssemblyObjectStat>;
+	parallels: Record<string, OrderFlowFinalAssemblyObjectStat>;
 }
 
 export interface OrderFlowProcurementPivotQuery {

+ 324 - 0
Web/src/views/aidop/s8/monitoring/components/order-execution/FinalAssemblyCollabPanel.vue

@@ -0,0 +1,324 @@
+<script setup lang="ts" name="FinalAssemblyCollabPanel">
+// ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:总装发货 · 末端协同达成面板。
+// 顶部摘要走 stageSnapshot 真实字段;主门禁/并行准备矩阵中无真实来源字段统一显示 "--",
+// 状态灯仅在真实状态可派生时着色,不复制总装发货整体绿灯到每个门禁。
+import { computed } from 'vue';
+import type { S8OrderFlowStageSnapshotItem } from '/@/views/aidop/s8/monitoring/data/order-execution/domainMapper';
+import {
+	MAIN_GATES,
+	PARALLEL_PREPS,
+	type FinalAssemblyCollabSummary,
+	type FinalAssemblyObjectStat,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/final-assembly-collab';
+
+interface Props {
+	aggregateFinal: S8OrderFlowStageSnapshotItem | null;
+	collabSummary: FinalAssemblyCollabSummary | null;
+	hitCount: number;
+}
+
+const props = defineProps<Props>();
+
+const DASH = '--';
+
+type Tone = 'green' | 'yellow' | 'red';
+
+function classifyTone(actual: number, kpi: number): Tone {
+	if (kpi <= 0) return 'green';
+	if (actual <= kpi) return 'green';
+	return (actual - kpi) / kpi <= 0.2 ? 'yellow' : 'red';
+}
+
+const TONE_LABEL: Record<Tone, string> = { green: '绿', yellow: '黄', red: '红' };
+const TONE_COLOR: Record<Tone, string> = {
+	green: '#88fd54',
+	yellow: '#ffc107',
+	red: '#ff4d4f',
+};
+
+const summary = computed(() => {
+	const final = props.aggregateFinal;
+	if (!final) {
+		return {
+			kpiText: DASH,
+			actualText: DASH,
+			rateText: DASH,
+			toneLabel: DASH,
+			toneColor: null as string | null,
+		};
+	}
+	const kpi = final.kpiAvgDays;
+	const actual = final.actualAvgDays;
+	const tone = classifyTone(actual, kpi);
+	return {
+		kpiText: `${kpi.toFixed(1)} 天`,
+		actualText: `${actual.toFixed(1)} 天`,
+		rateText: `${final.onTimeRate}%`,
+		toneLabel: TONE_LABEL[tone],
+		toneColor: TONE_COLOR[tone],
+	};
+});
+
+const hitText = computed(() =>
+	props.hitCount > 0 ? `${props.hitCount} 单` : DASH,
+);
+
+interface GateCell {
+	code: string;
+	name: string;
+	ownerSide: string;
+	kpiText: string;
+	actualText: string;
+	varianceText: string;
+	impactedOrderText: string;
+	complianceText: string;
+	complianceTone: Tone | null;
+}
+
+interface ParallelRow {
+	code: string;
+	name: string;
+	ownerSide: string;
+	kittingRateText: string;
+	riskOrderText: string;
+	topRiskText: string;
+	topRiskColor: string | null;
+	statusText: string;
+	statusTone: Tone | null;
+}
+
+function pickStat(map: Map<string, FinalAssemblyObjectStat> | undefined, code: string): FinalAssemblyObjectStat | null {
+	if (!map) return null;
+	return map.get(code) ?? null;
+}
+
+const gateCells = computed<GateCell[]>(() =>
+	MAIN_GATES.map((g) => {
+		const stat = pickStat(props.collabSummary?.gates, g.code);
+		const impacted = stat?.impactedOrderCount;
+		return {
+			code: g.code,
+			name: g.name,
+			ownerSide: g.ownerSideText,
+			// 门禁级 KPI / 实际达成 / 偏差 / 达标情况:当前系统无门禁级字段,统一 "--",不复制节点级数据。
+			kpiText: DASH,
+			actualText: DASH,
+			varianceText: DASH,
+			impactedOrderText: impacted == null ? DASH : `${impacted} 单`,
+			complianceText: DASH,
+			complianceTone: null,
+		};
+	}),
+);
+
+function pickSeverityColor(severity: string | null): string | null {
+	if (!severity) return null;
+	const s = severity.toUpperCase();
+	if (s === 'SERIOUS') return TONE_COLOR.red;
+	if (s === 'FOLLOW') return TONE_COLOR.yellow;
+	return null;
+}
+
+const parallelRows = computed<ParallelRow[]>(() =>
+	PARALLEL_PREPS.map((p) => {
+		const stat = pickStat(props.collabSummary?.parallels, p.code);
+		const risk = stat?.riskOrderCount;
+		const title = stat?.topRiskTitle;
+		return {
+			code: p.code,
+			name: p.name,
+			ownerSide: p.ownerSideText,
+			// 齐套率:无公式 / 无数据源,统一 "--"。
+			kittingRateText: DASH,
+			riskOrderText: risk == null ? DASH : `${risk} 单`,
+			topRiskText: title && title.length > 0 ? title : DASH,
+			topRiskColor: pickSeverityColor(stat?.topRiskSeverity ?? null),
+			statusText: DASH,
+			statusTone: null,
+		};
+	}),
+);
+</script>
+
+<template>
+	<section class="fac-panel">
+		<header class="fac-panel__head">
+			<span class="fac-panel__bar" />
+			<h2 class="fac-panel__title">总装发货 · 末端协同达成</h2>
+		</header>
+
+		<div class="fac-panel__summary">
+			<div class="fac-panel__summary-cell">
+				<span class="fac-panel__summary-label">KPI</span>
+				<span class="fac-panel__summary-value">{{ summary.kpiText }}</span>
+			</div>
+			<div class="fac-panel__summary-cell">
+				<span class="fac-panel__summary-label">实际</span>
+				<span class="fac-panel__summary-value">{{ summary.actualText }}</span>
+			</div>
+			<div class="fac-panel__summary-cell">
+				<span class="fac-panel__summary-label">达标率</span>
+				<span class="fac-panel__summary-value">{{ summary.rateText }}</span>
+			</div>
+			<div class="fac-panel__summary-cell">
+				<span class="fac-panel__summary-label">状态</span>
+				<span class="fac-panel__summary-value fac-panel__summary-value--with-dot">
+					<span
+						v-if="summary.toneColor"
+						class="fac-panel__dot"
+						:style="{ background: summary.toneColor }"
+					/>
+					{{ summary.toneLabel }}
+				</span>
+			</div>
+			<div class="fac-panel__summary-cell">
+				<span class="fac-panel__summary-label">命中订单</span>
+				<span class="fac-panel__summary-value">{{ hitText }}</span>
+			</div>
+		</div>
+
+		<div class="fac-panel__block">
+			<div class="fac-panel__caption">主门禁状态</div>
+			<table class="fac-panel__table">
+				<thead>
+					<tr>
+						<th class="fac-panel__row-head">门禁环节</th>
+						<th v-for="cell in gateCells" :key="cell.code">{{ cell.name }}</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr>
+						<td class="fac-panel__row-label">责任侧</td>
+						<td v-for="cell in gateCells" :key="`os-${cell.code}`">{{ cell.ownerSide }}</td>
+					</tr>
+					<tr>
+						<td class="fac-panel__row-label">KPI</td>
+						<td v-for="cell in gateCells" :key="`k-${cell.code}`">{{ cell.kpiText }}</td>
+					</tr>
+					<tr>
+						<td class="fac-panel__row-label">实际达成</td>
+						<td v-for="cell in gateCells" :key="`a-${cell.code}`">{{ cell.actualText }}</td>
+					</tr>
+					<tr>
+						<td class="fac-panel__row-label">偏差</td>
+						<td v-for="cell in gateCells" :key="`v-${cell.code}`">{{ cell.varianceText }}</td>
+					</tr>
+					<tr>
+						<td class="fac-panel__row-label">影响订单</td>
+						<td v-for="cell in gateCells" :key="`io-${cell.code}`">{{ cell.impactedOrderText }}</td>
+					</tr>
+					<tr>
+						<td class="fac-panel__row-label">达标情况</td>
+						<td v-for="cell in gateCells" :key="`c-${cell.code}`">
+							<template v-if="cell.complianceTone">
+								<span class="fac-panel__dot" :style="{ background: TONE_COLOR[cell.complianceTone] }" />
+								{{ cell.complianceText }}
+							</template>
+							<template v-else>{{ cell.complianceText }}</template>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+
+		<div class="fac-panel__block">
+			<div class="fac-panel__caption">并行准备状态</div>
+			<table class="fac-panel__table">
+				<thead>
+					<tr>
+						<th>准备项</th>
+						<th>责任侧</th>
+						<th>齐套率</th>
+						<th>风险订单</th>
+						<th>主要风险</th>
+						<th>状态</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr v-for="row in parallelRows" :key="row.code">
+						<td class="fac-panel__row-label">{{ row.name }}</td>
+						<td>{{ row.ownerSide }}</td>
+						<td>{{ row.kittingRateText }}</td>
+						<td>{{ row.riskOrderText }}</td>
+						<td :style="row.topRiskColor ? { color: row.topRiskColor, fontWeight: 600 } : undefined">
+							{{ row.topRiskText }}
+						</td>
+						<td>
+							<template v-if="row.statusTone">
+								<span class="fac-panel__dot" :style="{ background: TONE_COLOR[row.statusTone] }" />
+								{{ row.statusText }}
+							</template>
+							<template v-else>{{ row.statusText }}</template>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	</section>
+</template>
+
+<style scoped>
+.fac-panel { display: flex; flex-direction: column; gap: 12px; }
+.fac-panel__head { display: flex; align-items: center; gap: 10px; }
+.fac-panel__bar { width: 5px; height: 18px; border-radius: 999px; background: var(--order-accent, #7bd0ff); }
+.fac-panel__title { margin: 0; font-size: 16px; font-weight: 700; }
+
+.fac-panel__summary {
+	display: grid;
+	grid-template-columns: repeat(5, minmax(0, 1fr));
+	gap: 12px;
+	padding: 12px 14px;
+	border-radius: 10px;
+	background: rgba(25, 28, 34, 0.55);
+	border: 1px solid rgba(69, 70, 77, 0.28);
+}
+.fac-panel__summary-cell { display: flex; flex-direction: column; gap: 4px; }
+.fac-panel__summary-label { font-size: 11px; color: var(--order-text-muted, #909097); }
+.fac-panel__summary-value {
+	font-size: 15px; font-weight: 700;
+	color: var(--order-text-primary, #e1e2eb);
+	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
+}
+.fac-panel__summary-value--with-dot { display: inline-flex; align-items: center; gap: 6px; }
+
+.fac-panel__block {
+	background: rgba(25, 28, 34, 0.55);
+	border: 1px solid rgba(69, 70, 77, 0.28);
+	border-radius: 10px;
+	padding: 12px;
+	overflow-x: auto;
+}
+.fac-panel__caption {
+	font-size: 12px; color: var(--order-text-muted, #909097); margin-bottom: 8px;
+}
+.fac-panel__table {
+	width: 100%; border-collapse: collapse; font-size: 12px;
+	color: var(--order-text-primary, #e1e2eb);
+}
+.fac-panel__table th, .fac-panel__table td {
+	padding: 8px 10px; text-align: center;
+	border: 1px solid rgba(69, 70, 77, 0.18);
+	font-family: 'JetBrains Mono', 'Cascadia Code', Menlo, monospace;
+}
+.fac-panel__table th {
+	background: rgba(15, 19, 26, 0.7);
+	font-weight: 600; font-family: 'PingFang SC', sans-serif;
+}
+.fac-panel__row-head, .fac-panel__row-label {
+	width: 110px; text-align: left;
+	color: var(--order-text-muted, #909097);
+	background: rgba(15, 19, 26, 0.55);
+	font-family: 'PingFang SC', sans-serif; font-weight: 600;
+}
+.fac-panel__dot {
+	display: inline-block; width: 10px; height: 10px;
+	border-radius: 50%; margin-right: 4px; vertical-align: middle;
+}
+
+@media (max-width: 1080px) {
+	.fac-panel__summary { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+}
+@media (max-width: 640px) {
+	.fac-panel__summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+</style>

+ 54 - 13
Web/src/views/aidop/s8/monitoring/data/order-execution/domainMapper.ts

@@ -10,12 +10,19 @@ import type {
 } from '/@/views/aidop/s8/monitoring/data/order-execution/types';
 import type {
 	OrderFlowAggregate,
+	OrderFlowFinalAssemblyCollabSummary,
+	OrderFlowFinalAssemblyObjectStat,
 	OrderFlowOrderDetail,
 	OrderFlowOrderListItem,
 	OrderFlowStage,
 	OrderFlowSubstep,
 	OrderFlowSubstepUnit,
 } from '/@/views/aidop/s8/api/s8OrderFlowDomainApi';
+import type {
+	FinalAssemblyCollabSummary,
+	FinalAssemblyObjectStat,
+} from '/@/views/aidop/s8/monitoring/data/order-execution/final-assembly-collab';
+import { ORDER_CHAIN_OWNER_DEPT_BY_NODE_KEY } from '/@/views/aidop/s8/monitoring/data/order-execution/stage-meta';
 
 // t4a:legacy ChainBaselineMatrix 消费的 baseline snapshot shape;原 owner 是旧 demo client,
 // 旧 client 删除后由 mapper 自持,保留字段语义与 ChainBaselineMatrix 当前依赖一致。
@@ -42,6 +49,8 @@ export interface S8OrderFlowAggregateSnapshotDto {
 	avgLossMinutes: number;
 	stageSnapshots: S8OrderFlowStageSnapshotItem[];
 	remark: string | null;
+	/** ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:FINAL_ASSEMBLY_DELIVERY 末端协同摘要;后端未返 / 无数据时为 null。 */
+	finalAssemblyCollabSummary: FinalAssemblyCollabSummary | null;
 }
 
 // 'YYYY-MM-DD HH:mm' → 'MM-DD HH:mm';'YYYY-MM-DD' → 'MM-DD'。与旧 store.mapStage 同义,复制以避免跨文件依赖。
@@ -146,23 +155,26 @@ export function mapDomainOrderDetailLifecycleWithSubsteps(
 }
 
 // t3i:正式 aggregate → 旧 S8OrderFlowAggregateSnapshotDto shape,让 ChainBaselineMatrix 无需修改。
-// ownerDept 后端未返,用 '--' 兜底,不引入硬编码部门名;snapshotCode/Label 沿用 baseline 语义
+// ownerDept 聚合接口未返;链路矩阵沿用旧 demo 5 阶段固定负责部门文案
 export function mapDomainAggregateToLegacyBaseline(
 	agg: OrderFlowAggregate,
 ): S8OrderFlowAggregateSnapshotDto {
 	const stageSnapshots: S8OrderFlowStageSnapshotItem[] = Array.isArray(agg?.stageAggregates)
-		? agg.stageAggregates.map((s) => ({
-			stageKey: mapOrderFlowCodeToNodeKey(s.orderFlowCode),
-			stageName: s.orderFlowName,
-			ownerDept: '--',
-			kpiAvgDays: s.kpiAvgDays,
-			actualAvgDays: s.actualAvgDays,
-			onTimeRate: s.onTimeRate,
-			green: s.green,
-			yellow: s.yellow,
-			red: s.red,
-			pending: s.pending,
-		}))
+		? agg.stageAggregates.map((s) => {
+			const stageKey = mapOrderFlowCodeToNodeKey(s.orderFlowCode);
+			return {
+				stageKey,
+				stageName: s.orderFlowName,
+				ownerDept: ORDER_CHAIN_OWNER_DEPT_BY_NODE_KEY[stageKey],
+				kpiAvgDays: s.kpiAvgDays,
+				actualAvgDays: s.actualAvgDays,
+				onTimeRate: s.onTimeRate,
+				green: s.green,
+				yellow: s.yellow,
+				red: s.red,
+				pending: s.pending,
+			};
+		})
 		: [];
 	return {
 		snapshotCode: 'BASELINE_PPT',
@@ -174,9 +186,38 @@ export function mapDomainAggregateToLegacyBaseline(
 		avgLossMinutes: agg?.avgLossMinutes ?? 0,
 		stageSnapshots,
 		remark: null,
+		finalAssemblyCollabSummary: mapFinalAssemblyCollabSummary(agg?.finalAssemblyCollabSummary ?? null),
 	};
 }
 
+// ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:后端 Record<string, stat> → 前端 Map<string, FinalAssemblyObjectStat>。
+// 后端缺省 / null / 空字典分别走不同分支,但都不引入假值。
+export function mapFinalAssemblyCollabSummary(
+	raw: OrderFlowFinalAssemblyCollabSummary | null | undefined,
+): FinalAssemblyCollabSummary | null {
+	if (!raw) return null;
+	return {
+		gates: toStatMap(raw.gates),
+		parallels: toStatMap(raw.parallels),
+	};
+}
+
+function toStatMap(
+	src: Record<string, OrderFlowFinalAssemblyObjectStat> | null | undefined,
+): Map<string, FinalAssemblyObjectStat> {
+	const m = new Map<string, FinalAssemblyObjectStat>();
+	if (!src) return m;
+	for (const [code, raw] of Object.entries(src)) {
+		m.set(code, {
+			impactedOrderCount: raw?.impactedOrderCount ?? null,
+			riskOrderCount: raw?.riskOrderCount ?? null,
+			topRiskTitle: raw?.topRiskTitle ?? null,
+			topRiskSeverity: raw?.topRiskSeverity ?? null,
+		});
+	}
+	return m;
+}
+
 export function mapDomainOrderListItem(dto: OrderFlowOrderListItem): SalesOrderExecution {
 	const currentKey = mapOrderFlowCodeToNodeKey(dto.currentOrderFlowCode);
 	return {

+ 48 - 0
Web/src/views/aidop/s8/monitoring/data/order-execution/final-assembly-collab.ts

@@ -0,0 +1,48 @@
+// ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:FINAL_ASSEMBLY_DELIVERY 末端协同字典 + 类型。
+// 培训态字典:9 个对象的 code / 中文名 / 责任侧文案 / 异常归因候选清单全部前端常量化;
+// 责任侧文案为首版业务口径(未对接 SysOrg/SysRole),未来真主数据接入时切换数据源不改 UI。
+// 任何"--"展示由 panel 组件内 displayOrDash 兜底;本文件不出现示意数。
+
+export type FinalAssemblyObjectType = 'GATE' | 'PARALLEL';
+
+export interface FinalAssemblyCollabObject {
+	/** 与后端 FinalAssemblyCollabSummary 字典 key 对齐(GATES / PARALLELS 任一) */
+	code: string;
+	name: string;
+	type: FinalAssemblyObjectType;
+	/** 责任侧文案(培训态业务口径,非 SysOrg 字典) */
+	ownerSideText: string;
+	/** 后端归因时使用的候选 ado_s8_exception_type type_code 列表;空数组表示该对象当前无映射,UI 全 -- */
+	riskExceptionTypeCodes: string[];
+}
+
+/** 主门禁 5 项(固定顺序,UI 横向矩阵列顺序) */
+export const MAIN_GATES: ReadonlyArray<FinalAssemblyCollabObject> = [
+	{ code: 'ASSEMBLY_COMPLETION',    name: '总装完工', type: 'GATE', ownerSideText: '总装线',     riskExceptionTypeCodes: [] },
+	{ code: 'QUALITY_RELEASE',        name: '质量放行', type: 'GATE', ownerSideText: '质量/终检',  riskExceptionTypeCodes: [] },
+	{ code: 'PACKAGING_COMPLETION',   name: '包装完成', type: 'GATE', ownerSideText: '包装线',     riskExceptionTypeCodes: [] },
+	{ code: 'GOODS_HANDOVER',         name: '成品交接', type: 'GATE', ownerSideText: '成品库',     riskExceptionTypeCodes: ['PENDING_SHIPMENT'] },
+	{ code: 'SHIPMENT_CONFIRMATION',  name: '发运确认', type: 'GATE', ownerSideText: '发运协调',   riskExceptionTypeCodes: ['DELIVERY_DELAY', 'SHIPMENT_ABNORMAL', 'DELIVERY_DELAY_WARNING', 'FINISHED_GOODS_PENDING_SHIPMENT'] },
+];
+
+/** 并行准备 4 项(固定顺序,UI 纵向矩阵行顺序) */
+export const PARALLEL_PREPS: ReadonlyArray<FinalAssemblyCollabObject> = [
+	{ code: 'PACKAGING_MATERIAL_PREP', name: '包装材料准备',     type: 'PARALLEL', ownerSideText: '包装线/物料', riskExceptionTypeCodes: [] },
+	{ code: 'LABEL_DOC_PREP',          name: '标签/随箱资料准备', type: 'PARALLEL', ownerSideText: '技术/质量',   riskExceptionTypeCodes: [] },
+	{ code: 'SHIPPING_PLAN',           name: '发运计划',          type: 'PARALLEL', ownerSideText: '发运协调',    riskExceptionTypeCodes: ['FINISHED_GOODS_PENDING_SHIPMENT'] },
+	{ code: 'SHIPPING_DOC_PREP',       name: '出货单据准备',      type: 'PARALLEL', ownerSideText: '计划/商务',   riskExceptionTypeCodes: [] },
+];
+
+/** 单对象统计:与后端 AdoS8FinalAssemblyObjectStatDto 1:1,无数据字段保持 null(不转假值) */
+export interface FinalAssemblyObjectStat {
+	impactedOrderCount: number | null;
+	riskOrderCount: number | null;
+	topRiskTitle: string | null;
+	topRiskSeverity: string | null;
+}
+
+/** 聚合摘要:与后端 AdoS8FinalAssemblyCollabSummaryDto 1:1;Map 而非 Record 便于 panel 用 Map.get(code) */
+export interface FinalAssemblyCollabSummary {
+	gates: Map<string, FinalAssemblyObjectStat>;
+	parallels: Map<string, FinalAssemblyObjectStat>;
+}

+ 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.119</AssemblyVersion>
-    <FileVersion>1.0.119</FileVersion>
-    <Version>1.0.119</Version>
+    <AssemblyVersion>1.0.120</AssemblyVersion>
+    <FileVersion>1.0.120</FileVersion>
+    <Version>1.0.120</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 31 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Dto/S8/OrderFlow/AdoS8OrderFlowDtos.cs

@@ -131,6 +131,37 @@ public class AdoS8OrderFlowAggregateDto
     public decimal AvgProcessingMinutes { get; set; }
     public decimal AvgLossMinutes { get; set; }
     public List<AdoS8OrderFlowStageAggregateDto> StageAggregates { get; set; } = new();
+
+    /// <summary>
+    /// FINAL_ASSEMBLY_DELIVERY 末端协同聚合摘要(基线 / 当前两条路径均填充)。
+    /// 派生自 ado_s8_exception 现有异常类型,不新增业务表 / 不新增异常类型 / 不读 S0|S4 业务表。
+    /// 字段全 nullable,老调用方无感知。
+    /// </summary>
+    public AdoS8FinalAssemblyCollabSummaryDto? FinalAssemblyCollabSummary { get; set; }
+}
+
+public class AdoS8FinalAssemblyCollabSummaryDto
+{
+    /// <summary>主门禁(GATE)—— key 为前端固定 object_code(如 GOODS_HANDOVER / SHIPMENT_CONFIRMATION)。无映射的门禁不出现在字典中。</summary>
+    public Dictionary<string, AdoS8FinalAssemblyObjectStatDto> Gates { get; set; } = new();
+
+    /// <summary>并行准备(PARALLEL)—— key 为前端固定 object_code(如 SHIPPING_PLAN)。无映射的准备项不出现在字典中。</summary>
+    public Dictionary<string, AdoS8FinalAssemblyObjectStatDto> Parallels { get; set; } = new();
+}
+
+public class AdoS8FinalAssemblyObjectStatDto
+{
+    /// <summary>主门禁口径下的"影响订单"数 = 命中候选异常类型的去重 RelatedObjectCode 数;并行准备口径下保持 null。</summary>
+    public int? ImpactedOrderCount { get; set; }
+
+    /// <summary>并行准备口径下的"风险订单"数 = 命中候选异常类型的去重 RelatedObjectCode 数;主门禁口径下保持 null。</summary>
+    public int? RiskOrderCount { get; set; }
+
+    /// <summary>主要风险标题:候选异常中按 Severity(SERIOUS 优先) → CreatedAt(降序) 取首条 Title。无命中保持 null。</summary>
+    public string? TopRiskTitle { get; set; }
+
+    /// <summary>主要风险严重度:与 TopRiskTitle 来自同一条异常。无命中保持 null。</summary>
+    public string? TopRiskSeverity { get; set; }
 }
 
 public class AdoS8OrderFlowStageAggregateDto

+ 90 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Service/S8/OrderFlow/S8OrderFlowService.cs

@@ -19,6 +19,19 @@ public class S8OrderFlowService : ITransient
     private const string BaselineSnapshotCode = "CHAIN_AGGREGATE_BASELINE";
     private const string PivotTotalCode = "TOTAL";
 
+    // ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:异常类型 → 末端协同对象 映射常量。
+    // 这些 type_code 均来自 ado_s8_exception_type seed 中 enabled=true、monitoring_category_key=FINAL_ASSEMBLY_DELIVERY 的子集;
+    // 不新增异常类型、不依赖业务执行表。空数组对象(如 ASSEMBLY_COMPLETION / 包装材料准备)在前端展示 "--"。
+    private const string FinalAssemblyObjectGoodsHandover = "GOODS_HANDOVER";
+    private const string FinalAssemblyObjectShipmentConfirmation = "SHIPMENT_CONFIRMATION";
+    private const string FinalAssemblyObjectShippingPlan = "SHIPPING_PLAN";
+    private static readonly string[] GoodsHandoverExceptionTypes = { "PENDING_SHIPMENT" };
+    private static readonly string[] ShipmentConfirmationExceptionTypes =
+    {
+        "DELIVERY_DELAY", "SHIPMENT_ABNORMAL", "DELIVERY_DELAY_WARNING", "FINISHED_GOODS_PENDING_SHIPMENT",
+    };
+    private static readonly string[] ShippingPlanExceptionTypes = { "FINISHED_GOODS_PENDING_SHIPMENT" };
+
     private static readonly JsonSerializerOptions JsonOpts = new()
     {
         PropertyNameCaseInsensitive = true,
@@ -366,6 +379,7 @@ public class S8OrderFlowService : ITransient
         if (snapshot == null) return EmptyAggregate(scope);
 
         var stages = ParseStageSnapshots(snapshot.StageSnapshotsJson);
+        var collab = await BuildFinalAssemblyCollabSummaryAsync(tenantId, factoryId, orderCodes: null);
         return new AdoS8OrderFlowAggregateDto
         {
             Scope = scope,
@@ -375,6 +389,7 @@ public class S8OrderFlowService : ITransient
             AvgProcessingMinutes = snapshot.AvgProcessingMinutes,
             AvgLossMinutes = snapshot.AvgLossMinutes,
             StageAggregates = ReorderByCanonical(stages),
+            FinalAssemblyCollabSummary = collab,
         };
     }
 
@@ -406,6 +421,7 @@ public class S8OrderFlowService : ITransient
             stageAggregates.Add(BuildStageAggregateFromRows(code, rows ?? new List<AdoS8OrderFlowStage>()));
         }
 
+        var collab = await BuildFinalAssemblyCollabSummaryAsync(tenantId, factoryId, orderCodes);
         return new AdoS8OrderFlowAggregateDto
         {
             Scope = scope,
@@ -415,9 +431,83 @@ public class S8OrderFlowService : ITransient
             AvgProcessingMinutes = AvgOrZero(processingList),
             AvgLossMinutes = AvgOrZero(lossList),
             StageAggregates = stageAggregates,
+            FinalAssemblyCollabSummary = collab,
         };
     }
 
+    // ORDER-CHAIN-FINAL-ASSEMBLY-DEPT-COLLAB-MVP-1:FINAL_ASSEMBLY_DELIVERY 末端协同派生。
+    // 仅查 ado_s8_exception 现有数据;只覆盖有真实映射的 2 个主门禁 + 1 个并行准备,其它 6 个对象前端展示 "--"。
+    // 不读取 S0|S4 业务表;不新增表 / seed / 异常类型。
+    private async Task<AdoS8FinalAssemblyCollabSummaryDto> BuildFinalAssemblyCollabSummaryAsync(
+        long tenantId, long factoryId, List<string>? orderCodes)
+    {
+        var allTypes = GoodsHandoverExceptionTypes
+            .Concat(ShipmentConfirmationExceptionTypes)
+            .Concat(ShippingPlanExceptionTypes)
+            .Distinct()
+            .ToList();
+
+        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!))
+            .Select(e => new ExceptionAttribution
+            {
+                ExceptionTypeCode = e.ExceptionTypeCode,
+                RelatedObjectCode = e.RelatedObjectCode,
+                Title = e.Title,
+                Severity = e.Severity,
+                CreatedAt = e.CreatedAt,
+            })
+            .ToListAsync();
+
+        return new AdoS8FinalAssemblyCollabSummaryDto
+        {
+            Gates = new Dictionary<string, AdoS8FinalAssemblyObjectStatDto>
+            {
+                [FinalAssemblyObjectGoodsHandover] = BuildObjectStat(rows, GoodsHandoverExceptionTypes, isGate: true),
+                [FinalAssemblyObjectShipmentConfirmation] = BuildObjectStat(rows, ShipmentConfirmationExceptionTypes, isGate: true),
+            },
+            Parallels = new Dictionary<string, AdoS8FinalAssemblyObjectStatDto>
+            {
+                [FinalAssemblyObjectShippingPlan] = BuildObjectStat(rows, ShippingPlanExceptionTypes, isGate: false),
+            },
+        };
+    }
+
+    private static AdoS8FinalAssemblyObjectStatDto BuildObjectStat(
+        List<ExceptionAttribution> rows, string[] candidateTypes, bool isGate)
+    {
+        var subset = rows.Where(r => r.ExceptionTypeCode != null && candidateTypes.Contains(r.ExceptionTypeCode!)).ToList();
+        var count = subset
+            .Where(r => !string.IsNullOrEmpty(r.RelatedObjectCode))
+            .Select(r => r.RelatedObjectCode!)
+            .Distinct()
+            .Count();
+        var top = subset
+            .OrderByDescending(r => string.Equals(r.Severity, "SERIOUS", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
+            .ThenByDescending(r => r.CreatedAt)
+            .FirstOrDefault();
+        return new AdoS8FinalAssemblyObjectStatDto
+        {
+            ImpactedOrderCount = isGate ? count : (int?)null,
+            RiskOrderCount = isGate ? (int?)null : count,
+            TopRiskTitle = top?.Title,
+            TopRiskSeverity = top?.Severity,
+        };
+    }
+
+    private sealed class ExceptionAttribution
+    {
+        public string? ExceptionTypeCode { get; set; }
+        public string? RelatedObjectCode { get; set; }
+        public string? Title { get; set; }
+        public string? Severity { get; set; }
+        public DateTime CreatedAt { get; set; }
+    }
+
     private static AdoS8OrderFlowStageAggregateDto BuildStageAggregateFromRows(string flowCode, List<AdoS8OrderFlowStage> rows)
     {
         var dto = new AdoS8OrderFlowStageAggregateDto