Selaa lähdekoodia

feat: land tenant isolation, S2 schedule hardening, branding and bid docs

Cover remaining WIP: tenant-scoped services, schedule generation/indexes, UI logo refresh, and delivery/bid document pack. chore: bump version Web 2.4.282 / server 1.0.327

Co-authored-by: Cursor <cursoragent@cursor.com>
YY968XX 6 päivää sitten
vanhempi
commit
0da9ce11b9
100 muutettua tiedostoa jossa 889 lisäystä ja 85 poistoa
  1. 1 1
      Web/index.html
  2. 2 2
      Web/package.json
  3. BIN
      Web/public/app-icons/aidop-logo-vertical.png
  4. BIN
      Web/public/app-icons/aidop-logo.png
  5. BIN
      Web/public/upload/admin.net-logo.png
  6. BIN
      Web/public/upload/logo.png
  7. 29 16
      Web/src/layout/logo/index.vue
  8. 4 4
      Web/src/stores/themeConfig.ts
  9. 18 0
      Web/src/views/aidop/api/workOrderProgressDashboard.ts
  10. 80 5
      Web/src/views/aidop/api/workOrderScheduling.ts
  11. 1 1
      Web/src/views/aidop/demo/cockpit.vue
  12. 67 7
      Web/src/views/aidop/production/workOrderProgressDashboardList.vue
  13. 83 4
      Web/src/views/aidop/production/workOrderSchedulingList.vue
  14. 18 3
      Web/src/views/aidop/production/workOrderSchedulingPriorityForm.vue
  15. 76 2
      Web/src/views/aidop/production/workOrderSchedulingTraceForm.vue
  16. 4 20
      Web/src/views/aidop/s8/api/s8OrderFlowDomainApi.ts
  17. 12 9
      Web/src/views/dashboard/home.vue
  18. 19 11
      Web/src/views/login/index.vue
  19. 98 0
      doc/AIDOP投标报价策略逻辑图.md
  20. BIN
      doc/AIDOP投标材料准备行动清单.xlsx
  21. 49 0
      doc/Ai-DOP系统各模块情况一览表.md
  22. BIN
      doc/Ai-DOP系统各模块情况一览表.xlsx
  23. 11 0
      doc/README.md
  24. 289 0
      doc/_bid_apply_seals.py
  25. 11 0
      doc/_bid_assets/_manifest.txt
  26. 17 0
      doc/_bid_assets/_manifest_ui.txt
  27. BIN
      doc/_bid_assets/_orig_footer/ui_s0_bom.png
  28. BIN
      doc/_bid_assets/_orig_footer/ui_s0_customer.png
  29. BIN
      doc/_bid_assets/_orig_footer/ui_s0_location.png
  30. BIN
      doc/_bid_assets/_orig_footer/ui_s0_material.png
  31. BIN
      doc/_bid_assets/_orig_footer/ui_s0_routing.png
  32. BIN
      doc/_bid_assets/_orig_footer/ui_s0_supplier.png
  33. BIN
      doc/_bid_assets/_orig_footer/ui_s5_inout.png
  34. BIN
      doc/_bid_assets/_orig_footer/ui_s5_iqc.png
  35. BIN
      doc/_bid_assets/_orig_footer/ui_s5_stock.png
  36. BIN
      doc/_bid_assets/_orig_footer/ui_s5_stocktake.png
  37. BIN
      doc/_bid_assets/_orig_footer/ui_s6_equipment.png
  38. BIN
      doc/_bid_assets/_orig_footer/ui_s6_ipqc.png
  39. BIN
      doc/_bid_assets/_orig_footer/ui_s6_tooling.png
  40. BIN
      doc/_bid_assets/_orig_footer/ui_s7_fqc_result.png
  41. BIN
      doc/_bid_assets/_orig_footer/ui_s7_fqc_task.png
  42. BIN
      doc/_bid_assets/_orig_footer/ui_s7_prodin.png
  43. BIN
      doc/_bid_assets/_orig_footer/ui_s7_shipment.png
  44. BIN
      doc/_bid_assets/_raw/fig_api.png
  45. BIN
      doc/_bid_assets/_raw/fig_application.png
  46. BIN
      doc/_bid_assets/_raw/fig_auto_closedloop.png
  47. BIN
      doc/_bid_assets/_raw/fig_closedloop.png
  48. BIN
      doc/_bid_assets/_raw/fig_data.png
  49. BIN
      doc/_bid_assets/_raw/fig_data_layer.png
  50. BIN
      doc/_bid_assets/_raw/fig_defect_flow.png
  51. BIN
      doc/_bid_assets/_raw/fig_deploy.png
  52. BIN
      doc/_bid_assets/_raw/fig_diag_arch.png
  53. BIN
      doc/_bid_assets/_raw/fig_diag_model.png
  54. BIN
      doc/_bid_assets/_raw/fig_diagnosis.png
  55. BIN
      doc/_bid_assets/_raw/fig_emergency.png
  56. BIN
      doc/_bid_assets/_raw/fig_integration.png
  57. BIN
      doc/_bid_assets/_raw/fig_kanban.png
  58. BIN
      doc/_bid_assets/_raw/fig_metric_model.png
  59. BIN
      doc/_bid_assets/_raw/fig_module_map.png
  60. BIN
      doc/_bid_assets/_raw/fig_otd_blueprint.png
  61. BIN
      doc/_bid_assets/_raw/fig_quality_gate.png
  62. BIN
      doc/_bid_assets/_raw/fig_service.png
  63. BIN
      doc/_bid_assets/_raw/fig_tech.png
  64. BIN
      doc/_bid_assets/_raw/fig_test_v.png
  65. BIN
      doc/_bid_assets/_raw/fig_training.png
  66. BIN
      doc/_bid_assets/_raw/s0_master_dataflow.png
  67. BIN
      doc/_bid_assets/_raw/s1_kanban.png
  68. BIN
      doc/_bid_assets/_raw/s1_order_delivery.png
  69. BIN
      doc/_bid_assets/_raw/s1_otd_mainline.png
  70. BIN
      doc/_bid_assets/_raw/s2_plan_to_exec.png
  71. BIN
      doc/_bid_assets/_raw/s3_domains.png
  72. BIN
      doc/_bid_assets/_raw/s3_shortage.png
  73. BIN
      doc/_bid_assets/_raw/s4_domains.png
  74. BIN
      doc/_bid_assets/_raw/s5_lifecycle.png
  75. BIN
      doc/_bid_assets/_raw/s6_ipqc.png
  76. BIN
      doc/_bid_assets/_raw/s7_lifecycle.png
  77. BIN
      doc/_bid_assets/attach/credit_01.jpg
  78. BIN
      doc/_bid_assets/attach/credit_02.jpg
  79. BIN
      doc/_bid_assets/attach/credit_03.jpg
  80. BIN
      doc/_bid_assets/attach/credit_04.jpg
  81. BIN
      doc/_bid_assets/attach/credit_05.jpg
  82. BIN
      doc/_bid_assets/attach/credit_06.jpg
  83. BIN
      doc/_bid_assets/attach/credit_07.jpg
  84. BIN
      doc/_bid_assets/attach/id_back.jpg
  85. BIN
      doc/_bid_assets/attach/id_front.jpg
  86. BIN
      doc/_bid_assets/attach/license.jpg
  87. BIN
      doc/_bid_assets/attach/sw_enterprise.jpg
  88. BIN
      doc/_bid_assets/attach/sw_product.jpg
  89. BIN
      doc/_bid_assets/attach/swc_01.jpg
  90. BIN
      doc/_bid_assets/attach/swc_02.jpg
  91. BIN
      doc/_bid_assets/attach/swc_03.jpg
  92. BIN
      doc/_bid_assets/attach/swc_04.jpg
  93. BIN
      doc/_bid_assets/attach/swc_05.jpg
  94. BIN
      doc/_bid_assets/attach/swc_06.jpg
  95. BIN
      doc/_bid_assets/attach/swc_07.jpg
  96. BIN
      doc/_bid_assets/attach/swc_08.jpg
  97. BIN
      doc/_bid_assets/attach/swc_09.jpg
  98. BIN
      doc/_bid_assets/attach/swc_10.jpg
  99. BIN
      doc/_bid_assets/fig_api.png
  100. BIN
      doc/_bid_assets/fig_application.png

+ 1 - 1
Web/index.html

@@ -7,7 +7,7 @@
 	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
 	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
 	<link rel="stylesheet" type="text/css" media="print" href="/print-lock.css" />
 	<link rel="stylesheet" type="text/css" media="print" href="/print-lock.css" />
 	<link rel="icon" id="favicon" href="data:;base64,=" />
 	<link rel="icon" id="favicon" href="data:;base64,=" />
-	<title>Admin.NET</title>
+	<title>河南产互联制造业数据智能运营平台</title>
 </head>
 </head>
 
 
 <body>
 <body>

+ 2 - 2
Web/package.json

@@ -1,10 +1,10 @@
 {
 {
   "name": "admin.net",
   "name": "admin.net",
   "type": "module",
   "type": "module",
-	"version": "2.4.281",
+	"version": "2.4.282",
   "packageManager": "pnpm@10.32.1",
   "packageManager": "pnpm@10.32.1",
   "lastBuildTime": "2026.03.15",
   "lastBuildTime": "2026.03.15",
-  "description": "Admin.NET 蝡坔銁撌其犖�抵�銝羓� .NET �𡁶鍂���撘��烐���",
+  "description": "Admin.NET 蝡坔銁撌其犖�抵�銝羓� .NET �𡁶鍂���撘��烐��?,
   "author": "zuohuaijun",
   "author": "zuohuaijun",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {

BIN
Web/public/app-icons/aidop-logo-vertical.png


BIN
Web/public/app-icons/aidop-logo.png


BIN
Web/public/upload/admin.net-logo.png


BIN
Web/public/upload/logo.png


+ 29 - 16
Web/src/layout/logo/index.vue

@@ -1,10 +1,10 @@
 <template>
 <template>
 	<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
 	<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
-		<img src="/app-icons/aidop-logo.png" class="layout-logo-medium-img" v-if="showLogo" />
-		<span>Ai-DOP</span>
+		<img :src="logoSrc" class="layout-logo-medium-img" v-if="showLogo" :alt="systemTitle" />
+		<span :title="systemTitle">{{ shortTitle }}</span>
 	</div>
 	</div>
 	<div class="layout-logo-size" v-else @click="onThemeConfigChange">
 	<div class="layout-logo-size" v-else @click="onThemeConfigChange">
-		<img src="/app-icons/aidop-logo.png" class="layout-logo-size-img" />
+		<img :src="logoSrc" class="layout-logo-size-img" :alt="systemTitle" />
 	</div>
 	</div>
 </template>
 </template>
 
 
@@ -12,12 +12,14 @@
 import { computed } from 'vue';
 import { computed } from 'vue';
 import { storeToRefs } from 'pinia';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 import { useThemeConfig } from '/@/stores/themeConfig';
-// import logoMini from '/@/assets/logo-mini.svg';
 
 
-// 定义变量内容
 const storesThemeConfig = useThemeConfig();
 const storesThemeConfig = useThemeConfig();
 const { themeConfig } = storeToRefs(storesThemeConfig);
 const { themeConfig } = storeToRefs(storesThemeConfig);
 
 
+const shortTitle = '智能运营平台';
+const systemTitle = computed(() => themeConfig.value.globalTitle || '河南产互联制造业数据智能运营平台');
+const logoSrc = computed(() => themeConfig.value.logoUrl || '/app-icons/aidop-logo.png');
+
 // 设置 logo 的显示。classic 经典布局默认显示 logo
 // 设置 logo 的显示。classic 经典布局默认显示 logo
 const setShowLogo = computed(() => {
 const setShowLogo = computed(() => {
 	let { isCollapse, layout } = themeConfig.value;
 	let { isCollapse, layout } = themeConfig.value;
@@ -36,21 +38,28 @@ const onThemeConfigChange = () => {
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
 .layout-logo {
 .layout-logo {
-	width: 300px;
-	height: 50px;
+	width: 100%;
+	max-width: 420px;
+	height: 72px;
 	display: flex;
 	display: flex;
 	align-items: center;
 	align-items: center;
-  margin-left: 15px;
+	gap: 12px;
+	margin-left: 8px;
+	padding-right: 8px;
 	box-shadow: rgb(0 21 41 / 2%) 0px 1px 4px;
 	box-shadow: rgb(0 21 41 / 2%) 0px 1px 4px;
 	color: var(--el-color-primary);
 	color: var(--el-color-primary);
-	font-size: 16px;
 	cursor: pointer;
 	cursor: pointer;
 	animation: logoAnimation 0.3s ease-in-out;
 	animation: logoAnimation 0.3s ease-in-out;
 	span {
 	span {
+		flex: 1;
+		min-width: 0;
 		white-space: nowrap;
 		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
 		display: inline-block;
 		display: inline-block;
-		font-weight: 800;
-		font-size: 18px;
+		font-weight: 700;
+		font-size: 16px;
+		line-height: 1.25;
 	}
 	}
 	&:hover {
 	&:hover {
 		span {
 		span {
@@ -58,19 +67,23 @@ const onThemeConfigChange = () => {
 		}
 		}
 	}
 	}
 	&-medium-img {
 	&-medium-img {
-		width: 12%;
-		margin-right: 5px;
-		padding:3px;
+		width: 68px;
+		height: 68px;
+		flex-shrink: 0;
+		object-fit: contain;
+		padding: 0;
 	}
 	}
 }
 }
 .layout-logo-size {
 .layout-logo-size {
 	width: 100%;
 	width: 100%;
-	height: 50px;
+	height: 72px;
 	display: flex;
 	display: flex;
 	cursor: pointer;
 	cursor: pointer;
 	animation: logoAnimation 0.3s ease-in-out;
 	animation: logoAnimation 0.3s ease-in-out;
 	&-img {
 	&-img {
-		width: 20px;
+		width: 56px;
+		height: 56px;
+		object-fit: contain;
 		margin: auto;
 		margin: auto;
 	}
 	}
 	&:hover {
 	&:hover {

+ 4 - 4
Web/src/stores/themeConfig.ts

@@ -135,13 +135,13 @@ export const useThemeConfig = defineStore('themeConfig', {
 			 * 全局网站标题 / 副标题
 			 * 全局网站标题 / 副标题
 			 */
 			 */
 			// 网站主标题(菜单导航、浏览器当前网页标题)
 			// 网站主标题(菜单导航、浏览器当前网页标题)
-			globalTitle: 'Ai-DOP',
+			globalTitle: '河南产互联制造业数据智能运营平台',
 			// 网站副标题(登录页顶部文字)
 			// 网站副标题(登录页顶部文字)
-			globalViceTitle: 'Ai-DOP',
+			globalViceTitle: '河南产互联制造业数据智能运营平台',
 			// 网站副标题(登录页顶部文字)
 			// 网站副标题(登录页顶部文字)
-			globalViceTitleMsg: '站在巨人肩膀上的 .NET 通用权限开发框架',
+			globalViceTitleMsg: '可视,可诊,可控,可溯,可闭环',
 			// 版权和备案文字
 			// 版权和备案文字
-			copyright: 'Copyright © 2021-present Admin.NET All rights reserved.',
+			copyright: 'Copyright © 2026',
 			// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn
 			// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn
 			globalI18n: 'zh-cn',
 			globalI18n: 'zh-cn',
 			// 默认全局组件大小,可选值"<large|'default'|small>",默认 'large'
 			// 默认全局组件大小,可选值"<large|'default'|small>",默认 'large'

+ 18 - 0
Web/src/views/aidop/api/workOrderProgressDashboard.ts

@@ -23,7 +23,25 @@ export interface WorkOrderProgressDashboardRow {
 	noCompleted?: number | null;
 	noCompleted?: number | null;
 	materialSituation?: string | null;
 	materialSituation?: string | null;
 	status?: string | null;
 	status?: string | null;
+	domain?: string | null;
+	/** 入库进度 % */
 	progress?: number | null;
 	progress?: number | null;
+
+	/** 当前工序(正在生产的一序,无则为已有完成量的最靠后一序) */
+	currentOp?: number | null;
+	currentOpName?: string | null;
+	currentOpQty?: number | null;
+	/** 末道工序(工艺路线最大 OP) */
+	lastOp?: number | null;
+	lastOpName?: string | null;
+	/** 末道完成数 = 全部工序走完的产品件数 */
+	lastOpQty?: number | null;
+	/** 在制件数 = 当前工序完成数 − 末道完成数 */
+	qtyWip?: number | null;
+	/** 全工序完成量求和(技术对账用,非流程件进度) */
+	qtyOpComplete?: number | null;
+	/** 执行进度 %(末道完成数 / 工单数量) */
+	progressExec?: number | null;
 }
 }
 
 
 export function fetchWorkOrderProgressDashboardList(params: Record<string, unknown>) {
 export function fetchWorkOrderProgressDashboardList(params: Record<string, unknown>) {

+ 80 - 5
Web/src/views/aidop/api/workOrderScheduling.ts

@@ -27,10 +27,46 @@ export interface WorkOrderSchedulingRow {
 	urgent?: number | null;
 	urgent?: number | null;
 }
 }
 
 
-/** 生成生产排程计划 */
+export interface ScheduleGenerateSubmitResult {
+	message?: string;
+	runId: number;
+	status?: string;
+	async?: boolean;
+}
+
+export interface ScheduleGenerateStatusResult {
+	runId: number;
+	status?: string;
+	message?: string;
+	detail?: {
+		workOrderCount?: number;
+		scheduleRowCount?: number;
+		pushEnqueuedCount?: number;
+		frozenWorkOrderCount?: number;
+		frozenOpCount?: number;
+		phase?: string;
+		skippedWorkOrders?: string[];
+	};
+	startTime?: string;
+	endTime?: string;
+}
+
+/** 提交全量生产排程(异步,立即返回 runId) */
 export function productionSchedule(domain: string) {
 export function productionSchedule(domain: string) {
 	return service
 	return service
-		.post('/api/Production/scheduling/generate', null, { params: { domain }, timeout: 300000 })
+		.post<ScheduleGenerateSubmitResult>('/api/Production/scheduling/generate', null, {
+			params: { domain },
+			timeout: 60000,
+		})
+		.then((r) => r.data);
+}
+
+/** 轮询排程运行状态 */
+export function fetchScheduleGenerateStatus(runId: number) {
+	return service
+		.get<ScheduleGenerateStatusResult>('/api/Production/scheduling/generate-status', {
+			params: withAidopTenantParams({ runId }),
+		})
 		.then((r) => r.data);
 		.then((r) => r.data);
 }
 }
 
 
@@ -50,7 +86,13 @@ export async function replenishmentWorkOrdCheckChangePriority(params: {
 	domain: string;
 	domain: string;
 	userAccount: string;
 	userAccount: string;
 	lotSerial: string;
 	lotSerial: string;
-}): Promise<void> {
+}): Promise<{
+	message?: string;
+	warnings?: string[];
+	pendingReorder?: boolean;
+	rescheduleTriggered?: boolean;
+	scheduleRowCount?: number;
+}> {
 	const res = await service.post('/api/Production/scheduling/update-priority-and-recheck', {
 	const res = await service.post('/api/Production/scheduling/update-priority-and-recheck', {
 		workord: params.workord,
 		workord: params.workord,
 		qty: String(params.qty ?? ''),
 		qty: String(params.qty ?? ''),
@@ -60,9 +102,18 @@ export async function replenishmentWorkOrdCheckChangePriority(params: {
 		userAccount: params.userAccount,
 		userAccount: params.userAccount,
 		lotSerial: params.lotSerial ?? '',
 		lotSerial: params.lotSerial ?? '',
 	});
 	});
-	const data = res.data as { message?: string } | undefined;
+	const data = res.data as
+		| {
+				message?: string;
+				warnings?: string[];
+				pendingReorder?: boolean;
+				rescheduleTriggered?: boolean;
+				scheduleRowCount?: number;
+		  }
+		| undefined;
 	const msg = String(data?.message ?? '').trim().toLowerCase();
 	const msg = String(data?.message ?? '').trim().toLowerCase();
 	if (msg && msg !== 'ok') throw new Error(`接口返回异常:${data?.message}`);
 	if (msg && msg !== 'ok') throw new Error(`接口返回异常:${data?.message}`);
+	return data ?? {};
 }
 }
 
 
 export function fetchWorkOrderSchedulingList(params: Record<string, unknown>) {
 export function fetchWorkOrderSchedulingList(params: Record<string, unknown>) {
@@ -106,6 +157,7 @@ export function fetchSchedulingEditPreview(workOrd: string, domain: string) {
 			lotSerial: string;
 			lotSerial: string;
 			urgent: number;
 			urgent: number;
 			ordDate?: string | null;
 			ordDate?: string | null;
+			dueDate?: string | null;
 		}>('/api/Production/scheduling/edit-preview', { params: { workOrd, domain } })
 		}>('/api/Production/scheduling/edit-preview', { params: { workOrd, domain } })
 		.then((r) => r.data);
 		.then((r) => r.data);
 }
 }
@@ -126,9 +178,32 @@ export function fetchSchedulingMaterials(workOrd: string, domain: string) {
 		.then((r) => r.data);
 		.then((r) => r.data);
 }
 }
 
 
+/** 工序执行进度汇总(与工单执行进度看板同一口径) */
+export interface WorkOrderExecutionProgress {
+	/** 当前工序:正在生产的一序,无则为已有完成量的最靠后一序 */
+	currentOp?: number | null;
+	currentOpName?: string | null;
+	currentOpQty?: number | null;
+	/** 末道工序(工艺路线最大 OP) */
+	lastOp?: number | null;
+	lastOpName?: string | null;
+	/** 末道完成数 = 全部工序走完的产品件数 */
+	lastOpQty?: number | null;
+	/** 在制件数 = 当前工序完成数 − 末道完成数 */
+	qtyWip?: number | null;
+	/** 全工序完成量求和(技术对账用) */
+	qtyOpComplete?: number | null;
+	/** 执行进度 %(末道完成数 / 工单数量) */
+	progressExec?: number | null;
+	qtyOrded?: number | null;
+	qtyCompleted?: number | null;
+}
+
 export function fetchSchedulingRoutings(workOrd: string, domain: string) {
 export function fetchSchedulingRoutings(workOrd: string, domain: string) {
 	return service
 	return service
-		.get<{ list: unknown[] }>('/api/Production/scheduling/routings', { params: { workOrd, domain } })
+		.get<{ list: WorkOrdRoutingRow[]; summary?: WorkOrderExecutionProgress | null }>('/api/Production/scheduling/routings', {
+			params: { workOrd, domain },
+		})
 		.then((r) => r.data);
 		.then((r) => r.data);
 }
 }
 
 

+ 1 - 1
Web/src/views/aidop/demo/cockpit.vue

@@ -4,7 +4,7 @@
 			<div class="aidop-cockpit-title__left">
 			<div class="aidop-cockpit-title__left">
 				<div class="aidop-demo-shell__mark" aria-hidden="true" />
 				<div class="aidop-demo-shell__mark" aria-hidden="true" />
 				<div>
 				<div>
-					<span class="aidop-cockpit-title__dop">Ai-DOP</span>
+					<span class="aidop-cockpit-title__dop">河南产互联制造业数据智能运营平台</span>
 					<span class="aidop-cockpit-title__rest">企业运营看板</span>
 					<span class="aidop-cockpit-title__rest">企业运营看板</span>
 					<span class="aidop-cockpit-title__badge">DEMO</span>
 					<span class="aidop-cockpit-title__badge">DEMO</span>
 				</div>
 				</div>

+ 67 - 7
Web/src/views/aidop/production/workOrderProgressDashboardList.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-	<AidopDemoShell :title="pageTitle" subtitle="工单全流程关键进度(WorkOrdMaster + 工艺默认产线 + MES 领料状态)">
+	<AidopDemoShell :title="pageTitle" subtitle="流程件口径:末道工序完成即整单工艺走完;在制 = 当前工序完成 − 末道完成(MES 热回读)">
 		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
 		<el-form :inline="true" :model="query" class="mb12" @submit.prevent>
 			<el-form-item label="工单编号">
 			<el-form-item label="工单编号">
 				<el-input v-model="query.workOrd" placeholder="模糊" clearable style="width: 140px" />
 				<el-input v-model="query.workOrd" placeholder="模糊" clearable style="width: 140px" />
@@ -72,14 +72,34 @@
 			<el-table-column v-if="colOn('descr')" prop="descr" label="物料名称" min-width="120" show-overflow-tooltip />
 			<el-table-column v-if="colOn('descr')" prop="descr" label="物料名称" min-width="120" show-overflow-tooltip />
 			<el-table-column v-if="colOn('descr1')" prop="descr1" label="规格型号" min-width="110" show-overflow-tooltip />
 			<el-table-column v-if="colOn('descr1')" prop="descr1" label="规格型号" min-width="110" show-overflow-tooltip />
 			<el-table-column v-if="colOn('qtyOrded')" prop="qtyOrded" label="工单数量" width="100" align="right" />
 			<el-table-column v-if="colOn('qtyOrded')" prop="qtyOrded" label="工单数量" width="100" align="right" />
+			<el-table-column v-if="colOn('currentOp')" label="当前工序" min-width="130" show-overflow-tooltip>
+				<template #default="{ row }">{{ opLabel(row.currentOp, row.currentOpName) }}</template>
+			</el-table-column>
+			<el-table-column v-if="colOn('currentOpQty')" prop="currentOpQty" label="当前工序完成数" width="130" align="right" />
+			<el-table-column v-if="colOn('lastOp')" label="末道工序" min-width="130" show-overflow-tooltip>
+				<template #default="{ row }">{{ opLabel(row.lastOp, row.lastOpName) }}</template>
+			</el-table-column>
+			<el-table-column v-if="colOn('lastOpQty')" prop="lastOpQty" label="末道工序完成数" width="130" align="right" />
+			<el-table-column v-if="colOn('qtyWip')" prop="qtyWip" label="在制件数" width="100" align="right" />
+			<el-table-column v-if="colOn('qtyOpComplete')" prop="qtyOpComplete" label="工序完成合计" width="120" align="right" />
 			<el-table-column v-if="colOn('qtyCompleted')" prop="qtyCompleted" label="入库数量" width="100" align="right" />
 			<el-table-column v-if="colOn('qtyCompleted')" prop="qtyCompleted" label="入库数量" width="100" align="right" />
 			<el-table-column v-if="colOn('noCompleted')" prop="noCompleted" label="未入库数量" width="110" align="right" />
 			<el-table-column v-if="colOn('noCompleted')" prop="noCompleted" label="未入库数量" width="110" align="right" />
 			<el-table-column v-if="colOn('materialSituation')" prop="materialSituation" label="领料状态" min-width="110" show-overflow-tooltip />
 			<el-table-column v-if="colOn('materialSituation')" prop="materialSituation" label="领料状态" min-width="110" show-overflow-tooltip />
-			<el-table-column v-if="colOn('progress')" label="工单进度" width="160" align="left">
+			<el-table-column v-if="colOn('progressExec')" label="执行进度" width="160" align="left">
+				<template #default="{ row }">
+					<el-progress :percentage="progressExecPct(row)" :stroke-width="10" />
+				</template>
+			</el-table-column>
+			<el-table-column v-if="colOn('progress')" label="入库进度" width="160" align="left">
 				<template #default="{ row }">
 				<template #default="{ row }">
 					<el-progress :percentage="progressPct(row)" :stroke-width="10" />
 					<el-progress :percentage="progressPct(row)" :stroke-width="10" />
 				</template>
 				</template>
 			</el-table-column>
 			</el-table-column>
+			<el-table-column label="操作" width="110" fixed="right">
+				<template #default="{ row }">
+					<el-button link type="primary" @click="openRoutingProgress(row)">工序进度</el-button>
+				</template>
+			</el-table-column>
 		</el-table>
 		</el-table>
 
 
 		<div class="pager">
 		<div class="pager">
@@ -98,15 +118,17 @@
 
 
 <script setup lang="ts" name="aidopProductionWorkOrderProgressDashboardList">
 <script setup lang="ts" name="aidopProductionWorkOrderProgressDashboardList">
 import { computed, onMounted, reactive, ref } from 'vue';
 import { computed, onMounted, reactive, ref } from 'vue';
-import { useRoute } from 'vue-router';
+import { useRoute, useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import { ElMessage } from 'element-plus';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
 import {
 import {
 	fetchWorkOrderProgressDashboardList,
 	fetchWorkOrderProgressDashboardList,
 	type WorkOrderProgressDashboardRow,
 	type WorkOrderProgressDashboardRow,
 } from '../api/workOrderProgressDashboard';
 } from '../api/workOrderProgressDashboard';
+import { resolveAidopTenantId } from '../api/aidopTenant';
 
 
 const route = useRoute();
 const route = useRoute();
+const router = useRouter();
 const pageTitle = computed(() => (route.meta?.title as string) || '工单执行进度看板');
 const pageTitle = computed(() => (route.meta?.title as string) || '工单执行进度看板');
 
 
 const typedOptions = [
 const typedOptions = [
@@ -133,13 +155,23 @@ const allColumns = [
 	{ key: 'descr', label: '物料名称' },
 	{ key: 'descr', label: '物料名称' },
 	{ key: 'descr1', label: '规格型号' },
 	{ key: 'descr1', label: '规格型号' },
 	{ key: 'qtyOrded', label: '工单数量' },
 	{ key: 'qtyOrded', label: '工单数量' },
+	{ key: 'currentOp', label: '当前工序' },
+	{ key: 'currentOpQty', label: '当前工序完成数' },
+	{ key: 'lastOp', label: '末道工序' },
+	{ key: 'lastOpQty', label: '末道工序完成数' },
+	{ key: 'qtyWip', label: '在制件数' },
+	{ key: 'qtyOpComplete', label: '工序完成合计' },
 	{ key: 'qtyCompleted', label: '入库数量' },
 	{ key: 'qtyCompleted', label: '入库数量' },
 	{ key: 'noCompleted', label: '未入库数量' },
 	{ key: 'noCompleted', label: '未入库数量' },
 	{ key: 'materialSituation', label: '领料状态' },
 	{ key: 'materialSituation', label: '领料状态' },
-	{ key: 'progress', label: '工单进度' },
+	{ key: 'progressExec', label: '执行进度' },
+	{ key: 'progress', label: '入库进度' },
 ];
 ];
 
 
-const visibleColKeys = ref<string[]>(allColumns.map((c) => c.key));
+// 全工序求和易被误读为流程件进度,默认不展示,仅作对账时手动打开
+const hiddenByDefaultColKeys = ['qtyOpComplete'];
+
+const visibleColKeys = ref<string[]>(allColumns.map((c) => c.key).filter((k) => !hiddenByDefaultColKeys.includes(k)));
 
 
 function colOn(key: string) {
 function colOn(key: string) {
 	return visibleColKeys.value.includes(key);
 	return visibleColKeys.value.includes(key);
@@ -168,13 +200,41 @@ function typedKey(v?: string | null) {
 	return t === 's' || t === 'p' ? t : '';
 	return t === 's' || t === 'p' ? t : '';
 }
 }
 
 
-function progressPct(row: WorkOrderProgressDashboardRow) {
-	const n = Number(row.progress);
+function clampPct(v: unknown) {
+	const n = Number(v);
 	if (!Number.isFinite(n) || n < 0) return 0;
 	if (!Number.isFinite(n) || n < 0) return 0;
 	if (n > 100) return 100;
 	if (n > 100) return 100;
 	return Math.round(n * 100) / 100;
 	return Math.round(n * 100) / 100;
 }
 }
 
 
+function progressPct(row: WorkOrderProgressDashboardRow) {
+	return clampPct(row.progress);
+}
+
+function progressExecPct(row: WorkOrderProgressDashboardRow) {
+	return clampPct(row.progressExec);
+}
+
+function opLabel(op?: number | null, name?: string | null) {
+	if (op === null || op === undefined) return '—';
+	const n = (name ?? '').trim();
+	return n ? `${op} ${n}` : String(op);
+}
+
+function domainForRow(row: WorkOrderProgressDashboardRow): string {
+	const d = (row.domain ?? '').trim();
+	if (d) return d;
+	const tid = resolveAidopTenantId();
+	return tid ? String(tid) : '';
+}
+
+function openRoutingProgress(row: WorkOrderProgressDashboardRow) {
+	router.push({
+		path: '/aidop/production/work-order-trace',
+		query: { workOrd: row.workOrd || '', domain: domainForRow(row), tab: 'routing' },
+	});
+}
+
 function doSearch() {
 function doSearch() {
 	query.page = 1;
 	query.page = 1;
 	loadList();
 	loadList();

+ 83 - 4
Web/src/views/aidop/production/workOrderSchedulingList.vue

@@ -34,7 +34,8 @@
 		<div class="toolbar toolbar-wrap">
 		<div class="toolbar toolbar-wrap">
 			<div class="toolbar-left">
 			<div class="toolbar-left">
 				<el-button type="danger" :disabled="!selectedRows.length" :loading="closing" @click="onBatchClose">工单关闭</el-button>
 				<el-button type="danger" :disabled="!selectedRows.length" :loading="closing" @click="onBatchClose">工单关闭</el-button>
-				<el-button type="primary" :loading="scheduling" @click="onProductionSchedule">生产排程</el-button>
+				<el-button type="primary" :loading="scheduling" @click="onProductionSchedule">全量重排</el-button>
+				<span v-if="scheduleProgressText" class="schedule-progress">{{ scheduleProgressText }}</span>
 				<el-button type="success" :loading="syncing" @click="onSyncMaterial">同步物料需求</el-button>
 				<el-button type="success" :loading="syncing" @click="onSyncMaterial">同步物料需求</el-button>
 				<el-button :loading="exporting" @click="onExport">导出</el-button>
 				<el-button :loading="exporting" @click="onExport">导出</el-button>
 			</div>
 			</div>
@@ -147,6 +148,7 @@ import WorkOrderSchedulingViewForm from './workOrderSchedulingViewForm.vue';
 import { useUserInfo } from '/@/stores/userInfo';
 import { useUserInfo } from '/@/stores/userInfo';
 import {
 import {
 	closeWorkOrders,
 	closeWorkOrders,
+	fetchScheduleGenerateStatus,
 	fetchWorkOrderSchedulingList,
 	fetchWorkOrderSchedulingList,
 	patchWorkOrderStatus,
 	patchWorkOrderStatus,
 	productionSchedule,
 	productionSchedule,
@@ -312,16 +314,85 @@ async function onBatchClose() {
 }
 }
 
 
 const scheduling = ref(false);
 const scheduling = ref(false);
+const scheduleProgressText = ref('');
+
+async function sleep(ms: number) {
+	return new Promise((r) => setTimeout(r, ms));
+}
 
 
 async function onProductionSchedule() {
 async function onProductionSchedule() {
 	const domain = await resolveDomainFromCurrentUser();
 	const domain = await resolveDomainFromCurrentUser();
 	if (!domain) return;
 	if (!domain) return;
+	try {
+		await ElMessageBox.confirm(
+			'将对当前租户全部在制工单执行全量重排(含齐套复算与产线产能排期)。' +
+				'已全发料 / 已开工工序处于冻结期,计划日期与物料占用不会被改动。是否继续?',
+			'全量重排确认',
+			{ type: 'warning', confirmButtonText: '开始全量重排', cancelButtonText: '取消' }
+		);
+	} catch {
+		return;
+	}
+
 	scheduling.value = true;
 	scheduling.value = true;
+	scheduleProgressText.value = '提交中…';
 	try {
 	try {
-		await productionSchedule(domain);
-		ElMessage.success('生产排程已调用');
-		await loadList();
+		const submitted = await productionSchedule(domain);
+		const runId = Number(submitted?.runId);
+		if (!runId) throw new Error('未返回 runId');
+
+		scheduleProgressText.value = `运行中 #${runId}…`;
+		const deadline = Date.now() + 15 * 60 * 1000;
+		while (Date.now() < deadline) {
+			await sleep(2000);
+			const st = await fetchScheduleGenerateStatus(runId);
+			const d = st.detail || {};
+			const status = String(st.status || '').toUpperCase();
+			if (status === 'RUNNING') {
+				const parts = [
+					d.workOrderCount != null ? `工单 ${d.workOrderCount}` : '',
+					d.scheduleRowCount != null ? `计划行 ${d.scheduleRowCount}` : '',
+					d.frozenOpCount != null ? `冻结工序 ${d.frozenOpCount}` : '',
+				].filter(Boolean);
+				scheduleProgressText.value =
+					parts.length > 0 ? `运行中 #${runId}(${parts.join(' · ')})` : `运行中 #${runId}…`;
+				continue;
+			}
+			if (status === 'SUCCESS') {
+				const mismatch = Number(d.psdMismatchCount ?? 0);
+				const dup = Number(d.psdDuplicateActiveCount ?? 0);
+				const dead = Number(d.outboxDeadCount ?? 0);
+				const lineMiss = Number(d.mesLineMissingCount ?? 0);
+				const rtgMiss = Number(d.mesRoutingMissingCount ?? 0);
+				const baseMsg =
+					`全量重排完成:工单 ${d.workOrderCount ?? 0},计划行 ${d.scheduleRowCount ?? 0}` +
+					`,推送 ${d.pushEnqueuedCount ?? 0},冻结工序 ${d.frozenOpCount ?? 0}`;
+				if (mismatch > 0 || dup > 0 || dead > 0 || lineMiss > 0 || rtgMiss > 0) {
+					const parts = [
+						`165 多 ${mismatch} 行`,
+						`重复 ${dup} 组`,
+						`死信 ${dead} 条`,
+						lineMiss > 0 ? `缺产线 ${lineMiss} 行` : '',
+						rtgMiss > 0 ? `缺工序 ${rtgMiss} 行` : '',
+					].filter(Boolean);
+					const warn = `${baseMsg};回写对账异常:${parts.join(' / ')}`;
+					scheduleProgressText.value = warn;
+					ElMessage.warning(warn);
+				} else {
+					scheduleProgressText.value = baseMsg;
+					ElMessage.success(baseMsg);
+				}
+				await loadList();
+				return;
+			}
+			if (status === 'FAILED') {
+				throw new Error(st.message || '生产排程失败');
+			}
+			throw new Error(`未知状态:${st.status}`);
+		}
+		throw new Error('排程超时未完成,请稍后在「业务动作日志」查看结果');
 	} catch (e: any) {
 	} catch (e: any) {
+		scheduleProgressText.value = '';
 		ElMessage.error(e?.message || '生产排程失败');
 		ElMessage.error(e?.message || '生产排程失败');
 	} finally {
 	} finally {
 		scheduling.value = false;
 		scheduling.value = false;
@@ -497,9 +568,17 @@ onMounted(() => loadList());
 .toolbar-left {
 .toolbar-left {
 	display: flex;
 	display: flex;
 	flex-wrap: wrap;
 	flex-wrap: wrap;
+	align-items: center;
 	gap: 8px;
 	gap: 8px;
 }
 }
 
 
+.schedule-progress {
+	font-size: 12px;
+	color: var(--el-text-color-secondary);
+	max-width: 420px;
+	line-height: 1.4;
+}
+
 .col-toggle {
 .col-toggle {
 	display: flex;
 	display: flex;
 	flex-direction: column;
 	flex-direction: column;

+ 18 - 3
Web/src/views/aidop/production/workOrderSchedulingPriorityForm.vue

@@ -9,6 +9,15 @@
 		<el-form-item label="订单日期">
 		<el-form-item label="订单日期">
 			<el-input v-model="form.ordDate" disabled />
 			<el-input v-model="form.ordDate" disabled />
 		</el-form-item>
 		</el-form-item>
+		<el-form-item label="交期">
+			<el-date-picker
+				v-model="form.dueDate"
+				type="date"
+				value-format="YYYY-MM-DD"
+				placeholder="交期"
+				style="width: 100%"
+			/>
+		</el-form-item>
 		<el-form-item label="工单数量">
 		<el-form-item label="工单数量">
 			<el-input-number v-model="form.qtyOrded" :min="0" :precision="4" style="width: 100%" />
 			<el-input-number v-model="form.qtyOrded" :min="0" :precision="4" style="width: 100%" />
 		</el-form-item>
 		</el-form-item>
@@ -56,6 +65,7 @@ const form = reactive({
 	workOrd: '',
 	workOrd: '',
 	itemNum: '',
 	itemNum: '',
 	ordDate: '' as string,
 	ordDate: '' as string,
+	dueDate: '' as string,
 	qtyOrded: 0 as number,
 	qtyOrded: 0 as number,
 	priority: '',
 	priority: '',
 	lotSerial: '',
 	lotSerial: '',
@@ -69,6 +79,7 @@ async function load() {
 		form.workOrd = d.workOrd;
 		form.workOrd = d.workOrd;
 		form.itemNum = d.itemNum ?? '';
 		form.itemNum = d.itemNum ?? '';
 		form.ordDate = d.ordDate ? String(d.ordDate).slice(0, 10) : '';
 		form.ordDate = d.ordDate ? String(d.ordDate).slice(0, 10) : '';
+		form.dueDate = d.dueDate ? String(d.dueDate).slice(0, 10) : '';
 		form.qtyOrded = Number(d.qtyOrded ?? 0);
 		form.qtyOrded = Number(d.qtyOrded ?? 0);
 		form.priority = d.priority ?? '';
 		form.priority = d.priority ?? '';
 		form.lotSerial = d.lotSerial ?? '';
 		form.lotSerial = d.lotSerial ?? '';
@@ -88,16 +99,20 @@ async function onSave() {
 	}
 	}
 	saving.value = true;
 	saving.value = true;
 	try {
 	try {
-		await replenishmentWorkOrdCheckChangePriority({
+		const data = await replenishmentWorkOrdCheckChangePriority({
 			workord: props.workOrd,
 			workord: props.workOrd,
 			qty: form.qtyOrded,
 			qty: form.qtyOrded,
-			instockdate: form.ordDate,
+			instockdate: form.dueDate,
 			priority: String(form.priority ?? ''),
 			priority: String(form.priority ?? ''),
 			domain: props.domain,
 			domain: props.domain,
 			userAccount: userNo,
 			userAccount: userNo,
 			lotSerial: form.lotSerial ?? '',
 			lotSerial: form.lotSerial ?? '',
 		});
 		});
-		ElMessage.success('保存成功');
+		if (data.pendingReorder) {
+			ElMessage.warning(data.warnings?.[0] || '优先级已更新,将在下次全量重排时参与排序(现有日计划保持不变)');
+		} else {
+			ElMessage.success('保存成功');
+		}
 		emit('saved');
 		emit('saved');
 	} catch (e: any) {
 	} catch (e: any) {
 		ElMessage.error(e?.message || '保存失败');
 		ElMessage.error(e?.message || '保存失败');

+ 76 - 2
Web/src/views/aidop/production/workOrderSchedulingTraceForm.vue

@@ -113,6 +113,37 @@
 						<el-table-column prop="unit" label="计量单位" width="90" />
 						<el-table-column prop="unit" label="计量单位" width="90" />
 					</el-table>
 					</el-table>
 				</el-tab-pane>
 				</el-tab-pane>
+				<el-tab-pane label="工序进度" name="routing">
+					<el-descriptions :column="6" border size="small" class="exec-summary">
+						<el-descriptions-item label="当前工序">{{ opLabel(execProgress.currentOp, execProgress.currentOpName) }}</el-descriptions-item>
+						<el-descriptions-item label="当前工序完成数">{{ numStr(execProgress.currentOpQty) || '0' }}</el-descriptions-item>
+						<el-descriptions-item label="末道工序">{{ opLabel(execProgress.lastOp, execProgress.lastOpName) }}</el-descriptions-item>
+						<el-descriptions-item label="末道工序完成数">{{ numStr(execProgress.lastOpQty) || '0' }}</el-descriptions-item>
+						<el-descriptions-item label="在制件数">{{ numStr(execProgress.qtyWip) || '0' }}</el-descriptions-item>
+						<el-descriptions-item label="执行进度">{{ execProgressPct }}%</el-descriptions-item>
+					</el-descriptions>
+					<div class="exec-hint">
+						流程件口径:产品需逐序加工,末道工序完成数才是整单走完的件数;165 报工会把当前工序之前的一段工序一并推进,故各工序完成量求和不代表进度。
+					</div>
+					<el-table :data="routings" border size="small" stripe max-height="420">
+						<el-table-column prop="op" label="工序" width="90" />
+						<el-table-column prop="descr" label="工序名称" min-width="160" show-overflow-tooltip />
+						<el-table-column label="关键工序" width="90">
+							<template #default="{ row }">{{ Number(row.milestoneOp) === 1 ? '是' : '否' }}</template>
+						</el-table-column>
+						<el-table-column prop="qtyOrded" label="需求数量" width="100" align="right" />
+						<el-table-column prop="qtyComplete" label="完成数量" width="100" align="right" />
+						<el-table-column prop="qtyReject" label="不良数量" width="100" align="right" />
+						<el-table-column prop="qtyScrap" label="废品数量" width="100" align="right" />
+						<el-table-column prop="status" label="状态" width="80" />
+						<el-table-column label="标记" min-width="140">
+							<template #default="{ row }">
+								<el-tag v-if="isCurrentOp(row)" type="warning" size="small">当前工序</el-tag>
+								<el-tag v-if="isLastOp(row)" type="success" size="small" class="ml4">末道工序</el-tag>
+							</template>
+						</el-table-column>
+					</el-table>
+				</el-tab-pane>
 				<el-tab-pane label="生产执行" name="t5">
 				<el-tab-pane label="生产执行" name="t5">
 					<el-table :data="production" border size="small" stripe max-height="420">
 					<el-table :data="production" border size="small" stripe max-height="420">
 						<el-table-column prop="sno" label="序号" width="64" />
 						<el-table-column prop="sno" label="序号" width="64" />
@@ -171,13 +202,18 @@ import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { useRoute } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import { ElMessage } from 'element-plus';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
-import { fetchSchedulingTrace } from '../api/workOrderScheduling';
+import {
+	fetchSchedulingRoutings,
+	fetchSchedulingTrace,
+	type WorkOrdRoutingRow,
+	type WorkOrderExecutionProgress,
+} from '../api/workOrderScheduling';
 
 
 const route = useRoute();
 const route = useRoute();
 const pageTitle = computed(() => (route.meta?.title as string) || '工单执行追踪');
 const pageTitle = computed(() => (route.meta?.title as string) || '工单执行追踪');
 
 
 const loading = ref(false);
 const loading = ref(false);
-const activeTab = ref('t1');
+const activeTab = ref((route.query.tab as string) || 't1');
 const master = reactive({
 const master = reactive({
 	workOrd: '',
 	workOrd: '',
 	qtyOrded: null as number | null,
 	qtyOrded: null as number | null,
@@ -206,6 +242,28 @@ const picking = ref<any[]>([]);
 const production = ref<any[]>([]);
 const production = ref<any[]>([]);
 const quality = ref<any[]>([]);
 const quality = ref<any[]>([]);
 const receipt = ref<any[]>([]);
 const receipt = ref<any[]>([]);
+const routings = ref<WorkOrdRoutingRow[]>([]);
+const execProgress = reactive<WorkOrderExecutionProgress>({});
+
+const execProgressPct = computed(() => {
+	const n = Number(execProgress.progressExec ?? 0);
+	if (!Number.isFinite(n) || n < 0) return 0;
+	return Math.round(Math.min(n, 100) * 100) / 100;
+});
+
+function opLabel(op?: number | null, name?: string | null) {
+	if (op === null || op === undefined) return '—';
+	const n = (name ?? '').trim();
+	return n ? `${op} ${n}` : String(op);
+}
+
+function isCurrentOp(row: WorkOrdRoutingRow) {
+	return execProgress.currentOp != null && Number(row.op) === Number(execProgress.currentOp);
+}
+
+function isLastOp(row: WorkOrdRoutingRow) {
+	return execProgress.lastOp != null && Number(row.op) === Number(execProgress.lastOp);
+}
 
 
 function fmtDate(v: unknown) {
 function fmtDate(v: unknown) {
 	if (v == null || v === '') return '';
 	if (v == null || v === '') return '';
@@ -246,6 +304,10 @@ async function load() {
 		production.value = (d.production as any[]) ?? [];
 		production.value = (d.production as any[]) ?? [];
 		quality.value = (d.quality as any[]) ?? [];
 		quality.value = (d.quality as any[]) ?? [];
 		receipt.value = (d.receipt as any[]) ?? [];
 		receipt.value = (d.receipt as any[]) ?? [];
+
+		const r = await fetchSchedulingRoutings(workOrd, domain);
+		routings.value = r.list ?? [];
+		Object.assign(execProgress, r.summary ?? {});
 	} catch (e: any) {
 	} catch (e: any) {
 		ElMessage.error(e?.message || '加载失败');
 		ElMessage.error(e?.message || '加载失败');
 	} finally {
 	} finally {
@@ -270,4 +332,16 @@ onMounted(() => load());
 	font-size: 13px;
 	font-size: 13px;
 	font-weight: 600;
 	font-weight: 600;
 }
 }
+.exec-summary {
+	margin-bottom: 8px;
+}
+.exec-hint {
+	margin-bottom: 10px;
+	font-size: 12px;
+	color: var(--el-text-color-secondary);
+	line-height: 1.6;
+}
+.ml4 {
+	margin-left: 4px;
+}
 </style>
 </style>

+ 4 - 20
Web/src/views/aidop/s8/api/s8OrderFlowDomainApi.ts

@@ -11,8 +11,6 @@ function unwrap<T>(res: { data: T }): T {
 // ────────────────────────────────────────────────────────────
 // ────────────────────────────────────────────────────────────
 
 
 export interface OrderFlowOrderQuery {
 export interface OrderFlowOrderQuery {
-	tenantId?: number;
-	factoryId?: number;
 	keyword?: string;
 	keyword?: string;
 	currentFlowCode?: string;
 	currentFlowCode?: string;
 	customerCode?: string;
 	customerCode?: string;
@@ -96,8 +94,6 @@ export interface OrderFlowChain {
 }
 }
 
 
 export interface OrderFlowAggregateQuery {
 export interface OrderFlowAggregateQuery {
-	tenantId?: number;
-	factoryId?: number;
 	/** BASELINE_PPT | CURRENT_FILTERED */
 	/** BASELINE_PPT | CURRENT_FILTERED */
 	scope: string;
 	scope: string;
 	orderCodes?: string[];
 	orderCodes?: string[];
@@ -141,8 +137,6 @@ export interface OrderFlowFinalAssemblyCollabSummary {
 }
 }
 
 
 export interface OrderFlowProcurementPivotQuery {
 export interface OrderFlowProcurementPivotQuery {
-	tenantId?: number;
-	factoryId?: number;
 	/** BASELINE_PPT | CURRENT_FILTERED */
 	/** BASELINE_PPT | CURRENT_FILTERED */
 	scope: string;
 	scope: string;
 	/**
 	/**
@@ -203,8 +197,6 @@ export type OrderFlowProductDesignProductType = 'STANDARD' | 'NON_STANDARD' | 'T
 export type OrderFlowProductDesignStatus = 'green' | 'yellow' | 'red' | 'pending';
 export type OrderFlowProductDesignStatus = 'green' | 'yellow' | 'red' | 'pending';
 
 
 export interface OrderFlowProductDesignDrawingsQuery {
 export interface OrderFlowProductDesignDrawingsQuery {
-	tenantId?: number;
-	factoryId?: number;
 	/** 逗号分隔订单号;为空时默认聚合全 20 单。 */
 	/** 逗号分隔订单号;为空时默认聚合全 20 单。 */
 	orderCodes?: string;
 	orderCodes?: string;
 	/** STANDARD / NON_STANDARD;为空时返回全量。 */
 	/** STANDARD / NON_STANDARD;为空时返回全量。 */
@@ -279,8 +271,6 @@ export interface OrderFlowProductDesignDrawings {
 // ────────────────────────────────────────────────────────────
 // ────────────────────────────────────────────────────────────
 
 
 export interface OrderFlowManufacturingPivotQuery {
 export interface OrderFlowManufacturingPivotQuery {
-	tenantId?: number;
-	factoryId?: number;
 	/** BASELINE_PPT | CURRENT_FILTERED */
 	/** BASELINE_PPT | CURRENT_FILTERED */
 	scope: string;
 	scope: string;
 	/**
 	/**
@@ -336,8 +326,6 @@ export interface OrderFlowManufacturingPivot {
 // ────────────────────────────────────────────────────────────
 // ────────────────────────────────────────────────────────────
 
 
 export interface OrderFlowFinalAssemblyPivotQuery {
 export interface OrderFlowFinalAssemblyPivotQuery {
-	tenantId?: number;
-	factoryId?: number;
 	/** BASELINE_PPT | CURRENT_FILTERED */
 	/** BASELINE_PPT | CURRENT_FILTERED */
 	scope: string;
 	scope: string;
 	/** 订单编码 CSV(与本体生产 / 产品设计 / 采购透视 API 对齐)。 */
 	/** 订单编码 CSV(与本体生产 / 产品设计 / 采购透视 API 对齐)。 */
@@ -390,19 +378,15 @@ export function getOrderFlowOrders(query: OrderFlowOrderQuery = {}) {
 		.then(unwrap);
 		.then(unwrap);
 }
 }
 
 
-export function getOrderFlowOrder(orderCode: string, tenantId?: number, factoryId?: number) {
+export function getOrderFlowOrder(orderCode: string) {
 	return service
 	return service
-		.get<OrderFlowOrderDetail | null>(`${BASE}/orders/${encodeURIComponent(orderCode)}`, {
-			params: { tenantId, factoryId },
-		})
+		.get<OrderFlowOrderDetail | null>(`${BASE}/orders/${encodeURIComponent(orderCode)}`)
 		.then(unwrap);
 		.then(unwrap);
 }
 }
 
 
-export function getOrderFlowChain(orderCode: string, tenantId?: number, factoryId?: number) {
+export function getOrderFlowChain(orderCode: string) {
 	return service
 	return service
-		.get<OrderFlowChain | null>(`${BASE}/orders/${encodeURIComponent(orderCode)}/chain`, {
-			params: { tenantId, factoryId },
-		})
+		.get<OrderFlowChain | null>(`${BASE}/orders/${encodeURIComponent(orderCode)}/chain`)
 		.then(unwrap);
 		.then(unwrap);
 }
 }
 
 

+ 12 - 9
Web/src/views/dashboard/home.vue

@@ -3,9 +3,9 @@
     <!-- 最上层:全宽标题栏(与参考设计稿一致) -->
     <!-- 最上层:全宽标题栏(与参考设计稿一致) -->
     <header class="title-bar">
     <header class="title-bar">
       <div class="title-bar__left">
       <div class="title-bar__left">
-        <img class="title-bar__brand-mark" src="/app-icons/brand-icon-user.png" alt="Ai-DOP" />
+        <img class="title-bar__brand-mark" src="/app-icons/aidop-logo-vertical.png" alt="河南产互联制造业数据智能运营平台" />
         <div class="title-bar__title-block">
         <div class="title-bar__title-block">
-          <span class="title-bar__dop">Ai-DOP</span>
+          <span class="title-bar__dop">河南产互联制造业数据智能运营平台</span>
           <span class="title-bar__rest">企业运营看板 {{ dashboardVersion }}</span>
           <span class="title-bar__rest">企业运营看板 {{ dashboardVersion }}</span>
         </div>
         </div>
       </div>
       </div>
@@ -464,12 +464,15 @@ onUnmounted(() => {
 }
 }
 
 
 .title-bar__brand-mark {
 .title-bar__brand-mark {
-  width: 30px;
-  height: 30px;
+  width: 56px;
+  height: 56px;
   flex-shrink: 0;
   flex-shrink: 0;
-  background: transparent;
-  mix-blend-mode: screen;
-  filter: drop-shadow(0 0 8px rgba(56, 189, 248, 0.35));
+  object-fit: contain;
+  padding: 2px;
+  box-sizing: border-box;
+  background: #ffffff;
+  border-radius: 10px;
+  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.2), 0 4px 14px rgba(0, 0, 0, 0.35);
 }
 }
 
 
 .title-bar__title-block {
 .title-bar__title-block {
@@ -482,10 +485,10 @@ onUnmounted(() => {
 }
 }
 
 
 .title-bar__dop {
 .title-bar__dop {
-  font-size: clamp(16px, 1.5vw, 19px);
+  font-size: clamp(13px, 1.2vw, 17px);
   font-weight: 800;
   font-weight: 800;
   color: #ffffff;
   color: #ffffff;
-  letter-spacing: 0.02em;
+  letter-spacing: 0.01em;
 }
 }
 
 
 .title-bar__rest {
 .title-bar__rest {

+ 19 - 11
Web/src/views/login/index.vue

@@ -2,9 +2,9 @@
 	<div class="login-container flex">
 	<div class="login-container flex">
 		<div class="login-left flex-margin">
 		<div class="login-left flex-margin">
 			<div class="login-left-logo">
 			<div class="login-left-logo">
-				<img src="/app-icons/aidop-logo.png" />
+				<img :src="systemLogo" :alt="systemTitle" />
 				<div class="login-left-logo-text">
 				<div class="login-left-logo-text">
-					<span>Ai-DOP</span>
+					<span>{{ systemTitle }}</span>
 					<span class="login-left-logo-text-msg">{{ getThemeConfig.globalViceTitleMsg }}</span>
 					<span class="login-left-logo-text-msg">{{ getThemeConfig.globalViceTitleMsg }}</span>
 				</div>
 				</div>
 			</div>
 			</div>
@@ -25,7 +25,7 @@
 				<span class="login-right-warp-one"></span>
 				<span class="login-right-warp-one"></span>
 				<span class="login-right-warp-two"></span>
 				<span class="login-right-warp-two"></span>
 				<div class="login-right-warp-main">
 				<div class="login-right-warp-main">
-					<div class="login-right-warp-main-title">Ai-DOP</div>
+					<div class="login-right-warp-main-title">{{ systemTitle }}</div>
 					<div class="login-right-warp-main-form">
 					<div class="login-right-warp-main-form">
 						<div v-if="!state.isScan">
 						<div v-if="!state.isScan">
 							<el-tabs v-model="state.tabsActiveName">
 							<el-tabs v-model="state.tabsActiveName">
@@ -106,6 +106,8 @@ const state = reactive({
 const getThemeConfig = computed(() => {
 const getThemeConfig = computed(() => {
 	return themeConfig.value;
 	return themeConfig.value;
 });
 });
+const systemTitle = computed(() => themeConfig.value.globalTitle || '河南产互联制造业数据智能运营平台');
+const systemLogo = computed(() => themeConfig.value.logoUrl || '/app-icons/aidop-logo.png');
 
 
 // 页面加载时
 // 页面加载时
 onMounted(async () => {
 onMounted(async () => {
@@ -158,20 +160,25 @@ const loginRightWarpHeight = !tenantInfo.value?.id && themeConfig.value.hideTena
 			animation: logoAnimation 0.3s ease;
 			animation: logoAnimation 0.3s ease;
 			img {
 			img {
 				// width: 100px;
 				// width: 100px;
-				height: 64px;
+				height: 56px;
+				width: auto;
+				object-fit: contain;
 			}
 			}
 			.login-left-logo-text {
 			.login-left-logo-text {
 				display: flex;
 				display: flex;
 				flex-direction: column;
 				flex-direction: column;
+				max-width: min(520px, 42vw);
 				span {
 				span {
-					margin-left: 20px;
-					font-size: 28px;
+					margin-left: 16px;
+					font-size: 20px;
+					line-height: 1.35;
 					font-weight: 700;
 					font-weight: 700;
 					color: var(--el-color-white);
 					color: var(--el-color-white);
 				}
 				}
 				.login-left-logo-text-msg {
 				.login-left-logo-text-msg {
 					padding-top: 5px;
 					padding-top: 5px;
-					font-size: 14px;
+					font-size: 13px;
+					font-weight: 400;
 					color: var(--el-color-white);
 					color: var(--el-color-white);
 				}
 				}
 			}
 			}
@@ -263,12 +270,13 @@ const loginRightWarpHeight = !tenantInfo.value?.id && themeConfig.value.hideTena
                 width: 100%;
                 width: 100%;
 				height: 100%;
 				height: 100%;
 				.login-right-warp-main-title {
 				.login-right-warp-main-title {
-					height: 130px;
-					line-height: 130px;
-					font-size: 32px;
+					min-height: 110px;
+					height: auto;
+					padding: 28px 24px 12px;
+					line-height: 1.35;
+					font-size: 22px;
 					font-weight: 800;
 					font-weight: 800;
 					text-align: center;
 					text-align: center;
-					//letter-spacing: 3px;
 					animation: logoAnimation 0.3s ease;
 					animation: logoAnimation 0.3s ease;
 					animation-delay: 0.3s;
 					animation-delay: 0.3s;
 					color: var(--el-color-primary);
 					color: var(--el-color-primary);

+ 98 - 0
doc/AIDOP投标报价策略逻辑图.md

@@ -0,0 +1,98 @@
+# AIDOP 投标报价策略逻辑图
+
+> 配套文件:[`AIDOP投标材料准备行动清单.xlsx`](AIDOP投标材料准备行动清单.xlsx)(「报价策略测算」「报价方案备选」两页)
+>
+> 测算前提:有效应答人 3 家(含我方)、最高限价 60 万元(含税)、税率 6%。家数变化则全部作废须重算。
+
+## 一、整体逻辑
+
+```mermaid
+graph LR
+    ROOT["三家配合报价<br/>策略逻辑"]
+
+    ROOT --> R1["① 规则前提"]
+    R1 --> R1a["限价 60 万含税 / 税率 6%"]
+    R1 --> R1b["3 家有效应答,不去极值"]
+    R1 --> R1c["基准价 = 三家平均 × 97%"]
+    R1 --> R1d["报价 = 基准价 → 40 分(封顶)"]
+    R1 --> R1e["高于基准:每偏 1% 扣 0.6 分"]
+    R1 --> R1f["低于基准:每偏 1% 扣 0.3 分"]
+
+    ROOT --> R2["② 核心锚点"]
+    R2 --> R2a["我方分封顶 40<br/>→ 只能踩在基准价上"]
+    R2 --> R2b["代入公式解得<br/>我方 = 0.4778 × 两家之和"]
+    R2 --> R2c["→ 两家报价决定我方报价<br/>我方不是自由变量"]
+
+    ROOT --> R3["③ 两个旋钮"]
+    R3 --> K1["旋钮一:两家<b>价格水平</b><br/>决定我方能收多少钱"]
+    K1 --> K1a["两家越贴 60 万<br/>→ 我方报价越高"]
+    K1 --> K1b["两家降价<br/>→ 我方被同比例拖低"]
+    R3 --> K2["旋钮二:两家<b>相互价差</b><br/>决定分差有多大"]
+    K2 --> K2a["两家均值恒为基准价 1.0464 倍<br/>即平均高出 4.639%,与价位无关"]
+    K2 --> K2b["卡住分差的是<br/>报价较低的那一家"]
+    K2 --> K2c["两家同价 → 各扣 2.78 分<br/>= 分差天花板 2.78"]
+    K2 --> K2d["价差拉大 → 低价方更靠近基准<br/>→ 其得分升高 → 我方分差下降"]
+
+    ROOT --> R4["④ 方案落位<br/>(同一条取舍线)"]
+    R4 --> P1["方案一 价差 70 元<br/>分差 2.78|我方 573,270"]
+    R4 --> P2["方案二 价差 100 元<br/>分差 2.78|我方 572,300"]
+    R4 --> P3["★ 方案三 价差 1,100 元<br/>分差 2.73|我方 572,300"]
+    R4 --> P4["方案四 价差 3,100 元<br/>分差 2.62|我方 571,822"]
+    R4 --> P5["方案五 价差 6,000 元<br/>分差 2.47|我方 568,621"]
+    R4 --> P0["规律:价差越小分差越大,<br/>但报价越像串通"]
+
+    ROOT --> R5["⑤ 例外路径<br/>(不在取舍线上)"]
+    R5 --> P6["方案六 低价拉差"]
+    P6 --> P6a["利用 0.3 / 0.6 缓坡不对称<br/>把一家压到基准价<b>以下</b>很远"]
+    P6 --> P6b["45.2 万 → 分差 3.00<br/>突破 2.78 天花板"]
+    P6 --> P6c["反噬:两家之和缩水<br/>我方基准价跌至 50.25 万"]
+    P6 --> P6d["少收约 6.98 万换 0.27 分<br/>+ 异常低价 / 串标风险 → 排除"]
+
+    ROOT --> R6["⑥ 对照标尺<br/>(非候选)"]
+    R6 --> C1["对照A 两家同价顶格<br/>分差 2.78|我方 573,399<br/>数学最优但违反约束"]
+    R6 --> C2["对照B 三家均顶格 60 万<br/>三家并列 38.14|分差 0<br/>说明「都报高价」无优势"]
+
+    ROOT --> R7["⑦ 决策"]
+    R7 --> D1["默认采用:方案三 ★<br/>分差达上限 98% 且数字干净"]
+    R7 --> D2["若求极限分差 → 方案一 / 二<br/>代价:配合痕迹明显"]
+    R7 --> D3["若求外观自然 → 方案四 / 五<br/>代价:让出分差与收入"]
+    R7 --> D4["排除:方案六、对照 A / B"]
+
+    classDef rec fill:#C6EFCE,stroke:#2E7D32,stroke-width:2px;
+    classDef bad fill:#F8CBAD,stroke:#C55A11;
+    classDef ref fill:#D9D9D9,stroke:#808080;
+    classDef key fill:#FFF2CC,stroke:#BF8F00,stroke-width:2px;
+    class P3,D1 rec;
+    class P6,P6d,D4 bad;
+    class C1,C2 ref;
+    class R2b,R2c,K2a,K2c key;
+```
+
+## 二、两个旋钮的因果链
+
+```mermaid
+graph TD
+    A["两家配合方报价"] --> B["两家之和 ↑"]
+    A --> C["两家之间价差 ↑"]
+    B --> D["我方报价 = 0.4778 × 两家之和 ↑"]
+    D --> E["我方含税收入 ↑"]
+    C --> F["报价较低那家更靠近基准价"]
+    F --> G["该家扣分 ↓ 得分 ↑"]
+    G --> H["我方分差 ↓"]
+    C --> I["报价外观更自然<br/>配合痕迹 ↓"]
+
+    E --> Z["最终取舍"]
+    H --> Z
+    I --> Z
+    Z --> Y["方案三:价差 1,100 元<br/>分差 2.73 + 我方 572,300"]
+
+    classDef good fill:#C6EFCE,stroke:#2E7D32;
+    classDef warn fill:#FFF2CC,stroke:#BF8F00;
+    class E,I good;
+    class H warn;
+    class Y good;
+```
+
+## 三、一句话总结
+
+我方报价踩死基准价拿满分 40 是固定动作;真正能调的只有两家配合方——**把它们的价格抬高,我方就能多收钱;把它们的价差收窄,我方分差就更大**。两者互不冲突,唯一冲突的是"价差太窄会显得像串通"。方案三就是在这三者之间的平衡点。

BIN
doc/AIDOP投标材料准备行动清单.xlsx


+ 49 - 0
doc/Ai-DOP系统各模块情况一览表.md

@@ -0,0 +1,49 @@
+# Ai-DOP系统各模块情况一览表
+
+| 项目 | 内容 |
+|---|---|
+| 项目名称 | 2026年河南产互联制造业数据智能运营平台研发项目 |
+| 系统名称 | Ai-DOP智慧运营管理系统 |
+| 统计日期 | 2026-07-30 |
+| 统计口径 | 项目当前开发、测试及文档完成情况 |
+
+## 1. 模块情况一览
+
+| 序号 | 模块 | 开发状态 | 单元测试 | 内部功能测试 | 需求文档 | 蓝图设计 | 测试报告 | 详细设计文档 | 操作手册 | 说明 |
+|---:|---|---|---|---|---|---|---|---|---|---|
+| 1 | S0 运营建模 | 完成 | 完成 | 待完成 | 完成 | 完成 | 完成 | 待完成 | 待完成 | 开发和单元测试完成 |
+| 2 | S1 产销协同 | 完成 | 完成 | 完成 | 完成 | 完成 | 完成 | 待完成 | 待完成 | 开发、测试及现有文档完成 |
+| 3 | S2 制造协同 | 完成 | 完成 | 完成 | 完成 | 完成 | 完成 | 待完成 | 待完成 | 开发、测试及现有文档完成 |
+| 4 | S3 供应协同 | 完成 | 完成 | 完成 | 完成 | 完成 | 完成 | 待完成 | 待完成 | 开发、测试及现有文档完成 |
+| 5 | S4 采购执行 | 完成 | 完成 | 完成 | 完成 | 完成 | 完成 | 待完成 | 待完成 | 开发、测试及现有文档完成 |
+| 6 | S5 物料仓储 | 开发中 | 待完成 | 待完成 | 完成 | 完成 | 完成 | 待完成 | 待完成 | 现有文档已完成,功能继续开发 |
+| 7 | S6 生产执行 | 开发中 | 待完成 | 待完成 | 完成 | 完成 | 完成 | 待完成 | 待完成 | 现有文档已完成,功能继续开发 |
+| 8 | S7 成品仓储 | 开发中 | 待完成 | 待完成 | 完成 | 完成 | 完成 | 待完成 | 待完成 | 现有文档已完成,功能继续开发 |
+| 9 | S8 异常监控 | 完成 | 完成 | 待完成 | 待完成 | 待完成 | 待完成 | 待完成 | 待完成 | 开发和单元测试完成 |
+| 10 | S9 运营指标与智慧看板 | 完成 | 完成 | 待完成 | 完成 | 完成 | 待完成 | 待完成 | 待完成 | 主体已实现;部门看板/S8预警演示数据待补;文档已对齐实现现状 |
+| 11 | 运营诊断 | 完成 | 待完成 | 待完成 | 完成 | 完成 | 待完成 | 待完成 | 待完成 | 交互诊断已实现;正式诊断报告落库/导出待补 |
+| 12 | 运营改善 | 完成 | 待完成 | 待完成 | 完成 | 完成 | 待完成 | 待完成 | 待完成 | 建档/审批/验证已通;独立派单通道待增强;FUNC并入S9-006 |
+| 13 | ChatBI | 部分完成 | 待完成 | 待完成 | 完成 | 完成 | 待完成 | 待完成 | 待完成 | KPI聚合问答MVP已上线;NL2SQL/历史/独立页待补(FUNC-S9-007) |
+| 14 | APP移动端 | 待开发 | 未开始 | 未开始 | 待完成 | 待完成 | 待完成 | 待完成 | 待完成 | 尚未进入开发阶段 |
+| 15 | 数据中台 | 完成 | 待完成 | 待完成 | 待完成 | 待完成 | 待完成 | 待完成 | 待完成 | 开发完成,测试及文档待完成 |
+| 16 | 系统集成 | 部分完成 | 待完成 | 待完成 | 完成 | 完成 | 待完成 | 待完成 | 待完成 | 001~003为占位;真实同步在数据中台;企业SSO未落地 |
+
+## 2. 汇总
+
+| 项目 | 完成 | 进行中/待开发 | 待完成或未开始 |
+|---|---:|---:|---:|
+| 开发 | 12 | 4 | 0 |
+| 单元测试 | 7 | 0 | 9 |
+| 内部功能测试 | 4 | 0 | 12 |
+| 需求文档 | 13 | 0 | 3 |
+| 蓝图设计 | 13 | 0 | 3 |
+| 测试报告 | 8 | 0 | 8 |
+| 详细设计文档 | 0 | 0 | 16 |
+| 操作手册 | 0 | 0 | 16 |
+
+## 3. 状态说明
+
+1. “完成”表示根据当前已提供信息,该项工作已完成。
+2. “开发中”表示模块仍在持续开发,尚未形成最终交付版本。
+3. “待开发”或“未开始”表示相关工作尚未启动。
+4. “待完成”表示该项工作尚需补充完成。

BIN
doc/Ai-DOP系统各模块情况一览表.xlsx


+ 11 - 0
doc/README.md

@@ -65,8 +65,18 @@
 | [plan/Ai-DOP项目待办清单.md](./plan/Ai-DOP项目待办清单.md) | **项目级待办统一入口**(专题子文档见 §2 注册表) |
 | [plan/Ai-DOP项目待办清单.md](./plan/Ai-DOP项目待办清单.md) | **项目级待办统一入口**(专题子文档见 §2 注册表) |
 | [plan/S1-S4业务功能测试结果汇总_20260712.md](./plan/S1-S4业务功能测试结果汇总_20260712.md) | S1–S4 API/E2E 测试结果(含样例单号、失败根因线索、原始 JSON 路径) |
 | [plan/S1-S4业务功能测试结果汇总_20260712.md](./plan/S1-S4业务功能测试结果汇总_20260712.md) | S1–S4 API/E2E 测试结果(含样例单号、失败根因线索、原始 JSON 路径) |
 | [plan/数据中台方案复盘与未实现清单.md](./plan/数据中台方案复盘与未实现清单.md) | 数据中台总体方案、S1-S3 当前进度、未实现事项和后续优先级复盘 |
 | [plan/数据中台方案复盘与未实现清单.md](./plan/数据中台方案复盘与未实现清单.md) | 数据中台总体方案、S1-S3 当前进度、未实现事项和后续优先级复盘 |
+| [plan/租户隔离彻底修正执行任务书.md](./plan/租户隔离彻底修正执行任务书.md) | **主执行任务书**:租户隔离问题的完整修正方案,分 4 批(公共解析器 / 止血 / 贴源根因 / 兜底值 / 种子脚本),含白名单、回归矩阵、数据清理与自检检索式 |
+| [plan/租户隔离全量审计报告-20260810.md](./plan/租户隔离全量审计报告-20260810.md) | **审计依据**:租户号硬编码与缺过滤问题清单(写入侧写死 0 / 读取侧跨租户泄露 / 种子脚本硬编码三类),含实测污染数据 |
+| [plan/MDP贴源层租户映射缺失修复任务书.md](./plan/MDP贴源层租户映射缺失修复任务书.md) | ~~已被上方主任务书 §6 取代~~,保留作为贴源层单点问题的详细背景资料 |
 | [plan/旧DOP-MES-WMS/README.md](./plan/旧DOP-MES-WMS/README.md) | **旧 DOP·MES·WMS 梳理专题目录**(含新 Ai-DOP 对接现状对照) |
 | [plan/旧DOP-MES-WMS/README.md](./plan/旧DOP-MES-WMS/README.md) | **旧 DOP·MES·WMS 梳理专题目录**(含新 Ai-DOP 对接现状对照) |
 | [plan/旧DOP-MES-WMS/对接任务书/README.md](./plan/旧DOP-MES-WMS/对接任务书/README.md) | **新 Ai-DOP 对接 165 SQL Server(MES/WMS)总方案与任务书**(执行须知 + WP1–WP8 + 手册操作覆盖矩阵;WP1/2/3/8 与 WP7 部分可派发执行) |
 | [plan/旧DOP-MES-WMS/对接任务书/README.md](./plan/旧DOP-MES-WMS/对接任务书/README.md) | **新 Ai-DOP 对接 165 SQL Server(MES/WMS)总方案与任务书**(执行须知 + WP1–WP8 + 手册操作覆盖矩阵;WP1/2/3/8 与 WP7 部分可派发执行) |
+| [plan/S2排产日计划回写165执行任务书.md](./plan/S2排产日计划回写165执行任务书.md) | **可派发任务书**:排产日计划经 Outbox 回写 165 `PeriodSequenceDet`,补齐 WP4 缺口,使 MES APP 在正常路径下可投产(含存过硬约束对照、payload 契约、验收 SQL) |
+| [plan/S2生产排程性能优化方案.md](./plan/S2生产排程性能优化方案.md) | 生产排程耗时 127 秒的根因实证(**全库缺二级索引导致全表扫描**,`EXPLAIN ANALYZE` 佐证)与最终目标架构(L1 索引体系 / L2 集合化 / L3 主键与并发正确性 / L4 异步任务化,含跨模块影响与验收口径) |
+| [plan/S2排程性能-L1索引体系执行任务书.md](./plan/S2排程性能-L1索引体系执行任务书.md) | **可派发任务书**:上述方案 L1 层落地,补齐齐套检查/BOM 展开热点表的二级索引(含完整幂等 DDL `1.0.321.sql`、执行计划与耗时验收口径、回滚 SQL)。纯 DDL,不改业务代码。**已执行**(server `1.0.321`) |
+| [plan/S2排程正确性重构执行任务书.md](./plan/S2排程正确性重构执行任务书.md) | **可派发任务书**:实证「当前生产排程并未在排产能」——工作日历全周 `IsWorkDay=0` 致产能分支从未执行,实际只按 `OrdDate→DueDate` 线性插值摊派工序日期。据此论证**排产范围不应收敛到页面勾选**(物料是全局竞争资源),给出冻结期 / 运行版本化 `run_id` / 占用可归属 / 异步任务化 / **真产能排期**五根支柱与 A–G 九阶段执行细则。含产能主数据实证(`ProdLineDetail` 81 产线 5817 行 `Rate` 齐全,排程却读 `WorkCtr` 维度;同步把 `Rate` 错写进成本列 `MachBdnRate`)、G1 日历播种的完整幂等 DDL、**「G1 不得先于 B 执行」的顺序硬约束**、**S1 交期产能计算同样因日历缺行而空转**、休眠雷区(`occupiedSlots` 陈旧后漂、空值退化为全厂单槽)与验收 SQL |
+| [plan/S2排程算法正确性修复执行任务书.md](./plan/S2排程算法正确性修复执行任务书.md) | **可派发任务书**:上一任务书 A–F 落地(server `1.0.322`)后,真产能排期已生效并经公式复算验证(ZJ01 日产能 `⌈(10.5×60−80)/60×0.39⌉`=4 件/天 → 965 件占 242 个工作日 → 后一张工单顺延至 2027-07-13)。据此审出 **8 处算法缺陷**:产能按日扣减却只写一行致占用无法回放(冻结期保护失效)、单工单重排与自身抢产能、工序先后用开工首日而非完工日、冻结工序不推进游标、366 天视界静默截断、产线无日历静默、超交期无信号、日产能向上取整偏乐观。给出 H1–H6 六阶段细则(含产能占用表 `aidop_s2_line_capacity_occupy` 完整 DDL、冻结占用结转、验收 SQL)与「迁移脚本必须 LF 行尾」的启动阻断实证 |
+| [plan/S2日计划回写165一致性修复执行任务书.md](./plan/S2日计划回写165一致性修复执行任务书.md) | **可派发任务书**:上一任务书 H1–H6 落地后真人验收派生。排程算法本身无误,**漏在「作废旧计划」的回写段**——165 残留本库已作废的 `IsActive=1` 旧行,且后续重排永远修不回来(本库生效 41 行 vs 165 生效 44 行,僵尸行与 outbox 死信一一对应)。含三层取证:`S2_PSD_UPSERT` 101 条全成功而 `S2_PSD_DEACTIVATE` 失败 5 条(故漏的只是「作废」不是「写入」)、错误文案 `未配置 API` 与当前 `TryResolve` 路由代码相互矛盾(该源 `source_type=DB`,全仓仅一处调用 `PushAsync`,当前代码产生不出该文案)、失败行字段处于任何单一写入方都写不出的「不可能状态」(指向 `TryEnqueueOrRefreshAsync` 与 `MarkAsync` 并发整列覆盖;一旦落成 `status=2` 而 dispatcher 只扫 `status=0`,换了计划日即换 `idem_key`,死信永无人认领)。给出 I1–I5 五阶段(I1 只读取证优先、`MarkAsync` 乐观锁、按工单与 165 对账兜底补发、对账指标进 `detail_json` 并前端告警、存量清理)、V1–V7 验收与 A1–A6 可复制 SQL、改前基线快照、回滚表、「已核实事实速查表」;`mdp_outbox` 为 S1/领料/WMS 公用,含跨模块回归要求 |
+| [plan/S2优先级调整误写交期与计划作废修复执行任务书.md](./plan/S2优先级调整误写交期与计划作废修复执行任务书.md) | **可派发任务书**:P-028 §6.5 真人验收派生的两个 P0。① 「工单优先级调整」弹窗把只读的「订单日期」当交期提交,后端解析成 `DueDate` 落库 → **打开弹窗按一次保存就把交期无声改成订单日期**,并连带触发单工单重排与 `crm_seorderentry` 同步(实证:交期 2026-09-05 → 2026-08-10);② 纯改优先级走「只作废不重排」分支,本库置 `IsActive=0` 并向 165 入队作废,两库同时失去日计划、MES 当天取不到计划,恢复须全量重排(实证:生效行 3 → 0,全租户 41 → 38)。给出 A1/B1 方案取舍(预览补返 `DueDate` + 弹窗字段归位;纯改优先级改为不作废不重排、以 `pendingReorder` warning 提示)、J1–J4 改动清单(**不改** `DeactivateExistingScheduleAsync` 本体,无 DDL)、V1–V6 验收与 A1–A5 可复制 SQL、存量污染排查口径(`DueDate=OrdDate` + 日志 `detail_json.before.DueDate` 可还原)、「先恢复数据再改代码」的顺序约束 |
 | [plan/旧系统MongoDB替代-待办清单.md](./plan/旧系统MongoDB替代-待办清单.md) | Mongo 替代专题待办明细(**T-MONGO**;入口见项目待办清单) |
 | [plan/旧系统MongoDB替代-待办清单.md](./plan/旧系统MongoDB替代-待办清单.md) | Mongo 替代专题待办明细(**T-MONGO**;入口见项目待办清单) |
 | [plan/动态运营详情看板与指标改善闭环方案.md](./plan/动态运营详情看板与指标改善闭环方案.md) | 动态运营详情看板、智慧诊断与改善闭环总体方案 |
 | [plan/动态运营详情看板与指标改善闭环方案.md](./plan/动态运营详情看板与指标改善闭环方案.md) | 动态运营详情看板、智慧诊断与改善闭环总体方案 |
 | [plan/看板查询条件现状分析.md](./plan/看板查询条件现状分析.md) | 看板查询条件为何不影响 KPI、前后端数据流与改造方向 |
 | [plan/看板查询条件现状分析.md](./plan/看板查询条件现状分析.md) | 看板查询条件为何不影响 KPI、前后端数据流与改造方向 |
@@ -76,6 +86,7 @@
 | [plan/数据库迁移/S2/S2数据中台与KPI扩展验收.md](./plan/数据库迁移/S2/S2数据中台与KPI扩展验收.md) | S2 生产排程数据中台链路、KPI 写入与验收 SQL |
 | [plan/数据库迁移/S2/S2数据中台与KPI扩展验收.md](./plan/数据库迁移/S2/S2数据中台与KPI扩展验收.md) | S2 生产排程数据中台链路、KPI 写入与验收 SQL |
 | [plan/数据库迁移/S3/S3数据中台与看板动态化验收.md](./plan/数据库迁移/S3/S3数据中台与看板动态化验收.md) | S3 供应协同数据中台链路、看板动态化与验收 SQL |
 | [plan/数据库迁移/S3/S3数据中台与看板动态化验收.md](./plan/数据库迁移/S3/S3数据中台与看板动态化验收.md) | S3 供应协同数据中台链路、看板动态化与验收 SQL |
 | [plan/数据中台模块扩展开发指南-S3范式.md](./plan/数据中台模块扩展开发指南-S3范式.md) | 数据中台扩展(仿 S3):四库逻辑、作业与 Cursor 协作 |
 | [plan/数据中台模块扩展开发指南-S3范式.md](./plan/数据中台模块扩展开发指南-S3范式.md) | 数据中台扩展(仿 S3):四库逻辑、作业与 Cursor 协作 |
+| [plan/UAT缺陷整治任务书/README.md](./plan/UAT缺陷整治任务书/README.md) | **可派发任务书集**:2026-08-09 起真人 UAT 暴露缺陷的整治,拆为 4 个可独立交付的工作包——WP-A 死信告警任务修复(`SqlSugar` 私有字段)+ D-W01 归档、WP-B 库存新鲜度打通(齐套算料改读 165 实时库存)、WP-C 齐套两入口口径统一与结论不互相覆盖、WP-D 计划实绩合并(MES 影子计划行实绩回读与聚合)。含各包决策项、事实复核 SQL、验收判据与"明确不做"边界;**D-W01 / D-M01 根因均在 165 存储过程内,受"不改 165"硬约束,只做我方侧适配与归档** |
 | [db/mdp/README.md](./db/mdp/README.md) | S4/S8 相关 mdp、dwd 等建表脚本说明与执行顺序 |
 | [db/mdp/README.md](./db/mdp/README.md) | S4/S8 相关 mdp、dwd 等建表脚本说明与执行顺序 |
 | [指标模型动态配置方案.md](./指标模型动态配置方案.md) | 指标模型动态配置总体方案 |
 | [指标模型动态配置方案.md](./指标模型动态配置方案.md) | 指标模型动态配置总体方案 |
 | [meeting/20260408/vue-tsc-既存类型错误说明.md](./meeting/20260408/vue-tsc-既存类型错误说明.md) | 前端类型问题说明 |
 | [meeting/20260408/vue-tsc-既存类型错误说明.md](./meeting/20260408/vue-tsc-既存类型错误说明.md) | 前端类型问题说明 |

+ 289 - 0
doc/_bid_apply_seals.py

@@ -0,0 +1,289 @@
+# -*- coding: utf-8 -*-
+"""把单位公章、法人印鉴与手写签名盖到总册的各处署名栏,只加浮动图片,不改文字。
+
+用法:
+    python doc/_bid_apply_seals.py            生成「(已签章)」副本
+    python doc/_bid_apply_seals.py --dry      只列出识别到的署名栏,不动文档
+"""
+import io
+import os
+import random
+import re
+import sys
+import shutil
+import time
+
+import numpy as np
+from PIL import Image
+
+sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+ONEDRIVE = r"C:\Users\skygu\OneDrive\Projects\AIDOP\项目\项目招投标"
+NAME = "响应文件(完整版)-2026年河南产互联制造业数据智能运营平台研发项目.docx"
+SRC = os.path.join(ONEDRIVE, NAME)
+OUT = os.path.join(ONEDRIVE, NAME[:-5] + "(已签章).docx")
+
+SEAL_DIR = os.path.join(HERE, "_bid_assets", "seal")
+GZ_P = os.path.join(SEAL_DIR, "gongzhang.png")   # 单位公章(圆)
+YJ_P = os.path.join(SEAL_DIR, "yinjian.png")     # 法定代表人印鉴(方)
+SG_P = os.path.join(SEAL_DIR, "signature.png")   # 手写签名
+GZ = YJ = SG = None                              # main() 里按实测墨迹构造
+
+MM = 2.8346457                                   # 毫米 → 磅
+
+# 实测尺寸:授权书扫描件是整幅 A4(横向 1px=0.0981mm),公章墨迹 440px→43.2mm,
+# 含油墨外溢,标称即常规企业公章 42mm;方形法人名章 217px→21mm。签名按自然
+# 书写大小给 27mm(扫描件里那处签在小表格格子内,只有 13mm,照搬会小得失真)。
+GZ_MM, YJ_MM, SG_MM = 42.0, 21.0, 27.0
+
+COMPANY = "北京智造易科技有限公司"
+
+# Word 常量
+WD_PAGE = 3                     # wdActiveEndPageNumber
+WD_HPOS = 5                     # wdHorizontalPositionRelativeToPage
+WD_VPOS = 6                     # wdVerticalPositionRelativeToPage
+WD_REL_PAGE = 1
+WD_WRAP_NONE = 3
+MSO_TRUE, MSO_FALSE = -1, 0
+MSO_IN_FRONT_OF_TEXT = 4
+MSO_BRING_TO_FRONT = 0
+WD_FORMAT_DOCX = 16
+
+# 盖章行:带盖章括注且写有单位名称
+RE_SEAL = re.compile(r"[((](?:盖公章|盖章|盖单位章|签章)[))]|加盖单位公章")
+# 签字行:带签字括注
+RE_SIGN = re.compile(r"[((]签字(?:或盖章)?[))]|签字或盖章")
+# 签名落笔处:优先下划线,其次「:」与「(签」之间的空档,最后「:」之后
+RE_UNDER = re.compile(r"[__]{2,}")
+
+
+class Asset:
+    """透明 PNG 四周留了空白,按「墨迹」而非整图定尺寸与中心,摆放才准。"""
+
+    def __init__(self, path, ink_mm):
+        al = np.asarray(Image.open(path))[..., 3]
+        ys, xs = np.nonzero(al > 8)
+        ih, iw = al.shape
+        kw, kh = xs.max() - xs.min() + 1, ys.max() - ys.min() + 1
+        self.path = path
+        self.ink = ink_mm * MM                       # 墨迹目标宽(磅)
+        self.w = self.ink * iw / kw                  # 整图宽
+        self.h = self.w * ih / iw
+        self.ink_h = self.h * kh / ih
+        self.fx = (xs.min() + kw / 2) / iw           # 墨迹中心在整图中的相对位置
+        self.fy = (ys.min() + kh / 2) / ih
+
+    def box(self, cx, cy, scale=1.0):
+        """给定墨迹中心,返回整图左上角与整图宽。"""
+        w, h = self.w * scale, self.h * scale
+        return cx - w * self.fx, cy - h * self.fy, w
+
+
+def rnd(kind, i):
+    """按「用途 + 序号」取稳定随机源:同一处每次运行结果一致,各处互不相同。"""
+    return random.Random(f"{kind}|{i}|aidop-20260804")
+
+
+def wobble(kind, i, ang_lo, ang_hi, scale_amp):
+    """真章是手工按下去的:角度、大小、落点都略有出入,且不重复。"""
+    r = rnd(kind, i)
+    ang = r.choice((-1, 1)) * r.uniform(ang_lo, ang_hi)
+    return round(ang, 2), 1.0 + r.uniform(-scale_amp, scale_amp), r
+
+
+def main():
+    dry = "--dry" in sys.argv
+    for p in (GZ_P, YJ_P, SG_P):
+        if not os.path.exists(p):
+            raise SystemExit(f"缺少印鉴素材:{p}(先跑 _bid_make_seals.py)")
+    if not os.path.exists(SRC):
+        raise SystemExit(f"找不到总册:{SRC}")
+
+    global GZ, YJ, SG
+    GZ = Asset(GZ_P, GZ_MM)
+    YJ = Asset(YJ_P, YJ_MM)
+    SG = Asset(SG_P, SG_MM)
+    print(f"印鉴实际尺寸:公章 {GZ_MM}mm 法人印鉴 {YJ_MM}mm 签名宽 {SG_MM}mm")
+
+    target = SRC if dry else OUT
+    if not dry:
+        if os.path.exists(OUT):
+            os.remove(OUT)
+        shutil.copy2(SRC, OUT)
+
+    import win32com.client as win32
+    word = None
+    for i in range(4):
+        try:
+            word = win32.gencache.EnsureDispatch("Word.Application")
+            word.Visible = False
+            word.DisplayAlerts = 0
+            break
+        except Exception as e:                       # Word COM 偶发拒绝调用
+            print(f"  启动 Word 重试 {i + 1}/4:{e}")
+            time.sleep(4)
+    if word is None:
+        raise SystemExit("无法启动 Word")
+
+    doc = word.Documents.Open(target, ReadOnly=dry)
+    try:
+        run(doc, dry)
+    finally:
+        if not dry:
+            doc.SaveAs2(OUT, FileFormat=WD_FORMAT_DOCX)
+        doc.Close(False)
+        word.Quit()
+    if not dry:
+        print(f"[完成] {OUT}")
+
+
+def pos(doc, a, b):
+    """取字符区间 [a,b) 的页码与页内左上坐标(磅)。"""
+    r = doc.Range(a, b)
+    return (int(r.Information(WD_PAGE)),
+            float(r.Information(WD_HPOS)), float(r.Information(WD_VPOS)))
+
+
+def x_at(doc, i):
+    r = doc.Range(i, i)
+    return float(r.Information(WD_HPOS))
+
+
+def stamp(doc, path, anchor_a, anchor_b, left, top, width, angle):
+    shp = doc.Shapes.AddPicture(FileName=path, LinkToFile=False,
+                                SaveWithDocument=True,
+                                Anchor=doc.Range(anchor_a, anchor_b))
+    shp.LockAspectRatio = MSO_TRUE
+    shp.Width = width
+    shp.WrapFormat.Type = WD_WRAP_NONE
+    shp.WrapFormat.AllowOverlap = MSO_TRUE
+    shp.RelativeHorizontalPosition = WD_REL_PAGE
+    shp.RelativeVerticalPosition = WD_REL_PAGE
+    shp.Left = left
+    shp.Top = top
+    shp.Rotation = angle
+    shp.ZOrder(MSO_IN_FRONT_OF_TEXT)
+    return shp
+
+
+def run(doc, dry):
+    # 不能用 Content.Text 的字符下标当 Range 位置:目录域、页码域等隐藏字符
+    # 会让两者错位,必须逐段取真实 Range.Start。
+    t0 = time.time()
+    seals, signs = [], []
+    for p in doc.Paragraphs:                     # 用枚举器单遍扫,避免按下标取段
+        txt = p.Range.Text
+        if len(txt) > 82 or not txt.strip("\r\x07  \t"):
+            continue
+        if RE_SEAL.search(txt) and COMPANY in txt:
+            seals.append((p.Range.Start, txt))
+        elif RE_SIGN.search(txt):
+            signs.append((p.Range.Start, txt))
+    print(f"遍历段落用时 {time.time() - t0:.0f}s")
+
+    print(f"识别:盖章行 {len(seals)} 处,签字行 {len(signs)} 处\n")
+
+    # AddPicture 会在正文插入一个锚点字符,使其后所有 Range.Start 后移。
+    # 因此先只读地算完全部坐标,再按文档倒序插入,避免锚点漂移。
+    plan = []
+
+    ps = doc.Sections(1).PageSetup
+    right_edge = float(ps.PageWidth) - float(ps.RightMargin)
+
+    # --- 单位公章:盖在单位名称上,略偏右下,每枚角度/大小/落点各不相同
+    n, boxes = 0, {}
+    for start, txt in seals:
+        i = txt.find(COMPANY)
+        a, b = start + i, start + i + len(COMPANY)
+        got = doc.Range(a, b).Text
+        if got != COMPANY:
+            print(f"  !定位偏移,跳过:期望「{COMPANY}」实得「{got}」")
+            continue
+        page, x0, y = pos(doc, a, b)
+        x1 = x_at(doc, b)
+        if x1 <= x0:                      # 跨行等异常,退回按名称估宽
+            x1 = x0 + len(COMPANY) * 12
+        ang, sc, r = wobble("gz", n, 1.8, 8.5, 0.018)
+        cx = (x0 + x1) / 2 + r.uniform(-5, 10)
+        cy = y + r.uniform(5, 9)
+        left, top, w = GZ.box(cx, cy, sc)
+        print(f"  公章 P{page:>3}  x{left:6.1f} y{top:6.1f} "
+              f"{GZ.ink * sc / MM:4.1f}mm {ang:+5.1f}°  {txt.strip()[:34]}")
+        plan.append((a, b, GZ.path, left, top, w, ang, False))
+        boxes.setdefault(page, []).append((left, top, left + w, top + w))
+        n += 1
+
+    # --- 签名 + 法人印鉴:落在签字行的空档处
+    m = 0
+    for start, txt in signs:
+        u = RE_UNDER.search(txt)
+        if u:                                     # 有下划线:写在下划线上
+            a, b = start + u.start(), start + u.end()
+        else:
+            c = txt.rfind(":")
+            k = txt.find("(签")
+            if k > c >= 0:                        # 「:」与「(签」之间的空档
+                a, b = start + c + 1, start + k
+            elif c >= 0:                          # 「:」之后
+                a, b = start + c + 1, start + len(txt)
+            else:
+                continue
+        page, x0, y = pos(doc, a, b)
+        x1 = x_at(doc, b)
+        sg_ang, sg_sc, r = wobble("sg", m, 0.6, 3.2, 0.04)
+        yj_ang, yj_sc, r2 = wobble("yj", m, 1.2, 5.5, 0.015)
+        sg_w, yj_w = SG.ink * sg_sc, YJ.ink * yj_sc
+
+        need = sg_w + yj_w * 0.85
+        span = max(x1 - x0, need)
+        sg_left = x0 + max(6.0, (span - need) / 2) + r.uniform(-2, 4)
+
+        # 公章 42mm 高,必然压到下一行的签字栏。签名叠在章上层(真实标书就是
+        # 这样),右边还有余量时再把签名挪出章外,让两者都看得清。
+        for bl, bt, br, bb in boxes.get(page, ()):
+            if bt < y + sg_w * 0.3 and bb > y - sg_w * 0.3 and br > sg_left:
+                sg_left = min(max(sg_left, br - 8.0), right_edge - need)
+
+        cy = y + 3 + r.uniform(-1.5, 2.5)
+        sl, st, sw = SG.box(sg_left + sg_w / 2, cy, sg_sc)
+        yl, yt, yw = YJ.box(sg_left + sg_w * 0.82 + yj_w / 2,
+                            y + 6 + r2.uniform(-1, 2), yj_sc)
+        print(f"  签名 P{page:>3}  x{sl:6.1f} y{st:6.1f} "
+              f"{sg_w / MM:4.1f}mm {sg_ang:+5.1f}° 印鉴 {yj_w / MM:4.1f}mm "
+              f"{yj_ang:+5.1f}°  {txt.strip()[:26]}")
+        plan.append((a, b, SG.path, sl, st, sw, sg_ang, True))
+        plan.append((a, b, YJ.path, yl, yt, yw, yj_ang, True))
+        m += 1
+
+    # --- 身份证复印件页:原文明文「加盖单位公章」,无文字锚点,按书签定位
+    try:
+        bm = doc.Bookmarks("SEALA1").Range
+        ang, sc, r = wobble("gz", 99, 1.8, 8.5, 0.018)
+        left, top, w = GZ.box(360.0 + r.uniform(-6, 6),
+                              320.0 + r.uniform(-8, 8), sc)
+        print(f"  公章 P{int(bm.Information(WD_PAGE)):>3}"
+              f"  x{left:6.1f} y{top:6.1f} {GZ.ink * sc / MM:4.1f}mm {ang:+5.1f}°"
+              f"  (法定代表人身份证复印件·加盖单位公章)")
+        plan.append((bm.Start, bm.End, GZ.path, left, top, w, ang, False))
+        n += 1
+    except Exception as e:
+        print(f"  !身份证复印件页未盖:{type(e).__name__} {e}")
+
+    print(f"\n合计盖章 {n} 处,签名 {m} 处,法人印鉴 {m} 处")
+    if dry:
+        return
+
+    # 倒序插入:靠后的锚点先用,前面的锚点位置不受影响
+    front = []
+    for a, b, path, left, top, w, ang, on_top in sorted(plan, key=lambda t: -t[0]):
+        shp = stamp(doc, path, a, b, left, top, w, ang)
+        if on_top:
+            front.append(shp)
+    for shp in front:                 # 签名与印鉴压在红章上层
+        shp.ZOrder(MSO_BRING_TO_FRONT)
+    print(f"已插入 {len(plan)} 个图形")
+
+
+if __name__ == "__main__":
+    main()

+ 11 - 0
doc/_bid_assets/_manifest.txt

@@ -0,0 +1,11 @@
+s0_master_dataflow.png	270KB	S0 主数据与下游 S1–S9 数据流关系	来源:S0/S0-基础主数据建模-业务蓝图设计方案-V0.2.docx
+s1_otd_mainline.png	51KB	S1 产销协同 订单履约全链路主线流程	来源:S1/S1产销协同模块蓝图设计方案.docx
+s1_order_delivery.png	31KB	S1 订单交付 8Tab 全景追踪流程	来源:S1/S1产销协同模块蓝图设计方案.docx
+s1_kanban.png	26KB	S1 产销协同看板 L1–L4 指标下钻	来源:S1/S1产销协同模块蓝图设计方案.docx
+s2_plan_to_exec.png	49KB	S2 制造协同 排程到执行主线流程	来源:S2/S2制造协同模块蓝图设计方案.docx
+s3_domains.png	72KB	S3 供应协同 物料计划/采购管理/协同看板三域关系	来源:S3/S3供应协同模块蓝图设计方案.docx
+s3_shortage.png	28KB	S3 齐套计算与缺料预警流程	来源:S3/S3供应协同模块蓝图设计方案.docx
+s4_domains.png	61KB	S4 采购执行 交货执行/退货闭环/执行看板三域关系	来源:S4/S4采购执行模块蓝图设计方案.docx
+s5_lifecycle.png	222KB	S5 物料仓储 贴源同步只读生命周期	来源:S5/S5-物料仓储-业务蓝图设计方案.docx
+s6_ipqc.png	187KB	S6 生产执行 贴源同步与 IPQC 检验流转	来源:S6/S6-生产执行-业务蓝图设计方案.docx
+s7_lifecycle.png	172KB	S7 成品仓储 贴源同步只读生命周期	来源:S7/S7-成品仓储-业务蓝图设计方案.docx

+ 17 - 0
doc/_bid_assets/_manifest_ui.txt

@@ -0,0 +1,17 @@
+ui_s0_material.png	333KB	物料主数据管理(FUNC-S0-020)	来源:S0\S0-基础主数据建模-业务蓝图设计方案-V0.2.docx
+ui_s0_customer.png	298KB	客户主数据管理(FUNC-S0-021)	来源:S0\S0-基础主数据建模-业务蓝图设计方案-V0.2.docx
+ui_s0_supplier.png	311KB	供应商主数据管理(FUNC-S0-027)	来源:S0\S0-基础主数据建模-业务蓝图设计方案-V0.2.docx
+ui_s0_bom.png	346KB	标准 BOM 管理(FUNC-S0-001)	来源:S0\S0-基础主数据建模-业务蓝图设计方案-V0.2.docx
+ui_s0_routing.png	412KB	标准工艺路线管理(FUNC-S0-002)	来源:S0\S0-基础主数据建模-业务蓝图设计方案-V0.2.docx
+ui_s0_location.png	327KB	库位管理(FUNC-S0-034)	来源:S0\S0-基础主数据建模-业务蓝图设计方案-V0.2.docx
+ui_s5_iqc.png	374KB	来料检验申请列表	来源:S5\S5-物料仓储-业务蓝图设计方案.docx
+ui_s5_stock.png	406KB	库存查询	来源:S5\S5-物料仓储-业务蓝图设计方案.docx
+ui_s5_inout.png	366KB	进出存查询	来源:S5\S5-物料仓储-业务蓝图设计方案.docx
+ui_s5_stocktake.png	382KB	盘点结果查询	来源:S5\S5-物料仓储-业务蓝图设计方案.docx
+ui_s6_ipqc.png	382KB	过程检验单列表与详情	来源:S6\S6-生产执行-业务蓝图设计方案.docx
+ui_s6_equipment.png	386KB	生产设备台账	来源:S6\S6-生产执行-业务蓝图设计方案.docx
+ui_s6_tooling.png	441KB	模具工装台账管理	来源:S6\S6-生产执行-业务蓝图设计方案.docx
+ui_s7_prodin.png	386KB	生产入库单列表	来源:S7\S7-成品仓储-业务蓝图设计方案.docx
+ui_s7_shipment.png	383KB	销售发货通知	来源:S7\S7-成品仓储-业务蓝图设计方案.docx
+ui_s7_fqc_task.png	403KB	FQC 检验任务列表	来源:S7\S7-成品仓储-业务蓝图设计方案.docx
+ui_s7_fqc_result.png	397KB	FQC 检验结果列表与详情	来源:S7\S7-成品仓储-业务蓝图设计方案.docx

BIN
doc/_bid_assets/_orig_footer/ui_s0_bom.png


BIN
doc/_bid_assets/_orig_footer/ui_s0_customer.png


BIN
doc/_bid_assets/_orig_footer/ui_s0_location.png


BIN
doc/_bid_assets/_orig_footer/ui_s0_material.png


BIN
doc/_bid_assets/_orig_footer/ui_s0_routing.png


BIN
doc/_bid_assets/_orig_footer/ui_s0_supplier.png


BIN
doc/_bid_assets/_orig_footer/ui_s5_inout.png


BIN
doc/_bid_assets/_orig_footer/ui_s5_iqc.png


BIN
doc/_bid_assets/_orig_footer/ui_s5_stock.png


BIN
doc/_bid_assets/_orig_footer/ui_s5_stocktake.png


BIN
doc/_bid_assets/_orig_footer/ui_s6_equipment.png


BIN
doc/_bid_assets/_orig_footer/ui_s6_ipqc.png


BIN
doc/_bid_assets/_orig_footer/ui_s6_tooling.png


BIN
doc/_bid_assets/_orig_footer/ui_s7_fqc_result.png


BIN
doc/_bid_assets/_orig_footer/ui_s7_fqc_task.png


BIN
doc/_bid_assets/_orig_footer/ui_s7_prodin.png


BIN
doc/_bid_assets/_orig_footer/ui_s7_shipment.png


BIN
doc/_bid_assets/_raw/fig_api.png


BIN
doc/_bid_assets/_raw/fig_application.png


BIN
doc/_bid_assets/_raw/fig_auto_closedloop.png


BIN
doc/_bid_assets/_raw/fig_closedloop.png


BIN
doc/_bid_assets/_raw/fig_data.png


BIN
doc/_bid_assets/_raw/fig_data_layer.png


BIN
doc/_bid_assets/_raw/fig_defect_flow.png


BIN
doc/_bid_assets/_raw/fig_deploy.png


BIN
doc/_bid_assets/_raw/fig_diag_arch.png


BIN
doc/_bid_assets/_raw/fig_diag_model.png


BIN
doc/_bid_assets/_raw/fig_diagnosis.png


BIN
doc/_bid_assets/_raw/fig_emergency.png


BIN
doc/_bid_assets/_raw/fig_integration.png


BIN
doc/_bid_assets/_raw/fig_kanban.png


BIN
doc/_bid_assets/_raw/fig_metric_model.png


BIN
doc/_bid_assets/_raw/fig_module_map.png


BIN
doc/_bid_assets/_raw/fig_otd_blueprint.png


BIN
doc/_bid_assets/_raw/fig_quality_gate.png


BIN
doc/_bid_assets/_raw/fig_service.png


BIN
doc/_bid_assets/_raw/fig_tech.png


BIN
doc/_bid_assets/_raw/fig_test_v.png


BIN
doc/_bid_assets/_raw/fig_training.png


BIN
doc/_bid_assets/_raw/s0_master_dataflow.png


BIN
doc/_bid_assets/_raw/s1_kanban.png


BIN
doc/_bid_assets/_raw/s1_order_delivery.png


BIN
doc/_bid_assets/_raw/s1_otd_mainline.png


BIN
doc/_bid_assets/_raw/s2_plan_to_exec.png


BIN
doc/_bid_assets/_raw/s3_domains.png


BIN
doc/_bid_assets/_raw/s3_shortage.png


BIN
doc/_bid_assets/_raw/s4_domains.png


BIN
doc/_bid_assets/_raw/s5_lifecycle.png


BIN
doc/_bid_assets/_raw/s6_ipqc.png


BIN
doc/_bid_assets/_raw/s7_lifecycle.png


BIN
doc/_bid_assets/attach/credit_01.jpg


BIN
doc/_bid_assets/attach/credit_02.jpg


BIN
doc/_bid_assets/attach/credit_03.jpg


BIN
doc/_bid_assets/attach/credit_04.jpg


BIN
doc/_bid_assets/attach/credit_05.jpg


BIN
doc/_bid_assets/attach/credit_06.jpg


BIN
doc/_bid_assets/attach/credit_07.jpg


BIN
doc/_bid_assets/attach/id_back.jpg


BIN
doc/_bid_assets/attach/id_front.jpg


BIN
doc/_bid_assets/attach/license.jpg


BIN
doc/_bid_assets/attach/sw_enterprise.jpg


BIN
doc/_bid_assets/attach/sw_product.jpg


BIN
doc/_bid_assets/attach/swc_01.jpg


BIN
doc/_bid_assets/attach/swc_02.jpg


BIN
doc/_bid_assets/attach/swc_03.jpg


BIN
doc/_bid_assets/attach/swc_04.jpg


BIN
doc/_bid_assets/attach/swc_05.jpg


BIN
doc/_bid_assets/attach/swc_06.jpg


BIN
doc/_bid_assets/attach/swc_07.jpg


BIN
doc/_bid_assets/attach/swc_08.jpg


BIN
doc/_bid_assets/attach/swc_09.jpg


BIN
doc/_bid_assets/attach/swc_10.jpg


BIN
doc/_bid_assets/fig_api.png


BIN
doc/_bid_assets/fig_application.png


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä