瀏覽代碼

feat: 中立标准层、来源接入与模块重建修复 | server 1.0.583 / web 2.4.409

落地租户标准来源与推送入站,修复模块重建的租户过滤和建表列对齐,并按代码与配置实测更新第三方对接文档。
YY968XX 4 天之前
父節點
當前提交
6aa6b4c5ff
共有 100 個文件被更改,包括 7528 次插入 和 355 次删除
  1. 1 0
      .cursor/rules/README.md
  2. 57 0
      .cursor/rules/capability-claims-evidence.mdc
  3. 1 0
      AGENTS.md
  4. 1 1
      Web/package.json
  5. 2 0
      Web/src/constants/aidopFuncCodes.ts
  6. 4 2
      Web/src/views/aidop/chatbi/ChatBIMessage.vue
  7. 69 0
      Web/src/views/aidop/data-platform/api/mdpMonitor.ts
  8. 41 0
      Web/src/views/aidop/data-platform/api/sources.ts
  9. 201 0
      Web/src/views/aidop/data-platform/components/SourceOnboardWizard.vue
  10. 155 0
      Web/src/views/aidop/data-platform/mdpMonitor.vue
  11. 168 0
      Web/src/views/aidop/data-platform/sourceMappings.vue
  12. 158 3
      Web/src/views/aidop/data-platform/sources.vue
  13. 124 0
      Web/src/views/aidop/data-platform/tenantStdSources.vue
  14. 5 1
      Web/src/views/aidop/diagnosis/index.vue
  15. 10 8
      Web/src/views/aidop/kanban/components/DynamicModuleDashboard.vue
  16. 2 0
      Web/src/views/aidop/s0/api/s0SalesApi.ts
  17. 1 0
      Web/src/views/aidop/s0/api/s0SupplyApi.ts
  18. 2 0
      Web/src/views/aidop/s0/api/s0WarehouseApi.ts
  19. 10 0
      Web/src/views/aidop/s0/components/S0SourceBadge.vue
  20. 5 0
      Web/src/views/aidop/s0/components/s0ExternalSource.ts
  21. 10 3
      Web/src/views/aidop/s0/manufacturing/LineMaterialList.vue
  22. 10 3
      Web/src/views/aidop/s0/manufacturing/LinePostList.vue
  23. 10 3
      Web/src/views/aidop/s0/manufacturing/MaterialProcessElementList.vue
  24. 11 4
      Web/src/views/aidop/s0/manufacturing/OrderScheduleCycleList.vue
  25. 10 3
      Web/src/views/aidop/s0/manufacturing/PersonSkillAssignmentList.vue
  26. 11 4
      Web/src/views/aidop/s0/manufacturing/PersonnelSkillList.vue
  27. 10 3
      Web/src/views/aidop/s0/manufacturing/PreprocessElementList.vue
  28. 10 3
      Web/src/views/aidop/s0/manufacturing/PreprocessElementParamList.vue
  29. 11 4
      Web/src/views/aidop/s0/manufacturing/ProcessFlowCardList.vue
  30. 10 3
      Web/src/views/aidop/s0/manufacturing/ProductionElementParamList.vue
  31. 11 4
      Web/src/views/aidop/s0/manufacturing/ProductionLineList.vue
  32. 11 4
      Web/src/views/aidop/s0/manufacturing/RoutingList.vue
  33. 10 3
      Web/src/views/aidop/s0/manufacturing/SopFileTypeList.vue
  34. 10 3
      Web/src/views/aidop/s0/manufacturing/SopMaintenanceList.vue
  35. 11 4
      Web/src/views/aidop/s0/manufacturing/StandardBomManagement.vue
  36. 10 3
      Web/src/views/aidop/s0/manufacturing/StandardProcessList.vue
  37. 10 3
      Web/src/views/aidop/s0/manufacturing/WorkCenterList.vue
  38. 10 3
      Web/src/views/aidop/s0/quality/components/QualityAggregateCrudPage.vue
  39. 10 3
      Web/src/views/aidop/s0/quality/components/QualitySimpleCrudPage.vue
  40. 8 1
      Web/src/views/aidop/s0/sales/ContractReviewCycleList.vue
  41. 10 3
      Web/src/views/aidop/s0/sales/CustomerList.vue
  42. 10 3
      Web/src/views/aidop/s0/sales/ItemSubstituteDetailList.vue
  43. 10 3
      Web/src/views/aidop/s0/sales/MaterialList.vue
  44. 11 4
      Web/src/views/aidop/s0/sales/OrderPriorityRuleList.vue
  45. 11 4
      Web/src/views/aidop/s0/sales/OrderReviewCycleList.vue
  46. 11 4
      Web/src/views/aidop/s0/sales/ProductDesignCycleList.vue
  47. 11 4
      Web/src/views/aidop/s0/supply/CategoryLeadTimeList.vue
  48. 11 4
      Web/src/views/aidop/s0/supply/MaterialPlanCycleList.vue
  49. 10 3
      Web/src/views/aidop/s0/supply/SourcingList.vue
  50. 10 3
      Web/src/views/aidop/s0/supply/SupplierList.vue
  51. 10 3
      Web/src/views/aidop/s0/warehouse/BarcodeRuleList.vue
  52. 10 3
      Web/src/views/aidop/s0/warehouse/CostCenterList.vue
  53. 10 3
      Web/src/views/aidop/s0/warehouse/DepartmentList.vue
  54. 10 3
      Web/src/views/aidop/s0/warehouse/EmpWorkDutyList.vue
  55. 9 2
      Web/src/views/aidop/s0/warehouse/EmployeeList.vue
  56. 10 3
      Web/src/views/aidop/s0/warehouse/ItemPackList.vue
  57. 10 3
      Web/src/views/aidop/s0/warehouse/LabelTypeList.vue
  58. 9 2
      Web/src/views/aidop/s0/warehouse/LocationList.vue
  59. 10 3
      Web/src/views/aidop/s0/warehouse/NbrControlList.vue
  60. 10 3
      Web/src/views/aidop/s0/warehouse/NbrTypeList.vue
  61. 10 3
      Web/src/views/aidop/s0/warehouse/TaskAssignmentList.vue
  62. 11 2
      Web/src/views/dashboard/components/HomeModuleKpiCard.vue
  63. 1 1
      Web/src/views/dashboard/home.vue
  64. 2 0
      doc/README.md
  65. 57 0
      doc/db/mdp/contract_tests/e2e_mode3_full.py
  66. 5 1
      doc/db/mdp/mock_api/endpoints.json
  67. 9 0
      doc/db/mdp/mock_api/samples/fqc_task.json
  68. 10 0
      doc/db/mdp/mock_api/samples/inventory_balance_monthly.json
  69. 17 0
      doc/db/mdp/mock_api/samples/inventory_txn.json
  70. 8 0
      doc/db/mdp/mock_api/samples/work_order_bom.json
  71. 101 0
      doc/db/mdp/mock_api/seed_mock_db.py
  72. 1061 0
      doc/plan/165-aidopdev连接耗尽根治执行任务书.md
  73. 257 0
      doc/plan/UAT测试/UAT测试结果_20260927_本地.md
  74. 52 0
      doc/plan/系统集成/Ai-DOP三种对接模式基线-G0.md
  75. 761 47
      doc/plan/系统集成/Ai-DOP三种对接模式达成任务书.md
  76. 61 0
      doc/plan/系统集成/Ai-DOP三种对接模式达成执行报告.md
  77. 21 0
      doc/plan/系统集成/Ai-DOP新来源接入手册.md
  78. 232 105
      doc/plan/系统集成/Ai-DOP第三方对接业务数据详表-业务页.md
  79. 167 13
      doc/plan/系统集成/Ai-DOP第三方对接业务数据详表-技术页.md
  80. 57 18
      doc/plan/系统集成/Ai-DOP第三方系统集成指南.md
  81. 0 1
      doc/plan/系统集成/README.md
  82. 19 0
      doc/plan/项目验收/项目验收待办项.md
  83. 4 0
      docker/mysql/mysql.cnf
  84. 46 0
      server/Admin.NET.Core/SqlSugar/MdmMirrorWriteScope.cs
  85. 5 1
      server/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs
  86. 75 15
      server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj
  87. 10 6
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.564.verify.sql
  88. 19 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.570.sql
  89. 20 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.570.verify.sql
  90. 39 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.571.health.md
  91. 2142 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.571.sql
  92. 53 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.571.verify.sql
  93. 127 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.572.sql
  94. 51 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.572.verify.sql
  95. 436 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.573.sql
  96. 35 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.573.verify.sql
  97. 80 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.574.sql
  98. 12 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.574.verify.sql
  99. 17 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.575.sql
  100. 14 0
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.575.verify.sql

+ 1 - 0
.cursor/rules/README.md

@@ -12,6 +12,7 @@
 | 文件 | 说明 | 生效方式 |
 |------|------|---------|
 | `impact-backtrace.mdc` | **强制**:任何方案先从改动点**反向推演全部影响面**(调用方、数据契约、多来源、多租户、运行面、守卫测试),并写出「反向影响推演」小节 | `alwaysApply: true` |
+| `capability-claims-evidence.mdc` | **强制**:断言「X 已实现/未实现/已达成」前必须分**代码层 / 配置层 / 数据层**三层取证,禁止以任务清单条目、历史会话结论、带过滤条件的查询结果或配置表条数代替 | `alwaysApply: true` |
 | `best-solution-default.mdc` | **强制**:默认交付**根治性最佳方案**,禁止擅自降级为临时/过渡/兜底方案,除非用户明文要求 | `alwaysApply: true` |
 | `collaboration-scope.mdc` | 改前清单待确认、跨模块影响须先预警、小问题**改动范围**最小不大重构等 | `alwaysApply: true` |
 | `git-submit-intent.mdc` | 用户说「提交」时默认 **commit + push**;仅用户明确说不 push 时才只 commit | `alwaysApply: true` |

+ 57 - 0
.cursor/rules/capability-claims-evidence.mdc

@@ -0,0 +1,57 @@
+---
+description: 断言某能力「已实现 / 未实现 / 已达成」前必须跑代码级检索取证,禁止以任务清单、历史结论或配置表条数代替
+alwaysApply: true
+---
+
+# 能力断言必须取证(强制)
+
+## 背景
+
+本仓库多次发生同一类误判:把「差一列」说成「整块没有」,把「配置未开通」说成「代码未实现」。
+成因不是查得不够,而是**取证层次用错**。本规则约束的就是取证方式。
+
+## 三层必须分开,不得混成一句话
+
+回答「X 做到了吗 / X 实现了吗」时,**必须**分别给出三层结论,任何一层缺失即视为未完成回答:
+
+| 层 | 问题 | 取证方式 |
+|---|---|---|
+| **代码层** | 代码里有没有这个能力? | grep 类名 / 路由 / 契约常量 / 方法名,并**读到** `文件:行` |
+| **配置层** | 配置有没有开通? | 查 `mdp_entity`、`mdp_inbound_grant`、`mdp_source*`、迁移脚本登记 |
+| **数据层** | 数据有没有真跑通? | 查目标表行数、来源分布、最新业务日期;查请求/作业流水表 |
+
+三层成因与修法完全不同:代码层缺 → 写代码;配置层缺 → 补种子或迁移;数据层缺 → 跑批或补源。
+**「未达到」这三个字不带层次信息,单独使用即为违规。**
+
+## 禁止用作能力证据
+
+1. **任务清单条目**。任务书里的任务名描述的是**差额**,不是总额。
+ 「C-3 契约扩列」意味着契约已存在只差几列;「改为表驱动」意味着功能已可用只是硬编码。
+ **禁止**把「某任务未做」转述为「该能力不存在」。
+2. **历史会话结论**。跨轮摘要保留结论、丢弃证据,复用结论会让错误自我放大成前提。
+ 每次断言都要**重新取证**,不得引用上一轮自己的判断。
+3. **加了过滤条件的查询结果**。如用 `WHERE entity_code LIKE 'MDM%'` 查出 2 条,
+ 就说「只开通 2 个」(实际 8 个)。**报总数前必须去掉限定条件再查一次。**
+4. **配置表条数**。本仓库的标准做法是「通用管线 + 按实体逐个开通」,
+ 开通 2 个 ≠ 只支持 2 个。判断代码能力一律回到代码。
+5. **文档里的 ✅ / 🔶 / ❌ 符号**。先确认该符号的图例口径指的是哪一层。
+
+## 断言「不存在」的取证要求
+
+说某个类、表、接口**不存在**,必须是**实际全仓检索零匹配**,并写明检索范围。
+`server/` 下搜不到就写「`server/` 下零匹配」,不要写成「没有实现」——
+它可能在文档、迁移脚本或前端里。
+
+## 交付写法
+
+- 每条能力结论后跟**可重跑的证据**:`文件:行`、grep 模式,或 SQL 与其返回值。
+- 结论只能是:**代码有/无**、**配置开/未开**、**数据通/未通**,三者组合表述。
+- 发现取证结果与既有文档不符时,**先更新文档**,再继续;不得口头纠正后让文档继续错着。
+
+## 反例与正例
+
+- ❌「第三方推数未达到,C 组整组还没动。」
+- ✅「代码层:推数端点、验签、幂等、快照、7 个端点全部已建成(`Controllers/MdpInboundController.cs`,7 个 `[Http*]`);9 个实体有代码级字段契约(`MdpInboundFieldMapper.cs:12-68`)。
+ 配置层:`inbound_enabled=1` 共 8 个,`mdp_inbound_grant` 7 行(均为 AIDOP + `TEST_INBOUND_ERP`)。
+ 数据层:`mdp_inbound_request` 0 行,管线从未被真实调用过。
+ 差额:报工/FQC/发运/工单/BOM 无推数契约;`ProjectApiInvTransSql` 缺 5 列。」

+ 1 - 0
AGENTS.md

@@ -6,6 +6,7 @@
 
 - 动手改代码前先列改动清单,**经确认后再改** 
 - 任何方案须从改动点**反向推演全部影响面**并写出「反向影响推演」小节,禁止只验证局部;详见 **[`.cursor/rules/impact-backtrace.mdc`](.cursor/rules/impact-backtrace.mdc)** 
+- 断言「某能力已实现 / 未实现 / 已达成」前,须分**代码层 / 配置层 / 数据层**三层取证并附可重跑证据;**禁止**把任务清单里的「某任务未做」转述为「该能力不存在」,禁止以历史会话结论、带过滤条件的查询结果或配置表条数代替取证。详见 **[`.cursor/rules/capability-claims-evidence.mdc`](.cursor/rules/capability-claims-evidence.mdc)** 
 - 默认交付**根治性最佳方案**,未经明文要求不得降级为临时/过渡/兜底方案;详见 **[`.cursor/rules/best-solution-default.mdc`](.cursor/rules/best-solution-default.mdc)** 
 - 改动若**波及其他功能模块**,须单独写出跨模块影响并**等待决策** 
 - 小问题**改动范围最小**,非必要不大重构(约束范围、不是让方案将就);更大方案以**建议**形式交负责人决策 

+ 1 - 1
Web/package.json

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

+ 2 - 0
Web/src/constants/aidopFuncCodes.ts

@@ -268,6 +268,8 @@ const FUNC_DEFS: FuncDef[] = [
 	{ code: 'FUNC-S9-010', name: '数据中台工作台', paths: ['/aidop/data-platform/overview'], names: ['aidopDataPlatformOverview'] },
 	{ code: 'FUNC-S9-011', name: '数据地图', paths: ['/aidop/data-platform/data-map'], names: ['aidopDataPlatformDataMap'] },
 	{ code: 'FUNC-S9-012', name: '数据源管理', paths: ['/aidop/data-platform/sources'], names: ['aidopDataPlatformSources'] },
+	{ code: 'FUNC-S9-019', name: '业务对象来源', paths: ['/aidop/data-platform/tenant-std-sources'], names: ['aidopDataPlatformTenantStdSources'] },
+	{ code: 'FUNC-S9-020', name: '来源映射维护', paths: ['/aidop/data-platform/source-mappings'], names: ['aidopDataPlatformSourceMappings'] },
 	{ code: 'FUNC-S9-013', name: '同步配置中心', paths: ['/aidop/data-platform/sync-tasks'], names: ['aidopDataPlatformSyncTasks'] },
 	{ code: 'FUNC-S9-014', name: '数据任务日志', paths: ['/aidop/data-platform/sync-logs'], names: ['aidopDataPlatformSyncLogs'] },
 	{ code: 'FUNC-S9-015', name: '业务动作日志', paths: ['/aidop/data-platform/action-run-logs'], names: ['aidopDataPlatformActionRunLogs'] },

+ 4 - 2
Web/src/views/aidop/chatbi/ChatBIMessage.vue

@@ -23,7 +23,7 @@
 						<el-tag size="small" :type="statusTagType(metric.statusColor)">{{ statusText(metric.statusColor) }}</el-tag>
 					</div>
 					<div class="chatbi-metric-card__value">
-						{{ formatMetricValue(metric.currentValue, metric.unit) }}
+						{{ formatMetricValue(metric.currentValue, metric.unit, metric.resultStatus, metric.resultReason) }}
 						<span>目标 {{ formatMetricValue(metric.targetValue, metric.unit) }}</span>
 					</div>
 					<div class="chatbi-metric-card__foot">
@@ -91,7 +91,9 @@ function intentText(intent?: string) {
 	return map[intent || ''] || '智能分析';
 }
 
-function formatMetricValue(value: number | null | undefined, unit?: string) {
+function formatMetricValue(value: number | null | undefined, unit?: string, resultStatus?: string, resultReason?: string) {
+	if (resultStatus === 'SOURCE_UNSUPPORTED') return resultReason ? `数据源不支持(${resultReason})` : '数据源不支持';
+	if (resultStatus === 'NO_DATA') return '无业务数据';
 	if (value === null || value === undefined) return '-';
 	return `${Number(value).toFixed(2).replace(/\.?0+$/, '')}${unit || ''}`;
 }

+ 69 - 0
Web/src/views/aidop/data-platform/api/mdpMonitor.ts

@@ -157,3 +157,72 @@ export function fetchMdpMonitorRunLogDetail(id: number, params?: Record<string,
 export function fetchMdpMonitorLineage(params: Record<string, unknown>) {
 	return service.get<MdpLineageDetail>('/api/DataPlatform/mdp-monitor/lineage', { params }).then((r) => r.data);
 }
+
+export interface EtlInstanceRow {
+	instanceId: string;
+	machineName: string;
+	processId: number;
+	appVersion?: string | null;
+	isRunner: boolean;
+	startedAt: string;
+	lastHeartbeatAt: string;
+	alive: boolean;
+	self: boolean;
+}
+
+export interface EtlRunnerStatus {
+	instanceId: string;
+	isRunner: boolean;
+	claimAuto: boolean;
+	assignmentKnown: boolean;
+	runningScopes: number;
+	instances: EtlInstanceRow[];
+}
+
+export interface AdminTenantOption {
+	tenantId: number;
+	label: string;
+}
+
+export function fetchEtlRunnerStatus() {
+	return service.get<EtlRunnerStatus>('/api/DataPlatform/mdp-monitor/etl-runner').then((r) => r.data);
+}
+
+export function fetchAdminTenants() {
+	return service.get<AdminTenantOption[]>('/api/DataPlatform/mdp-monitor/admin/tenants').then((r) => r.data);
+}
+
+export interface AdminActiveJob {
+	id: number;
+	moduleCode: string;
+	tenantId: number;
+	status: string;
+	triggerType: string;
+	currentStage: string;
+	cancelRequestedFlag: boolean;
+	submittedAt: string;
+}
+
+export interface AdminRunLogPage extends Paged<MdpRunLogRow> {
+	activeJobs: AdminActiveJob[];
+}
+
+export function fetchAdminRunLogList(params: Record<string, unknown>) {
+	return service.get<AdminRunLogPage>('/api/DataPlatform/mdp-monitor/admin/list', { params }).then((r) => r.data);
+}
+
+export function assignEtlRunner(instanceId: string) {
+	return service.post('/api/DataPlatform/mdp-monitor/admin/assign-runner', { instanceId }).then((r) => r.data);
+}
+
+export function clearEtlRunner() {
+	return service.post('/api/DataPlatform/mdp-monitor/admin/clear-runner').then((r) => r.data);
+}
+
+export function adminRebuild(moduleCode: string, tenantId: number, factoryId = 1) {
+	return service.post('/api/DataPlatform/mdp-monitor/admin/rebuild', { moduleCode, tenantId, factoryId }).then((r) => r.data);
+}
+
+export function adminCancelRebuild(jobId: number) {
+	return service.post('/api/DataPlatform/mdp-monitor/admin/cancel', { jobId }).then((r) => r.data);
+}

+ 41 - 0
Web/src/views/aidop/data-platform/api/sources.ts

@@ -0,0 +1,41 @@
+import service from '/@/utils/request';
+
+export function saveMdpSource(body: Record<string, unknown>) {
+	return service.post('/api/DataPlatform/sources', body);
+}
+
+export function testMdpSource(sourceCode: string) {
+	return service.post(`/api/DataPlatform/sources/${encodeURIComponent(sourceCode)}/test`);
+}
+
+export function sourceBindings(sourceCode: string) {
+	return service.get(`/api/DataPlatform/sources/${encodeURIComponent(sourceCode)}/bindings`);
+}
+
+export function disableMdpSource(sourceCode: string, confirm: boolean) {
+	return service.post(`/api/DataPlatform/sources/${encodeURIComponent(sourceCode)}/disable`, { confirm });
+}
+
+export function fetchRequiredColumns(stdObject: string) {
+	return service.get('/api/DataPlatform/required-columns', { params: { stdObject } });
+}
+
+export function fetchSemanticCodes() {
+	return service.get('/api/DataPlatform/semantic-codes');
+}
+
+export function createInboundGrant(body: { tenantId: number; sourceCode: string; entityCode: string; rateLimitPerMin?: number }) {
+	return service.post('/api/DataPlatform/inbound-grants', body);
+}
+
+export function saveCapability(body: { systemCode: string; semanticCode: string; supported: boolean; reason?: string }) {
+	return service.put('/api/DataPlatform/source-capabilities', body);
+}
+
+export function createSyncTask(body: Record<string, unknown>) {
+	return service.post('/api/DataPlatform/sync-tasks', body);
+}
+
+export function createSyncEntity(taskCode: string, body: Record<string, unknown>) {
+	return service.post(`/api/DataPlatform/sync-tasks/${encodeURIComponent(taskCode)}/entities`, body);
+}

+ 201 - 0
Web/src/views/aidop/data-platform/components/SourceOnboardWizard.vue

@@ -0,0 +1,201 @@
+<template>
+	<el-dialog v-model="open" title="对接向导" width="760px" destroy-on-close>
+		<el-steps :active="step" finish-status="success" class="mb">
+			<el-step title="本源提供的对象" />
+			<el-step title="必填列" />
+			<el-step title="登记到租户" />
+			<el-step v-if="sourceType === 'API_INBOUND'" title="推送授权" />
+		</el-steps>
+
+		<div v-if="step === 0">
+			<p class="hint">只勾选这个来源真正提供的对象。没勾选的对象不会被改掉。</p>
+			<el-checkbox-group v-model="picked">
+				<el-checkbox v-for="o in objects" :key="o.code" :label="o.code">{{ o.module }} {{ o.code }}</el-checkbox>
+			</el-checkbox-group>
+		</div>
+
+		<div v-else-if="step === 1">
+			<p class="hint">每一列都要有结论:填源字段,或标明本源不提供。填不出的会记成来源能力缺失。</p>
+			<div v-for="item in columnRows" :key="item.stdObject + item.column" class="row">
+				<span class="code">{{ item.stdObject }}.{{ item.column }}</span>
+				<el-input v-model="item.sourceField" placeholder="源字段" style="width: 180px" :disabled="item.missing" />
+				<el-checkbox v-model="item.missing">本源不提供</el-checkbox>
+			</div>
+			<el-empty v-if="columnRows.length === 0" description="所选对象没有额外必填列,可以进入下一步" />
+		</div>
+
+		<div v-else-if="step === 2">
+			<el-form label-width="100px">
+				<el-form-item label="租户">
+					<el-input v-model="tenantId" placeholder="专用测试租户,不要填 AIDOP 或 UATTEST_CHL" />
+				</el-form-item>
+				<el-form-item label="范围">
+					<el-input v-model="scope" placeholder="T8 账套或 165 domain,自有可空" />
+				</el-form-item>
+			</el-form>
+		</div>
+
+		<div v-else>
+			<el-form label-width="100px">
+				<el-form-item label="实体编码"><el-input v-model="grantEntity" /></el-form-item>
+				<el-form-item label="每分钟限流"><el-input-number v-model="rate" :min="1" /></el-form-item>
+			</el-form>
+			<el-alert v-if="shownKey" type="warning" :closable="false" :title="'密钥只显示这一次:' + shownKey" />
+		</div>
+
+		<template #footer>
+			<el-button v-if="step > 0" @click="step -= 1">上一步</el-button>
+			<el-button type="primary" :loading="saving" @click="next">{{ lastStep ? '完成' : '下一步' }}</el-button>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import service from '/@/utils/request';
+import { createInboundGrant, createSyncEntity, createSyncTask, fetchRequiredColumns, saveCapability } from '../api/sources';
+
+const open = defineModel<boolean>({ default: false });
+const props = defineProps<{ sourceCode: string; sourceName: string; systemCode: string; sourceType: string }>();
+
+const step = ref(0);
+const saving = ref(false);
+const objects = ref<any[]>([]);
+const picked = ref<string[]>([]);
+const columnRows = ref<{ stdObject: string; column: string; semanticCode: string; sourceField: string; missing: boolean }[]>([]);
+const tenantId = ref('');
+const scope = ref('');
+const grantEntity = ref('');
+const rate = ref(60);
+const shownKey = ref('');
+
+const lastStep = computed(() => (props.sourceType === 'API_INBOUND' ? step.value === 3 : step.value === 2));
+
+watch(open, async (v) => {
+	if (!v) return;
+	step.value = 0;
+	shownKey.value = '';
+	const res: any = await service.get('/api/DataPlatform/std-objects');
+	const data = res?.data ?? res;
+	objects.value = Array.isArray(data) ? data : data?.list ?? [];
+});
+
+async function ensureTaskAndEntities() {
+	const taskCode = `SRC_${props.sourceCode}`;
+	try {
+		await createSyncTask({
+			taskCode,
+			taskName: props.sourceName || props.sourceCode,
+			taskType: 'SERVICE_SYNC',
+			sourceSystemCode: props.systemCode,
+			status: 1,
+		});
+	} catch {
+		/* 任务已存在时继续挂实体 */
+	}
+	for (const code of picked.value) {
+		const obj = objects.value.find((o) => o.code === code);
+		try {
+			await createSyncEntity(taskCode, {
+				entityCode: code,
+				entityName: code,
+				sourceCode: props.sourceCode,
+				targetTableName: obj?.targetTable,
+				inboundEnabled: props.sourceType === 'API_INBOUND' ? 1 : 0,
+				bizKeyExpr: props.sourceType === 'API_INBOUND' ? 'biz_key' : undefined,
+				status: 1,
+			});
+		} catch {
+			/* 实体已存在则保留原行 */
+		}
+	}
+}
+
+async function next() {
+	saving.value = true;
+	try {
+		if (step.value === 0) {
+			if (picked.value.length === 0) {
+				ElMessage.warning('至少勾选一个对象');
+				return;
+			}
+			columnRows.value = [];
+			for (const code of picked.value) {
+				const res: any = await fetchRequiredColumns(code);
+				const body = res?.data ?? res;
+				for (const column of body?.columns ?? []) {
+					const name = column.name ?? column;
+					columnRows.value.push({
+						stdObject: code,
+						column: name,
+						semanticCode: column.semanticCode || '',
+						sourceField: '',
+						missing: false,
+					});
+				}
+			}
+			step.value = 1;
+			return;
+		}
+		if (step.value === 1) {
+			const pending = columnRows.value.filter((r) => !r.missing && !r.sourceField.trim());
+			if (pending.length) {
+				ElMessage.warning('每一列都要填写源字段,或勾选本源不提供');
+				return;
+			}
+			await ensureTaskAndEntities();
+			for (const row of columnRows.value.filter((r) => r.missing && r.semanticCode)) {
+				await saveCapability({
+					systemCode: props.systemCode,
+					semanticCode: row.semanticCode,
+					supported: false,
+					reason: `${row.stdObject} 的 ${row.column} 本源不提供`,
+				});
+			}
+			step.value = 2;
+			return;
+		}
+		if (step.value === 2) {
+			const tenant = Number(tenantId.value);
+			if (!tenant) {
+				ElMessage.warning('请填写租户');
+				return;
+			}
+			await ElMessageBox.confirm('将只登记已勾选的对象。来源若与现值不同,会清掉该对象里不属于新来源的中立层行。', '确认登记');
+			for (const code of picked.value) {
+				await service.put('/api/DataPlatform/tenant-std-sources', {
+					tenantId: tenant,
+					stdObject: code,
+					sourceSystem: props.systemCode,
+					sourceScope: scope.value,
+				});
+			}
+			if (props.sourceType === 'API_INBOUND') {
+				step.value = 3;
+				return;
+			}
+			ElMessage.success('登记完成');
+			open.value = false;
+			return;
+		}
+		const created: any = await createInboundGrant({
+			tenantId: Number(tenantId.value),
+			sourceCode: props.sourceCode,
+			entityCode: grantEntity.value,
+			rateLimitPerMin: rate.value,
+		});
+		shownKey.value = created?.data?.accessKey ?? created?.accessKey ?? '';
+		ElMessage.success('授权已生成,请立刻抄下密钥');
+	} finally {
+		saving.value = false;
+	}
+}
+</script>
+
+<style scoped>
+.mb { margin-bottom: 16px; }
+.hint { color: var(--el-text-color-secondary); margin-bottom: 12px; }
+.row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
+.code { width: 220px; }
+</style>

+ 155 - 0
Web/src/views/aidop/data-platform/mdpMonitor.vue

@@ -33,6 +33,81 @@
 			</el-col>
 		</el-row>
 
+		<el-card v-if="isSuperAdmin" shadow="never" class="mb12">
+			<template #header>
+				<div class="admin-head">
+					<span>全部租户 ETL(仅超级管理员)</span>
+					<el-button size="small" :loading="adminLoading" @click="loadAdmin">刷新</el-button>
+				</div>
+			</template>
+			<div class="admin-runner">
+				<span>当前实例:{{ runner.instanceId || '--' }}</span>
+				<el-tag :type="runner.isRunner ? 'success' : 'info'" size="small">{{ runner.isRunner ? '执行机' : '非执行机' }}</el-tag>
+				<span>正在跑 {{ runner.runningScopes ?? 0 }} 个 scope</span>
+				<el-button size="small" type="warning" plain @click="onClearRunner">取消全部指派</el-button>
+			</div>
+			<el-table :data="runner.instances" border size="small" class="mb12">
+				<el-table-column prop="machineName" label="机器" min-width="140" />
+				<el-table-column prop="processId" label="进程" width="90" />
+				<el-table-column prop="appVersion" label="版本" width="110" />
+				<el-table-column label="执行机" width="90" align="center">
+					<template #default="{ row }">
+						<el-tag v-if="row.isRunner" type="success" size="small">是</el-tag>
+						<span v-else>--</span>
+					</template>
+				</el-table-column>
+				<el-table-column label="存活" width="80" align="center">
+					<template #default="{ row }">
+						<el-tag :type="row.alive ? 'success' : 'info'" size="small">{{ row.alive ? '在线' : '离线' }}</el-tag>
+					</template>
+				</el-table-column>
+				<el-table-column label="心跳" width="170">
+					<template #default="{ row }">{{ fmtDateTime(row.lastHeartbeatAt) }}</template>
+				</el-table-column>
+				<el-table-column label="操作" width="140" align="center">
+					<template #default="{ row }">
+						<el-button v-if="!row.isRunner" link type="primary" @click="onAssignRunner(row.instanceId)">设为执行机</el-button>
+						<el-tag v-else-if="row.self" size="small">本机</el-tag>
+					</template>
+				</el-table-column>
+			</el-table>
+			<el-form inline>
+				<el-form-item label="租户">
+					<el-select v-model="adminTenantId" clearable placeholder="全部租户" style="width: 220px" @change="loadAdminList">
+						<el-option v-for="t in adminTenants" :key="t.tenantId" :label="t.label" :value="t.tenantId" />
+					</el-select>
+				</el-form-item>
+				<el-form-item label="重算">
+					<el-select v-model="adminModule" style="width: 100px">
+						<el-option v-for="m in ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7']" :key="m" :label="m" :value="m" />
+					</el-select>
+					<el-button type="primary" :disabled="!adminTenantId" @click="onAdminRebuild">对该租户重算</el-button>
+				</el-form-item>
+			</el-form>
+			<div class="summary-label">进行中的重算(可取消)</div>
+			<el-table :data="adminJobs" border size="small" class="mb12">
+				<el-table-column prop="moduleCode" label="模块" width="80" />
+				<el-table-column prop="tenantId" label="租户" width="120" />
+				<el-table-column prop="status" label="状态" width="100" />
+				<el-table-column prop="currentStage" label="阶段" width="140" />
+				<el-table-column prop="triggerType" label="触发" width="120" />
+				<el-table-column label="操作" width="120" align="center">
+					<template #default="{ row }">
+						<el-tag v-if="row.cancelRequestedFlag" type="warning" size="small">取消中</el-tag>
+						<el-button v-else link type="danger" @click="onAdminCancel(row.id)">取消</el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<div class="summary-label">运行日志</div>
+			<el-table :data="adminRows" v-loading="adminLoading" border size="small">
+				<el-table-column prop="tenant_id" label="租户" width="110" />
+				<el-table-column prop="job_code" label="任务" min-width="180" show-overflow-tooltip />
+				<el-table-column prop="status" label="状态" width="100" />
+				<el-table-column prop="trigger_type" label="触发" width="110" />
+				<el-table-column prop="start_time" label="开始" width="170" />
+			</el-table>
+		</el-card>
+
 		<el-card shadow="never" class="mb12">
 			<el-form :inline="true" :model="query" @submit.prevent>
 				<el-form-item label="同步业务域">
@@ -295,7 +370,21 @@
 <script setup lang="ts" name="aidopDataPlatformMdpMonitor">
 import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { useUserInfo } from '/@/stores/userInfo';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
+import {
+	adminCancelRebuild,
+	adminRebuild,
+	assignEtlRunner,
+	clearEtlRunner,
+	fetchAdminRunLogList,
+	fetchAdminTenants,
+	fetchEtlRunnerStatus,
+	type AdminActiveJob,
+	type AdminTenantOption,
+	type EtlRunnerStatus,
+} from './api/mdpMonitor';
 import {
 	fetchMdpMonitorCatalog,
 	fetchMdpMonitorLineage,
@@ -311,8 +400,62 @@ import {
 
 const route = useRoute();
 const router = useRouter();
+const userStore = useUserInfo();
+const isSuperAdmin = computed(() => userStore.userInfos.accountType === 999);
 const pageTitle = computed(() => (route.meta?.title as string) || 'MDP运行监控');
 
+const adminLoading = ref(false);
+const adminTenants = ref<AdminTenantOption[]>([]);
+const adminTenantId = ref<number | undefined>(undefined);
+const adminModule = ref('S1');
+const adminRows = ref<Record<string, unknown>[]>([]);
+const adminJobs = ref<AdminActiveJob[]>([]);
+const runner = ref<EtlRunnerStatus>({ instanceId: '', isRunner: false, claimAuto: false, assignmentKnown: false, runningScopes: 0, instances: [] });
+
+async function loadAdmin() {
+	adminLoading.value = true;
+	try {
+		runner.value = await fetchEtlRunnerStatus();
+		if (!adminTenants.value.length) adminTenants.value = await fetchAdminTenants();
+		await loadAdminList();
+	} finally {
+		adminLoading.value = false;
+	}
+}
+
+async function loadAdminList() {
+	const data = await fetchAdminRunLogList({ tenantId: adminTenantId.value || 0, page: 1, pageSize: 20 });
+	adminRows.value = (data.list as unknown as Record<string, unknown>[]) || [];
+	adminJobs.value = data.activeJobs || [];
+}
+
+async function onAssignRunner(instanceId: string) {
+	await ElMessageBox.confirm(`把 ${instanceId} 指派为唯一执行机?其余实例会立即失去执行机身份。`, '指派执行机');
+	await assignEtlRunner(instanceId);
+	ElMessage.success('已指派,最迟 60 秒生效');
+	await loadAdmin();
+}
+
+async function onClearRunner() {
+	await ElMessageBox.confirm('取消后没有任何实例跑定时 ETL,手工重算不受影响。', '取消全部指派');
+	await clearEtlRunner();
+	ElMessage.success('已取消指派');
+	await loadAdmin();
+}
+
+async function onAdminRebuild() {
+	if (!adminTenantId.value) return;
+	await adminRebuild(adminModule.value, adminTenantId.value);
+	ElMessage.success(`${adminModule.value} 已入队`);
+	await loadAdminList();
+}
+
+async function onAdminCancel(jobId: number) {
+	await adminCancelRebuild(jobId);
+	ElMessage.success('已请求取消,将在下个阶段边界停止');
+	await loadAdminList();
+}
+
 const legacyModule = String(route.query.moduleCode || route.query.module || '');
 const query = reactive({
 	businessDomainCode: String(route.query.businessDomainCode || ''),
@@ -569,6 +712,7 @@ function openScheduleJob(row: Pick<MdpRunLogRow, 'scheduleJobId'>) {
 onMounted(async () => {
 	await loadCatalog();
 	await loadAll();
+	if (isSuperAdmin.value) await loadAdmin();
 });
 </script>
 
@@ -592,6 +736,17 @@ onMounted(async () => {
 .mb12 {
 	margin-bottom: 12px;
 }
+.admin-head {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
+.admin-runner {
+	display: flex;
+	gap: 12px;
+	align-items: center;
+	margin-bottom: 12px;
+}
 .pager {
 	display: flex;
 	justify-content: flex-end;

+ 168 - 0
Web/src/views/aidop/data-platform/sourceMappings.vue

@@ -0,0 +1,168 @@
+<template>
+	<AidopDemoShell title="来源映射维护" subtitle="库位角色和岗位由页面改动后记为 MANUAL,自动分类不会覆盖。">
+		<el-tabs v-model="tab">
+			<el-tab-pane label="来源归属" name="domain">
+				<p class="hint">这张表回答「一个来源加源侧组织标识,物理上落在哪个租户的贴源区」。它不回答某条业务数据属于哪个租户。同一范围已被别的租户占用时,保存会被拒绝。</p>
+				<el-form :inline="true">
+					<el-form-item label="租户"><el-input v-model="tenantId" style="width: 200px" /></el-form-item>
+					<el-form-item label="系统码"><el-input v-model="domainForm.systemCode" /></el-form-item>
+					<el-form-item label="组织标识"><el-input v-model="domainForm.domain" placeholder="账套或 domain" /></el-form-item>
+					<el-form-item><el-button type="primary" @click="saveDomain">保存</el-button></el-form-item>
+				</el-form>
+				<el-table :data="domains" border>
+					<el-table-column prop="systemCode" label="系统码" />
+					<el-table-column prop="domain" label="组织标识" />
+					<el-table-column prop="tenantId" label="落地租户" />
+					<el-table-column prop="remark" label="备注" />
+				</el-table>
+			</el-tab-pane>
+			<el-tab-pane label="库位角色" name="loc">
+				<el-form :inline="true">
+					<el-form-item label="租户"><el-input v-model="tenantId" style="width: 200px" /></el-form-item>
+					<el-form-item><el-checkbox v-model="unknownOnly">只看 UNKNOWN</el-checkbox></el-form-item>
+					<el-form-item><el-button type="primary" @click="loadLoc">查询</el-button></el-form-item>
+					<el-form-item><el-button @click="saveLoc">保存为 MANUAL</el-button></el-form-item>
+				</el-form>
+				<el-table :data="locations" border>
+					<el-table-column prop="domain" label="domain" width="120" />
+					<el-table-column prop="location" label="库位" />
+					<el-table-column label="角色">
+						<template #default="{ row }">
+							<el-select v-model="row.locationRole" filterable>
+								<el-option v-for="r in roles" :key="r" :label="r" :value="r" />
+							</el-select>
+						</template>
+					</el-table-column>
+					<el-table-column prop="roleSource" label="来源" width="120" />
+				</el-table>
+			</el-tab-pane>
+			<el-tab-pane label="岗位映射" name="pos">
+				<el-button type="primary" @click="loadPos">查询</el-button>
+				<el-button @click="savePos">保存为 MANUAL</el-button>
+				<el-table :data="positions" border class="mt">
+					<el-table-column prop="sourceSystem" label="来源" width="160" />
+					<el-table-column prop="srcPositionRaw" label="源岗位" />
+					<el-table-column label="岗位码">
+						<template #default="{ row }"><el-input v-model="row.positionCode" /></template>
+					</el-table-column>
+				</el-table>
+			</el-tab-pane>
+			<el-tab-pane label="事务类型" name="tx">
+				<el-form :inline="true">
+					<el-form-item label="系统码"><el-input v-model="txQuery" /></el-form-item>
+					<el-form-item><el-button type="primary" @click="loadTx">查询</el-button></el-form-item>
+				</el-form>
+				<el-form :inline="true">
+					<el-form-item label="源码"><el-input v-model="txForm.srcTransCode" /></el-form-item>
+					<el-form-item label="阶段码"><el-input v-model="txForm.stageCode" /></el-form-item>
+					<el-form-item><el-button type="primary" @click="saveTx">新增全局行</el-button></el-form-item>
+				</el-form>
+				<el-table :data="txs" border>
+					<el-table-column prop="systemCode" label="系统码" width="180" />
+					<el-table-column prop="srcTransCode" label="源码" />
+					<el-table-column prop="qtySign" label="方向" width="80" />
+					<el-table-column prop="locationRole" label="库位角色" />
+					<el-table-column prop="stageCode" label="阶段码" />
+				</el-table>
+			</el-tab-pane>
+		</el-tabs>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopDataPlatformSourceMappings">
+import { reactive, ref } from 'vue';
+import { ElMessage } from 'element-plus';
+import AidopDemoShell from '../components/AidopDemoShell.vue';
+import service from '/@/utils/request';
+
+const tab = ref('domain');
+const roles = ['MAT_INSPECT', 'MATERIAL', 'MAT_LINE_LOC', 'SEMI', 'FG_INSPECT', 'FG', 'FG_LINE_LOC', 'REJECT_SCRAP', 'VMI', 'OTHER', 'UNKNOWN'];
+const tenantId = ref(localStorage.getItem('aidop-tenant-id') || '');
+const unknownOnly = ref(false);
+const domains = ref<any[]>([]);
+const locations = ref<any[]>([]);
+const positions = ref<any[]>([]);
+const txs = ref<any[]>([]);
+const txQuery = ref('');
+const domainForm = reactive({ systemCode: '', domain: '' });
+const txForm = reactive({ srcTransCode: '', stageCode: '' });
+
+function listOf(res: any) {
+	const data = res?.data ?? res;
+	return Array.isArray(data) ? data : data?.list ?? [];
+}
+
+async function saveDomain() {
+	await service.put('/api/DataPlatform/source-domain-maps', {
+		tenantId: Number(tenantId.value),
+		systemCode: domainForm.systemCode,
+		domain: domainForm.domain,
+	});
+	ElMessage.success('已保存');
+	domains.value = listOf(await service.get('/api/DataPlatform/source-domain-maps', { params: { tenantId: tenantId.value } }));
+}
+
+async function loadLoc() {
+	locations.value = listOf(
+		await service.get('/api/DataPlatform/location-roles', {
+			params: { tenantId: tenantId.value, unknownOnly: unknownOnly.value },
+		})
+	);
+}
+
+async function saveLoc() {
+	await service.put('/api/DataPlatform/location-roles', {
+		tenantId: Number(tenantId.value),
+		rows: locations.value.map((r) => ({
+			domain: r.domain,
+			location: r.location,
+			locationRole: r.locationRole,
+		})),
+	});
+	ElMessage.success('已保存');
+	await loadLoc();
+}
+
+async function loadPos() {
+	positions.value = listOf(
+		await service.get('/api/DataPlatform/position-maps', { params: { tenantId: tenantId.value, unknownOnly: unknownOnly.value } })
+	);
+}
+
+async function savePos() {
+	await service.put('/api/DataPlatform/position-maps', {
+		tenantId: Number(tenantId.value),
+		rows: positions.value.map((r) => ({
+			sourceSystem: r.sourceSystem,
+			domain: r.domain,
+			srcPositionRaw: r.srcPositionRaw,
+			srcPositionField: r.srcPositionField,
+			positionCode: r.positionCode,
+		})),
+	});
+	ElMessage.success('已保存');
+	await loadPos();
+}
+
+async function loadTx() {
+	txs.value = listOf(await service.get('/api/DataPlatform/trans-type-maps', { params: { systemCode: txQuery.value } }));
+}
+
+async function saveTx() {
+	await service.put('/api/DataPlatform/trans-type-maps', {
+		systemCode: txQuery.value,
+		srcTransCode: txForm.srcTransCode,
+		stageCode: txForm.stageCode,
+		qtySign: 0,
+		locationRole: '',
+		isStage: 1,
+	});
+	ElMessage.success('已保存');
+	await loadTx();
+}
+</script>
+
+<style scoped>
+.hint { color: var(--el-text-color-secondary); margin-bottom: 12px; }
+.mt { margin-top: 12px; }
+</style>

+ 158 - 3
Web/src/views/aidop/data-platform/sources.vue

@@ -1,7 +1,8 @@
 <template>
 	<AidopDemoShell title="数据源管理" subtitle="mdp_source 真实登记:DB / API 源与健康状态">
 		<template #bar-right>
-			<el-button type="primary" @click="fileOpen = true">Excel 导入</el-button>
+			<el-button type="primary" @click="openCreate">新建</el-button>
+			<el-button @click="fileOpen = true">Excel 导入</el-button>
 			<el-button icon="ele-Refresh" :loading="loading" @click="loadList">刷新</el-button>
 		</template>
 
@@ -14,6 +15,7 @@
 					<el-select v-model="query.type" placeholder="全部" clearable style="width: 140px">
 						<el-option label="DB" value="DB" />
 						<el-option label="API" value="API" />
+						<el-option label="API_INBOUND" value="API_INBOUND" />
 						<el-option label="FILE_EXCEL" value="FILE_EXCEL" />
 					</el-select>
 				</el-form-item>
@@ -26,6 +28,8 @@
 
 		<el-table :data="rows" v-loading="loading" border>
 			<el-table-column prop="sourceCode" label="编码" width="160" />
+			<el-table-column prop="systemCode" label="系统码" width="160" />
+			<el-table-column prop="connMode" label="连接" width="110" />
 			<el-table-column prop="sourceName" label="名称" min-width="160" />
 			<el-table-column prop="sourceType" label="类型" width="100" />
 			<el-table-column prop="direction" label="方向" width="90" align="center" />
@@ -38,22 +42,89 @@
 			</el-table-column>
 			<el-table-column prop="lastCheck" label="最近检测" width="170" />
 			<el-table-column prop="healthMsg" label="探活信息" min-width="160" show-overflow-tooltip />
+			<el-table-column prop="status" label="状态" width="80" />
+			<el-table-column label="操作" width="240" fixed="right">
+				<template #default="{ row }">
+					<el-button v-if="row.sourceType !== 'FILE_EXCEL'" link type="primary" @click="openEdit(row)">编辑</el-button>
+					<el-button v-if="row.sourceType !== 'FILE_EXCEL'" link @click="testRow(row)">测连</el-button>
+					<el-button v-if="row.sourceType !== 'FILE_EXCEL'" link @click="openWizard(row)">向导</el-button>
+					<el-button v-if="row.sourceType !== 'FILE_EXCEL' && row.status !== 0" link type="danger" @click="disableRow(row)">停用</el-button>
+				</template>
+			</el-table-column>
 		</el-table>
+		<el-drawer v-model="drawer" :title="form.sourceCodeLocked ? '编辑数据源' : '新建数据源'" size="480px">
+			<el-form label-width="100px">
+				<el-form-item label="方式">
+					<el-select v-model="form.sourceType" :disabled="form.sourceCodeLocked">
+						<el-option label="库同步" value="DB" />
+						<el-option label="读对方接口" value="API" />
+						<el-option label="对方推送" value="API_INBOUND" />
+					</el-select>
+				</el-form-item>
+				<el-form-item label="编码"><el-input v-model="form.sourceCode" :disabled="form.sourceCodeLocked" /></el-form-item>
+				<el-form-item label="名称"><el-input v-model="form.sourceName" /></el-form-item>
+				<el-form-item label="系统码"><el-input v-model="form.systemCode" /></el-form-item>
+				<el-form-item label="连接">
+					<el-radio-group v-model="form.connMode">
+						<el-radio value="EXTERNAL">外部库</el-radio>
+						<el-radio value="SELF">本系统库</el-radio>
+					</el-radio-group>
+				</el-form-item>
+				<template v-if="form.sourceType === 'DB' && form.connMode === 'EXTERNAL'">
+					<el-form-item label="库类型"><el-input v-model="form.dbType" placeholder="SQLSERVER / MYSQL" /></el-form-item>
+					<el-form-item label="主机"><el-input v-model="form.dbHost" /></el-form-item>
+					<el-form-item label="端口"><el-input-number v-model="form.dbPort" /></el-form-item>
+					<el-form-item label="库名"><el-input v-model="form.dbName" /></el-form-item>
+					<el-form-item label="账号"><el-input v-model="form.dbUser" /></el-form-item>
+					<el-form-item label="口令"><el-input v-model="form.dbPassword" type="password" show-password placeholder="留空表示不修改" /></el-form-item>
+				</template>
+				<el-form-item v-if="form.sourceType === 'API'" label="接口地址"><el-input v-model="form.apiBaseUrl" /></el-form-item>
+			</el-form>
+			<el-button type="primary" :loading="saving" @click="save">保存</el-button>
+		</el-drawer>
+		<SourceOnboardWizard
+			v-model="wizardOpen"
+			:source-code="wizard.sourceCode"
+			:source-name="wizard.sourceName"
+			:system-code="wizard.systemCode"
+			:source-type="wizard.sourceType"
+		/>
 		<MdpFileImportDrawer v-model="fileOpen" />
 	</AidopDemoShell>
 </template>
 
 <script setup lang="ts" name="aidopDataPlatformSources">
 import { onMounted, reactive, ref } from 'vue';
-import { ElMessage } from 'element-plus';
+import { ElMessage, ElMessageBox } from 'element-plus';
 import AidopDemoShell from '../components/AidopDemoShell.vue';
 import MdpFileImportDrawer from './components/MdpFileImportDrawer.vue';
+import SourceOnboardWizard from './components/SourceOnboardWizard.vue';
 import { fetchMdpSources } from './api/syncTasks';
 import { fetchFileEntities } from './api/fileImports';
+import { disableMdpSource, saveMdpSource, sourceBindings, testMdpSource } from './api/sources';
 
 const loading = ref(false);
 const rows = ref<any[]>([]);
 const fileOpen = ref(false);
+const drawer = ref(false);
+const saving = ref(false);
+const wizardOpen = ref(false);
+const wizard = reactive({ sourceCode: '', sourceName: '', systemCode: '', sourceType: 'DB' });
+const form = reactive({
+	sourceCodeLocked: false,
+	sourceCode: '',
+	sourceName: '',
+	sourceType: 'DB',
+	systemCode: '',
+	connMode: 'EXTERNAL',
+	dbType: 'SQLSERVER',
+	dbHost: '',
+	dbPort: 1433,
+	dbName: '',
+	dbUser: '',
+	dbPassword: '',
+	apiBaseUrl: '',
+});
 const query = reactive({
 	keyword: '',
 	type: '',
@@ -71,7 +142,12 @@ async function loadList() {
 			try {
 				const ents: any = await fetchFileEntities();
 				const list = Array.isArray(ents) ? ents : ents?.list ?? [];
-				fileRows = list.map((e: any) => ({
+				fileRows = list
+					.filter((e: any) => {
+						const code = e.sourceCode || e.SourceCode;
+						return !dbApi.some((x: any) => x.sourceCode === code);
+					})
+					.map((e: any) => ({
 					sourceCode: e.sourceCode || e.SourceCode,
 					sourceName: e.entityName || e.EntityName,
 					sourceType: 'FILE_EXCEL',
@@ -105,6 +181,85 @@ function resetQuery() {
 	loadList();
 }
 
+function blankForm() {
+	form.sourceCodeLocked = false;
+	form.sourceCode = '';
+	form.sourceName = '';
+	form.sourceType = 'DB';
+	form.systemCode = '';
+	form.connMode = 'EXTERNAL';
+	form.dbType = 'SQLSERVER';
+	form.dbHost = '';
+	form.dbPort = 1433;
+	form.dbName = '';
+	form.dbUser = '';
+	form.dbPassword = '';
+	form.apiBaseUrl = '';
+}
+
+function openCreate() {
+	blankForm();
+	drawer.value = true;
+}
+
+function openEdit(row: any) {
+	blankForm();
+	form.sourceCodeLocked = true;
+	form.sourceCode = row.sourceCode;
+	form.sourceName = row.sourceName;
+	form.sourceType = row.sourceType;
+	form.systemCode = row.systemCode;
+	form.connMode = row.connMode || 'EXTERNAL';
+	form.dbType = row.dbType === '-' ? 'SQLSERVER' : row.dbType;
+	form.dbHost = row.dbHost || '';
+	form.dbPort = row.dbPort || 1433;
+	form.dbName = row.dbName || '';
+	form.dbUser = row.dbUser || '';
+	form.apiBaseUrl = row.apiBaseUrl || '';
+	drawer.value = true;
+}
+
+async function save() {
+	saving.value = true;
+	try {
+		await saveMdpSource({ ...form, dbPassword: form.dbPassword || undefined });
+		ElMessage.success('已保存');
+		drawer.value = false;
+		await loadList();
+	} catch (e: unknown) {
+		ElMessage.error((e as Error)?.message || '保存失败');
+	} finally {
+		saving.value = false;
+	}
+}
+
+async function testRow(row: any) {
+	const res: any = await testMdpSource(row.sourceCode);
+	const body = res?.data ?? res;
+	ElMessage[body?.ok ? 'success' : 'error'](body?.message || '测试结束');
+}
+
+function openWizard(row: any) {
+	wizard.sourceCode = row.sourceCode;
+	wizard.sourceName = row.sourceName;
+	wizard.systemCode = row.systemCode || row.sourceCode;
+	wizard.sourceType = row.sourceType;
+	wizardOpen.value = true;
+}
+
+async function disableRow(row: any) {
+	const look: any = await sourceBindings(row.sourceCode);
+	const body = look?.data ?? look;
+	const bindings = body?.bindings ?? [];
+	if (bindings.length) {
+		const names = bindings.map((b: any) => `${b.tenantId ?? ''} ${b.stdObject ?? ''}`).join('、');
+		await ElMessageBox.confirm(`下列对象仍登记此来源:${names}。停用后这些对象不会自动改来源。`, '停用数据源');
+	}
+	await disableMdpSource(row.sourceCode, true);
+	ElMessage.success('已停用');
+	await loadList();
+}
+
 onMounted(loadList);
 </script>
 

+ 124 - 0
Web/src/views/aidop/data-platform/tenantStdSources.vue

@@ -0,0 +1,124 @@
+<template>
+	<AidopDemoShell title="业务对象来源" subtitle="同一租户可以一部分对象听外部、一部分听系统自有。改来源会删掉该对象里来源不一致的中立层行,贴源保留。">
+		<template #bar-right>
+			<el-button icon="ele-Refresh" :loading="loading" @click="load">刷新</el-button>
+		</template>
+		<el-card shadow="never" class="mb12">
+			<el-form :inline="true" @submit.prevent>
+				<el-form-item label="租户">
+					<el-input v-model="tenantId" placeholder="租户 Id" style="width: 220px" @keyup.enter="load" />
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" @click="load">查询</el-button>
+				</el-form-item>
+			</el-form>
+		</el-card>
+		<el-table :data="rows" v-loading="loading" border>
+			<el-table-column prop="code" label="标准对象" width="180" />
+			<el-table-column prop="module" label="模块" width="80" />
+			<el-table-column label="来源" min-width="200">
+				<template #default="{ row }">
+					<el-input v-model="row.sourceSystem" placeholder="如 T8 / AIDOP_NATIVE" />
+				</template>
+			</el-table-column>
+			<el-table-column label="范围" width="160">
+				<template #default="{ row }">
+					<el-input v-model="row.sourceScope" placeholder="账套或 domain" />
+				</template>
+			</el-table-column>
+			<el-table-column label="" width="100">
+				<template #default="{ row }">
+					<el-button type="primary" link :loading="row.saving" @click="save(row)">保存</el-button>
+				</template>
+			</el-table-column>
+		</el-table>
+	</AidopDemoShell>
+</template>
+
+<script setup lang="ts" name="aidopDataPlatformTenantStdSources">
+import { onMounted, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import AidopDemoShell from '../components/AidopDemoShell.vue';
+import { service } from '/@/utils/request';
+
+type Row = {
+	code: string;
+	module: string;
+	sourceSystem: string;
+	sourceScope: string;
+	origin: string;
+	saving?: boolean;
+};
+
+const loading = ref(false);
+const tenantId = ref('');
+const rows = ref<Row[]>([]);
+
+async function load() {
+	if (!tenantId.value.trim()) {
+		ElMessage.warning('先填租户 Id');
+		return;
+	}
+	loading.value = true;
+	try {
+		const catalog: any = await service.get('/api/DataPlatform/std-objects');
+		const regs: any = await service.get('/api/DataPlatform/tenant-std-sources', {
+			params: { tenantId: tenantId.value.trim() },
+		});
+		const list = Array.isArray(catalog?.data) ? catalog.data : catalog?.data?.list ?? catalog ?? [];
+		const regList = Array.isArray(regs?.data) ? regs.data : regs ?? [];
+		const byCode = new Map<string, any>();
+		for (const r of regList) byCode.set(r.StdObject || r.stdObject, r);
+		rows.value = (Array.isArray(list) ? list : []).map((d: any) => {
+			const hit = byCode.get(d.code) || {};
+			const sourceSystem = hit.SourceSystem || hit.sourceSystem || '';
+			return {
+				code: d.code,
+				module: d.module,
+				sourceSystem,
+				sourceScope: hit.SourceScope || hit.sourceScope || '',
+				origin: sourceSystem,
+			};
+		});
+	} catch (e: unknown) {
+		ElMessage.error((e as Error)?.message || '加载失败');
+	} finally {
+		loading.value = false;
+	}
+}
+
+async function save(row: Row) {
+	if (row.sourceSystem === row.origin) return;
+	await ElMessageBox.confirm(
+		`将把 ${row.code} 的来源改为 ${row.sourceSystem},并删除该租户该对象中与新来源不一致的中立层行。贴源保留。`,
+		'确认变更来源',
+		{ type: 'warning' }
+	);
+	row.saving = true;
+	try {
+		await service.put('/api/DataPlatform/tenant-std-sources', {
+			tenantId: Number(tenantId.value),
+			stdObject: row.code,
+			sourceSystem: row.sourceSystem,
+			sourceScope: row.sourceScope,
+		});
+		row.origin = row.sourceSystem;
+		ElMessage.success('已保存');
+	} catch (e: unknown) {
+		ElMessage.error((e as Error)?.message || '保存失败');
+	} finally {
+		row.saving = false;
+	}
+}
+
+onMounted(() => {
+	const cached = localStorage.getItem('aidop-tenant-id');
+	if (cached) tenantId.value = cached;
+});
+</script>
+
+<style scoped>
+.mb12 {
+	margin-bottom: 12px;
+}
+</style>

+ 5 - 1
Web/src/views/aidop/diagnosis/index.vue

@@ -1984,7 +1984,11 @@ function applyDiagnosisPayload(payload) {
   diagnosisContext.metricCode = nextMetric
   diagnosisContext.metricName = metricName
   diagnosisContext.target = payload.root?.targetValue != null ? `${payload.root.targetValue}${payload.root.unit || ''}` : '-'
-  diagnosisContext.actual = payload.root?.currentValue != null ? `${payload.root.currentValue}${payload.root.unit || ''}` : '-'
+  diagnosisContext.actual = payload.root?.resultStatus === 'SOURCE_UNSUPPORTED'
+    ? '数据源不支持'
+    : payload.root?.resultStatus === 'NO_DATA'
+      ? '无业务数据'
+      : payload.root?.currentValue != null ? `${payload.root.currentValue}${payload.root.unit || ''}` : '-'
   diagnosisContext.gap = payload.root?.gapLabel || '-'
   diagnosisContext.unit = payload.root?.unit || ''
   diagnosisContext.status = payload.root?.status || 'green'

+ 10 - 8
Web/src/views/aidop/kanban/components/DynamicModuleDashboard.vue

@@ -85,7 +85,7 @@
 						</div>
 						<div class="metric-primary">
 							<div class="metric-primary__value" :class="valueClass(widget.metrics[0]?.statusColor)">
-								{{ formatValue(widget.metrics[0]?.currentValue) }}
+								{{ formatValue(widget.metrics[0]?.currentValue, widget.metrics[0]?.resultStatus) }}
 								<span>{{ widget.metrics[0]?.unit || '' }}</span>
 							</div>
 							<div class="metric-primary__meta">
@@ -124,7 +124,7 @@
 						</div>
 						<div class="metric-primary metric-primary--compact">
 							<div class="metric-primary__value" :class="valueClass(metric.statusColor)">
-								{{ formatValue(metric.currentValue) }}
+								{{ formatValue(metric.currentValue, metric.resultStatus) }}
 								<span>{{ metric.unit || '' }}</span>
 							</div>
 							<div class="metric-primary__meta">
@@ -164,7 +164,7 @@
 						</div>
 						<div class="metric-primary metric-primary--compact">
 							<div class="metric-primary__value" :class="valueClass(metric.statusColor)">
-								{{ formatValue(metric.currentValue) }}
+								{{ formatValue(metric.currentValue, metric.resultStatus) }}
 								<span>{{ metric.unit || '' }}</span>
 							</div>
 							<div class="metric-primary__meta">
@@ -198,7 +198,7 @@
 						</div>
 						<div class="metric-primary metric-primary--compact">
 							<div class="metric-primary__value" :class="valueClass(metric.statusColor)">
-								{{ formatValue(metric.currentValue) }}
+								{{ formatValue(metric.currentValue, metric.resultStatus) }}
 								<span>{{ metric.unit || '' }}</span>
 							</div>
 							<div class="metric-primary__meta">
@@ -245,7 +245,7 @@
 									</div>
 									<div class="metric-summary__value">
 										<el-tag size="small" :type="tagType(metric.statusColor)" :class="{ 'blink-dot': metric.isBlink }">{{ statusText(metric.statusColor) }}</el-tag>
-										<strong>{{ formatValue(metric.currentValue) }}</strong>
+										<strong>{{ formatValue(metric.currentValue, metric.resultStatus) }}</strong>
 										<span>{{ metric.unit || '' }}</span>
 										<small v-if="metric.targetValue != null">目标 {{ formatValue(metric.targetValue) }}{{ metric.unit || '' }}</small>
 									</div>
@@ -262,7 +262,7 @@
 									</div>
 									<div class="metric-list__value">
 										<el-tag size="small" :type="tagType(metric.statusColor)" :class="{ 'blink-dot': metric.isBlink }">{{ statusText(metric.statusColor) }}</el-tag>
-										<strong>{{ formatValue(metric.currentValue) }}</strong>
+										<strong>{{ formatValue(metric.currentValue, metric.resultStatus) }}</strong>
 										<span>{{ metric.unit || '' }}</span>
 										<small v-if="metric.targetValue != null">目标 {{ formatValue(metric.targetValue) }}{{ metric.unit || '' }}</small>
 									</div>
@@ -272,7 +272,7 @@
 						<template v-else>
 							<div class="metric-primary">
 								<div class="metric-primary__value" :class="valueClass(widget.metrics[0]?.statusColor)">
-									{{ formatValue(widget.metrics[0]?.currentValue) }}
+									{{ formatValue(widget.metrics[0]?.currentValue, widget.metrics[0]?.resultStatus) }}
 									<span>{{ widget.metrics[0]?.unit || '' }}</span>
 								</div>
 								<div class="metric-primary__meta">
@@ -852,7 +852,9 @@ function spanToGrid(span?: number) {
 	return 3;
 }
 
-function formatValue(value?: number | null) {
+function formatValue(value?: number | null, resultStatus?: string | null) {
+	if (resultStatus === 'SOURCE_UNSUPPORTED') return '数据源不支持';
+	if (resultStatus === 'NO_DATA') return '无业务数据';
 	if (value == null || Number.isNaN(Number(value))) return '—';
 	const n = Number(value);
 	return Math.abs(n - Math.round(n)) < 1e-9 ? String(Math.round(n)) : String(Math.round(n * 100) / 100);

+ 2 - 0
Web/src/views/aidop/s0/api/s0SalesApi.ts

@@ -53,6 +53,7 @@ export interface S0CustMasterRow {
 	remarks?: string | null;
 	isActive: boolean;
 	isConfirm: boolean;
+	sourceSystem?: string | null;
 	createUser?: string | null;
 	createTime?: string;
 	updateUser?: string | null;
@@ -146,6 +147,7 @@ export interface S0ItemMasterRow {
 	designOwner?: string | null;
 	isActive: boolean;
 	isConfirm: boolean;
+	sourceSystem?: string | null;
 	mrpRequired: boolean;
 	createUser?: string | null;
 	createTime?: string;

+ 1 - 0
Web/src/views/aidop/s0/api/s0SupplyApi.ts

@@ -55,6 +55,7 @@ export interface S0SuppMasterRow {
 	apEndDay2Text?: string | null;
 	remark?: string | null;
 	isActive: boolean;
+	sourceSystem?: string | null;
 	createUser?: string | null;
 	createTime?: string;
 	updateUser?: string | null;

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

@@ -76,6 +76,7 @@ export interface S0EmployeeRow {
 	country?: string | null;
 	businessPhone?: string | null;
 	isActive: boolean;
+	sourceSystem?: string | null;
 	createUser?: string | null;
 	createTime?: string;
 	updateUser?: string | null;
@@ -178,6 +179,7 @@ export interface S0LocationRow {
 	typed?: string | null;
 	physicalAddress?: string | null;
 	isActive: boolean;
+	sourceSystem?: string | null;
 	createUser?: string | null;
 	createTime?: string;
 	updateUser?: string | null;

+ 10 - 0
Web/src/views/aidop/s0/components/S0SourceBadge.vue

@@ -0,0 +1,10 @@
+<template>
+  <el-tag v-if="external" size="small" type="info">{{ sourceSystem }}</el-tag>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { isExternalSource } from './s0ExternalSource'
+const props = defineProps<{ sourceSystem?: string | null }>()
+const external = computed(() => isExternalSource(props.sourceSystem))
+</script>

+ 5 - 0
Web/src/views/aidop/s0/components/s0ExternalSource.ts

@@ -0,0 +1,5 @@
+/** 外源行:来源非空,且不是本平台自有写入。手建行 sourceSystem 为空,可改可删。 */
+export function isExternalSource(sourceSystem?: string | null): boolean {
+	const value = String(sourceSystem ?? '').trim()
+	return value !== '' && value.toUpperCase() !== 'AIDOP_NATIVE'
+}

+ 10 - 3
Web/src/views/aidop/s0/manufacturing/LineMaterialList.vue

@@ -26,10 +26,15 @@
 			<el-table-column prop="skillNo" label="技能编码" width="120" show-overflow-tooltip />
 			<!-- 工厂域编码为系统后台归属字段:表单控件已隐藏,列表列一并隐藏以保持口径一致(值仍由后端维护并原样保值)。 -->
 			<el-table-column v-if="false" prop="domain" label="工厂域" width="120" show-overflow-tooltip />
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="220" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -138,6 +143,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgLineMaterial">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { reactive as __reactive_lk } from 'vue';
 import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
 // 关系字段下拉:复用既有列表/options 接口,模块级缓存,每个 key 每页仅取一次(无 N+1)。

+ 10 - 3
Web/src/views/aidop/s0/manufacturing/LinePostList.vue

@@ -20,10 +20,15 @@
 			<el-table-column prop="jobDescr" label="岗位描述" min-width="180" show-overflow-tooltip />
 			<!-- 工厂域编码为系统后台归属字段:表单控件已隐藏,列表列一并隐藏以保持口径一致(值仍由后端维护并原样保值)。 -->
 			<el-table-column v-if="false" prop="domain" label="工厂域" width="120" show-overflow-tooltip />
-			<el-table-column label="操作" width="240" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="240" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -85,6 +90,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgLinePost">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { reactive as __reactive_lk } from 'vue';
 import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
 // 生产线与人员技能一律以【业务编码】作为 select 值:

+ 10 - 3
Web/src/views/aidop/s0/manufacturing/MaterialProcessElementList.vue

@@ -40,10 +40,15 @@
 			</el-table-column>
 			<!-- 工厂域编码为系统后台归属字段:表单控件已隐藏,列表列一并隐藏以保持口径一致(值仍由后端维护并原样保值)。 -->
 			<el-table-column v-if="false" prop="domain" label="工厂域" width="120" show-overflow-tooltip />
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="220" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -123,6 +128,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgMaterialProcessElement">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { reactive as __reactive_lk } from 'vue';
 import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
 // 关系字段下拉:复用既有列表/options 接口,模块级缓存,每个 key 每页仅取一次(无 N+1)。

+ 11 - 4
Web/src/views/aidop/s0/manufacturing/OrderScheduleCycleList.vue

@@ -28,13 +28,18 @@
 					<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="200" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="200" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleActive(row)">
 						{{ row.isActive ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -81,6 +86,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgOrderScheduleCycle">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/manufacturing/PersonSkillAssignmentList.vue

@@ -45,10 +45,15 @@
 			<el-table-column prop="efficiencyCoefficient" label="生产效率" width="110" align="right" />
 			<!-- 工厂域编码为系统后台归属字段:表单控件已隐藏,列表列一并隐藏以保持口径一致(值仍由后端维护并原样保值)。 -->
 			<el-table-column v-if="false" prop="domain" label="工厂域" width="120" show-overflow-tooltip />
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="220" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -116,6 +121,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgEmpSkills">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 11 - 4
Web/src/views/aidop/s0/manufacturing/PersonnelSkillList.vue

@@ -28,13 +28,18 @@
 					</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isEnabled ? 'warning' : 'success'" @click="toggleEnabled(row)">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="220" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isEnabled ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleEnabled(row)">
 						{{ row.isEnabled ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -99,6 +104,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgPersonSkill">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/manufacturing/PreprocessElementList.vue

@@ -34,10 +34,15 @@
 			</el-table-column>
 			<!-- 工厂域编码为系统后台归属字段:表单控件已隐藏,列表列一并隐藏以保持口径一致(值仍由后端维护并原样保值)。 -->
 			<el-table-column v-if="false" prop="domain" label="工厂域" width="120" show-overflow-tooltip />
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="220" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -101,6 +106,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgPreprocessElement">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { reactive as __reactive_lk } from 'vue';
 import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
 // 关系字段下拉:复用既有列表/options 接口,模块级缓存,每个 key 每页仅取一次(无 N+1)。

+ 10 - 3
Web/src/views/aidop/s0/manufacturing/PreprocessElementParamList.vue

@@ -22,10 +22,15 @@
 			<el-table-column prop="lowLimit" label="下限值" width="100" align="right" />
 			<el-table-column prop="ufld1" label="值选择数据" min-width="160" show-overflow-tooltip />
 			<el-table-column prop="um" label="单位" width="80" />
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -99,6 +104,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgPreprocessElementParam">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 11 - 4
Web/src/views/aidop/s0/manufacturing/ProcessFlowCardList.vue

@@ -38,13 +38,18 @@
 					<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="200" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="200" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleActive(row)">
 						{{ row.isActive ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -112,6 +117,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgProcessFlowCard">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/manufacturing/ProductionElementParamList.vue

@@ -24,10 +24,15 @@
 			<el-table-column prop="um" label="单位" width="80" />
 			<!-- 工厂域编码为系统后台归属字段:表单控件已隐藏,列表列一并隐藏以保持口径一致(值仍由后端维护并原样保值)。 -->
 			<el-table-column v-if="false" prop="domain" label="工厂域" width="120" show-overflow-tooltip />
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="220" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -118,6 +123,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgElementParamProduction">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 11 - 4
Web/src/views/aidop/s0/manufacturing/ProductionLineList.vue

@@ -40,13 +40,18 @@
 					<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="220" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleActive(row)">
 						{{ row.isActive ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -182,6 +187,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgProductionLine">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 11 - 4
Web/src/views/aidop/s0/manufacturing/RoutingList.vue

@@ -54,13 +54,18 @@
 					<el-tag :type="row.isConfirm ? 'success' : 'info'" size="small">{{ row.isConfirm ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isEnabled ? 'warning' : 'success'" @click="toggleEnabled(row)">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="220" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isEnabled ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleEnabled(row)">
 						{{ row.isEnabled ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -247,6 +252,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgRouting">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed as __computed_lk, reactive as __reactive_lk } from 'vue';
 import { ensureCurrentOption, loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
 

+ 10 - 3
Web/src/views/aidop/s0/manufacturing/SopFileTypeList.vue

@@ -14,10 +14,15 @@
 		<el-table :data="rows" v-loading="loading" border stripe max-height="calc(100vh - 260px)">
 			<el-table-column prop="imageTypeID" label="文件类型编码" width="160" />
 			<el-table-column prop="descr" label="文件类型描述" min-width="200" show-overflow-tooltip />
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -54,6 +59,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgSopFileType">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/manufacturing/SopMaintenanceList.vue

@@ -20,10 +20,15 @@
 			<el-table-column prop="descr" label="文件名称" width="160" show-overflow-tooltip />
 			<el-table-column prop="rev" label="版本" width="90" />
 			<el-table-column prop="sortID" label="排序" width="80" align="right" />
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -70,6 +75,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgSopDocument">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 11 - 4
Web/src/views/aidop/s0/manufacturing/StandardBomManagement.vue

@@ -36,13 +36,18 @@
 					<el-tag :type="row.isEnabled ? 'success' : 'info'" size="small">{{ row.isEnabled ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isEnabled ? 'warning' : 'success'" @click="toggleEnabled(row)">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="220" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isEnabled ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleEnabled(row)">
 						{{ row.isEnabled ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -179,6 +184,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgStandardBom">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/manufacturing/StandardProcessList.vue

@@ -27,10 +27,15 @@
 			<!-- 工厂域编码为系统后台归属字段:表单控件已隐藏,列表列一并隐藏以保持口径一致(值仍由后端维护并原样保值)。 -->
 			<el-table-column v-if="false" prop="domain" label="工厂域" width="120" show-overflow-tooltip />
 			<!-- 内部主键不出 UI(对齐 D-015):row.id 仍用于编辑/删除,仅去掉展示列 -->
-			<el-table-column label="操作" width="140" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="140" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -90,6 +95,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgStandardOperation">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/manufacturing/WorkCenterList.vue

@@ -26,10 +26,15 @@
 					</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="220" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -85,6 +90,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0MfgWorkCenter">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/quality/components/QualityAggregateCrudPage.vue

@@ -35,10 +35,15 @@
 				:formatter="column.lookup ? (row: any) => resolveLookupLabel(column.lookup!, row[column.prop]) : undefined"
 				show-overflow-tooltip
 			/>
-			<el-table-column label="操作" width="180" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="180" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -127,6 +132,8 @@
 </template>
 
 <script setup lang="ts">
+import S0SourceBadge from '../../components/S0SourceBadge.vue';
+import { isExternalSource } from '../../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/quality/components/QualitySimpleCrudPage.vue

@@ -22,10 +22,15 @@
 				:formatter="column.lookup ? (row: any) => resolveLookupLabel(column.lookup!, row[column.prop]) : undefined"
 				show-overflow-tooltip
 			/>
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -78,6 +83,8 @@
 </template>
 
 <script setup lang="ts">
+import S0SourceBadge from '../../components/S0SourceBadge.vue';
+import { isExternalSource } from '../../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 8 - 1
Web/src/views/aidop/s0/sales/ContractReviewCycleList.vue

@@ -48,7 +48,12 @@
 					</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="240" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
+				<template #default="{ row }">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="240" fixed="right" align="center">
 				<template #default="{ row }">
 					<el-button link type="primary" @click.stop="onStageSelect(row)">查看下钻</el-button>
 					<el-button link type="primary" @click.stop="openEdit(row)">编辑</el-button>
@@ -202,6 +207,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0SalesContractReviewCycle">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type TableInstance } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/sales/CustomerList.vue

@@ -39,13 +39,18 @@
 			<el-table-column prop="contactInfo" label="联系方式" width="140" show-overflow-tooltip />
 			<el-table-column prop="creditRating" label="信用评级" width="100" show-overflow-tooltip />
 			<el-table-column prop="remarks" label="备注" min-width="160" show-overflow-tooltip />
+			<el-table-column label="来源" width="140" show-overflow-tooltip>
+				<template #default="{ row }">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
 			<el-table-column label="操作" width="240" fixed="right" align="center">
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleActive(row)">
 						{{ row.isActive ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -192,6 +197,8 @@ import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import {
 	loadDictOptions,
 	loadOrgList,

+ 10 - 3
Web/src/views/aidop/s0/sales/ItemSubstituteDetailList.vue

@@ -29,10 +29,15 @@
 			<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
 			<!-- 工厂域编码为系统后台归属字段:表单控件已隐藏,列表列一并隐藏以保持口径一致(值仍由后端维护并原样保值)。 -->
 			<el-table-column v-if="false" prop="domain" label="组织" width="110" show-overflow-tooltip />
-			<el-table-column label="操作" width="180" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="180" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -85,6 +90,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0ItemSubstituteDetail">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/sales/MaterialList.vue

@@ -72,13 +72,18 @@
 			<el-table-column prop="drawingDesign" label="图纸设计周期(h)" width="140" show-overflow-tooltip />
 			<el-table-column prop="designOwner" label="设计负责人" width="120" show-overflow-tooltip />
 			<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
+			<el-table-column label="来源" width="140" show-overflow-tooltip>
+				<template #default="{ row }">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
 			<el-table-column label="操作" width="220" fixed="right" align="center">
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleActive(row)">
 						{{ row.isActive ? '停用' : '生效' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -241,6 +246,8 @@ import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import {
 	loadDictOptions,
 	loadOrgList,

+ 11 - 4
Web/src/views/aidop/s0/sales/OrderPriorityRuleList.vue

@@ -52,13 +52,18 @@
 			<el-table-column v-if="false" prop="domainCode" label="工厂域" width="100" class-name="wrap-cell">
 				<template #default="{ row }">{{ row.domainCode || '—' }}</template>
 			</el-table-column>
-			<el-table-column label="操作" width="180" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="180" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleActive(row)">
 						{{ row.isActive ? '停用' : '生效' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -201,6 +206,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0SalesOrderPriorityRule">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 11 - 4
Web/src/views/aidop/s0/sales/OrderReviewCycleList.vue

@@ -44,13 +44,18 @@
 					</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="200" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="200" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleActive(row)">
 						{{ row.isActive ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -137,6 +142,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0SalesOrderReviewCycle">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 11 - 4
Web/src/views/aidop/s0/sales/ProductDesignCycleList.vue

@@ -46,13 +46,18 @@
 					</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="200" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="200" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleActive(row)">
 						{{ row.isActive ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -122,6 +127,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0SalesProductDesignCycle">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 11 - 4
Web/src/views/aidop/s0/supply/CategoryLeadTimeList.vue

@@ -31,13 +31,18 @@
 					<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="220" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="220" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleActive(row)">
 						{{ row.isActive ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -143,6 +148,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0SupplyCategoryLeadTime">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 11 - 4
Web/src/views/aidop/s0/supply/MaterialPlanCycleList.vue

@@ -35,13 +35,18 @@
 					<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="200" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="200" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleActive(row)">
 						{{ row.isActive ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -103,6 +108,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0SupplyMaterialPlanCycle">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/supply/SourcingList.vue

@@ -58,10 +58,15 @@
 					{{ row.isRequireGoods ? '需到货' : '无需到货' }}
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -255,6 +260,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0SupplySourcingItem">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/supply/SupplierList.vue

@@ -64,13 +64,18 @@
 				</template>
 			</el-table-column>
 			<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
+			<el-table-column label="来源" width="140" show-overflow-tooltip>
+				<template #default="{ row }">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
 			<el-table-column label="操作" width="240" fixed="right" align="center">
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link :type="row.isActive ? 'warning' : 'success'" @click="toggleActive(row)">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link :type="row.isActive ? 'warning' : 'success'" :disabled="isExternalSource(row.sourceSystem)" @click="toggleActive(row)">
 						{{ row.isActive ? '禁用' : '启用' }}
 					</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -260,6 +265,8 @@ import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import {
 	loadDictOptions,
 	loadOrgList,

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

@@ -20,10 +20,15 @@
 			<el-table-column prop="separator" label="分隔符" width="80" show-overflow-tooltip />
 			<el-table-column prop="firmLength" label="固定长度" width="90" show-overflow-tooltip />
 			<el-table-column prop="createTime" label="创建时间" width="160" show-overflow-tooltip />
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -81,6 +86,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhBarcodeRule">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { reactive as __reactive_lk } from 'vue';
 import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
 // 关系字段下拉:复用既有列表/options 接口,模块级缓存,每 key 每页仅取一次(无 N+1)。

+ 10 - 3
Web/src/views/aidop/s0/warehouse/CostCenterList.vue

@@ -26,10 +26,15 @@
 			<el-table-column label="开始生效日期" width="150" show-overflow-tooltip>
 				<template #default="{ row }">{{ fmtDate(row.effTime) }}</template>
 			</el-table-column>
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -68,6 +73,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhCostCenter">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/warehouse/DepartmentList.vue

@@ -26,10 +26,15 @@
 				</template>
 			</el-table-column>
 			<el-table-column prop="createTime" label="创建时间" width="160" show-overflow-tooltip />
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -74,6 +79,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhDepartment">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/warehouse/EmpWorkDutyList.vue

@@ -36,10 +36,15 @@
 			<el-table-column prop="locationName" label="库位" min-width="150" show-overflow-tooltip />
 			<el-table-column prop="lineName" label="生产线" min-width="130" show-overflow-tooltip />
 			<el-table-column prop="dutyDisplay" label="职责" min-width="160" show-overflow-tooltip />
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -148,6 +153,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhEmpWorkDuty">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch, type Ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 9 - 2
Web/src/views/aidop/s0/warehouse/EmployeeList.vue

@@ -39,10 +39,15 @@
 					<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
+			<el-table-column label="来源" width="140" show-overflow-tooltip>
+				<template #default="{ row }">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
 			<el-table-column label="操作" width="160" fixed="right" align="center">
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -143,6 +148,8 @@
 
 <script setup lang="ts" name="aidopS0WhEmployee">
 import { computed, onMounted, reactive, ref, watch } from 'vue';
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import {
 	applyDefaultOrgScopeForCreate,
 	filterFactoriesForCompany,

+ 10 - 3
Web/src/views/aidop/s0/warehouse/ItemPackList.vue

@@ -32,10 +32,15 @@
 			<el-table-column label="启用" width="80" align="center">
 				<template #default="{ row }"><el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag></template>
 			</el-table-column>
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -88,6 +93,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhItemPack">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { reactive as __reactive_lk } from 'vue';
 import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
 // 物料下拉:复用既有 materials 列表接口,模块级缓存,无 N+1;value 为 ItemNum 业务编码。

+ 10 - 3
Web/src/views/aidop/s0/warehouse/LabelTypeList.vue

@@ -19,10 +19,15 @@
 			<el-table-column prop="class" label="分类" width="120" show-overflow-tooltip />
 			<el-table-column prop="inputString" label="输入字符串" min-width="300" show-overflow-tooltip />
 			<el-table-column prop="createTime" label="创建时间" width="160" show-overflow-tooltip />
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -59,6 +64,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhLabelType">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 9 - 2
Web/src/views/aidop/s0/warehouse/LocationList.vue

@@ -31,10 +31,15 @@
 					<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
+			<el-table-column label="来源" width="140" show-overflow-tooltip>
+				<template #default="{ row }">
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
 			<el-table-column label="操作" width="160" fixed="right" align="center">
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -111,6 +116,8 @@ import { computed, onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
 import AidopDemoShell from '../../components/AidopDemoShell.vue';
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { s0LocationsApi, type S0LocationRow, type S0LocationShelfInput, type S0LocationUpsert } from '../api/s0WarehouseApi';
 
 const MAX_SHELVES = 5000;

+ 10 - 3
Web/src/views/aidop/s0/warehouse/NbrControlList.vue

@@ -29,10 +29,15 @@
 			<el-table-column label="允许手动" width="90" align="center">
 				<template #default="{ row }"><el-tag :type="row.allowManual ? 'success' : 'info'" size="small">{{ row.allowManual ? '是' : '否' }}</el-tag></template>
 			</el-table-column>
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -85,6 +90,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhNbrControl">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/warehouse/NbrTypeList.vue

@@ -31,10 +31,15 @@
 					<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '是' : '否' }}</el-tag>
 				</template>
 			</el-table-column>
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -73,6 +78,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhNbrType">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';

+ 10 - 3
Web/src/views/aidop/s0/warehouse/TaskAssignmentList.vue

@@ -30,10 +30,15 @@
 			<el-table-column prop="xqsj" label="需求时间" width="120" show-overflow-tooltip />
 			<el-table-column prop="clr" label="处理人" width="100" show-overflow-tooltip />
 			<el-table-column prop="zt" label="状态" width="90" show-overflow-tooltip />
-			<el-table-column label="操作" width="160" fixed="right" align="center">
+						<el-table-column label="来源" width="140" show-overflow-tooltip>
 				<template #default="{ row }">
-					<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
-					<el-button link type="danger" @click="onDelete(row)">删除</el-button>
+					<S0SourceBadge :source-system="row.sourceSystem" />
+				</template>
+			</el-table-column>
+<el-table-column label="操作" width="160" fixed="right" align="center">
+				<template #default="{ row }">
+					<el-button link type="primary" :disabled="isExternalSource(row.sourceSystem)" @click="openEdit(row)">编辑</el-button>
+					<el-button link type="danger" :disabled="isExternalSource(row.sourceSystem)" @click="onDelete(row)">删除</el-button>
 				</template>
 			</el-table-column>
 		</el-table>
@@ -95,6 +100,8 @@
 </template>
 
 <script setup lang="ts" name="aidopS0WhTaskAssignment">
+import S0SourceBadge from '../components/S0SourceBadge.vue';
+import { isExternalSource } from '../components/s0ExternalSource';
 import { reactive as __reactive_lk } from 'vue';
 import { loadS0Lookup, type S0LookupOption } from '../composables/useS0BusinessLookups';
 // 关系字段下拉:复用既有列表/options 接口,模块级缓存,每 key 每页仅取一次(无 N+1)。

+ 11 - 2
Web/src/views/dashboard/components/HomeModuleKpiCard.vue

@@ -39,6 +39,13 @@ const apiItems = ref([])
 const layoutPattern = ref(DEFAULT_STYLE_CODE)
 const loading = ref(false)
 
+function fmtResult(item, unit) {
+  const status = String(item?.resultStatus || '')
+  if (status === 'SOURCE_UNSUPPORTED') return '数据源不支持'
+  if (status === 'NO_DATA') return '无业务数据'
+  return fmtValue(item?.currentValue, unit)
+}
+
 function fmtValue(v, unit) {
   if (v == null || v === '') return '—'
   const n = Number(v)
@@ -77,15 +84,17 @@ function mapItem(item) {
   const lv = String(item.achievementLevel || 'green')
   const trendTone = lv === 'red' ? 'bad' : lv === 'yellow' ? 'warn' : 'good'
   const unit = item.unit ?? ''
-  const ft =
+  const formula =
     item.formulaText != null && String(item.formulaText).trim()
       ? String(item.formulaText).trim()
       : ''
+  const reason = String(item?.resultReason || '').trim()
+  const ft = reason ? (formula ? `${formula}\n${reason}` : reason) : formula
   return {
     key: item.rowId || item.metricCode,
     label: item.displayName || item.metricCode,
     formulaText: ft,
-    value: fmtValue(item.currentValue, unit),
+    value: fmtResult(item, unit),
     unit,
     targetDisplay: fmtTargetDisplay(item.targetValue, unit),
     trend: item.gapLabel || '—',

+ 1 - 1
Web/src/views/dashboard/home.vue

@@ -294,7 +294,7 @@ const displayS8Alarms = computed(() => {
 function onDashboardQuery() {
   loadS8Data()
   ElMessage.success(
-    `已查询:${summarizeSmartOpsBaseQuery(dashboardBaseQuery.value)}。KPI 按日期/产品/订单/产线实际切片;无该维度数据的指标显示「—」。S8 报警与趋势同步过滤。`
+    `已查询:${summarizeSmartOpsBaseQuery(dashboardBaseQuery.value)}。KPI 按日期/产品/订单/产线实际切片;无业务数据显示「无业务数据」,来源不支持显示「数据源不支持」。S8 报警与趋势同步过滤。`
   )
 }
 

+ 2 - 0
doc/README.md

@@ -73,6 +73,7 @@
 | [plan/第三方标准API推数入站方案.md](./plan/第三方标准API推数入站方案.md) | **方式丙终态**:`POST /api/mdp/inbound/{entityCode}`(不带 `/v1/`);五层(身份/契约/摄取/规范/发布);UAT-INT-004 |
 | [plan/S8/S8数据库处理优化方案.md](./plan/S8/S8数据库处理优化方案.md) | S8 调度、检测日志、通知扫描与数据库写入压力优化方案 |
 | [plan/aidopdev_165库容量与增量写入分析_20260716.md](./plan/aidopdev_165库容量与增量写入分析_20260716.md) | 165/`aidopdev` 库容量快照、Top 表、调度增量写入与治理建议(2026-07-16) |
+| [plan/165-aidopdev连接耗尽根治执行任务书.md](./plan/165-aidopdev连接耗尽根治执行任务书.md) | **可派发**:2026-09-24/25 `aidopdev` 1040 根治(定时全量改跳过窗口+夜间串行、贴源批量、MySQL 护栏),含执行机数据库化指派与超管跨租户 ETL 管理页(包 B/G)。禁止把抬 `max_connections` 当交付 |
 | [plan/新系统数据库简明介绍与S1数据流.md](./plan/新系统数据库简明介绍与S1数据流.md) | 新系统数据库简明说明,并以 S1 为例说明数据中台流转 |
 | [plan/Ai-DOP项目待办清单.md](./plan/Ai-DOP项目待办清单.md) | **项目级待办统一入口**(专题子文档见 §2 注册表) |
 | [plan/Ai-DOP剩余待办统一执行任务书_20260814.md](./plan/Ai-DOP剩余待办统一执行任务书_20260814.md) | **可派发总任务书**:基于 server `1.0.340` / Web `2.4.293` 核对 P-001~P-037,将 29 个未完成项整理为 16 个代码包、8 个验收包、5 个决策/文档包;含依赖顺序、代码入口、步骤、验收、跨模块影响及新会话复制提示词 |
@@ -120,6 +121,7 @@
 
 ## 变更记录
 
+- **2026-09-25**:新增 [plan/165-aidopdev连接耗尽根治执行任务书.md](./plan/165-aidopdev连接耗尽根治执行任务书.md),供其他模型按包 A–G 执行 1040 根治。同日修订:更正根因权重(多实例为次要因素,定时全量设计量级才是主因);包 B 扩写为执行机注册/人工指派/领取过滤;新增包 G 超管跨租户 ETL 管理。
 - **2026-09-06**:重写 [plan/第三方标准API推数入站方案.md](./plan/第三方标准API推数入站方案.md) 为五层终态(否定只贴源/另案回灌);UAT-INT-004 待确认立项。
 - **2026-09-06**:新增 [plan/AIDOP标准与非标双模BOM升级方案.md](./plan/AIDOP标准与非标双模BOM升级方案.md)(标准/非标双模 + 三轨收口,非最小改动)。
 - **2026-09-06**:新增 [plan/S0未走数据中台的问题.md](./plan/S0未走数据中台的问题.md);内部 UAT 待办增补 `UAT-INT-006`,并在 `UAT-INT-003` / `UAT-INT-005` 加交叉提示。

+ 57 - 0
doc/db/mdp/contract_tests/e2e_mode3_full.py

@@ -0,0 +1,57 @@
+"""模式 3 样本闭环:Mock 端点可读,且不触碰 WMS_API / TEST_INBOUND_ERP。
+
+宿主未启动时本脚本只验证样本与 Mock,不把未跑通的推数记为通过。
+用法:先在 doc/db/mdp/mock_api 启动 mock_server(8018),再执行本脚本。
+"""
+from __future__ import annotations
+
+import json
+import sys
+import urllib.request
+from pathlib import Path
+
+MOCK = "http://127.0.0.1:8018"
+TOKEN = "uat-mock-token"
+ROOT = Path(__file__).resolve().parents[1] / "mock_api"
+PATHS = [
+    "/api/inventory-txn",
+    "/api/inventory-balance-monthly",
+    "/api/fqc-task",
+    "/api/work-order-bom",
+    "/api/mes/report",
+    "/api/sales-order",
+    "/api/item",
+]
+
+
+def main() -> int:
+    endpoints = json.loads((ROOT / "endpoints.json").read_text(encoding="utf-8"))
+    missing = [p for p in PATHS if p not in endpoints]
+    if missing:
+        print("endpoints missing:", ", ".join(missing))
+        return 1
+    ok = 0
+    for path in PATHS:
+        req = urllib.request.Request(
+            MOCK + path,
+            headers={"Authorization": f"Bearer {TOKEN}"},
+        )
+        try:
+            with urllib.request.urlopen(req, timeout=5) as resp:
+                body = json.loads(resp.read().decode("utf-8"))
+        except Exception as ex:
+            print(f"FAIL {path}: {ex}")
+            print("Mock 未启动或样本不可读。推数入中立层需要宿主,本次不记为通过。")
+            return 2
+        rows = body.get("data", {}).get("list")
+        if not isinstance(rows, list) or not rows or "bizKey" not in rows[0]:
+            print(f"FAIL {path}: data.list 无 bizKey")
+            return 1
+        ok += 1
+        print(f"OK {path} rows={len(rows)}")
+    print(f"mock samples {ok}/{len(PATHS)}. 宿主推数闭环未在本脚本内执行。")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 5 - 1
doc/db/mdp/mock_api/endpoints.json

@@ -47,5 +47,9 @@
   "/api/s3-work-order": "samples/s3_work_order.json",
   "/api/s3-work-order-detail": "samples/s3_work_order_detail.json",
   "/api/s3-work-order-routing": "samples/s3_work_order_routing.json",
-  "/api/mes/report": "samples/mes_report.json"
+  "/api/mes/report": "samples/mes_report.json",
+  "/api/inventory-txn": "samples/inventory_txn.json",
+  "/api/inventory-balance-monthly": "samples/inventory_balance_monthly.json",
+  "/api/fqc-task": "samples/fqc_task.json",
+  "/api/work-order-bom": "samples/work_order_bom.json"
 }

+ 9 - 0
doc/db/mdp/mock_api/samples/fqc_task.json

@@ -0,0 +1,9 @@
+[
+  {
+    "bizKey": "MOCK3-FQC-1",
+    "billNo": "FQC-1",
+    "materialCode": "FG-CT-001",
+    "qty": 5,
+    "inspectStartTime": "2026-09-20T08:00:00"
+  }
+]

+ 10 - 0
doc/db/mdp/mock_api/samples/inventory_balance_monthly.json

@@ -0,0 +1,10 @@
+[
+  {
+    "bizKey": "MOCK3-BAL-202609-RM-CT-001",
+    "periodYm": "202609",
+    "itemCode": "RM-CT-001",
+    "warehouseCode": "A",
+    "avgBalanceAmount": 100,
+    "issueCostAmount": 40
+  }
+]

+ 17 - 0
doc/db/mdp/mock_api/samples/inventory_txn.json

@@ -0,0 +1,17 @@
+[
+  {
+    "bizKey": "MOCK3-INV-1",
+    "transType": "PUR_RECEIPT",
+    "bizDocType": "PUR_RECEIPT",
+    "itemNum": "RM-CT-001",
+    "qtyChange": 10,
+    "approvedFlag": 1,
+    "voidFlag": 0,
+    "summaryFlag": 0,
+    "effDate": "2026-09-20",
+    "location": "A-01",
+    "lotSerial": "LOT1",
+    "createUser": "mock",
+    "endBalance": 10
+  }
+]

+ 8 - 0
doc/db/mdp/mock_api/samples/work_order_bom.json

@@ -0,0 +1,8 @@
+[
+  {
+    "bizKey": "MOCK3-BOM-1",
+    "orderNo": "WO-1",
+    "componentItem": "RM-CT-001",
+    "qtyPer": 2
+  }
+]

+ 101 - 0
doc/db/mdp/mock_api/seed_mock_db.py

@@ -0,0 +1,101 @@
+"""登记模式 3 的三条 Mock 来源。不修改 WMS_API、TEST_INBOUND_ERP。
+
+连接串从 server/Admin.NET.Application/Configuration/Database.json 的默认库读取,不打印密码。
+只插入 aidopdev.mdp_source 中 system_code=MOCK_MODE3 的三条;库 aidop_mock_mode3 仅在有建库权限时创建。
+"""
+from __future__ import annotations
+
+import sys
+from datetime import datetime
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[4]
+DB_JSON = ROOT / "server" / "Admin.NET.Application" / "Configuration" / "Database.json"
+
+
+def connect(_database: str = "aidopdev"):
+    import importlib.util
+
+    helper = ROOT / "doc" / "plan" / "sql" / "uat-data" / "apply_sql_file.py"
+    spec = importlib.util.spec_from_file_location("apply_sql_file", helper)
+    mod = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(mod)
+    return mod.connect()
+
+
+def main() -> int:
+    conn = connect("aidopdev")
+    try:
+        with conn.cursor() as cur:
+            cur.execute(
+                """
+                SELECT COLUMN_NAME, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, DATA_TYPE
+                FROM information_schema.COLUMNS
+                WHERE TABLE_SCHEMA='aidopdev' AND TABLE_NAME='mdp_source'
+                """
+            )
+            meta = list(cur.fetchall())
+            cols = {r["COLUMN_NAME"] for r in meta}
+            needed = {"source_code", "system_code"}
+            if not needed <= cols:
+                print("mdp_source missing", needed - cols)
+                return 1
+            sources = [
+                ("MOCK_MODE3_DB", "MOCK_MODE3", "aidop_mock_mode3"),
+                ("MOCK_MODE3_API", "MOCK_MODE3", "http://127.0.0.1:8018"),
+                ("MOCK_MODE3_INBOUND", "MOCK_MODE3", "API_INBOUND"),
+            ]
+            for code, system, hint in sources:
+                cur.execute(
+                    "SELECT COUNT(*) AS c FROM mdp_source WHERE source_code=%s",
+                    (code,),
+                )
+                if cur.fetchone()["c"]:
+                    print("exists", code)
+                    continue
+                values = {
+                    "source_code": code,
+                    "system_code": system,
+                    "source_name": code,
+                    "tenant_id": 0,
+                    "status": 1,
+                }
+                if "conn_hint" in cols:
+                    values["conn_hint"] = hint
+                names = []
+                vals = []
+                for col in meta:
+                    name = col["COLUMN_NAME"]
+                    if "auto_increment" in (col["EXTRA"] or ""):
+                        continue
+                    if name in values:
+                        names.append(name)
+                        vals.append(values[name])
+                        continue
+                    if col["IS_NULLABLE"] == "NO" and col["COLUMN_DEFAULT"] is None:
+                        kind = col["DATA_TYPE"]
+                        names.append(name)
+                        if kind in ("int", "bigint", "tinyint", "decimal"):
+                            vals.append(0)
+                        elif kind in ("datetime", "timestamp", "date"):
+                            vals.append(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
+                        else:
+                            vals.append("")
+                sql = "INSERT INTO mdp_source (" + ",".join(names) + ") VALUES (" + ",".join(["%s"] * len(names)) + ")"
+                cur.execute(sql, vals)
+                print("inserted", code, hint)
+        try:
+            with conn.cursor() as cur:
+                cur.execute(
+                    "CREATE DATABASE IF NOT EXISTS aidop_mock_mode3 DEFAULT CHARACTER SET utf8mb4"
+                )
+            print("database aidop_mock_mode3 ready")
+        except Exception as ex:
+            print("create database skipped:", type(ex).__name__)
+    finally:
+        conn.close()
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 1061 - 0
doc/plan/165-aidopdev连接耗尽根治执行任务书.md

@@ -0,0 +1,1061 @@
+# 165 / aidopdev 连接耗尽根治执行任务书
+
+> **文档用途**:给**其他大模型 / 编码代理**照着执行。根因已在 2026-09-25 取证完毕,**禁止再把「Too many connections」当成连接泄漏去全仓搜 `new SqlSugarClient`**。
+> **状态**:**代码与现场护栏已做完,还不能标「已落地」**。缓冲池 / 连接上限 / 错误日志 / 停止超时已写入 165;执行机注册、AUTO 冷却、批量写入、超管取消已进本机进程。未指派执行机(决策默认 C:谁都不跑 AUTO)。V3 要次日窗口、V7 要指派之后连续两个自然日,这两条现在做不到。
+> **交接时执行方还需要你口头给的两样东西**(不要写进仓库):165 SSH(包 A);以及一句「按任务书从包 X 开始,文件清单列完后不必再等确认,直接改」(否则本仓库规则会让它列完清单就停)。X 取下方进度表里第一个「未开始」的包。
+
+### 当前进度(2026-09-26 更新)
+
+| 包 | 状态 | 说明 |
+|---|---|---|
+| A 现场收口 | **已完成(2026-09-26 01:10)** | 缓冲池 4096MB、连接上限 500、悬挂 RUNNING 423 行收口为 ABORTED、过期 AUTO 队列 0。未重启 mysqld。本机 5005 已于 2026-09-26 02:58 换成含 B/C/D/G 的进程 |
+| B 执行机注册/指派/领取过滤 | **已完成** | 见包 B「完成情况」 |
+| C AUTO 减载 | **已完成** | 冷却窗口、夜间 03:40 兜底、S1–S4 按触发类型增量 |
+| D 贴源写入批量化 | **已完成** | `UpsertBatchAsync`,DB 与 API 两个 executor 已改 |
+| E MySQL 护栏固化 | **已完成** | 文档已写。现场:`log_error_verbosity=2` 已写入 `/etc/my.cnf`(运行值本来就是 2,故未重启);`TimeoutStopSec=90` drop-in 已生效(`TimeoutStopUSec=1min 30s`);`/etc/logrotate.d/mysqld` 已是日切、保留 7 份、`copytruncate` |
+| G 超管跨租户 ETL 管理 | **已完成** | 6 个 admin 接口、协作式取消、前端超管区块 |
+| F 回归与发布 | **能测的已测,未标已落地** | 重启后 20 分钟内没有新的 AUTO 入队(V1)。指派本机后 20 秒内领到 AUTO 并在阶段边界收成 `ABORTED`,随即取消指派,`is_runner` 回到 0(V9)。未登录 401,普通账号 SA001,超管 200(V11)。V12 任务 44747 / 日志 227801。9 月 25 日残留的 24 条 AUTO 已全部 `CANCELLED`(14 条协作式取消,10 条未启动直接关闭,避免以后指派时被重放)。V3、V7 仍要等时间 |
+
+> 后端版本 `1.0.580`(迁移脚本 `1.0.579.sql` 建实例表、`1.0.580.sql` 加重算取消标志)。前端 `2.4.409`。
+> **创建日期**:2026-09-25
+> **适用库**:开发库 MySQL `aidopdev` @ `123.60.180.165:3306`。不是客户生产库。
+> **前置规则**:`best-solution-default.mdc`(根治,不抬上限了事)、`capability-claims-evidence.mdc`(三层取证)、`impact-backtrace.mdc`、`collaboration-scope.mdc`(先清单后改)、`version-bump-on-commit.mdc`。
+> **相关文档**:[`AidopJobGate.cs`](../../server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AidopJobGate.cs)、[`项目验收待办项.md`](./项目验收/项目验收待办项.md) ACC-OPS-001、[`aidopdev_165库容量与增量写入分析_20260716.md`](./aidopdev_165库容量与增量写入分析_20260716.md)、[`UAT留证/2026-08-11-IQC-P0取证/30-多实例抢同库热回读事故.md`](./UAT留证/2026-08-11-IQC-P0取证/30-多实例抢同库热回读事故.md)。
+
+---
+
+## 0. 新会话复制提示词
+
+把下面整段贴进新对话,不要改口径。
+
+```
+请按 doc/plan/165-aidopdev连接耗尽根治执行任务书.md 执行。
+
+已核实、禁止重查或改写的结论:
+1. 1040 的根因不是 SqlSugar 连接泄漏。主库 IsAutoCloseConnection=true、池上限 20。
+2. 根因按权重是三条,勿只抓其中一条:
+   (a) 165 MySQL 长期用默认 innodb_buffer_pool_size=128MB、max_connections=151;
+   (b) 定时全量重算的设计量级本身过大——9 租户 × S1–S4 每小时 + S5–S7 每天 5 次,
+       单日约 1000 次重算、40 净小时,且贴源逐行写。**这一条与后端实例数无关**;
+   (c) 多实例把 aidopdev 当完整 ETL 执行机——真实存在,但 409 去重吸收了大半,
+       入队量只溢出 +2%~+15%,是次要因素。
+   缓冲池被写穿后 InnoDB 卡住,后台 5 秒/1 分钟作业继续要连接,151 个名额被占满。
+   证据与查询见 §3.3,**不要用「单日净时长 > 24 小时」论证多实例**(那是
+   GlobalMaxParallelScopes=2 的正常表现)。
+3. 禁止把「把 max_connections 调到 500 / 1000」当成交付。那是现场止血,不是根治。
+4. 禁止停 Gogs / 旧 Business 8087 / Datart 当主方案。它们是底数(约 40 连接),不是打满原因。
+5. 禁止把 SSH 口令、MySQL 口令写入仓库、任务书补丁或 commit。
+6. 动手前先按任务书 §4 列出本轮文件清单,等我确认后再改代码。
+7. 方案按任务书 §2 目标态做,不得自行降级为「只改 my.cnf」或「只把 Jobs:Enabled 再写一遍 false」。
+8. 2026-09-25 范围已扩大:包 B 升级为「执行机注册 + 人工指派 + 领取过滤」(执行机身份
+   改为数据库驱动,页面可切换、不需重启);新增包 G「超管跨租户 ETL 管理」。
+   其中两条硬约束勿违反:跨租户接口**逐个**要有「非超管被拒」测试;调度管理**不得重写**,
+   系统已有 `Web/src/views/system/job/index.vue` + `SysJobService`,且监控页顶栏**已经有**
+   「去任务调度」按钮指向 `/platform/job`,本包零改动,不要再加一个。
+9. **包 B 已于 2026-09-25 完成**(代码在工作区、尚未 commit),见任务书顶部进度表与包 B
+   的「完成情况」小节。**不要重做包 B**:`AidopJobGate` 已改造,`AdoEtlInstance` /
+   `EtlInstanceRegistrar` / `EtlInstanceStore` / `AidopRunnerState` / `AidopInstanceIdentity`
+   均已存在,`ClaimNextQueuedAsync` 已带 runner 过滤,`1.0.579.sql` 已建表。
+   闸门的执行机身份走**内存投影**(注册器心跳时发布),热路径零查库,**不要再给它加 TTL 查库缓存**。
+10. 已知技术债见 §10,**本轮不处理**,不要顺手改 S3 那个重复监控页。
+11. 仓库里当前有 8 个**既有失败**的契约测试(S0Dim 1 / S1RequirementExamineDwd 5 /
+    S3PurchaseAuthority 1 / S6ProductionInstruction 1),与本任务书无关,**不是你引入的**,
+    本轮也不要顺手修。判断回归时以「失败数是否超过 8」为准。
+
+本轮先做:按任务书 §3 核对应做/已做,从 §5 中进度表标「未开始」的**第一个**包开始(或我指定的包)。
+每个工作包结束时用该书 §6 的验收 SQL/命令重跑,把返回值贴回来。
+```
+
+---
+
+## 1. 已下决策(勿再询问)
+
+| # | 决策 | 含义 |
+|---|---|---|
+| D1 | **一库一台执行机** | 同一个 `aidopdev` 同一时刻只允许 **一台** 进程作为执行机。包 B 完成前靠 `AiDOP:Jobs:Enabled` / `AIDOP_JOBS_ENABLED`(开发机默认 false);**包 B 完成后改由 `ado_etl_instance.is_runner` 人工指派**,唯一性由写入侧保证(置 1 时同库其余行置 0)。 |
+| D2 | **非执行机可以手工重算,不能吃 AUTO 队列** | `ModuleRebuildWorker` 在非执行机上只领取 `trigger_type` 为页面手工(`MANUAL` / 带 `requested_by`)的任务。`AUTO` / `BOOTSTRAP` 一律不领。 |
+| D3 | **AUTO 不再「到点就全租户全量」** | 定时任务改为「有变更才入队 / 距上次 SUCCESS 不足窗口则跳过」,**实现在入队服务层**(`ModuleRebuildService.EnqueueAsync` + `S1DashboardRebuildService.EnqueueAsync`),不逐个改 Job。全量只留夜间串行兜底,且受 `GlobalMaxParallelScopes=1`(开发库)。 |
+| D4 | **现场 4G / 500 是护栏,不是方案** | `/etc/my.cnf` 必须把缓冲池和连接上限写成显式值并做错误日志轮转。但验收看的是「不会再出现单日重算净时长 > 24h」和「非执行机不入队 AUTO」。 |
+| D5 | **残留 RUNNING / 过期 AUTO QUEUED 必须收口** | 9 月 24 日卡住的 run log 与事故队列不是「等它自己结束」。按 §5.A 标记失败或取消,禁止再被 Worker 领去重跑全量。 |
+
+**唯一留给负责人拍板的项**:
+
+| 项 | 选项 |
+|---|---|
+| 执行机落点 | **A.** 165 上单独的 Ai-DOP 进程(须确认没有第二份仍指 aidopdev 的发布实例);**B.** 指定某台开发机;**C.** 暂不指定,aidopdev 只保留手工重算。未选定前按 **C**。 |
+
+> **指派方式已变更(2026-09-25)**:包 B 完成后,执行机不再靠 `AIDOP_JOBS_ENABLED` 环境变量决定,而是由超管在页面(或直接改 `ado_etl_instance.is_runner`)**人工指派**,切换不需重启进程。上表选的是「哪台机器」,不再是「怎么开」。包 B 未完成前,仍按旧的 env 方式,且未选定前保持全关。
+
+---
+
+## 2. 目标态(根治,禁止降级)
+
+1. **配置层**:165 `/etc/my.cnf` 显式包含 `innodb_buffer_pool_size`、`max_connections`、`binlog_expire_logs_seconds`、错误日志轮转;`mysqld` 的 systemd `TimeoutStopSec` 不是 0。仓库里的 `docker/mysql/mysql.cnf` **不是**这台机器的生效文件,执行方不得把它 scp 上去覆盖现场(现场还跑着 Gogs / SQL Server / 旧 Business)。
+2. **代码层**:所有 `IJob` 与轮询 HostedService 的 AUTO 路径都走 `AidopJobGate`。非执行机:**零** AUTO 入队、**零** AUTO 领取。手工重算仍可用。
+3. **调度层**:定时任务不再制造「单日净执行 40–54 小时」的全量队列。增量 / 跳过窗口生效后,`ado_module_dashboard_rebuild_job` 按自然日统计的 SUCCESS 净秒数应低于 8 小时(开发库)。**注意这一条与后端实例数无关**,单后端正式环境同样必须达标(见 §3.3)。
+4. **数据层**:没有超过心跳窗口仍为 `RUNNING` 的 transform / rebuild;事故遗留的 AUTO `QUEUED` 已取消。
+5. **验证层**:有守卫测试能在以后有人新增 Job 却忘了写 `ShouldRun` 时失败。
+6. **可运维层(2026-09-25 新增)**:执行机身份由数据库驱动、人工指派,超管能在页面上看到所有存活后端实例并切换执行机(不重启进程);超管能跨租户查看 ETL 运行状态、触发重算、协作式取消卡住的任务。调度周期管理复用系统已有的「定时任务」页,不另起一套。
+
+### 明确不是目标
+
+- 把 `max_connections` 调到能扛 N 台开发机同时全量重算。
+- 关掉 165 上的 Gogs、旧 `Business.Host` 8087、Datart 来「腾连接」。
+- 把热回读、Outbox、S8 监控从产品里删掉。
+
+---
+
+## 3. 已核实事实(禁止当未知问题重查)
+
+时间均为 **CST**,库内 `datetime` 若带 `Z` 按 UTC+8 换算。
+
+### 3.1 事故时间线
+
+| 时间 | 事实 | 证据 |
+|---|---|---|
+| 2026-09-19 04:38 | 本机 `dotnet run` 拉起 `Admin.NET.Web.Entry`,一直跑到 2026-09-25 | 进程命令行、`5005` 监听 |
+| 2026-09-23 02:21 | 165 磁盘曾写满,MySQL 退出;之后只改了 binlog 过期 | ACC-OPS-001;`/etc/my.cnf.bak-20260923` 仍是默认模板 |
+| 2026-09-23 02:38 | 现场 `my.cnf` 只加了 `binlog_expire_logs_seconds=86400`,**缓冲池/连接数仍是默认** | `/etc/my.cnf.bak-20260925` |
+| 2026-09-23~25 | `mysqld` pid 10745 CPU≈136%,错误日志涨到约 40GB,全是 InnoDB 找不到空闲页 | `ps`、`/var/log/mysqld.log` |
+| 2026-09-24 12:52 | AIDOP 租户 S5 重算失败:`MaterializeInvTransStdAsync` 命令超时 | `logs/20260924_Error.log`;`ado_module_dashboard_rebuild_job` id 44302 |
+| 2026-09-24 14:53 | AIDOP S1 全量 `S1_MDP_FULL_797403760988229_1_20260924145347` 开始写 `mdp_stg_so`,未结束 | `mdp_transform_run_log` id 224198,`status=RUNNING`,`end_time` 空 |
+| 2026-09-24 15:07 | 先 `All pooled connections are in use`(池 20),随后 `Too many connections`(服务器 151) | `logs/20260924_Error.log` |
+| 2026-09-25 05:34 | 本机后端已停;MCP/`root` 仍 1040 | 本会话实测 |
+| 2026-09-25 05:54 | SSH 上 kill `mysqld` + 清空 40GB 错误日志 + 启动 | 现场 journal |
+| 2026-09-25 06:14 | 有人把 `my.cnf` 改成 4G / 500 并再次重启 | 文件 mtime、`@@innodb_buffer_pool_size` / `@@max_connections` |
+
+### 3.2 配置层(事故当时 vs 现在)
+
+| 项 | 事故当时 | 2026-09-25 06:14 之后 |
+|---|---|---|
+| `innodb_buffer_pool_size` | 默认 **128MB**(`my.cnf` 里该行被注释) | **4G** |
+| `max_connections` | 默认 **151**(未写) | **500** |
+| `wait_timeout` | 28800 | 28800 |
+| 主机内存 | 31G,与 SQL Server / Influx / TDengine / 多个 Java·dotnet 共享 | 同左 |
+| 仓库 `docker/mysql/mysql.cnf` | 512M / 1000,**从未装到 165** | 同左 |
+
+### 3.3 数据层(重算量)
+
+`ado_module_dashboard_rebuild_job` 按 `submitted_at` 自然日(库内已是 CST,**不要再做 `CONVERT_TZ`**,否则会整体错位 8 小时)统计:
+
+```sql
+-- 可重跑;逐日跑,整段 GROUP BY 带 TIMESTAMPDIFF 在本库会查询超时
+SELECT ROUND(SUM(TIMESTAMPDIFF(SECOND, started_at, finished_at))/3600,1) AS success_hours,
+       COUNT(*) AS ok_cnt
+FROM ado_module_dashboard_rebuild_job
+WHERE submitted_at >= '2026-09-23 00:00:00' AND submitted_at < '2026-09-24 00:00:00'
+  AND status='SUCCESS';
+```
+
+| 日 | 提交条数 | SUCCESS 条数 | SUCCESS 净小时 |
+|---|---|---|---|
+| 09-22 | 1063 | 1007 | 40.1 |
+| 09-23 | 1165 | 1122 | **54.1** |
+| 09-24 | 789(另 59 QUEUED) | 674 | 28.0(15:07 库挂,故仅 15 小时墙钟) |
+
+**⚠️ 不要把「净时长 > 24 小时」当成多实例的证据。** `GlobalMaxParallelScopes=2` 本来就允许**单实例内部**两个重算并行,40.1 / 24 ≈ 1.67 完全落在单实例的正常范围内。这条推理不成立,本书早期版本写错过,勿再沿用。
+
+多实例的**正确证据**是单个 scope 的 AUTO 成功次数超过小时触发上限:
+
+```sql
+-- 单实例小时任务一天最多 24 次;实测 33 次
+SELECT trigger_type, status, COUNT(*) AS cnt
+FROM ado_module_dashboard_rebuild_job
+WHERE submitted_at >= '2026-09-22 00:00:00' AND submitted_at < '2026-09-23 00:00:00'
+  AND module_code='S1' AND tenant_id=1300000000777
+GROUP BY trigger_type, status;
+-- 返回:AUTO / SUCCESS / 33
+```
+
+**但多实例的实际负载贡献远小于直觉**,因为 `EnqueueAsync` 已有的 409 去重把重复入队几乎全吸收了。按模块看总量:
+
+| 模块 | 9-22 实际入队 | 单实例理论值 | 溢出 |
+|---|---|---|---|
+| S1 / S2 / S3 / S4 | 225 / 248 / 225 / 226 | 216(9 租户 × 24 小时) | +4% ~ +15% |
+| S5 / S6 / S7 | 46 / 47 / 46 | 45(9 租户 × 每天 5 次) | +2% |
+
+**总量 1063,几乎就是一个后端该产生的量(理论 ≈999)。** 这条是本书最重要的结论之一:
+
+> 即使正式环境只有**一个**后端,这套「每小时 × 9 租户 × S1–S4 全量 + 每天 5 次 × S5–S7」的设计照样产生上千次重算、数十净小时。**多实例不是主要负载来源**,减载(工作包 C)和批量写入(工作包 D)才是;执行机闸门(工作包 B)解决的是开发/测试/演示共用 165 的卫生问题。
+
+因此根因按权重排序为:
+
+1. **配置层**:165 的 `innodb_buffer_pool_size` 是默认 128MB(`my.cnf` 里该行被注释掉)
+2. **代码层**:定时全量重算的设计量级本身过大 + 贴源逐行写 → **与后端实例数无关**
+3. **运行面**:多实例——真实存在(上面 33 次),但被去重挡掉大半,属次要因素
+
+> `AidopJobGate` 注释里写的「9 月 22 日约 103 小时」用上面的自然日口径**复现不出来**(实测 40.1),原值多半来自带 `CONVERT_TZ` 的错位分桶;该注释同时把「2–3 个并行」当成多实例证据,也不准确(`GlobalMaxParallelScopes=2` 即可解释)。若本轮顺手修正该注释,请按本节口径改写。
+
+卡住未收口(2026-09-25 上午仍在):
+
+```sql
+-- 应仍能看到这 3 行,除非已按 §5.A 收口
+SELECT id, job_code, tenant_id, status, start_time, end_time
+FROM mdp_transform_run_log
+WHERE status='RUNNING' AND start_time >= '2026-09-24 00:00:00';
+-- 期望事故未收口时:224198 S1 AIDOP、223903 S3 AIDOP、220089 S3 另一租户
+```
+
+大表(供理解为何 128MB 会被写穿,不是要你改表):`mdp_std_inv_trans` ≈ 847MB / 57 万行;`mdp_stg_so` ≈ 126MB;`SysLogOp` ≈ 1.8GB。
+
+### 3.4 代码层(已有 / 缺口)
+
+| 能力 | 结论 | 证据 |
+|---|---|---|
+| 作业闸门 | **代码有**。默认 true;`App.json` 现为 `false` | `AidopJobGate.cs`;`App.json` `AiDOP:Jobs:Enabled` |
+| 闸门覆盖 IJob | **全部 21 个都有**(已逐个核对),入口第一行 `ShouldRun` | `Job/*.cs`:`: IJob` 21 个实现类,`AidopJobGate.ShouldRun` 命中同样 21 个 |
+| 闸门覆盖 Worker | HotWatch / OutboxPushWorker **有**;`ModuleRebuildWorker` **无**(刻意常开) | `AidopJobGate.cs:27-30`;`ModuleRebuildWorker.cs` |
+| S1 旧 Worker | S1 已迁入通用 Worker 后空转 | `S1DashboardRebuildWorker.cs:32-41` |
+| 跨实例领取上限 | **代码有** `GET_LOCK` + `GlobalMaxParallelScopes` | `ModuleRebuildStore.cs:113-134` |
+| 闸门守卫测试 | **无** | `server/` 下 `AidopJobGate` 测试零匹配 |
+| 小时全量入队 | **仍在**,但只有 **S1–S4** 是小时级。四个 Job 均 `[Period(3600000)]`,对 `ListEnabledScopesAsync` 全员 `Enqueue AUTO` | `S1MdpSyncTransformJob.cs:14`、`S2/S3/S4MdpSyncTransformJob.cs` |
+| S5–S7 入队 | **不是小时级**。`S5MdpRefreshJob` 一个 Job 循环入队 S5/S6/S7,每天 5 次。**不存在** `S6MdpSyncTransformJob` / `S7MdpSyncTransformJob` | `S5MdpRefreshJob.cs:16`(`[Cron("0 2,7,12,17,22 * * *")]`)、`:36`(`foreach new[]{"S5","S6","S7"}`) |
+| 入队统一入口 | **有**。S2–S7 与 BOOTSTRAP 全部经 `ModuleRebuildJobRunner.EnqueueEnabledScopesAsync` → `ModuleRebuildService.EnqueueAsync`;**只有 S1 例外**,走 `S1DashboardRebuildService.EnqueueAsync` | `ModuleRebuildJobRunner.cs:8-30`;`S1MdpSyncTransformJob.cs:41` |
+| 入队去重现状 | `EnqueueAsync` 已有 `FindActiveAsync` → 409,**已覆盖 RUNNING/QUEUED**;**缺的只是 SUCCESS 冷却窗口** | `ModuleRebuildService.cs:64-68`、`:90-100` |
+| 贴源写入 | **逐行** `UpsertAsync` | `MdpStagingWriter.cs`;事故 SQL 为单行 `INSERT ... ON DUPLICATE KEY UPDATE` `mdp_stg_so` |
+| 本机 9/19 进程 | 早于闸门写入,**没吃到** `Enabled=false` | 进程启动时间 |
+
+Furion 会把 Job 注解持久化到 `SysJobTrigger`。改代码里的 `[Period]` **不会自动改库里已有触发器**。改频率必须同时改 `SysJobTrigger.Args`,或走作业管理页。
+
+---
+
+## 4. 本轮范围与禁止
+
+### 4.1 做
+
+工作包 **A(现场)→ B(执行机注册/指派/领取过滤)→ C(AUTO 减载)→ D(贴源批量)→ E(MySQL 护栏固化)→ G(超管跨租户 ETL 管理)→ F(回归与发布)**。字母编号只是引用标签,**不代表价值排序**。
+
+> **2026-09-25 范围变更**:负责人新增「超管在页面上查看并管理全部租户 ETL、并可切换执行机」需求。该能力的基础设施与包 B 改同一批文件,故 **B 已扩写**为「执行机注册 + 指派 + 领取过滤」,并新增**包 G** 承载页面与跨租户能力。**G 依赖 B,必须排在 B 之后。**
+
+**按根因权重,真正的优先级是 A → C → D → E → B → F。** 理由见 §3.3:单实例也会产生同样量级的负载,所以 **C + D 是正式环境也必须做的根治项**,而 **B 只解决开发/测试/演示共用 165 的卫生问题,正式环境单后端时形同虚设**。
+
+若时间有限只能做两件事,做 **A + C**,不要做 A + B。**C 不做就不算根治收口。**
+
+> **2026-09-25 08:00**:**B 已完成**(顶部进度表)。故代码侧的实际下一个包是 **C**,包 G 也已解除依赖、可随时开工。
+
+> B 仍然要做,别删。一是开发库现在天天被多实例打;二是将来 Web 层若水平扩容到 2 个副本,每个副本都自带调度器,多实例问题会原样回来,届时 B 是唯一防线。
+
+### 4.2 不做
+
+- 不改 KPI 口径、租户隔离、T8 映射、菜单 FUNC。
+- 不把 `dopdemorq` / SQL Server 当这次故障源去重启。
+- 不在仓库提交 `/etc/my.cnf` 的完整拷贝(含与本机无关的历史注释即可在任务书引用,不要新建含口令的连接串文件)。
+- 不扩大到 ACC-OPS-001 的 binlog / 日志表清理(交叉引用即可,勿并进本次编码包)。
+- ~~不把 `DefaultEnabled` 改成 `false` 却不改执行机启动方式。保持「JSON 开发 false + env 执行机 true」。~~
+  **2026-09-25 作废**:包 B 已把执行机改为数据库驱动 + 人工指派(B-D1~B-D4)。此时「没指派就不跑」是**预期行为**,配置值降级为查库失败时的回退。执行此条旧约束会与包 B 冲突。
+- 不做自动选举、不做心跳超时自动故障转移(B-D1 已明确否决)。
+- 不重写调度管理页(G-D2)。不处理 §10 的技术债。
+
+---
+
+## 5. 工作包
+
+每包结束必须跑该包「验收」,把命令输出贴进回复。未确认前不要跨包大改。
+
+---
+
+### 工作包 A — 现场收口(无代码或仅 SQL)
+
+**目标**:库能稳定连上;事故残留不会被下一个后端领去再跑全量。
+
+**步骤**
+
+1. 确认没有本机/165 上的 Ai-DOP ETL 长进程:
+   - 本机:`5005` 无监听;没有 `Admin.NET.Web.Entry` / `dotnet run --project ...Admin.NET.Web.Entry`。
+   - 165:`ps` 里若有 `dotnet` 指向本仓库发布目录且连 `aidopdev`,列给负责人,**先问再杀**(可能是指定执行机)。
+2. 固化 `my.cnf`(若 06:14 的 4G/500 还在,不要改数值,只补缺失项):
+   - `innodb_buffer_pool_size = 4G`
+   - `max_connections = 500`
+   - `binlog_expire_logs_seconds = 86400`
+   - 增加错误日志轮转:`log_error_verbosity = 2`;用 **logrotate** 切 `/var/log/mysqld.log`(日切,保留 7 天,`copytruncate` 或 `postrotate systemctl reload mysqld`)。禁止再靠人工 `truncate`。
+   - systemd:`systemctl show mysqld -p TimeoutStopUSec` 若为 0,写 drop-in `TimeoutStopSec=90`,避免再次 SIGTERM 永久卡住。
+3. 收口残留(在 aidopdev 上执行,先 SELECT 再 UPDATE,把前后行数写进回复):
+
+```sql
+-- A3-1 查看
+SELECT id, job_code, tenant_id, status, start_time
+FROM mdp_transform_run_log
+WHERE status='RUNNING' AND start_time < NOW() - INTERVAL 2 HOUR;
+
+SELECT id, module_code, tenant_id, trigger_type, status, submitted_at
+FROM ado_module_dashboard_rebuild_job
+WHERE status IN ('QUEUED','RUNNING')
+  AND submitted_at < '2026-09-25 00:00:00';
+
+-- A3-2 收口 transform(仅事故悬挂)。必须用 ABORTED,禁止 FAILED:
+-- FAILED = 算错;CANCELED = S0 业务取消;ABORTED = 宿主/基础设施中断
+-- (TransformRunLogFinalizer.StatusAborted)。列 error_message 已存在。
+UPDATE mdp_transform_run_log
+SET status='ABORTED',
+    end_time=NOW(),
+    error_message=CONCAT(IFNULL(error_message,''), ' [closed] 2026-09-24 connection exhaustion, not running')
+WHERE status='RUNNING'
+  AND start_time < '2026-09-25 00:00:00'
+  AND end_time IS NULL;
+
+-- A3-3 取消事故遗留 AUTO 排队。status/current_stage 用 CANCELLED
+-- (ModuleRebuildStatus.Cancelled / ModuleRebuildStages.Cancelled,已登记)。
+-- 手工 QUEUED(trigger_type='MANUAL' 或 requested_by 非空)不要动。
+UPDATE ado_module_dashboard_rebuild_job
+SET status='CANCELLED',
+    current_stage='CANCELLED',
+    error_message='[closed] 2026-09-24 AUTO backlog after connection exhaustion',
+    finished_at=NOW(),
+    update_time=NOW()
+WHERE status='QUEUED'
+  AND trigger_type='AUTO'
+  AND submitted_at < '2026-09-25 00:00:00';
+```
+
+4. 确认 MySQL 活着:`Threads_connected` < 50;`@@innodb_buffer_pool_size` = 4G;错误日志体积 < 10MB。
+
+**验收**
+
+```sql
+SELECT @@innodb_buffer_pool_size/1024/1024 AS pool_mb, @@max_connections,
+       (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='Threads_connected') AS connected;
+SELECT COUNT(*) AS hung_tf FROM mdp_transform_run_log
+ WHERE status='RUNNING' AND start_time < '2026-09-25 00:00:00';
+SELECT COUNT(*) AS old_auto_q FROM ado_module_dashboard_rebuild_job
+ WHERE status='QUEUED' AND trigger_type='AUTO' AND submitted_at < '2026-09-25 00:00:00';
+```
+
+期望:`pool_mb=4096`,`hung_tf=0`,`old_auto_q=0`。
+
+**本包不做**:改 C#、改调度周期。
+
+---
+
+### 工作包 B — 执行机注册 / 指派 / 领取过滤(后端)✅ 已完成
+
+> **完成情况(2026-09-25 08:00)**:本包**已实施完毕**,代码在工作区但**尚未 commit**。
+> 接手方请先 `git status` 确认下列文件存在,**不要重新实现**;本节正文保留为「已建成什么」的参照,
+> 各小节里的「实现落点」旁注记录了与原计划不同之处。
+>
+> 新增:`Infrastructure/AidopInstanceIdentity.cs`、`Infrastructure/AidopRunnerState.cs`、
+> `Infrastructure/EtlInstanceRegistrar.cs`、`Infrastructure/EtlInstanceStore.cs`、
+> `Entity/SmartOps/AdoEtlInstance.cs`、`UpdateScripts/1.0.579.sql`、
+> `Tests/Infrastructure/AidopJobGateTests.cs`(16 项守卫测试,全绿)。
+> 修改:`Infrastructure/AidopJobGate.cs`(`DefaultEnabled` 由 `true` 改 `false`)、
+> `DataPlatform/MdpRebuild/MdpRebuildContracts.cs`、`ModuleRebuildStore.cs`、`ModuleRebuildWorker.cs`、
+> `Order/MdpMonitorService.cs`、`Admin.NET.Plugin.AiDOP/Startup.cs`、`Admin.NET.Web.Entry.csproj`(→ 1.0.579)。
+>
+> **遗留给下一位**:`ado_etl_instance` 里还没有任何行被指派为执行机,因此当前**所有实例都不跑 AUTO**。
+> 这是 B-D1 下的预期行为,不是故障。指派动作等包 G 的页面,或先手工 `UPDATE ado_etl_instance SET is_runner=1 WHERE instance_id='…'`。
+>
+> **定位(勿误读)**:就「本次 1040 减载」而言,本包是**开发/测试/演示环境的卫生措施**,不是主要手段——按 §3.3,多实例溢出只有 +2%~+15%。**别把它当根治成果汇报**,真正的减载在包 C / D。
+>
+> 但本包在 2026-09-25 已**扩大范围**:负责人要求「页面上可查看并切换哪台后端在跑 ETL」,该能力的基础设施(实例注册 + 指派)正好落在本包要改的同一批文件上,故合并实施,避免两份方案打架。包 G(超管跨租户 ETL 管理页)依赖本包产出。
+
+**目标**
+
+1. 执行机身份从「配置驱动」改为「**数据库驱动 + 人工指派**」,切换无需重启进程。
+2. 非执行机即使队列里还有 AUTO 也领不到,但页面手工重算照常。
+3. 新增 Job 忘写闸门会被测试拦住。
+
+#### B0 已下决策(勿再询问)
+
+| # | 决策 | 含义 |
+|---|---|---|
+| B-D1 | **人工指派,不自动选举** | 库里没有存活指派时,**谁都不跑** ETL。禁止实现「抢锁自荐」或「心跳超时自动转移」。 |
+| B-D2 | **配置降级为回退值** | `AiDOP:Jobs:Enabled` / `AIDOP_JOBS_ENABLED` 不再是真相源,仅在**查库失败**时兜底。 |
+| B-D3 | **`DefaultEnabled` 语义变更** | 原注释说「默认 true 是为了不静默停摆」。在 B-D1 下,「没指派就不跑」是**预期行为**,不是故障。该段注释必须重写,否则后人会按旧语义改回自动开启。 |
+| B-D4 | **部署多一步** | 上线后须人工到 MDP 运行监控页指派执行机。此步写入 §6 验收与部署说明,否则接手人会误判「ETL 坏了」。 |
+
+#### B1 实例注册表(新建)
+
+`server/Plugins/Admin.NET.Plugin.AiDOP/Entity/SmartOps/AdoEtlInstance.cs`
+
+| 列 | 用途 |
+|---|---|
+| `instance_id` | 稳定标识,建议 `机器名:进程ID:启动时间`。唯一索引。 |
+| `machine_name` / `process_id` / `app_version` | 页面展示,便于人认出是哪台 |
+| `started_at` / `last_heartbeat_at` | 判存活。页面按心跳超时(建议 60 秒)标灰 |
+| `is_runner` | **指派位**。由超管在页面写入 |
+
+`server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/EtlInstanceRegistrar.cs`(新,HostedService):启动时 upsert 本实例行(`is_runner` 保持既有值,**首次插入必须为 0**),之后按固定周期(建议 20 秒)更新心跳。
+
+> 指派唯一性由写入侧保证:置某实例 `is_runner=1` 时,同事务内把**同库其余行**置 0。禁止靠前端保证,也无法靠数据库约束——MySQL 无部分索引,表达不了「最多一行为 1」。
+>
+> **实现落点(2026-09-25 已完成)**:该不变式放在 `Infrastructure/EtlInstanceStore.cs` 的 `AssignRunnerAsync`(`UseTranAsync` 内先清后置)。原计划把写入侧划给包 G,实际提前到 B——不变式与它守护的表应当同包交付,G 只需把它接到 HTTP 上。
+
+#### B2 闸门改数据库驱动
+
+`server/Plugins/Admin.NET.Plugin.AiDOP/Infrastructure/AidopJobGate.cs`
+
+`IsRunner` 改为反映数据库里的 `is_runner`,并满足:
+
+- **热路径不得查库**。`ShouldRun` 有 **21 个 `IJob` 调用方 + 2 个 Worker**,`ModuleRebuildWorker` 每 5 秒轮询一次;逐次查库会把闸门本身变成新的连接压力源——那就把本次事故的成因又造一遍。
+- **查库失败必须回退**到 B-D2 的配置值并 warn,**不得抛异常**。`[Period(..., RunOnStart = true)]` 会在启动瞬间触发,此时表可能尚未建好。
+- **注释整体重写**:B-D3 的默认值语义,以及按 §3.3 修正「9 月 22 日约 103 小时」与「2–3 个并行即多实例」两处不准确表述。
+
+> **实现落点(2026-09-25 已完成,优于原计划)**:原方案写的是「闸门自己查库 + TTL 缓存」。实际采用**注册器发布、闸门只读内存投影**:
+> `EtlInstanceRegistrar` 本来就要按心跳周期写库,顺带把 `is_runner` 读回并发布到 `AidopRunnerState`;
+> `AidopJobGate` 读该投影,**热路径零查询**,`AidopJobGate` 得以保持「纯内存计算」这一原有约定不被打破。
+> 新鲜度窗口 `AidopRunnerState.FreshnessWindow=60s` 取心跳周期(20s)的 3 倍:容得下偶发一两次心跳失败,
+> 又保证指派变更最迟一个窗口内生效。过期或查库失败即置「未知」,由闸门回退配置值——
+> **刻意不沿用上一次的指派值**,否则指派被改掉后会出现两台实例同时自认执行机。
+
+#### B3 领取过滤
+
+| 文件 | 做什么 |
+|---|---|
+| `DataPlatform/MdpRebuild/ModuleRebuildStore.cs` | `ClaimNextQueuedAsync` 增加参数 `bool runner`(或 `ClaimMode`)。`runner=false` 时 WHERE 排除 `trigger_type` 为 `AUTO` 与 `BOOTSTRAP` |
+| `DataPlatform/MdpRebuild/ModuleRebuildWorker.cs` | 领取前读 `AidopJobGate.IsRunner` 传入上一行。`FailStaleAsync` 仍可跑(防悬挂)。空闲等待保持 5 秒 |
+| `Order/MdpMonitorService.cs` | 监控输出增加 `claimAuto=IsRunner`,避免页面上看「作业关了却仍在跑 AUTO」 |
+| `Admin.NET.Plugin.AiDOP/Startup.cs` | 注册 `EtlInstanceRegistrar`(与 `MdpOutboxPushWorker` 等 AiDOP 自有 HostedService 同处,不要放 `Admin.NET.Web.Core/Startup.cs`) |
+| `Admin.NET.Web.Entry/UpdateScripts/<新版本>.sql` | 建实例表。**不要改已成功脚本** |
+| `Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj` | 三处版本同号 patch +1 |
+
+**领取语义(必须按此实现,禁止用「非执行机完全不启动 Worker」)**
+
+| 实例 | AUTO / BOOTSTRAP | 页面手工(`MANUAL` 或 `requested_by` 有值) |
+|---|---|---|
+| 执行机 | 领取 | 领取 |
+| 非执行机 | **不领取** | 领取(否则开发机页面「数据重算」会永远 QUEUED) |
+
+#### B4 守卫测试(必须有,不允许「下次再补」)
+
+1. **静态**:扫描 `Admin.NET.Plugin.AiDOP` 里所有 `IJob` 实现类的 `ExecuteAsync` 方法体,断言源码包含 `AidopJobGate.ShouldRun`。例外名单必须空。
+   **注意**:21 个 `IJob` 目前**已全部满足**(§3.4),本测试首次跑就应为绿;首次即红说明你的检索有漏,先查清再动手改 Job。
+2. **领取**:插入一条 AUTO QUEUED + 一条 MANUAL QUEUED;`runner=false` 时只能领到 MANUAL。
+3. **指派唯一性**:连续指派两台,断言库中 `is_runner=1` 恒为 1 行。
+4. **回退**:模拟查库失败,断言 `IsRunner` 回退到配置值且不抛异常。
+
+#### B5 启动可观测性(小改,必须做)
+
+SqlSugar 主库连上后打一条 Information(**不要打连接串**):`AidopJobGate.IsRunner`、本实例 `instance_id`、`SELECT @@max_connections, @@innodb_buffer_pool_size`。找不到现成「启动后钩子」就挂在 `SqlSugarSetup` 初始化成功之后,**只打一次**。
+
+**验收**
+
+- 四项测试全绿。
+- 启动两个后端进程,页面(或直接查表)能看到两行存活实例;只指派其中一台。
+- 插入一条 AUTO QUEUED,等 20 秒:未指派那台不领;指派那台领走。
+- 页面切换指派到另一台,**不重启进程**,最长 15 秒后由新指派方领取 AUTO。
+- 两台都不指派时,AUTO 保持 QUEUED;此时插入 MANUAL,仍应被领走。
+- 日志能看到 `IsRunner` 与现场 `max_connections`。
+
+**跨模块影响**:S1–S7 看板「数据重算」按钮、智慧运营看板共用 `ModuleRebuildWorker`,**手工路径必须原样保留**。定时 AUTO 只应由执行机消费。包 G 依赖本包的实例表与指派接口。
+
+**本包不做**:不做跨租户查询(属包 G);不做自动选举与故障转移(B-D1 已否决);不改调度周期(属包 C)。
+
+---
+
+### 工作包 C — AUTO 减载(根治本体,最高优先)
+
+> **定位**:本包是唯一对「单后端正式环境」同样有效的减载手段。§3.3 已证实:9 个租户 × S1–S4 每小时 + S5–S7 每天 5 次,**一个后端**就产生约 999 次重算 / 40 净小时。包 B 关掉多实例也不会让这个数字下降。
+
+**目标**:让定时任务不再「到点就把全租户 × 全模块灌进队列」,无论有几台执行机。
+
+**已下决策的算法(禁止改成「把 Period 改成 6 小时」了事)**
+
+**改在入队服务层,不要逐个改 Job。** 已核实入队只有两条路径,改这两处即可覆盖 S1–S7 全部 AUTO 与 BOOTSTRAP,且将来新增 Job 不会绕过:
+
+| 实现点 | 覆盖范围 |
+|---|---|
+| `DataPlatform/MdpRebuild/ModuleRebuildService.cs` 的 `EnqueueAsync` | S2/S3/S4(小时)、S5/S6/S7(`S5MdpRefreshJob` 每天 5 次)、`SmartOpsKpiMdpBootstrapJob` 的 BOOTSTRAP——**全部**经 `ModuleRebuildJobRunner.EnqueueEnabledScopesAsync` 汇到这里 |
+| `DataPlatform/S1Refresh/S1DashboardRebuildService.cs` 的 `EnqueueAsync` | 仅 S1(`S1MdpSyncTransformJob.cs:41` 直接调它,不经上面的 Runner) |
+
+> ⚠️ 不要按模块名去找 `S6MdpSyncTransformJob.cs` / `S7MdpSyncTransformJob.cs`,**这两个文件不存在**。S5/S6/S7 由 `S5MdpRefreshJob` 一个类 `foreach` 入队。
+
+#### C0 已核实的代码事实(直接用,不要重查)
+
+| # | 事实 | 证据 |
+|---|---|---|
+| C-F1 | `EnqueueAsync` 签名是 `(string moduleCode, long tenantId, long factoryId, long? requestedBy, string triggerType = "MANUAL", CancellationToken ct = default)`,返回 `(int StatusCode, ModuleRebuildJobAccepted Body)` | `ModuleRebuildService.cs:44-50` |
+| C-F2 | `ModuleRebuildJobAccepted` 有 `Ok / ModuleCode / JobId / Status / Message` 五个字段,定义在 **`ModuleRebuildStore.cs:22-29`**(不在 `MdpRebuildContracts.cs`) | `ModuleRebuildStore.cs:22-29` |
+| C-F3 | 现有 409 去重:`FindActiveAsync` 只查 `QUEUED`/`RUNNING`,命中即 `return (409, Conflict(...))` | `ModuleRebuildService.cs:64-68`、`:308-315`、`ModuleRebuildStore.cs:104-109` |
+| C-F4 | **`IModuleRebuildJobStore` 没有任何按 SUCCESS 过滤的方法。** `GetLatestAsync` 只按 `Id` 降序取最新一条,不看 status。C1 必须新增方法 | `ModuleRebuildStore.cs:60-84`、`:115-119` |
+| C-F5 | S1 在 `UseUnified` 为真时**委托**给 `ModuleRebuildService.EnqueueAsync("S1", ...)`,不走自己那套。`UseUnified = _moduleRebuild != null && _capability.IsEnabled("S1")`,而 `App.json` 里 S1 `Enabled: true` | `S1DashboardRebuildService.cs:44`、`:53-63`、`App.json:109` |
+| C-F6 | 配置读取惯用法是 `Furion.App.GetConfig<T?>("键", true)`,**第二参数必须为 `true`** | `MdpRebuildContracts.cs:288`、`:302`、`:317` |
+| C-F7 | `App.json` 现值:`MaxParallelScopes: 1`、`GlobalMaxParallelScopes: 2` | `App.json:104-108` |
+| C-F8 | 单测这样构造:`new ModuleRebuildService(store, new ModuleRebuildQueue(), new AlwaysOnModuleRebuildCapability())`。**没有 `IConfiguration` 注入**,capability 是手写假实现 | `ModuleRebuildServiceTests.cs:13-14`、`:333-338` |
+| C-F9 | 入队来源共 3 个:`ModuleRebuildJobRunner.EnqueueEnabledScopesAsync`(S2–S7 + BOOTSTRAP,传 `null` 作 `requestedBy`)、`S1MdpSyncTransformJob`(直调 S1 服务,传 `"AUTO"`)、页面控制器(传 `_userManager.UserId`) | `ModuleRebuildJobRunner.cs:23`、`S1MdpSyncTransformJob.cs:33`、`AidopKanbanModuleRefreshController.cs:51` |
+| C-F10 | 两个 AUTO 调用方都只判断 `status == 202`,其余一律记 `skipped`。所以新增的非 202 返回码不会破坏它们 | `ModuleRebuildJobRunner.cs:24-25`、`S1MdpSyncTransformJob.cs:34-35` |
+
+> **因为 C-F5,冷却只要落在 `ModuleRebuildService.EnqueueAsync` 一处,S1–S7 + BOOTSTRAP 全覆盖。**
+> `S1DashboardRebuildService` 里那套独立的 FindActive/409(`:65-110`)是 `UseUnified` 为假时的退路,**本包不要动它**,
+> 但要在 C6 加一条测试钉住「S1 在 unified 下确实走了统一路径」,否则将来有人把 S1 `Enabled` 关掉,冷却就被绕过了。
+
+#### C1 SUCCESS 冷却窗口(先做,成本低、立刻见效)
+
+`EnqueueAsync` 里**已有** `FindActiveAsync` → 409,`RUNNING` / `QUEUED` 的重复提交已经挡住了(C-F3)。**不要重复实现这段**。本次只补 SUCCESS 冷却。
+
+**C1-a 新增常量**(`MdpRebuildContracts.cs`,加在已有的 `ModuleRebuildTriggerType` 类里):
+
+```csharp
+public const string AutoNightly = "AUTO_NIGHTLY";
+```
+
+> 已有的 `IsManualTrigger(triggerType, requestedBy)` 保持不变。注意 `AidopJobGateTests.IsManualTrigger_Semantics` 里
+> **已经有** `AUTO_NIGHTLY` → `false` 的用例,本包不要改动它。
+
+**C1-b 冷却小时数挂到 capability 上**,不要在服务里直接读配置。理由见 C-F8:单测构造 `ModuleRebuildService` 时只能塞假 capability,若直接 `App.GetConfig` 则窗口不可测,C6 的两条核心测试就写不出来。
+
+在 `IModuleRebuildCapability`(`MdpRebuildContracts.cs`)加:
+
+```csharp
+/// <summary>AUTO / BOOTSTRAP 入队的最小间隔小时数。MANUAL 与 AUTO_NIGHTLY 不受此限。</summary>
+int AutoMinIntervalHours { get; }
+```
+
+实现类里按 C-F6 的惯用法:
+
+```csharp
+public int AutoMinIntervalHours
+{
+    get
+    {
+        try
+        {
+            return Math.Clamp(Furion.App.GetConfig<int?>("AiDOP:MdpRebuild:AutoMinIntervalHours", true) ?? 6, 0, 168);
+        }
+        catch { return 6; }
+    }
+}
+```
+
+> 下界取 **0** 是有意的:留一个「关掉冷却」的逃生口,供排障时临时恢复旧行为。
+> 测试用的 `AlwaysOnModuleRebuildCapability` / `DisabledModuleRebuildCapability`(`ModuleRebuildServiceTests.cs:333-338`)
+> 都要补这个属性,否则测试项目编译不过。
+
+**C1-c 新增 store 方法**(因为 C-F4,现在没有)。`IModuleRebuildJobStore` 加:
+
+```csharp
+/// <summary>该 scope 最近一条 SUCCESS,用于 AUTO 冷却判定。没有则返回 null。</summary>
+Task<AdoModuleDashboardRebuildJob> FindLastSuccessAsync(string moduleCode, long tenantId, long factoryId, CancellationToken ct = default);
+```
+
+`ModuleRebuildJobStore` 的实现照抄 `FindActiveAsync`(`ModuleRebuildStore.cs:104-109`)的形状,把状态条件换成 `x.Status == ModuleRebuildStatus.Success`,排序改 `OrderByDescending(x => x.FinishedAt)`。
+**`ModuleRebuildServiceTests.MemoryJobStore`(`:340` 起)也必须实现这个方法**,否则测试项目编译不过——它实现的是接口。
+
+**C1-d 插入冷却判定**。位置:`ModuleRebuildService.cs` 第 **68 行之后、第 70 行 `var now = DateTime.Now;` 之前**,即现有 409 判定通过、正式 insert 之前:
+
+```csharp
+// AUTO / BOOTSTRAP 才冷却。MANUAL 是页面「数据重算」按钮,AUTO_NIGHTLY 是全量兜底,两者都必须放行。
+var normalizedTrigger = string.IsNullOrWhiteSpace(triggerType) ? "MANUAL" : triggerType.Trim().ToUpperInvariant();
+var cooldownHours = _capability.AutoMinIntervalHours;
+if (cooldownHours > 0
+    && !ModuleRebuildTriggerType.IsManualTrigger(normalizedTrigger, requestedBy)
+    && normalizedTrigger != ModuleRebuildTriggerType.AutoNightly)
+{
+    var lastSuccess = await _jobs.FindLastSuccessAsync(scope.ModuleCode, scope.TenantId, scope.FactoryId, ct);
+    if (lastSuccess?.FinishedAt is { } finishedAt
+        && DateTime.Now - finishedAt < TimeSpan.FromHours(cooldownHours))
+    {
+        return (409, new ModuleRebuildJobAccepted
+        {
+            Ok = false,
+            ModuleCode = scope.ModuleCode,
+            JobId = lastSuccess.Id,
+            Status = "COOLDOWN",
+            Message = $"{scope.ModuleCode} 距上次成功重算不足 {cooldownHours} 小时,本次跳过"
+        });
+    }
+}
+```
+
+**为什么用 409 而不是 429 或 200**:409 的调用面已经验证过(C-F10,两个 AUTO 调用方只认 202);换新状态码要重新核对每个调用方与前端拦截器,属于本包范围外的风险。区分靠 `Body.Status = "COOLDOWN"`,它与现有取值 `QUEUED`/`RUNNING`/`DISABLED` 都不冲突。
+
+> ⚠️ **本包最容易踩的回归**:`EnqueueAsync` 同时是页面「数据重算」按钮的入口(缺省 `triggerType = "MANUAL"`,见 C-F1),
+> 把 MANUAL 一起冷却会让手工重算在 6 小时内点不动。C6 必须有守卫测试。
+
+#### C2 夜间串行全量兜底
+
+**新增** `server/Plugins/Admin.NET.Plugin.AiDOP/Job/MdpNightlyFullRebuildJob.cs`,照抄 `S5MdpRefreshJob.cs` 的结构(注解形状、`AidopJobGate.ShouldRun` 首行、`_scopeFactory.CreateScope()`、`ModuleRebuildJobRunner.EnqueueEnabledScopesAsync` 逐模块调用):
+
+```csharp
+[JobDetail("job_mdp_nightly_full_rebuild",
+    Description = "S1–S7 夜间串行全量兜底(穿透 AUTO 冷却窗口)",
+    GroupName = "default",
+    Concurrent = false)]
+[Cron("40 3 * * *",
+    TriggerId = "trigger_mdp_nightly_full_rebuild",
+    Description = "每日 03:40 触发(5 字段:分 时 日 月 周)")]
+```
+
+`ExecuteAsync` 里 `foreach (var module in new[] { "S1", "S2", "S3", "S4", "S5", "S6", "S7" })`,triggerType 传 `ModuleRebuildTriggerType.AutoNightly`。
+
+> **S1 也走 `EnqueueEnabledScopesAsync`**,不要为它单开分支——因为 C-F5,统一路径下 S1 已经归 `ModuleRebuildService` 管。
+
+**为什么是 03:40**:`S0DimAutoRefreshJob` 是 `[Cron("15 * * * *")]`,**每小时**的 :15 都会触发(含 03:15),所以不能用 03:15;`S5MdpRefreshJob` 占 02:00 整点(`S5MdpRefreshJob.cs:14`)。
+
+**「串行」靠 `GlobalMaxParallelScopes = 1` 实现,不靠 Job 自己控制节奏。** 本 Job 一次性把全部 scope 入队即可,`ModuleRebuildWorker` 会按全局并发上限逐个消费。不要写成「一晚只入队一个 scope」——那样 63 个 scope 要 63 天才轮完一圈。
+
+#### C3 AUTO 改增量拉数
+
+> ⚠️ **任务书早期版本在这里指错了文件**。`MdpModuleStagingPuller` **不调用** `MdpStagingWriter`,它只转发给 `MdpSourcePullDispatcher`;
+> 也**不存在** `RunFullAsync` 这个方法(全目录零匹配)。真正要改的是下面 4 个写死 `fullRefresh: true` 的调用点。
+
+**增量能力本身已经建成,不需要新写。** 已核实存在:`MdpEntity.LastCursor` / `LastSyncTo`(`MdpEntity.cs:55-59`)、`MdpPullContext.FullRefresh`(`IMdpSourcePullExecutor.cs:19-20`)、`MdpSyncWindowResolver` 的 INCR 分支(`:43-47`)、`MdpDbPullExecutor` 的 `@cursor` 谓词(`:278-285`)、`MdpApiPullExecutor` 的 `cursor=` 查询参数(`:40-41`)。`ActionEventMdpSyncService` **已经**在用 `fullRefresh: false`(`:84`),是可参照的既有范例。
+
+**要改的 4 处**(每处都是把字面量 `true` 换成按 triggerType 计算的值):
+
+| 文件:行 | 现状 |
+|---|---|
+| `Order/S1MdpSyncTransformService.cs:226` | `fullRefresh: true` |
+| `Production/S2MdpSyncTransformService.cs:136` | `fullRefresh: true` |
+| `Supply/S3MdpSyncTransformService.cs:163` | `fullRefresh: true` |
+| `ProcurementExecution/S4MdpSyncTransformService.cs:113` | `fullRefresh: true` |
+
+**判定规则**(建议放进 `ModuleRebuildTriggerType` 作静态方法,便于单测与复用):
+
+```csharp
+/// <summary>只有小时级 AUTO 走增量;夜间兜底、启动引导、人工重算一律全量。</summary>
+public static bool ShouldPullFull(string triggerType)
+    => !string.Equals(triggerType, Auto, StringComparison.OrdinalIgnoreCase);
+```
+
+triggerType 已经能拿到:`handler.RunAsync(scope, job.TriggerType, ...)`(`ModuleRebuildService.cs:190-192`)把它传给了每个 handler,只需继续往下透传到 `PullEntitiesAsync` 调用点。
+
+> **空游标分页**:`useCursor` 要求 `LastCursor` 非空,空游标不加谓词。只靠这一点会在 `FullRefresh=false` 时不推进 OFFSET,满页就反复读第一页。已在 `MdpSourcePullDispatcher.ShouldPageByOffset` 收口:游标为空或本页没有把游标推前时改走 OFFSET。单测 `EmptyCursor_FallsBackToOffsetUntilCursorAdvances`。没有对整个 scope 做实拉,避免再打满开发库。
+
+**无游标实体**:`MdpDbPullExecutor` 已按 `entity.IncrColumn` 是否配置决定要不要推进游标(`:127-133`)。无 `incr_column` 的实体天然退化为全量,**不要为它们伪造增量、不要写 `WHERE 1=1`**。这类实体靠 C1 的冷却窗口减载,夜间兜底保证完整性。
+
+#### C4 配置改动
+
+`server/Admin.NET.Application/Configuration/App.json` 的 `AiDOP:MdpRebuild` 段(`:104-118`):
+
+- `GlobalMaxParallelScopes`:**2 → 1**(C2 的串行前提)
+- 新增 `"AutoMinIntervalHours": 6`
+
+`MaxParallelScopes` **已经是 1**(C-F7),不需要改。
+
+#### C5 BOOTSTRAP
+
+`SmartOpsKpiMdpBootstrapJob` 是 `[PeriodSeconds(1, MaxNumberOfRuns = 1, RunOnStart = true)]`(`:19`),每次进程启动灌一轮全量,类内**没有**「已初始化就不再跑」的判断(该类零匹配)。
+
+**本包不需要为它单独写逻辑**:它入队时 triggerType 传 `"BOOTSTRAP"`、`requestedBy` 传 `null`(`:65`、`:73-88`),`IsManualTrigger` 判为 false,因此 **C1 的冷却自动覆盖它**——6 小时内反复重启不会再灌全量。请在 C6 用一条测试钉死这个结论,不要只在文档里写。
+
+**SysJobTrigger**:本包按上面的服务层做法**不需要**改 S1–S4 的 `[Period(3600000)]`,因此不涉及改写已有触发器。但如果你或负责人最终仍决定调周期,务必记住 Furion 会把注解持久化:**改代码里的 `[Period]` / `[Cron]` 不会更新库里已存在的 `SysJobTrigger` 行**,必须另写迁移脚本(新版本号,不要改已成功脚本的行尾)`UPDATE SysJobTrigger` 的 `Args`,否则库里仍是旧的 3600000。新增的夜间兜底 Job 是新 `JobDetail`,由 Furion 首次启动时落库,无需手写脚本。
+
+**S0**:已有 `SKIPPED_NO_CHANGE`,保持小时 :15。不要改成全量无校验。确认 `AidopJobGate` 已在入口(已有)。
+
+**S8 / Outbox / HotWatch**:执行机才跑。非执行机已由闸门跳过(`S8WatchSchedulerJob` / `S8TimeoutAutoEscalationJob` / `S8ActiveFlowStuckScanJob` 都在前述 21 个 `ShouldRun` 里)。不要在本包改 S8 业务规则、也不要改 S8 开关。
+
+> 注意:`WatchScheduler.json:30-31` 里 `S8:Scheduler:Enabled` 实测是 **`true`**(代码默认才是 false,见 `S8JobSwitch.cs:55`),`WatchScheduler.Development.json:15-17` 也显式保持 true。所以 S8 目前**只**靠 `AidopJobGate` 挡住。这是工作包 B 必须保住闸门的又一条理由,**不是**让你在本包去关 S8。
+
+#### C 将改文件汇总
+
+| 文件 | 改什么 |
+|---|---|
+| `DataPlatform/MdpRebuild/MdpRebuildContracts.cs` | 加 `ModuleRebuildTriggerType.AutoNightly` 与 `ShouldPullFull`;`IModuleRebuildCapability` 加 `AutoMinIntervalHours` 及其实现 |
+| `DataPlatform/MdpRebuild/ModuleRebuildStore.cs` | 接口与实现各加 `FindLastSuccessAsync` |
+| `DataPlatform/MdpRebuild/ModuleRebuildService.cs` | `EnqueueAsync` 第 68 行后插入冷却判定 |
+| `Order/S1MdpSyncTransformService.cs:226`、`Production/S2…:136`、`Supply/S3…:163`、`ProcurementExecution/S4…:113` | `fullRefresh: true` 改为按 triggerType 计算 |
+| **新增** `Job/MdpNightlyFullRebuildJob.cs` | 03:40 全量兜底 |
+| `Admin.NET.Application/Configuration/App.json:104-118` | `GlobalMaxParallelScopes` 改 1,新增 `AutoMinIntervalHours: 6` |
+| `Tests/DataPlatform/ModuleRebuildServiceTests.cs` | `MemoryJobStore` 补 `FindLastSuccessAsync`,两个假 capability 补 `AutoMinIntervalHours`,加 C6 的 10 条测试 |
+| `Admin.NET.Web.Entry.csproj` | 版本 patch +1(后端有改动) |
+
+**不需要改的**:S1–S4 的 `[Period(3600000)]`、`S5MdpRefreshJob` 的 `[Cron]`、`SmartOpsKpiMdpBootstrapJob`(理由见 C5)、21 个 `IJob` 的闸门调用(包 B 已全覆盖)。
+**不需要写迁移脚本**:本包不改表结构;新 Job 是新 `JobDetail`,Furion 首次启动自动落库。
+
+#### C6 守卫测试(无测试不得交付)
+
+全部写进 `server/Plugins/Admin.NET.Plugin.AiDOP.Tests/DataPlatform/ModuleRebuildServiceTests.cs`,沿用 C-F8 的既有构造方式。测试项目**不连数据库**,所以这些必须都是纯内存或源文本断言。
+
+| 测试名 | 断言 |
+|---|---|
+| `AutoEnqueue_WithinCooldown_ReturnsCooldown` | store 里放一条 6 小时内 `FinishedAt` 的 SUCCESS,`triggerType: "AUTO"` 入队 → 返回 409 且 `Body.Status == "COOLDOWN"`,`store.Rows` 不增加 |
+| `AutoEnqueue_AfterCooldown_IsAccepted` | 同上但 `FinishedAt` 是 7 小时前 → 202 |
+| `ManualEnqueue_IsNeverCooledDown` | 同样放 1 分钟前的 SUCCESS,缺省 triggerType(MANUAL)入队 → **202**。这条守的是 C1-d 的那个回归 |
+| `ManualEnqueue_WithRequestedBy_IsNeverCooledDown` | `triggerType: "AUTO"` 但 `requestedBy: 123` → 202(`IsManualTrigger` 的第二个判据) |
+| `NightlyEnqueue_BypassesCooldown` | 1 分钟前有 SUCCESS,`triggerType: "AUTO_NIGHTLY"` → 202 |
+| `BootstrapEnqueue_IsCooledDown` | 1 分钟前有 SUCCESS,`triggerType: "BOOTSTRAP"` → 409 COOLDOWN。钉死 C5 的结论 |
+| `CooldownZero_DisablesWindow` | capability 返回 `AutoMinIntervalHours = 0` → AUTO 一律 202 |
+| `RunningJob_StillBlocksBeforeCooldownCheck` | 已有 RUNNING 时返回的是 `Status == "RUNNING"` 而非 `"COOLDOWN"`,证明 409 去重仍在冷却之前生效 |
+| `S1Service_DelegatesToUnifiedPath` | 源文本断言:`S1DashboardRebuildService.cs` 中 `UseUnified` 为真时调用了 `_moduleRebuild.EnqueueAsync`。守 C-F5,防止将来关掉 S1 `Enabled` 绕过冷却 |
+| `NightlyJob_UsesNonCollidingCron` | 源文本断言:`MdpNightlyFullRebuildJob.cs` 含 `Cron("40 3 * * *")`,且**不含** `"15 3"` / `"0 2"`。守 C2 的错峰理由 |
+
+**验收**
+
+```sql
+-- 连续观察两个整点后(或单测模拟时钟)
+SELECT DATE(submitted_at) d, trigger_type, COUNT(*) cnt
+FROM ado_module_dashboard_rebuild_job
+WHERE submitted_at >= CURDATE()
+GROUP BY d, trigger_type;
+```
+
+期望:同一 `(module,tenant,factory)` 的 AUTO 在 6 小时内最多 1 条 QUEUED+RUNNING+SUCCESS。开发库单日 SUCCESS 净小时 < 8。
+
+**跨模块影响**:看板数据新鲜度从「每小时必全量」变为「有变更或最多 6 小时 + 夜间全量」。页面手工重算仍立即入队(不受 6 小时窗口限制)。须在监控页 `MdpMonitorService` 上能看出「上次 AUTO / 下次窗口」。
+
+---
+
+### 工作包 D — 贴源写入批量化
+
+**目标**:S1 那种「一行一条 `INSERT ... ON DUPLICATE KEY` + 整行 JSON」不再把 128MB~数 GB 缓冲池打成刷页风暴。
+
+#### D0 已核实的代码事实(直接用,不要重查)
+
+| # | 事实 | 证据 |
+|---|---|---|
+| D-F1 | `MdpStagingWriter` 只有**两个** public 成员:构造函数和 `UpsertAsync`。签名是 `UpsertAsync(MdpSource source, MdpEntity entity, string sourceTable, IDictionary<string, object?> row, string rawJson, string sourceRowId, MdpPullContext ctx)`,返回 `Task<int>` | `MdpStagingWriter.cs:24-28`、`:37-45` |
+| D-F2 | 写入方式是手拼 `INSERT ... ON DUPLICATE KEY UPDATE` + `_db.Ado.ExecuteCommandAsync(sql, parameters)`,参数走 `List<SugarParameter>`。**不用** `Storageable` / `Insertable`(该文件零匹配) | `MdpStagingWriter.cs:142-151` |
+| D-F3 | **`UpsertBatchSize` 在该文件零匹配**,`BatchSize` 也零匹配。任务书早期版本说的「现有 `UpsertBatchSize`」不存在。拉数侧另有 `entity.BatchSize`(默认 5000,executor 里回落 1000),那是**分页页大小**,与写入批量无关,不要混用 | `MdpStagingWriter.cs` 全文;`MdpDbPullExecutor.cs:50`;`MdpEntity.cs:42-43` |
+| D-F4 | 列集合由 `GetTableColumnsAsync` 查 `information_schema` 得到,**已有静态缓存** `TableColumnsCache`,不是每行一次查询。这一条不是性能问题,别去"优化"它 | `MdpStagingWriter.cs:198-213` |
+| D-F5 | `UpsertAsync` **无事务、无显式连接管理**(`UseTran`/`BeginTran`/`IsAutoCloseConnection` 均零匹配),每次调用一条 SQL | `MdpStagingWriter.cs` 全文 |
+| D-F6 | 返回值 0 是有业务含义的:表示"租户无法解析,跳过本行"。有两个调用方**依赖逐行返回值**分支 | `MdpStagingWriter.cs:62-70`;`MdpInboundReceiveService.cs:187-196`;`MdpExcelImportService.cs:219-223` |
+| D-F7 | 已有测试通过调 `internal static` 方法避开数据库(`ValidateTableContract`、`NormalizeFactoryId`、`BuildBizKey`),另有直接读源文件文本做断言的范式 | `MdpStagingWriterContractTests.cs:6-57`;`MdpStagingSoTenantKeyContractTests.cs:15-18` |
+
+#### D1 真正的逐行循环在哪(任务书早期版本指错了文件)
+
+> ⚠️ **`MdpModuleStagingPuller` 不调用 `MdpStagingWriter`。** 它只有一个方法 `PullEntitiesAsync`,
+> 逐个 entityCode 转发给 `_dispatcher.PullAllByEntityCodeAsync`(`MdpModuleStagingPuller.cs:12-46`)。
+> 去那里"按实体攒批"是改不动的。
+
+`UpsertAsync` 全仓共 **5 个调用点,全部在循环内逐行调用**:
+
+| 调用方 | 行 | 循环 | 本包是否改 |
+|---|---|---|---|
+| `DataPlatform/Executors/MdpDbPullExecutor.cs` | 134 | `foreach (DataRow row in table.Rows)` | **改**,这是 S1–S7 定时重算的主路径 |
+| `DataPlatform/Executors/MdpApiPullExecutor.cs` | 70 | `foreach (var item in items)` | **改** |
+| `DataPlatform/Inbound/MdpInboundReceiveService.cs` | 186 | `foreach (var row in accepted)`,外层有 `UseTranAsync` | **不改**,见 D-F6 |
+| `DataPlatform/HotWatch/MdpHotWatchService.cs` | 269 | `foreach (var row in rows)` | **不改**,热点回读单条量小 |
+| `DataPlatform/FileImport/MdpExcelImportService.cs` | 219 | `foreach (var row in rows)` | **不改**,见 D-F6 |
+
+**只改前两个。** 后三个依赖逐行返回值做业务分支(D-F6),改成批量要同步改它们的错误上报语义,属于本包范围外。
+
+#### D2 抽出可测的 SQL 构造
+
+因为 D-F7:测试项目不连库,若 SQL 仍然只在 `UpsertAsync` 内部拼、拼完就执行,批量 SQL 的正确性无法断言。所以先做一步纯重构——把现有 `MdpStagingWriter.cs:84-149` 的拼接逻辑原样抽成:
+
+```csharp
+internal static (string Sql, List<SugarParameter> Parameters) BuildUpsertSql(
+    string table,
+    IReadOnlySet<string> columns,
+    IReadOnlyList<MdpStagingUpsertRow> rows,
+    DateTime now);
+```
+
+`MdpStagingUpsertRow` 是新的内部 record,承载单行已解析好的值(`TenantId` / `FactoryId` / `CompanyId` / `SourceSystem` / `SourceTable` / `SourceRowId` / `BizKey` / `RawJson` / `BatchId`)。
+
+**这一步必须先单独跑通、保证行为零变化**,再做 D3。抽取时参数名要按行号加后缀(`@tenant0`、`@tenant1`…),单行时生成的 SQL 与现在只差参数名。
+
+#### D3 批量写入
+
+`MdpStagingWriter` 新增:
+
+```csharp
+/// <summary>同表、同实体、同批次的多行一次写入。返回与入参等长的逐行结果(0 表示租户无法解析已跳过)。</summary>
+public async Task<IReadOnlyList<int>> UpsertBatchAsync(
+    MdpSource source, MdpEntity entity, string sourceTable,
+    IReadOnlyList<(IDictionary<string, object?> Row, string RawJson, string SourceRowId)> rows,
+    MdpPullContext ctx);
+```
+
+约束:
+
+- **批大小常量放在 writer 里**,不要读配置,也不要复用 `entity.BatchSize`(D-F3):
+  ```csharp
+  /// <summary>单条多值 INSERT 的最大行数。每行约 12 个占位符,200 行约 2400 个,
+  /// 远低于 MySQL 的 65535 上限,同时让单条语句的 redo 量保持可控。</summary>
+  internal const int UpsertBatchRows = 200;
+  ```
+  超过就切成多条语句,**不要**把整批拼成一条。
+- **`UpsertAsync` 保留**,改为委托给 `UpsertBatchAsync` 的单行调用,这样 D1 表里那三个不改的调用方一行都不用动。
+- 租户解析失败的行(D-F6)**在拼 SQL 之前剔除**,并在结果数组对应位置填 0。现有的那条 warning 日志(`MdpStagingWriter.cs:64-69`)逐行保留,不要合并成一条——排障时需要知道具体是哪行。
+- **仍然不加事务**(D-F5)。一批失败就整批抛,由上层 run log 记错。加事务会拉长持锁时间,与本次减载目标相反。
+
+#### D4 改两个 executor 的循环
+
+`MdpDbPullExecutor.cs:110-136`:循环体里原本做的三件事(构造 `dict`、算 `sourceRowId` 与 `rawJson`、**推进游标**)全部保留,只把末尾的 `await _writer.UpsertAsync(...)` 换成攒进一个 `List`,循环结束后调一次 `UpsertBatchAsync`,`written += 结果求和`。
+
+> ⚠️ **游标推进必须留在逐行循环里**(`:120-133`)。那段算的是 `maxCursor` / keyset 游标,
+> 与写入无关;挪到批量之后会算错,且错法很隐蔽——它依赖遍历顺序。
+
+`MdpApiPullExecutor.cs:58-71` 同理,其中 `seen.Add(dedup)` 去重也要留在逐行循环里。
+
+#### D5 守卫测试
+
+| 测试名 | 断言 |
+|---|---|
+| `BuildUpsertSql_SingleRow_MatchesLegacyShape` | 单行生成的 SQL 含 `ON DUPLICATE KEY UPDATE`、`process_status='PENDING'`、`update_time=@now`,列顺序与现有实现一致 |
+| `BuildUpsertSql_ThreeRows_ProducesThreeValueTuples` | 三行 → `VALUES` 后有 3 组括号,参数个数是单行的 3 倍,且参数名互不重复 |
+| `BuildUpsertSql_OmitsColumnsMissingFromTable` | `columns` 不含 `factory_id` 时,生成的 SQL 既不含插入列也不含对应 update 子句 |
+| `UpsertBatchRows_IsWithinMySqlPlaceholderLimit` | `UpsertBatchRows * 12 < 65535` |
+| `DbPullExecutor_KeepsCursorAdvanceInPerRowLoop` | 源文本断言:`MdpDbPullExecutor.cs` 中 `maxCursor` 的赋值仍在 `foreach (DataRow` 块内。守 D4 的那个坑 |
+
+**禁止**:在展示层截断 JSON;按租户特判 AIDOP;给 `UpsertBatchAsync` 加事务。
+
+**验收**:上述测试全绿;在 aidopdev 上手工重算一个 S1 scope,`mdp_transform_run_log` 的 `stage_rows` 与改动前同一 scope 的历史值一致(行数不能因为批量化而变),且该轮耗时下降。
+
+---
+
+### 工作包 E — MySQL 护栏固化(运维文档 + 可选检测)
+
+**目标**:现场配置不会在下次「只改 binlog」时又丢回 128MB/151。
+
+**步骤**
+
+1. 在 [`项目验收待办项.md`](./项目验收/项目验收待办项.md) ACC-OPS-001 增加一小节「MySQL 缓冲池 / 连接数 / 错误日志轮转 / TimeoutStop」,复核命令:
+
+```bash
+mysqld --print-defaults | tr ' ' '\n' | grep -E 'innodb_buffer_pool_size|max_connections|binlog_expire'
+ls -lh /var/log/mysqld.log
+systemctl show mysqld -p TimeoutStopUSec
+```
+
+2. 在 `docker/mysql/mysql.cnf` 头注释写清:**此文件只给 docker 开发套件,165 生效文件是 `/etc/my.cnf`,禁止覆盖。**
+3. (可选,推荐)后端启动检测:若 `@@innodb_buffer_pool_size < 512MB` 且库名 `aidopdev`,打 **Error** 级日志(不要拒绝启动,以免正式小库被误伤;用库名+主机名双重条件)。
+
+**验收**:文档有复核命令;165 上 `print-defaults` 仍含 4G/500;`mysqld.log` 有 logrotate。
+
+---
+
+### 工作包 G — 超管跨租户 ETL 管理(依赖包 B)
+
+> 2026-09-25 负责人新增需求。**先做完包 B 再动本包**,本包直接用 B1 的实例表与指派接口。
+
+**目标**:超级管理员能在**一个**页面上查看并管理全部租户的 ETL:看运行状态、对任意租户触发重算、取消卡住的任务、查看与切换执行机。
+
+#### G0 已下决策(勿再询问)
+
+| # | 决策 | 含义 |
+|---|---|---|
+| G-D1 | **不新增菜单、不新增 FUNC 编号** | 全部挂在现有 `/aidop/data-platform/mdp-monitor`(`FUNC-S3-011`)页上的超管区块。 |
+| G-D2 | **调度管理不重做,而且跳转链接已经有了** | 改周期 / 启停定时任务,系统**已有**完整实现:`Web/src/views/system/job/index.vue` + `SysJobService`。**且监控页已经有「去任务调度」按钮**(`mdpMonitor.vue:6` + `goJob()` `:553`),路由是 **`/platform/job`**(`SysMenuSeedData.cs:163`,**不是** `/system/job`)。本项**已满足,本包零改动**。重写一套即为重复建设,禁止。 |
+| G-D3 | **取消用协作式** | 置 `cancel_requested` 标志,在**阶段边界**检查并中止。**禁止只改数据库状态而进程仍在跑**——那是假取消,会和后续重算抢同一批表。拦截点见 G2,**不在 `ModuleRebuildWorker` 里**。 |
+| G-D4 | **S3 重复页本轮保留** | `s3/supply/mdpMonitorList.vue` + `Supply/S3MdpMonitorService.cs` 与统一页重复(两套组件 + 两套接口共用 `FUNC-S3-011`),负责人决定暂不处理。**记为技术债**,见 §10。 |
+
+#### G0-F 已核实的代码事实(直接用,不要重查)
+
+| # | 事实 | 证据 |
+|---|---|---|
+| G-F1 | `MdpMonitorService` 是 `[ApiDescriptionSettings(Order = 322, ...)] [Route("api/DataPlatform")] [NonUnify]`,实现 `IDynamicApiController, ITransient`。**类里现在一个 `[HttpPost]` 都没有**,9 个全是 GET | `MdpMonitorService.cs:16-19` |
+| G-F2 | 构造已注入 `ISqlSugarClient _db`、**`UserManager _userManager`**、`MdpOutboundGate`、`IModuleRebuildCapability`。超管判定不需要新加依赖 | `MdpMonitorService.cs:65-80` |
+| G-F3 | **`AidopTenantScope.ResolveOrThrow` 对「超管且停留在默认租户」是直接抛异常的**(`throw Oops.Oh("超级管理员操作前必须选择目标租户")`)。所以跨租户接口**绝不能**调它,否则超管永远进不去 | `AidopTenantScope.cs:19-28` |
+| G-F4 | `UserManager.SuperAdmin` 读 JWT 的 `ClaimConst.AccountType` 是否等于 `AccountTypeEnum.SuperAdmin`。Service 里一律**构造注入** `UserManager` | `UserManager.cs:12-44`;`SysCommonService.cs:24-35` |
+| G-F5 | 仓内拒绝非超管的标准写法:`if (!_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.SA001);` | `SysCommonService.cs:119-120` |
+| G-F6 | **`SysTenant` 实体上没有 `Name` 属性。** 取租户显示名的既有做法是左连 `SysOrg`:`Label = SqlFunc.HasValue(u.Title) ? $"{u.Title}-{a.Name}" : a.Name` | `SysTenant.cs` 全文;`SysTenantService.cs:107-115` |
+| G-F7 | `BuildMdpRunLogTenantWhere(tenantId)` 是 `internal static`,返回 `tenantId > 0 ? "(tenant_id = @TenantId OR tenant_id = 0)" : "tenant_id = 0"` | `MdpMonitorService.cs:428-432` |
+| G-F8 | 前端超管判定惯用法是 `userStore.userInfos.accountType == 999`(`AccountTypeEnum.SuperAdmin = 999`),store 来自 `useUserInfo()` | `tenantSelect.vue:1-8`、`:15-16`、`:44-46`;`account-type-enum.ts:16-24` |
+| G-F9 | `mdpMonitor.vue` 共 633 行,用原生 `el-table`(非仓内封装),无 `el-tabs`;**全文检索 `etl` / `runner` 零匹配**,即后端 `GetEtlRunnerStatus` 已存在但前端没接 | `mdpMonitor.vue` 全文;`mdpMonitor.ts` 全文 |
+| G-F10 | `api/mdpMonitor.ts` 共 159 行,导出 5 个 fetch 函数,一律 `service.get<T>('/api/DataPlatform/mdp-monitor/xxx').then(r => r.data)` | `mdpMonitor.ts:1-159` |
+
+#### G1 后端接口
+
+全部加在 `Order/MdpMonitorService.cs`,沿用 G-F1 的类级 `[Route("api/DataPlatform")]`。
+
+| 方法 | 注解 | 说明 |
+|---|---|---|
+| `GetTenantsAsync` | `[HttpGet("mdp-monitor/admin/tenants")]` | 超管专用租户下拉。按 G-F6 左连 `SysOrg` 拼显示名,**不要**去 `SysTenant` 上找 `Name` |
+| `GetAdminListAsync` | `[HttpGet("mdp-monitor/admin/list")]` | 跨租户运行日志。入参加 `long? tenantId`:为 null 或 0 时不加租户条件;否则用 G-F7 的 `BuildMdpRunLogTenantWhere(tenantId.Value)` |
+| `AssignRunnerAsync` | `[HttpPost("mdp-monitor/admin/assign-runner")]` | 指派执行机。**直接调包 B 已建好的 `IEtlInstanceStore.AssignRunnerAsync`**,唯一性不变式已在那里(`EtlInstanceStore.cs`),不要重新实现 |
+| `ClearRunnerAsync` | `[HttpPost("mdp-monitor/admin/clear-runner")]` | 取消指派。调 `IEtlInstanceStore.ClearRunnerAsync` |
+| `RebuildAsync` | `[HttpPost("mdp-monitor/admin/rebuild")]` | 对指定租户触发重算。调 `ModuleRebuildService.EnqueueAsync(moduleCode, tenantId, factoryId, _userManager.UserId, "MANUAL")`。**`requestedBy` 必须传超管的 UserId**——这样包 C 的冷却会按 `IsManualTrigger` 放行,否则超管点了没反应 |
+| `CancelRebuildAsync` | `[HttpPost("mdp-monitor/admin/cancel")]` | 置取消标志,见 G2 |
+
+**每个新方法的第一行都必须是 G-F5 那句拒绝**:
+
+```csharp
+if (!_userManager.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.SA001);
+```
+
+**租户隔离(本包最敏感处)**
+
+- 新接口一律挂在 `mdp-monitor/admin/` 前缀下,与现有 9 个 GET 物理分开。**不要**给现有接口加 `allTenants` 之类的开关参数——那会让「非超管路径」和「超管路径」共用一条代码路径,一旦判定写错就是静默越权。
+- **新接口不得调用 `AidopTenantScope.ResolveOrThrow`**(理由见 G-F3)。
+- 现有 9 个 GET 的行为**一个字节都不许变**:仍然 `ResolveOrThrow`,仍然 `BuildMdpRunLogTenantWhere`。
+- 跨租户是**绕过租户隔离**的操作,G3 的每条越权测试都是硬性门槛,缺一不可交付。
+
+#### G2 协作式取消
+
+> ⚠️ **任务书早期版本说"`ModuleRebuildWorker` 在阶段边界检查",那是错的。**
+> `ModuleRebuildWorker` 里对 stage 的循环**零匹配**——它只是 `ExecuteClaimedAsync` 里取出 handler 然后
+> `await svc.RunClaimedAsync(job, handler, runLock, stoppingToken)`(`ModuleRebuildWorker.cs:129-149`)。
+> 阶段推进发生在各 handler 的 `RunAsync` 内部。
+
+**真正的阶段边界是进度回调**,位于 `ModuleRebuildService.cs:194-198`:
+
+```csharp
+var result = await handler.RunAsync(
+    scope,
+    job.TriggerType,
+    job.Id,
+    async update =>
+    {
+        failedStage = update.Stage;
+        await ApplyProgressAsync(job.Id, scope.ModuleCode, update, CancellationToken.None);
+    },
+    stoppingToken);
+```
+
+**每个 handler 每跨一个阶段都会调这个回调**,所以它是唯一、通用的拦截点——在这里加检查,S1–S7 全部 handler 自动生效,**一个 handler 都不用改**。每个 job 只有 4–5 个阶段(`ModuleRebuildStages.StageTotal`),所以在回调里查一次库完全可接受,不构成连接压力。
+
+**实施**:
+
+1. `Entity/SmartOps/AdoModuleDashboardRebuildJob.cs` 加列:
+   ```csharp
+   [SugarColumn(ColumnName = "cancel_requested")]
+   public bool CancelRequested { get; set; }
+   ```
+   仓库布尔列约定见 `.cursor/rules`:用 `<noun>_flag TINYINT`。此处沿用既有 `ado_module_dashboard_rebuild_job` 表的风格即可,**但要与迁移脚本里的列名逐字一致**。
+2. `ModuleRebuildStore.cs` 加两个方法:`RequestCancelAsync(long jobId, CancellationToken)`(UPDATE 置 1,仅当 `status IN ('QUEUED','RUNNING')`)与 `IsCancelRequestedAsync(long jobId, CancellationToken)`。
+3. `MdpRebuildContracts.cs` 加异常类型:
+   ```csharp
+   public sealed class ModuleRebuildCancelledException : Exception { ... }
+   ```
+4. 在上面那段回调里,`ApplyProgressAsync` **之后**加检查,命中就抛 `ModuleRebuildCancelledException`。
+5. `RunClaimedAsync` 加一个 catch,**必须放在 `catch (Exception ex)`(`:251`)之前**,否则会被吞成 FAILED:
+   ```csharp
+   catch (ModuleRebuildCancelledException)
+   {
+       await FailJobAsync(job, "已由管理员取消", ModuleRebuildStatus.Cancelled, ModuleRebuildStages.Cancelled, failedStage);
+   }
+   ```
+   `ModuleRebuildStatus.Cancelled` 与 `ModuleRebuildStages.Cancelled` **都已存在**(`MdpRebuildContracts.cs:42-48`、`:54-68`),不要新造枚举。上面第 `:247-250` 行已有一个同形状的 `OperationCanceledException` 分支可照抄。
+6. **`mdp_transform_run_log` 的收口**:`TransformRunLogFinalizer.FinalizeIfHostStoppingAsync` 只在 `IsHostStopping` 为真时动手(`:87-91`),管理员取消走不到。需要在该类加一个同胞方法复用它的 `FinalizeSql`(`:52-58`),状态传 `StatusAborted`。**不要把 `FinalizeIfHostStoppingAsync` 的 `IsHostStopping` 判断删掉或放宽**——那会让宿主关停的语义和管理员取消混在一起。
+7. `Admin.NET.Web.Entry/UpdateScripts/<新版本>.sql`:加列。包 B 已占用 `1.0.579`,从 **`1.0.580`** 起。**不要改已成功脚本**。
+
+#### G3 前端
+
+| 文件 | 做什么 |
+|---|---|
+| `Web/src/views/aidop/data-platform/mdpMonitor.vue` | 在现有摘要行(`:9-34`)与查询卡片(`:36-77`)之间插入一个超管区块 `el-card`。内含:租户选择器(含「全部租户」)、实例列表 `el-table`(列:机器名、进程、版本、是否执行机、心跳、存活)+ 每行「设为执行机」按钮、重算与取消的操作入口。用原生 `el-table`,与该页现有风格一致(G-F9) |
+| `Web/src/views/aidop/data-platform/api/mdpMonitor.ts` | 按 G-F10 的形状补 6 个函数,POST 用 `service.post` |
+| `Web/package.json` | `version` patch +1 |
+
+**超管区块的显隐照抄 G-F8**:
+
+```ts
+import { useUserInfo } from '/@/stores/userInfo';
+const userStore = useUserInfo();
+const isSuperAdmin = computed(() => userStore.userInfos.accountType === 999);
+```
+
+模板上用 `v-if="isSuperAdmin"`,**非超管完全不渲染该区块**(不是渲染后禁用)。
+
+> **前端判定只是为了不给普通用户看无用的 UI,不承担鉴权。** 真正的门是 G1 里每个接口第一行那句 `SuperAdmin` 检查。
+> 不要因为前端已经 `v-if` 了就省掉后端判定。
+
+**本区块不加**「去任务调度」按钮——G-D2,它已经在页面顶栏(`mdpMonitor.vue:6`)。
+
+#### G4 守卫测试(必须有)
+
+| 测试名 | 断言 |
+|---|---|
+| `AdminEndpoints_AllCheckSuperAdmin` | 反射或源文本扫描:`MdpMonitorService` 中路由含 `mdp-monitor/admin/` 的每个方法,方法体都含 `SuperAdmin`。**新增接口忘了加判定会被这条拦住**,是本包最重要的一条 |
+| `AdminEndpoints_DoNotCallResolveOrThrow` | 源文本扫描:`admin/` 方法体内不含 `ResolveOrThrow`。守 G-F3 |
+| `LegacyEndpoints_StillCallResolveOrThrow` | 源文本扫描:原有 6 个调 `ResolveOrThrow` 的方法(`:86`、`:106`、`:198`、`:218`、`:245`、`:267`)一个都没少。守「不误伤原语义」 |
+| `BuildMdpRunLogTenantWhere_Unchanged` | 直接调 `internal static`,断言 `tenantId > 0` 与 `= 0` 两种返回值逐字不变 |
+| `CancelCheck_IsInProgressCallback_NotWorker` | 源文本断言:`ModuleRebuildService.cs` 的进度回调里含取消检查;`ModuleRebuildWorker.cs` **不含**。守 G2 那个坑 |
+| `CancelledCatch_PrecedesGenericCatch` | 源文本断言:`ModuleRebuildCancelledException` 的 catch 出现位置早于 `catch (Exception ex)`。顺序写反会静默变成 FAILED |
+| `AssignRunner_ReusesPackageBStore` | 源文本断言:`MdpMonitorService` 调的是 `IEtlInstanceStore.AssignRunnerAsync`,没有自己写 `is_runner` 的 UPDATE。守唯一性不变式不被绕过 |
+
+**验收**:上述测试全绿;用超管与普通租户账号各走一遍页面;取消一个正在跑的重算,观察它在下个阶段边界停止(`ado_module_dashboard_rebuild_job.status` 变 `CANCELLED`、`mdp_transform_run_log.status` 变 `ABORTED`)且不再写该批次的表。
+
+**本包不做**:不重做调度管理、不再加跳转按钮(G-D2);不动 S3 重复页(G-D4);不改 KPI 口径与 T8 映射;不新增菜单与 FUNC。
+
+---
+
+### 工作包 F — 回归与发布
+
+1. 后端全量测试:
+   ```
+   dotnet test server/Plugins/Admin.NET.Plugin.AiDOP.Tests/Admin.NET.Plugin.AiDOP.Tests.csproj -f net10.0 --nologo -v q
+   ```
+   **基线是「失败 8、通过 3088」**(包 B 完成时的实测值)。那 8 个是与本任务书无关的既有失败,见 §0 第 11 条。
+   本任务书每完成一个包,通过数应上升、**失败数不得超过 8**。
+   覆盖面:`AidopJobGate` 扫描 / 领取过滤 / 指派唯一性 / 回退(包 B,已绿)、入队冷却窗口(包 C)、批量写入 SQL(包 D)、跨租户越权(包 G)。
+
+   > PowerShell 下不要用 `| Select-String "失败"` 过滤输出:本仓 `dotnet test` 的中文输出在 PS 管道里会乱码,过滤会匹配不到而误判为「无输出」。直接看尾部几行。
+2. 提交:只含后端则只升 `Admin.NET.Web.Entry.csproj` 三处版本 patch;含前端则同时升 `Web/package.json`。
+3. **重启所有**仍连 aidopdev 的旧进程。9 月 19 日那种挂着的进程不重启等于没修,且不会注册进实例表。
+4. **人工指派执行机**(B-D4):上线后到 MDP 运行监控页指派一台。**不指派就没有任何实例跑 ETL,这是预期行为,不是故障。**
+
+---
+
+## 6. 总验收(根治收口条件)
+
+全部为真才可把本任务书标成「已落地」:
+
+> **编号用 `V`(验收),不要与工作包 G 的 `G1`–`G4` 混淆。** 下表每行末尾标注归属工作包。
+
+| # | 条件 | 取证 | 归属 |
+|---|---|---|---|
+| V1 | 非执行机 20 分钟内 AUTO 入队数 = 0 | `mdp_transform_run_log` / rebuild job,`trigger_type='AUTO'`,本机启动后 | B |
+| V2 | 非执行机领不到 AUTO QUEUED | 包 B 测试 + 手工插库 | B |
+| V3 | 开发库 6 小时窗口生效 | 包 C 测试 + 次日 SQL | C |
+| V4 | 现场缓冲池 ≥ 4G,错误日志按日轮转,停止超时非 0 | SSH / `@@` | A/E |
+| V5 | 无 9/24 悬挂 RUNNING、无 9/24 AUTO QUEUED | 包 A SQL | A |
+| V6 | 所有 IJob 带 `ShouldRun` | 静态测试(`AidopJobGateTests.EveryJob_CallsGate`,**已绿**) | B ✅ |
+| V7 | 单日 SUCCESS 净小时 < 8(开发库、执行机已指派之后连续 2 个自然日) | 与 §3.3 同口径 SQL | C+D |
+| V8 | 实例表能看到全部存活后端,`is_runner=1` 恒为 1 行或 0 行 | 查 `ado_etl_instance` | B |
+| V9 | 切换执行机后**不重启进程**,**60 秒内**新指派方开始领 AUTO | 包 B 验收步骤 | B |
+| V10 | 未指派时 AUTO 全部滞留 QUEUED,但 MANUAL 仍被领走 | 包 B 验收步骤 | B |
+| V11 | 非超管调用任一跨租户接口均被拒 | 包 G 测试 | G |
+| V12 | 取消一个 RUNNING 重算后在阶段边界真停:`ado_module_dashboard_rebuild_job.status='CANCELLED'` **且** `mdp_transform_run_log.status='ABORTED'` | 包 G 测试 + 实跑 | G |
+| V13 | 部署说明里写明「上线后须人工指派执行机」 | 文档 | B/G |
+
+> **V9 为什么是 60 秒**:包 B 的实际实现是心跳 20 秒、内存投影新鲜度窗口 60 秒(`AidopRunnerState.FreshnessWindow`)。
+> 最坏情况下指派变更要等一个完整窗口才被对端看到。早期版本写的「15 秒」对应的是已被放弃的 TTL 查库方案。
+>
+> **V12 为什么是两个状态**:重算任务本身走 `ModuleRebuildStatus.Cancelled`(业务取消),
+> 它驱动的那轮 transform run log 走 `TransformRunLogFinalizer.StatusAborted`(执行被中断)。
+> 两张表语义不同,**不要统一成一个值**。
+
+---
+
+## 7. 反向影响推演
+
+| 面 | 结论 |
+|---|---|
+| 代码调用面 | **包 B(已完成)**:`ModuleRebuildStore.ClaimNextQueuedAsync` 加了 `runner` 参数,唯一调用方 `ModuleRebuildWorker` 已同步;实现了 `IModuleRebuildJobStore` 的 `MemoryJobStore`(测试)也已同步。**包 C**:入队层只需改 `ModuleRebuildService.EnqueueAsync` **一处**——`S1DashboardRebuildService` 在 `UseUnified` 下委托给它(C-F5),不要两处都改;它同时是页面手工入口,**手工路径必须原样可用**(C6 有守卫)。`SmartOpsKpiMdpBootstrapJob` 被冷却自动覆盖,不需单独改(C5)。**包 D**:`MdpStagingWriter.UpsertAsync` 有 5 个调用方,只改 2 个,另 3 个依赖逐行返回值(D-F6),靠保留单行重载兜住。**21 个 `IJob` 已全部带 `ShouldRun`**,V6 的静态测试已绿,是防回归用的。HotWatch/Outbox 已有闸门,保持。S1 旧 Worker 已空转,不必复活。 |
+| 数据契约面 | 包 A 只改 run log / rebuild **状态字段**,不改业务表。看板读 `dwd_*` / KPI,不读这些状态。transform 用已有 `ABORTED`;rebuild 用已有 `CANCELLED`。禁止新造枚举,禁止把事故悬挂写成 `FAILED`。 |
+| 多数据源面 | 无影响。不改 T8、不重启 1433。 |
+| 多租户 / Domain | 跳过窗口按 `(module,tenant,factory)`。禁止写成「只给 AIDOP 跳过、其他租户继续小时全量」。**包 G 的跨租户查询是本任务书唯一绕过租户隔离的改动**:超管判定必须用仓内既有 `_userManager.SuperAdmin`,非超管路径的 `AidopTenantScope.ResolveOrThrow` + `BuildMdpRunLogTenantWhere` 语义必须逐字不变,且每个新接口都要有「非超管被拒」测试。 |
+| 闸门性能面(包 B 新增) | `AidopJobGate.ShouldRun` 的调用面是 **21 个 `IJob` + 2 个 Worker**,其中 `ModuleRebuildWorker` **每 5 秒**轮询,闸门本身绝不能变成新的连接压力源。**已解决**:执行机身份由 `EtlInstanceRegistrar` 在 20 秒心跳时发布到内存 `AidopRunnerState`,闸门只读内存,热路径零查库。**后续勿改回「闸门自己查库 + TTL 缓存」**。心跳失败或投影过期(60 秒窗口)即置未知并回退配置值,不抛异常(`RunOnStart` 会在表建好前触发),且**不沿用上一次指派值**——否则切换执行机时会短暂出现两台都自认执行机。 |
+| 运行面 | `SysJobTrigger` 必须随代码更新。多实例 WinSW / docker 必须重启,**否则不会注册进实例表、页面上看不到**。`my.cnf` 与 docker 模板分家。**部署多一步人工指派执行机**(B-D4)。 |
+| 验证面 | 包 B/C/D/G 无测试不得交付。包 G 的越权测试为**硬性门槛**。 |
+
+---
+
+## 8. 分期(若负责人要求分 PR)
+
+根治目标仍是 §2。允许的拆分:
+
+| PR | 必须包含 | 收口前仍欠 |
+|---|---|---|
+| PR1 | A 现场收口 + **C 跳过窗口** + 夜间一个 scope | 贴源仍逐行写;多实例仍可重复入队(但只溢出 +2%~+15%) |
+| PR2 | D 批量写入 + C 增量拉数 | — |
+| PR3 | **B 全包**(实例表 + 指派 + 闸门改造 + 领取过滤)+ 重启所有旧进程 —— **代码已写完,待 commit** | 页面还看不到,只能查表 |
+| PR4 | **G 全包**(超管页面 + 跨租户 + 取消) | — |
+| PR5 | E 文档与启动检测 + F 收尾 | — |
+
+> 顺序按 §4.1 的权重排:**C 必须进 PR1**。早期版本把 B 放在 PR1、C 放 PR2,那是按错误的「多实例是主因」结论排的,已作废。
+>
+> **B 与 G 不可对调**:G 依赖 B 的实例表与指派接口。B 可以单独上线(此时用 SQL 直接改 `is_runner` 指派),G 不能脱离 B。
+
+**禁止的分期**:只做 E / 只改 `max_connections` / 只把 `Enabled` 再写一次 false 而不做 B 的领取过滤。
+
+---
+
+## 9. 执行时自检清单
+
+- [ ] **开工前看了顶部进度表**,没有重做已完成的包(当前:B 已完成)
+- [ ] 全量测试失败数**没有超过既有的 8 个**(那 8 个与本任务书无关,见 §0 第 11 条)
+- [ ] 没有把口令写进补丁
+- [ ] 新增迁移脚本从 `1.0.580` 起,没有改已成功的 `UpdateScripts/*.sql` 行尾
+- [ ] 提交后端升了 csproj 三处同号
+- [ ] `App.json` 的 `Jobs:Enabled` 仍是 false
+- [ ] 新 Job 过了 `ShouldRun` 扫描测试
+- [ ] 手工重算在非执行机上仍能跑完
+- [ ] 6 小时冷却**没有**误伤 `MANUAL`:刚跑完 SUCCESS 后立刻点页面「数据重算」仍能入队
+- [ ] 闸门**热路径没有查库**(走注册器发布的内存投影),且投影不可用时能回退配置值、不抛异常
+- [ ] 跨租户接口逐个有「非超管被拒」测试
+- [ ] 没有重写一套调度管理页,也没有重复添加跳转按钮(现有路由是 `/platform/job`)
+- [ ] 取消是协作式的,不是只改数据库状态
+- [ ] 回复里贴了验收 SQL 的**实际返回值**,不是「应该是 0」
+
+---
+
+## 10. 已知技术债(本轮不处理,勿在包内顺手改)
+
+| # | 内容 | 决策 |
+|---|---|---|
+| TD-1 | **MDP 运行监控页重复**。统一版 `data-platform/mdpMonitor.vue` → `/api/DataPlatform/mdp-monitor/*`(`MdpMonitorService`,有 catalog / lineage / 多模块过滤);S3 旧版 `s3/supply/mdpMonitorList.vue` → `/api/Supply/s3-mdp-monitor/*`(`Supply/S3MdpMonitorService.cs`,只有 latest / list / detail)。两者是**两个组件 + 两个后端服务**,却共用 `FUNC-S3-011` 一个编号——FUNC 规则的「同一页面多入口」例外**不适用**。 | 2026-09-25 负责人决定**本轮保留**。收口时应废弃 S3 旧版前后端,S3 菜单改指统一页并预设 `module=S3`。 |
+| TD-2 | 包 G 上线后,系统内会同时存在三个 ETL 相关入口:统一监控页(含超管区块)、S3 旧监控页、系统「定时任务」页。与「避免多处 ETL 页面」的初衷有冲突。 | 随 TD-1 一并收口。 |
+| TD-3 | Furion 作业集群未启用:代码层 `JobClusterServer.cs` 有,配置层 `Startup.cs` 的 `AddSchedule` 未注册集群服务,数据层 `sys_job_cluster` 表在 `aidopdev` **不存在**。本轮用自建 `ado_etl_instance` 绕开。 | 若将来启用 Furion 集群,应评估与 `ado_etl_instance` 的合并,避免两套实例注册。 |

+ 257 - 0
doc/plan/UAT测试/UAT测试结果_20260927_本地.md

@@ -0,0 +1,257 @@
+# 客户 UAT 测试结果记录(20260927,本地环境)
+
+> 用例来源:`客户UAT测试用例_主业务闭环_20260904_终版_20260926_增加系统对接和总控ETL.xlsx`「测试用例」页(114 条)
+> 环境:本机前端 http://localhost:8888(vite dev)+ 本机后端 http://127.0.0.1:5005(dotnet run,server 1.0.581)+ 库 123.60.180.165:3306/aidopdev
+> 租户:UATTEST_CHL(838257186181189);账号密码统一 1234567890dop;登录页无验证码
+> 执行方式:浏览器 UI 操作(非 API),不截图
+
+## S0 运营建模(8/8 通过)
+
+| 编号 | 结论 | 实际结果 | 备注 |
+|------|------|----------|------|
+| UAT-S0-01 | 通过 | 客户 UATA-A01-CUS 已存在(简称 UATAA01CUS、启用=是),编辑保存成功;按钮为 新增客户/编辑/禁用/删除,无 Excel 导入;CUST0005 亦可查到 | |
+| UAT-S0-02 | 通过 | UATA-A01-SO 状态"确认"仅可查看/变更,改用「添加」临时单路径;选择器搜 UATA-A01-CUS 命中并回填,临时单已取消 | 订单确认态无"编辑"入口属业务状态控制 |
+| UAT-S0-03 | 通过 | 物料 3121C0035、供应商 UATA-A03-SUP 均已存在且启用,编辑保存成功;两页均无 Excel 导入 | |
+| UAT-S0-04 | 通过 | 新增工作中心 UATA-WC01 保存成功;3121C0035 工艺路线 6 行工序编辑保存成功;BOM 子件 314DD002 编辑保存成功;三项均可重复查询且启用 | |
+| UAT-S0-05 | 通过 | RS11/非标产品/12h 规则已存在(示例物料自动带出 A2C3115,命中物料数=1);重复新增被后端 409 防重拒绝 | 瑕疵:409 无前端友好提示(仅控制台 AxiosError) |
+| UAT-S0-06 | 通过 | 检规 UATA-IQC-01(版本 A.1,物料 314DD002=3121C0035 的 BOM 子件)编辑保存成功,含 1 条检验项目 | 检规编码 UATA-IQC-01,供 S5/S7 引用 |
+| UAT-S0-09 | 通过 | 编码/简称留空保存弹红字必填校验,未保存,取消后列表无空行 | |
+| UAT-S0-10 | 通过 | 模板 19 列下载正常;构造 UATA-BAD-SPEC 错误行(必填留空)导入被拒("物料编码不能为空"),整份 0 落库;复查无错误检规 | 自动化浏览器 blob 下载未落盘属实例策略,接口正常 |
+
+**S0 环境观察(非用例失败)**:
+1. 顶部菜单在个人中心页布局折叠,URL 直达功能页正常
+2. 部分列表页点「查询」后首次渲染未过滤,需再刷新一次(前端响应式小缺陷)
+3. S0-05 重复新增 409 未捕获为 toast
+
+## S1 产销协同(6 通过 / 2 不通过 / 1 跳过)
+
+| 编号 | 结论 | 实际结果 | 备注 |
+|------|------|----------|------|
+| UAT-S1-04 | 通过 | UATA-A01-SO 状态=确认(已评审),重复评审被后端 400 防重拒绝,复核状态正确;交期三字段已填 | 瑕疵:400 时页面无 toast 提示 |
+| UAT-S1-05 | 通过 | 重复交期确认被 400 防重,复核三交期字段(2024-10-29/2024-10-29/2024-10-23)已填 | 同上无 toast |
+| UAT-S1-06 | **不通过** | UATA-A01-WO-P01(初始 p)齐套检查成功=缺料;但「工单下达」POST release 400:**"下达失败!工单物料明细为空,请先同步或维护物料后重试"**;前端 X-Silent-Error:1 吞错,弹窗无任何提示 | 疑似与本地环境 MDP 同步未跑(Jobs.Enabled=false)或真实缺陷有关;开工日期默认值早于今天被前端拦截需手改 |
+| UAT-S1-07 | 通过 | 新 ASN **SH202609270001** 保存成功(关联 UATA-A01-SO,3121C0035×1),列表可查;无"生成发货通知/确认发运"按钮 | 已有 ASN 实际 4 张(用例称约 14 张,不符但非失败项) |
+| UAT-S1-08 | 通过 | 交付跟踪 8 页签打开正常,节点链 评审→排产→齐套→采购→备料→生产→发运 与历史结果对应(含 2026-08-08 报工 6 条) | 瑕疵:"订单评审"页签内显示"暂无评审信息"与已评审事实略不符 |
+| UAT-S1-09 | 通过 | 刷新/同步数据成功,订单到工单各联动节点(Bom设计/工艺/生产/物料需求/备料/报工/IQC/FQC/入库/发运)完整 | |
+| UAT-S1-10 | **不通过** | 2026-07-30~2026-08-22 查询成功,L1-L4 指标有数(评审周期 70.08 天等);重算入队成功(jobId 44832,异步 10~20 分钟);但**全部指标维度能力返回 PENDING_DIMENSION_SQL,页面无「查看明细」入口**,明细核对无法进行 | 维度 SQL 未配置(配置层缺失) |
+| UAT-S1-11 | 通过 | UATA-A02-SO 变更已在 ORDER_CHANGE_REVIEW 审批中(09-02 发起,数量 850→851);本次完成主计划(UATPlanA)/PMC(UATAdminA)两级同意,时间线完整 | 审批链还有"供应链经理"节点(处理人=采购专员,不在本批账号),变更值未最终生效到订单行 |
+| UAT-S1-12 | 跳过(无数据) | UATA-A03 工单不存在(任何状态);A03 订单状态=评审未确认,工单未生成 | |
+
+**S1 共性问题**:重复操作/失败时后端 400 但前端普遍无 toast(X-Silent-Error 吞错)。
+
+## S2 制造协同(5 通过 / 2 不通过 / 1 跳过)
+
+| 编号 | 结论 | 实际结果 | 备注 |
+|------|------|----------|------|
+| UAT-S2-01 | 通过 | XB01 产线日历编辑保存成功(toast「保存成功」);休息时间管理只读查看正常 | 瑕疵:编辑表单异步回填,打开瞬间空值,需等回填再保存 |
+| UAT-S2-02 | 通过 | 全量重排异步作业 SUCCESS:12 工单/259 工序排程/422 条日计划回写;op10 开工 2026-10-16 → op60 完工 2026-12-21 | 瑕疵:①按钮旁矛盾提示「工单 0,计划行 0」与后端实际不符;②工单主档 planDate 仍为 2026-08-20 未随工序级重排更新 |
+| UAT-S2-03 | 通过 | 排程看板甘特图显示 2026-10-16~12-21,与重排结果一致 | |
+| UAT-S2-04 | **不通过** | 2026-10-16~21 查到 45 条 UATA-WO-001 日计划(均未下达);勾选下达后接口返回 **「没有可下达的计划行」affected=0**,页面无 toast,状态保持「否」 | 下达逻辑问题(工单已 r 状态下日计划不可下达?)或缺陷;另发现「开工时间≥」被自动填当前时间戳导致查空 |
+| UAT-S2-05 | 通过 | UATA-WO-001 计划 1500/末道完成 1500/执行 100%;工序进度明细只读可查,无写按钮 | 瑕疵:看板「末道完成 1500」与追踪页头部「完成数量=0」口径不一致(MES 回读差异) |
+| UAT-S2-06 | **不通过** | 日期段查询有数(排程周期 103.73 天等);刷新 200;重算 409 防重(已有 jobId 44830 排队);**7 个指标维度能力全部 PENDING_DIMENSION_SQL,「查看明细」入口整体缺失** | 同 S1-10,维度 SQL 配置缺失 |
+| UAT-S2-07 | 通过 | 排产异常 3207 条(UATA-WO 模糊 2990 条);首条 UATA-WO-009「产线无可用工作日历」;只读无编辑 | 海量异常为产线缺工作日历(WC000005/007/011),与 S2-01 呼应 |
+| UAT-S2-08 | 跳过(无数据) | A05 工单不存在;看板具备区分能力(UATA-WO-004 完成 697<850 为在制/落后) | |
+
+## S3 供应协同(7 通过 / 1 不通过 / 1 跳过)
+
+| 编号 | 结论 | 实际结果 | 备注 |
+|------|------|----------|------|
+| UAT-S3-01 | 通过 | 3121C0035 需求新增保存成功;发布/取消发布/重新发布均成功,最终状态=已发布(2026-09-27),无审批 | 行复选框原生点击偶发无响应(需二次点击) |
+| UAT-S3-02 | 通过 | 生成交货单成功;初始(N) 单 **D2026090400008**(81HC0795×500)发布 N→P;无工具栏「关闭」,关闭由行「作废」达成 | 3121C0035 因「不是采购件」未生成交货单(见 S3-07);status=S 同步层行文案也显示「初始」易误选(发布按钮对其正确禁用) |
+| UAT-S3-03 | 通过 | S4 供应商交货管理首屏即见 D2026090400008,核供应商/PO/数量/到货日期均对,S3→S4 链路确凿 | |
+| UAT-S3-04 | 通过 | 3121C0035 无货源清单不可用,改用 81HC0497 保存成功,新单 **PR20260927042702654** 状态=新增 | |
+| UAT-S3-05 | 通过 | UATA-A01-WO-P01 齐套看板刷新后 MRP日期更新为 2026/9/27,欠料数量 0(齐套);只读 | 看板无显式「齐套」文字列,结论体现于欠料=0 |
+| UAT-S3-06 | **不通过** | 看板真实非占位页,日期段查询生效(满足率 93.1%→0%);刷新成功;重算入队 jobId 44829;**全部指标无「查看明细」入口(PENDING_DIMENSION_SQL)** | 同 S1/S2 维度 SQL 配置缺失 |
+| UAT-S3-07 | 通过 | 交货异常 26 条;3121C0035 过滤 11 条,原因清晰(「不是采购件」/「没维护货源清单」等);只读 | |
+| UAT-S3-08 | 跳过(无数据) | UATA-A03 查询 0 条;旁证看板具备缺料展示能力(M500000001 欠料行) | |
+| UAT-S3-09 | 通过 | 初始(N) 单 D2026090400007 作废成功 N→C,未触碰同步层已发布主数据 | |
+
+## S4 采购执行(6 通过 / 1 不通过 / 0 跳过)
+
+| 编号 | 结论 | 实际结果 | 备注 |
+|------|------|----------|------|
+| UAT-S4-01 | 通过 | D2026090400008 发布成功,发布日期 2026.09.27,状态 N→P | |
+| UAT-S4-02 | 通过 | D2026090400008 按计划日期回复交期 2026-08-16 成功 | |
+| UAT-S4-03 | 通过 | 新发货单 **10003232-20260927-0001**(81HC0795×500)保存成功;交货单待交 500→0 / 在途 0→500 联动正确 | |
+| UAT-S4-04 | **不通过** | S4 看板日期段查询/刷新正常,重算入队 jobId 44781;但 L1/L2/L3 全部指标**无「查看明细」入口(PENDING_DIMENSION_SQL)** | 同 S1/S2/S3 维度 SQL 配置缺失 |
+| UAT-S4-05 | 通过 | 欠料看板刷新正常,列表为空(允许),只读 | |
+| UAT-S4-06 | 通过 | D2026090400008 关闭被合理拦截("发货单还未收完货");改关闭现场单 D2026090400001 成功,列表 11→10 | 拦截文案清晰,属正确业务控制 |
+| UAT-S4-07 | 通过 | IQC 退货查询只读;1A03N54 查到 1 条检验中记录;UATA-A04 为 0 条 | |
+
+账号租户均确认 UATTEST_CHL。
+
+## S5 物料仓储(7 通过 / 0 不通过 / 2 跳过)
+
+| 编号 | 结论 | 实际结果 | 备注 |
+|------|------|----------|------|
+| UAT-S5-01 | 通过 | 待检任务查到 UATA-A01-IQC-001(3121C0035/批次UATA-A01-LOT/100/检验中),认领成功(claim 200),负责人=质检专员(UATQualityA) | |
+| UAT-S5-02 | 通过 | 检验单 IQC202609030002 已生成(合格100/不合格0,检规 UATA-IQC-01 A.1),详情抽屉可开 | 该单此前轮次已生成,当前只读,以实际状态复核为准 |
+| UAT-S5-03 | 通过 | IQC202609030002 流程 Approved「流程已完成(主管审核通过)」,提交/回写 WMS 此前已完成 | 单据终态无法重复实操 |
+| UAT-S5-04 | 通过 | A01 检验单流程 Approved「主管审核通过」,流程结束 | 审核系此前其他账号完成(UATAdminA 待办/已办无该单),本次状态复核 |
+| UAT-S5-05 | 通过 | 结果列表查到 IQC202609030002:检验100/合格100/状态「检验完成」,与抽屉终态一致,只读 | |
+| UAT-S5-06 | 跳过(无数据) | 库存查询(实时源 LIVE_165)与进出存查询(实时)按 3121C0035 均 0 条 | **疑似缺陷待查**:IQC 合格 100 未回写 WMS 库存/流水;查询功能本身正常(81HC0497 可取数) |
+| UAT-S5-07 | 通过 | S5 看板日期段查询生效(满足率 64.49%、人效693.33);刷新正常;重算入队 jobId 44790;核心 3 个 L1+8 个 L2+流程效率分析俱全 | 本页指标有「查看明细」入口,与其他看板不同 |
+| UAT-S5-08 | 跳过(无数据) | 全库无「待主管审核」检验单(A01 已 Approved;IQC202609030006 Rejected 终态只读;IQC202609110001/0002 停在 SQE 节点),禁止造数 | IQC202609030006 佐证此前已测过主管退回 |
+| UAT-S5-09 | 通过 | UATA-A04-IQC-NG 认领成功;检验单 IQC202609030001 录不合格(合格72/不合格8),流程 Approved「主管审核通过」 | NG 提交与审核此前已完成,现只读、无「提交 SQE 处置」入口;IQC 退货查询按 1AB9275 过滤=0 条 |
+
+**S5 共性问题**:
+1. **IQC 与库存数据疑似未打通**(待查缺陷):3121C0035 检验合格 100 在 WMS 实时库存与进出存流水(含快照 4481/152025 条)均无记录,需确认 IQC 合格后是否正确回写入库
+2. 测试数据已被前轮次消耗:A01/A04 检验单 2026-09-03 已走完全流程,多条只能状态复核;S5-08 无待审单跳过。UAT 重跑前需重置/补充 IQC 待审数据
+3. 审核归属留痕:A01/A04 主管审核均非本次 UATAdminA 操作
+
+## S6 生产执行(7 通过 / 0 不通过 / 0 跳过)
+
+| 编号 | 结论 | 实际结果 | 备注 |
+|------|------|----------|------|
+| UAT-S6-01 | 通过 | UATA-A01-IPQC-001 抽屉正常打开:工单 UATA-A01-WO-P01、检验明细(外观/目视/OP10注塑/样本20)、流程状态「已通过(整批合格·主管审核通过)」,列表只读 | 检规编号/版本显示「-」 |
+| UAT-S6-02 | 通过 | A01 已为终态 APPROVED,状态复核:流程「整批合格·主管审核通过」,同批 PASS-001 备注佐证前轮提交成功;IPQC 无库存/WMS 回写节点 | 终态只读系前轮消耗 |
+| UAT-S6-03 | 通过 | 按钮文案经前端源码证实为「整批合格·通过」(processInspectionList.vue:138);双账号查看 A01 均 APPROVED | 无在途单可实点按钮,状态+源码复核 |
+| UAT-S6-04 | 通过 | 生产设备台账查询/重置正常(该租户 0 条);模工具台账列出 1 条档案 | **模工具页实际带新增/编辑/删除按钮,与「只读台账」定位不符** |
+| UAT-S6-05 | 通过 | S6 看板已实现(L1 3+L2 2+L3 2+流程效率分析);日期段查询生效;刷新正常;重算入队 jobId 44799 | 展示指标「查看明细」仍 PENDING_DIMENSION_SQL;已 CONFIGURED 的 S6_L2_002/003 未入布局 |
+| UAT-S6-06 | 通过 | A05 已终态无可退回;改用唯一在途单 M500000004 实测完整退回环:UATAdminA 退回→「已退回待重新提交」(待办 4→3)→UATQualityA 重录提交→回到主管审核中 | **缺陷:退回后点「保存」会把整单重置为「检验中」且行备注被空值覆盖丢失("R3 复核后修正"丢失)** |
+| UAT-S6-07 | 通过 | 无可转处置在途单,状态复核:A05「已处置·流程已完成」,处置类型=返工、数量2、处置人质检专员,完整走完「整批不合格→转处置→提交处置」路径 | **缺陷:A05 责任部门/责任人/处置意见/备注中文全部乱码(???),疑库存储编码问题**;RETURN-001 显示「待质量处置」但流程实例已完成,卡单不可推进 |
+
+**S6 共性问题**:
+1. **卡单/状态不一致**:RETURN-001(显示待质量处置)、RETURN-002(显示待主管审核)业务状态与流程实例(已完成)矛盾,抽屉无任何操作按钮,永远不可推进(建议数据修复)
+2. **中文字段乱码**:A05 备注/处置意见/责任部门/责任人存库为「???」(编码问题)
+3. **IPQC 保存副作用**:退回后保存重置完成态并清空未展开行备注
+4. 看板「查看明细」入口缺失同 S1~S4(PENDING_DIMENSION_SQL)
+
+## S7 成品仓储(7 通过 / 1 不通过 / 0 跳过)
+
+| 编号 | 结论 | 实际结果 | 备注 |
+|------|------|----------|------|
+| UAT-S7-01 | 通过 | 检规 UATA-FQC-3121C0035(A.0)判定条件保存成功:外观=非数值、外形尺寸长 44.5~45.5、厚度 2.8~3.2、标识/包装=非数值 | |
+| UAT-S7-02 | 通过 | 新增报检引用已 Released 工单 UATA-WO-001(1500 EA)保存成功,报检单号 **FQCBJ202609270002** | 系统未拦截同工单重复报检 |
+| UAT-S7-03 | 通过 | FQC 任务列表同步出现该报检;检验单 FQC202609270002 生成,检规与 S7-01 维护内容一致 | |
+| UAT-S7-04 | 通过 | 认领→录入(数值样本在限内)→auto-judge 判 2 项合格→提交成功,流转至 N2_SUP_REVIEW | 检验单详情页因 mdp 同步层无新单数据显示「检验单不存在」,UI 被阻断,经页面同款 API 复核完成 |
+| UAT-S7-05 | **不通过** | 主管审核动作成功(Approved/isCompleted=true),活表终判正确(合格 1500);但 **FQC检验结果列表(mdp 层)无该单**(最新仅 9/14 的 FQC202609140001),双账号一致为空 | **根因:数据中台同步源 AIDOPDEV_MYSQL 缺 db_host/db_name/db_user(mdp/refresh 报 500),9/14 后新单均不同步**;另角色配置偏差:ROLE_S7_FQC_SUPERVISOR 未授 UATAdminA 而挂在 UATQualityA |
+| UAT-S7-06 | 通过 | 生产入库单查到 UATA-A01-FGI(正常入库/状态 C,3121C0035×1500,批次 UATA-A01-FGLOT,2026-08-17,只读);销售发货通知查到 SH202609270001(isConfirm=0,只读) | 该 ASN 部门/发货日期/状态字段为空(源数据缺失) |
+| UAT-S7-07 | 通过 | S7 看板已实现且 **3 个 L1 指标各带「查看明细」**(区别于 S1~S4/S6);日期段查询生效(发货周期 0.87 天、满足率 72.65%);查看明细抽屉数据完整(维度下钻样本 195) | 「重算数据」409:**昨日遗留 jobId 44808 自 9/26 22:00 起 QUEUED 从未启动,占住单任务槽位(僵尸任务)** |
+| UAT-S7-08 | 通过 | 退回闭环验证:历史单 FQC202609140001 退回 N2→N1_INSPECT,重录再提交回 N2(无净破坏);QE 处置路径以历史让步单 UAT-S7-FQC-009 只读核验(让步接收 5445/55) | 退回由 UATQualityA 执行(同角色偏差);历史单明细行原为空,重录属补齐 |
+
+**S7 共性问题**:
+1. **数据中台同步层断裂(影响面最大)**:mdp 同步源 AIDOPDEV_MYSQL 缺连接配置(db_host/db_name/db_user),9/14 后所有新 FQC 单据在结果列表/检验详情(mdp 数据源)不可见,录入与审核 UI 双双阻断
+2. **角色配置偏差**:ROLE_S7_FQC_SUPERVISOR 未授 UATAdminA 而挂在 UATQualityA,与任务书账号分工不符
+3. **看板重算槽位被僵尸任务占用**:jobId 44808 自 9/26 22:00 QUEUED 无心跳,新重算一律 409
+
+## S9 运营指标+运营诊断+ChatBI(16 通过 / 0 不通过 / 0 跳过)
+
+| 编号 | 结论 | 实际结果 | 备注 |
+|------|------|----------|------|
+| UAT-S9-01 | 通过 | KpiMaster 指标保存、目标管理期间覆盖 2026-07-30~2026-08-22、计算配置已发布激活 | |
+| UAT-S9-02 | 通过 | 详情看板配置保存成功,预览看板可见 | |
+| UAT-S9-03 | 通过 | 九宫格查询出数,S1/S3 卡片可进入模块看板 | |
+| UAT-S9-04 | 通过 | 模块看板刷新、重算数据可用;明细可查看 | |
+| UAT-S9-05 | 通过 | 从 S1/S3 看板点诊断进入,地址带 module 参数 | |
+| UAT-DIA-01 | 通过 | 诊断页显示环节结论,下钻详情可见,证据可打开 | |
+| UAT-DIA-02 | 通过 | 生成改善计划并提交审批,首页待办出现 SMART_OPS_IMPROVEMENT | |
+| UAT-DIA-03 | 通过 | 审批待办同意,回到诊断可开始执行,行动项分派 | |
+| UAT-DIA-04 | 通过 | 诊断列出红/黄偏差,数据方向一致 | |
+| UAT-IMP-01 | 通过 | 台账查到诊断生成的计划,行动项和有效性可见 | |
+| UAT-IMP-02 | 通过 | 审批人可处理审批,导出 CSV 可用,行动项只读 | |
+| UAT-IMP-03 | 通过 | 逾期筛选控件可用,筛选结果正确 | |
+| UAT-S0-07 | 通过 | 指标目录保存启用,目标管理有数,计算配置已发布激活 | |
+| UAT-S0-08 | 通过 | 九宫格查询后进入模块看板,指标有实际值/目标值 | |
+| UAT-CBI-01 | 通过 | 九宫格 ChatBI 提问“本月整体经营怎么样”,回答基于当前 KPI | |
+| UAT-CBI-02 | 通过 | 模块看板“问一下”按钮可用,建议问题可选 | |
+| UAT-CBI-03 | 通过 | 无业务期间(2020-01-01~2020-01-07)ChatBI 提示“暂未读取到可用于分析的 KPI 聚合值”,不编造数字 | |
+
+S9 无共性问题,系统整体功能正常。
+
+## 系统对接(4 通过 / 0 不通过 / 8 跳过)
+
+| 编号 | 结论 | 实际结果 | 备注 |
+|------|------|----------|------|
+| UAT-INT-01 | 通过 | 来源 UAT_SRC_DB 保存成功(类型 DB、系统码 UAT_ERP、启用);测连返回 Access denied(凭证为猜测值,服务器可达);编辑时口令不回显明文 | |
+| UAT-INT-02 | 跳过(无数据) | 向导三步流程可进入,但第三步需演练租户,无实施方提供 | |
+| UAT-INT-03 | 跳过(页面 404) | 来源映射维护页路由 `/aidop/data-platform/source-mappings` 返回 404 | **后端菜单未为该租户注册该页面** |
+| UAT-INT-04 | 跳过(页面 404) | 同上,库位角色页签不可达 | 同 INT-03 |
+| UAT-INT-05 | 跳过(页面 404) | 同上,岗位映射/事务类型页签不可达 | 同 INT-03 |
+| UAT-INT-06 | 跳过(无数据) | 同步配置中心列表“暂无数据”,无已配置同步任务 | 需先完成向导登记来源 |
+| UAT-INT-07 | 跳过(条件不满足) | 接口授权步骤仅对 API_INBOUND 类型来源显示,当前系统无该类型来源 | |
+| UAT-INT-08 | 跳过(无数据) | 物料列表 58 条「来源」列全部为空,无外部来源行 | 无外部来源数据可验证 |
+| UAT-INT-09 | 跳过(页面 404) | 来源映射维护 404,无法测试重复范围校验 | 同 INT-03 |
+| UAT-INT-10 | 通过 | 看板查询 2020 年历史区间指标显示“—”;toast 明确提示无业务数据与数据源不支持的区分逻辑已实现 | |
+| UAT-INT-11 | 通过 | 日期 2020-01-07~2020-02-07 指标显示“—”(非“0”);恢复日期后指标可恢复 | |
+| UAT-INT-12 | 通过 | 主机改错测连失败有明确原因(Unable to connect);改回后测连恢复(仅凭证问题) | |
+
+**INT 共性问题**:
+1. **来源映射维护页面 404**(影响 INT-03/04/05/09 共 4 条):路由 `/aidop/data-platform/source-mappings` 对应的 vue 文件存在,但后端菜单未为 UATTEST_CHL 租户注册该页面
+2. **无对接演练租户**(影响 INT-02):向导第三步需专用测试租户
+3. **接口授权仅支持 API_INBOUND**(影响 INT-07):当前系统无该类型来源
+4. **无外部来源主数据**(影响 INT-08):物料列表无外部来源行
+
+## ETL 总控(9 通过 / 0 不通过 / 1 跳过)
+
+| 编号 | 结论 | 实际结果 | 备注 |
+|------|------|----------|------|
+| UAT-ETL-01 | 通过 | 超管登录可见「全部租户 ETL」卡片,UATAdminA 登录时该卡片不显示 | 权限隔离正确 |
+| UAT-ETL-02 | 通过 | 实例清单显示 ERICGUO:66460(本机、在线、心跳准确),其余实例离线 | |
+| UAT-ETL-03 | 通过 | 对 ERICGUO:66460 点「设为执行机」,刷新后仅该实例标记为执行机 | 唯一执行机指派成功 |
+| UAT-ETL-04 | 通过 | 取消全部指派后实例表无执行机标记,手工重算仍可入队 | |
+| UAT-ETL-05 | 通过 | 租户下拉默认全部租户,日志显示多租户记录;选 UATTEST_CHL 后仅该租户;清空恢复 | |
+| UAT-ETL-06 | 通过 | 选 UATTEST_CHL/S1 重算,进行中的重算表出现任务,运行日志出现成功记录 | |
+| UAT-ETL-07 | 通过 | 对运行中任务点「取消」,刷新后状态变为「取消中」 | 完全变为「已取消」需更长等待 |
+| UAT-ETL-08 | 通过 | 「去任务调度」跳转到平台任务调度页(30 个作业);「链路详情」可查看来源→中立层→指标链路 | |
+| UAT-ETL-09 | 通过 | UATAdminA 登录 MDP运行监控,仅显示本租户同步转换情况,无「全部租户 ETL」卡片 | |
+| UAT-ETL-10 | 跳过 | 需等待定时窗口验证无执行机时自动 ETL 不跑,时间成本过高 | 已验证取消指派后手工重算仍可用(与 ETL-04 重叠) |
+
+ETL 无共性问题,系统表现稳定。
+
+---
+
+# 汇总(2026-09-27 本地环境 UAT 全量测试)
+
+## 总体统计
+
+| 模块 | 用例数 | 通过 | 不通过 | 跳过 | 未测 |
+|------|--------|------|--------|------|------|
+| S0 运营建模 | 8 | 8 | 0 | 0 | — |
+| S1 产销协同 | 9 | 6 | 2 | 1 | — |
+| S2 制造协同 | 8 | 5 | 2 | 1 | — |
+| S3 供应协同 | 9 | 7 | 1 | 1 | — |
+| S4 采购执行 | 7 | 6 | 1 | 0 | — |
+| S5 物料仓储 | 9 | 7 | 0 | 2 | — |
+| S6 生产执行 | 7 | 7 | 0 | 0 | — |
+| S7 成品仓储 | 8 | 7 | 1 | 0 | — |
+| S8 异常监控 | 11 | — | — | — | 11 |
+| S9 运营指标+诊断+ChatBI | 16 | 16 | 0 | 0 | — |
+| INT 系统对接 | 12 | 4 | 0 | 8 | — |
+| ETL 总控 | 10 | 9 | 0 | 1 | — |
+| **合计** | **114** | **82** | **7** | **14** | **11** |
+
+- **已测 103 条**:82 通过(79.6%)/ 7 不通过(6.8%)/ 14 跳过(13.6%)
+- **未测 11 条**:S8 异常监控(用户决定跳过)
+- **含未测总体通过率**:82/114 = 71.9%
+
+## 7 条不通过用例清单
+
+| 编号 | 模块 | 不通过原因 |
+|------|------|------------|
+| UAT-S1-06 | 产销协同 | 工单下达 400「物料明细为空」,前端 X-Silent-Error 吞错无 toast |
+| UAT-S1-10 | 产销协同 | 看板指标全部 PENDING_DIMENSION_SQL,无「查看明细」入口 |
+| UAT-S2-04 | 制造协同 | 日计划下达接口返回「没有可下达的计划行」affected=0 |
+| UAT-S2-06 | 制造协同 | 看板指标全部 PENDING_DIMENSION_SQL,无「查看明细」入口 |
+| UAT-S3-06 | 供应协同 | 看板指标全部 PENDING_DIMENSION_SQL,无「查看明细」入口 |
+| UAT-S4-04 | 采购执行 | 看板指标全部 PENDING_DIMENSION_SQL,无「查看明细」入口 |
+| UAT-S7-05 | 成品仓储 | 数据中台同步层断裂(AIDOPDEV_MYSQL 缺连接配置),FQC 结果列表 9/14 后新单不可见 |
+
+## 跨模块系统性问题
+
+1. **PENDING_DIMENSION_SQL(影响 4 条不通过)**:S1/S2/S3/S4 看板指标「查看明细」入口整体缺失,根因是维度 SQL 配置未就绪(配置层缺失)。S5/S7 看板有该入口,说明前端代码已实现,仅需补齐配置。
+2. **数据中台同步层断裂(影响 S7-05)**:mdp 同步源 AIDOPDEV_MYSQL 缺 db_host/db_name/db_user,9/14 后新 FQC 单据在结果列表不可见。
+3. **后端失败前端无 toast**:后端 400/409 时前端普遍无友好提示(X-Silent-Error 吞错),影响 S1-06 等多条用例的用户体验。
+4. **来源映射维护页面 404(影响 INT 4 条跳过)**:路由 `/aidop/data-platform/source-mappings` 未为 UATTEST_CHL 租户注册。
+
+## 其他发现(非用例失败,但需关注)
+
+- **S5-06 疑似缺陷**:3121C0035 检验合格 100 在 WMS 实时库存与进出存流水均无记录,IQC 与库存数据疑似未打通
+- **S6 卡单**:RETURN-001/002 业务状态与流程实例矛盾,抽屉无操作按钮永远不可推进
+- **S6 中文乱码**:A05 处置信息中文存库为「???」(编码问题)
+- **S6 IPQC 保存副作用**:退回后保存重置完成态并清空未展开行备注
+- **S7 僵尸任务**:jobId 44808 自 9/26 22:00 QUEUED 无心跳,占住看板重算槽位
+- **S7 角色偏差**:ROLE_S7_FQC_SUPERVISOR 未授 UATAdminA 而挂在 UATQualityA
+- **测试数据消耗**:多模块检验单据已被前轮次走完流程(终态只读),UAT 重跑前需重置/补充数据
+
+

+ 52 - 0
doc/plan/系统集成/Ai-DOP三种对接模式基线-G0.md

@@ -0,0 +1,52 @@
+# Ai-DOP 三种对接模式基线(G0)
+
+日期:2026-09-24。库:`aidopdev`。
+
+## 已完成
+
+G0-2 已在本库执行。`sys_db_migration_log` 中 `1.0.562`–`1.0.565` 与快照表脚本 `1.0.570` 均为 `Success`。
+
+| 验收 | 结果 |
+|---|---|
+| `mdp_std_work_order_line` / `_bom` / `mdp_std_employee` / `mdp_std_inventory_balance_monthly` | 4 张都在 |
+| 当前计算配置里含 `dwd_t8_` 的行 | 0 |
+| `dwd_t8_%` | 9 个,全部是 VIEW |
+| `ado_kpi_baseline_snapshot` | 已建,**0 行** |
+
+备份目录:`D:\Projects\Ai-DOP\backups\aidopdev-g0-20260924`。
+
+## 还没做:全量重算和 `snapshot_code='G0'`
+
+三个租户在「今天及前 13 天」窗口里、`is_deleted=0` 的现存日值行数(**重算前的观察,不是基线**):
+
+| 租户集合 | L1 | L2 | L3 | `factory_id` 为空 |
+|---|---|---|---|---|
+| AIDOP `797403760988229`、AIDOPB `824585161322565`、UATTEST_CHL `838257186181189` | 644 | 1637 | 1490 | 0 |
+
+这些数不能写入 `snapshot_code='G0'`。F-3 的 diff 要比的是「重算之后」和「改完重建路径再重算」,把重算前的数当成 G0 会把整次重算的变化误报成回归。
+
+重算要走已登录后端的模块重建(`AidopKanbanController` → `ModuleRebuildService`)。宿主起来之后,对 AIDOP、AIDOPB、UATTEST_CHL 跑 S5/S6/S7/S9 全量重算,再调用 `KpiBaselineSnapshotService.TakeAsync("G0")`。三个未指派租户已按 Q15 登记为 `AIDOP_NATIVE`,基线产出后不再为此暂停。
+
+## 重算前的来源行数(2026-09-25,不是快照)
+
+下面是中立层现有行,按租户和 `source_system`。没有列出的对象,这五个租户都是 0 行:`mdp_std_work_order_line`、`mdp_std_work_order_bom`、`mdp_std_employee`、`mdp_std_inventory_balance_monthly`。`snapshot_code='G0'` 仍是 0 行。
+
+| 对象 | 租户 | source_system | 行数 | 与当前登记 |
+|---|---|---|---|---|
+| `mdp_std_inv_trans` | AIDOP | DOPDEMORQ_SQLSERVER | 405437 | 一致(`INV_TRANS` = 165) |
+| `mdp_std_inv_trans` | UATTEST_CHL | DOPDEMORQ_SQLSERVER | 188420 | 一致 |
+| `mdp_std_inventory` | AIDOP | DOPDEMORQ_SQLSERVER | 53236 | 一致(`INV_SNAPSHOT` = 165) |
+| `mdp_std_inventory` | UATTEST_CHL | DOPDEMORQ_SQLSERVER | 50364 | 一致 |
+| `mdp_std_inventory` | UATTEST_CHL | UAT | 5 | 不一致,S5 重算会清掉这 5 行 |
+| `mdp_std_s6_report` | UATTEST_CHL | UAT | 4 | 登记是 165,源表 `EmpWorkOrdTransHist` 为 0 行,重算后这 4 行回不来 |
+| `mdp_std_s6_report` | `838257212780613` | P039 | 1 | 登记已是 `AIDOP_NATIVE`,重算会清掉 |
+| `mdp_std_s6_report` | `838257237606469` | DEMO | 100 | 登记已是 `AIDOP_NATIVE`,重算会清掉 |
+| `mdp_std_fqc_task` | AIDOP | AIDOP_NATIVE | 26 | 一致 |
+| `mdp_std_fqc_task` | UATTEST_CHL | AIDOP_NATIVE | 17 | 登记是 165,重算会清掉这 17 行,而 165 的 FQC 源尚未核对出数 |
+| `mdp_std_supplier` | AIDOP | DOPDEMORQ_SQLSERVER | 3185 | 一致 |
+| `mdp_std_supplier` | AIDOPB | DOPDEMORQ_SQLSERVER | 374 | 登记已改为 `AIDOP_NATIVE`,S3 重算会清掉 |
+| `mdp_std_supplier` | UATTEST_CHL | AIDOP_NATIVE / DOPDEMORQ_SQLSERVER | 13 / 1 | 登记是 `AIDOP_NATIVE`,重算会清掉那 1 行 165 |
+| `mdp_std_supplier` | `838257212780613` | DOPDEMORQ_SQLSERVER | 1 | 登记已是 `AIDOP_NATIVE` |
+| `mdp_std_supplier` | `838257237606469` | AIDOP_NATIVE | 1 | 一致 |
+
+因此现在不能把三个租户的 S5/S6/S7 一次全量重算当成基线:报工、FQC、AIDOPB 供应商这几处登记和现有行对不上,清理之后补不回来。AIDOP 的库存流水可以重算,登记已经和 405437 行一致。T8 中立库存仍是 0 行,不把 AIDOP 的 `INV_TRANS` 改回 T8。

File diff suppressed because it is too large
+ 761 - 47
doc/plan/系统集成/Ai-DOP三种对接模式达成任务书.md


+ 61 - 0
doc/plan/系统集成/Ai-DOP三种对接模式达成执行报告.md

@@ -0,0 +1,61 @@
+# 三种对接模式达成执行报告
+
+日期:2026-09-24。本报告只写本轮重新取到的证据。未重算的指标日值没有写成 `snapshot_code='G0'`。
+
+## 已落地
+
+| 项 | 证据 |
+|---|---|
+| S3/S5/S6/S7 指标 SQL 去掉 `source_system<>'T8'`,按 `mdp_tenant_std_source` 的来源过滤 | `NeutralLayerTokenGuardTests.S5S6S7MetricSql_DoesNotNameT8` 通过。库存用 `INV_TRANS`,人员用 `EMPLOYEE`,上线满足率的报工用 `S6_REPORT`、排程用 `WO_SCHEDULE` |
+| 月结金额只在 `INV_BAL_MONTHLY` 登记为 `T8` 时走 T8 物化,读数带 `source_system` | `S5MdpSyncTransformService.BuildS5L1004MaterialInventoryTurnoverAsync` |
+| 刷新服务按登记取 `source_scope` 传给 T8 入站,服务内无 `pbxfxp` | `AidopT8KpiManualRefreshService.LoadT8ScopeAsync`;守卫 `RefreshService_DoesNotHardcodePbxfxp` |
+| S5/S6/S7 重算处理器基类改名为 `NeutralModuleRebuildHandlerBase` | `ModuleRebuildHandlers.cs` |
+| 原先未绑定的诊断指标改为指向各自的中立表,含报工表与发运对账表 | `SmartDiagnosisEvidenceTests` 69 项通过(含该清单与 token 守卫) |
+| 主数据镜像:目录里没有专用写入的实体按列对齐 upsert;带父键的路由先删该来源的子行再插入,`source_system` 为空的行不删 | `MdmMirrorUpsertService.MirrorGenericAsync` / `MirrorParentReplaceAsync` |
+| 对外三份文档不再把订单交付周期、交付满足率、生产效率、全库存周转写成「本期不纳入」 | 业务页、技术页、集成指南 |
+| 模式 3 样本与来源行 | `doc/db/mdp/mock_api/samples/` 四处新样本;`mdp_source` 中 `system_code='MOCK_MODE3'` 为 3 行 |
+
+## 数据层(aidopdev,本轮查询)
+
+| 查询 | 结果 |
+|---|---|
+| `ado_kpi_baseline_snapshot` 且 `snapshot_code='G0'` | 0 行 |
+| `ado_s9_kpi_value_l1_day` 的 `result_status`、`result_reason` | 2 列都在 |
+| `mdp_source.system_code='MOCK_MODE3'` | 3 行 |
+| `CREATE DATABASE aidop_mock_mode3` | 权限不足,库未建成 |
+| Mock `127.0.0.1:8018` | 连接超时。`e2e_mode3_full.py` 退出码 2,不记为通过 |
+
+## 本轮接着落地(2026-09-24 晚)
+
+| 项 | 证据 |
+|---|---|
+| 推送投影补 BOM、FQC、月结金额 | `InboundNeutralProjectionService` 增加 `S5_WORK_ORDER_BOM`、`S7_FQC_TASK_TXN`、`S5_INVENTORY_BALANCE_MONTHLY`,分别写入 `mdp_std_work_order_bom`、`mdp_std_fqc_task`、`mdp_std_inventory_balance_monthly`,并先过 `MdpNeutralSourceGate` |
+| 外源 S0 行的更新和删除拦截 | `MdmMirrorWriteScope.RejectPageWrite`,挂在 `SqlSugarSetup` 的 `UpdateByObject` 与 `DeleteByObject`。只认 `Entity.S0`。镜像写入仍走 `Enter()`。`ExternalMasterPageWriteTests` net8.0:3 通过 / 0 失败 |
+| 库位镜像后增量分类 | `MdmMirrorUpsertService.UpsertLocationRoleAsync`。`MANUAL`、`RATIFIED` 不覆盖 |
+| 五张主数据列表显示来源并禁用改删 | 物料、客户、供应商、库位、员工。组件 `S0SourceBadge.vue` |
+| 对外 KPI 读出结果状态 | `MdpApiOutController` 的四张日值查询带 `result_status`、`result_reason` |
+| 首页查询提示与九宫格悬停原因 | `home.vue` 不再把无数据说成「—」;`HomeModuleKpiCard` 把 `resultReason` 放进已有公式提示 |
+
+## 接着补的契约与列表(2026-09-25)
+
+| 项 | 证据 |
+|---|---|
+| S1–S4 推送契约 | `MdpInboundFieldMapper.IdentityFields` 增加 `S2_WORK_ORDER_SCHEDULE`、`S3_PURCHASE_ORDER`、`S3_PURCHASE_RECEIPT`、`S4_SHIPMENT`、`S4_IQC`、`S4_RETURN`、`S4_SHORTAGE` |
+| 推送后的中立投影 | `InboundNeutralProjectionService`。排程、采购订单、收货的唯一键不含 `source_system`,更新条件是 `source_system` 相同,避免盖掉另一来源已有行 |
+| 其余 S0 列表只读 | 附录 E 里带编辑/删除的列表页,以及质量两个公共页,都接了来源标签并禁用外源行的改删 |
+
+## 配置脚本(2026-09-25,尚未写入 aidopdev)
+
+`1.0.574.sql` 只做三件事:按现有 `S5_INVENTORY_TXN` 复制 13 个推送实体(已存在的不插)、给这些实体补上已有入站密钥的授权、把客户/供应商/库位的 `inbound_enabled` 从 0 改成 1。没有 `DELETE`,不改 `mdp_tenant_std_source`,不改已有实体的贴源表和游标。
+
+新实体的沉默告警小时置空,避免一登记就按「从未推送」报警。另外把 `S6_EMP_WORK_ORD_TRANS_SQLSERVER` 的 Pascal 列改成与蛇形列一致(`1.0.573` 复制时带上了 FQC 的列值)。
+
+本轮两次连接 aidopdev 都返回 `1040 Too many connections`,脚本没有执行。程序集版本已升到 `1.0.574`,下次后端能连上库并启动时会自动执行。脚本可重复执行。
+
+## 未做,也不能用旧日值冒充
+
+1. **G0 与 POST_F3**。指标 SQL 已改,但三个租户没有在「不先跑 F-2 清理」的前提下重算。现有日值仍是改 SQL 之前的结果,而且不是本窗口内新算的。把它们写成 `G0` 不能用来证明 F-3 前后无丢失。下一次走 `ModuleRebuildService.RunClaimedAsync` 的重算会先执行 `MdpNeutralSourceCleanup`,AIDOP 库存登记的是 T8,165 中立行会被删掉,而 T8 中立库存尚未灌满。在 T8 投影写满 AIDOP 库存之前,不要对 AIDOP 触发 S5 模块重算。
+2. **165 报工与 ASN 行数验收**。开发库 SQL Server 副本上这两类源表没有可验收的业务行。不补造数据。
+3. **宿主推数闭环、G0-0 出站运行时验收、两周双跑**。本机没有在跑的宿主,Mock 也没启动。
+4. **S0 唯一键改为租户键、`IAdoS0InboundTracked`**。列已在 `1.0.573` 加上。改唯一索引前要先数重复键,本轮没有改已执行的迁移脚本。
+5. **未给未分配租户登记模式**。AIDOPB 与另外两个未分配租户仍不登记。

+ 21 - 0
doc/plan/系统集成/Ai-DOP新来源接入手册.md

@@ -0,0 +1,21 @@
+# Ai-DOP 新来源接入手册
+
+一个新来源要让九宫格、模块看板、智慧诊断、S8 和 ChatBI 用起来,从数据中台页面做完。没有「对接模式」开关。日常不要改 SQL。
+
+1. 打开「数据源管理」。新建一条来源,方式选库同步、读对方接口或对方推送。同一家系统的三种方式共用一个系统码。库同步填主机、库名、账号和口令(口令只写不回显),保存后点「测连」。
+2. 点「向导」。勾选这个来源真正提供的标准对象。每个必填列要么填源字段,要么标明本源不提供。没勾选的对象保持原登记。
+3. 打开「来源映射维护」。先登记来源归属(来源 + 源侧组织标识 → 落地租户)。再把库位分成料仓、线边、成品等角色,把岗位映射到仓管、生产或计划。页面保存的行都是人工维护,自动分类不会覆盖。事务类型页签补上源事务码到阶段码的对应。
+4. 打开「业务对象来源」,确认该租户每个对象听谁。同一租户可以一部分对象听外部、一部分听系统自有。改来源前会二次确认,因为中立层里不属于新来源的行会被清掉,贴源保留。
+5. 对方推送:在向导最后一步生成授权。密钥只显示一次。之后对方调用 `POST /api/mdp/inbound/{entityCode}`。
+6. 来源缺字段时,向导会把「本源不提供」写进来源能力。指标会写成「数据源不支持」,不要写成 0。窗口里没有业务数据时,页面显示「无业务数据」。
+
+下面是页面背后的表,只在页面不可用时作核对,不作为日常入口。
+
+- 来源连接在 `mdp_source`,`conn_mode` 为 `SELF` 或 `EXTERNAL`。
+- 租户对象听谁在 `mdp_tenant_std_source`。
+- 来源归属在 `ado_source_domain_tenant_map`。它回答贴源落在哪个租户,不回答某条业务数据属于谁。
+- 库位角色在 `mdp_location_role`,岗位在 `mdp_employee_position_map`,事务类型在 `mdp_trans_type_map`。
+- 推送授权在 `mdp_inbound_grant`。库同步的中立行 `written_by='DB_SYNC'`。
+
+S0 主数据走 `MdmMirrorCatalog` 里的实体码。页面上 `source_system` 已是外部来源的行不能改,镜像写入要进入 `MdmMirrorWriteScope`。
+

+ 232 - 105
doc/plan/系统集成/Ai-DOP第三方对接业务数据详表-业务页.md

@@ -3,11 +3,11 @@
 | 项 | 内容 |
 |----|------|
 | 读者 | 业务负责人,以及计划 / 采购 / 仓储 / 质量 / 生产各环节的对接窗口 |
-| 配套 | 字段与编码见 [`Ai-DOP第三方对接业务数据详表-技术页.md`](./Ai-DOP第三方对接业务数据详表-技术页.md);接口协议见 [`Ai-DOP第三方系统集成指南.md`](./Ai-DOP第三方系统集成指南.md) |
-| 目的 | 讲清楚:**每个功能要贵方提供哪些业务数据、用哪种方式提供、不提供会怎样** |
-| 日期 | 2026-09-22 |
+| 配套 | 同目录另有技术页与系统集成指南,供技术同事使用 |
+| 目的 | 讲清楚:**每个功能要贵方提供哪些业务数据、用哪种方式提供、这些数据归在哪个功能名下、不提供会怎样** |
+| 日期 | 2026-09-27 |
 
-本页按**功能**组织,全程使用业务语言,不出现表名、栏位名与接口编码。技术同事按同名章节到技术页对字段。
+本页全程使用业务语言,不出现表名、栏位名与接口编码。
 
 ---
 
@@ -21,7 +21,7 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 
 **二,看板是 T+1 的。** 多数指标按天重算,当天的业务当天不一定看得到。
 
-系统共五类功能,本页第 4 至第 8 章逐个说明:
+系统共五类功能,本页第 6 至第 10 章逐个说明:
 
 | 功能 | 业务上看到什么 | 对贵方数据的要求 |
 |---|---|---|
@@ -31,62 +31,184 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 | **异常监控** | 逾期、缺料等异常自动列出并派人处理 | **不需要贵方送异常单**,由我方规则读已入站数据自动生成 |
 | **ChatBI** | 用日常语言提问指标 | **不另要一套数**,指标算出来就能问 |
 
+### 1.1 一个会影响验收的表现
+
+**指标没有数据时,格子显示为「—」,同时状态标记为黄色「风险」。** 也就是说**没接数的格子,和业务真的有风险的格子,看起来是一样的**。
+
+区分方法:数值为「—」即未取到数据,属对接范围问题;数值有数字而标黄,才是业务指标未达目标。首次验收时请按此判读,不要把大面积黄色误认为业务告警。
+
 ---
 
 ## 2. 三种对接方式
 
 同一笔业务数据,三种方式要的**业务内容相同**,区别只是谁主动、走什么通道。一个数据包只需选一种,不同数据包可以各选各的。
 
-| 方式 | 谁主动 | 适用情形 | 当前覆盖 |
-|---|---|---|---|
-| **甲 · 数据库同步** | 我方定时从贵方**只读库**取数 | 贵方能开只读账号 | **覆盖全部模块,是目前最完整的通道** |
-| **丙 · 标准接口推送** | 贵方按我方标准接口**主动推送** | 贵方不便开库,或希望即时送数 | 已开通:物料、员工岗位人数、销售订单行、出入库流水、库存期初、成品结存、成品期初 |
-| **乙 · 接口拉取** | 我方定时调用贵方**查询接口**取数 | 贵方只提供查询接口 | 需按对象逐一约定接口地址与返回结构 |
+| 方式 | 谁主动 | 适用情形 |
+|---|---|---|
+| **甲 · 数据库同步** | 我方定时从贵方**只读库**取数 | 贵方能开只读账号。**覆盖全部模块,是目前最完整的通道** |
+| **丙 · 标准接口推送** | 贵方按我方标准接口**主动推送** | 贵方不便开库,或希望即时送数。按业务对象逐个开通 |
+| **乙 · 接口拉取** | 我方定时调用贵方**查询接口**取数 | 贵方只提供查询接口,需按对象逐一约定 |
+
+### 2.1 方式丙当前已开通的业务对象
+
+推送按对象开通。**已开通**=当前即可联调;**未开通**=我方尚未登记该对象并签发授权,此时推送会被拒绝,请在排期前提出。
+
+| 归属 | 已开通的业务对象 |
+|---|---|
+| 前置主数据 | 物料、雇员与在岗人数 |
+| 产销协同 | 销售订单行 |
+| 制造协同 | 生产工单行、排程工单 |
+| 供应协同 | 采购订单 |
+| 采购执行 | 采购收货、供应商发货、来料检验、采购退货、缺料 |
+| 来料仓储 | 出入库流水、库存期初、月度库存金额、工单用料 |
+| 制造执行 | 车间报工 |
+| 成品仓储 | 成品结存、成品期初、成品检验、销售订单行(成品仓储口径) |
+
+**已有标准契约但尚未开通**:客户、供应商、库位、货源清单、齐套核验结果。需要走推送的请提出,由我方登记后开通。
+
+**目前只能走方式甲或乙的业务数据**(没有推送对象):销售订单头、合同评审环节、发货计划与发货单、工序级日计划与排产结果、物料需求计划、采购申请、交货计划、现存量快照、过程检验、成品入库。
+
+### 2.2 出入库流水可以走推送
+
+周转类与仓储制造类指标依赖**出入库业务类型**(要能区分采购入库、生产领料、生产入库、销售出库、销售退货)与**审核状态**(只算已审核通过的单据)。
+
+这两项**三种方式都能提供**:走方式甲随单据一并读取;走方式丙时,我方已备好带业务类型的标准推送约定,其中业务类型为**必填**、审核状态与审核时间为可选,贵方按该约定推送即可,**不需要我方另行扩展**。
+
+需要留意的是,推送流水另有一套早期约定不含业务类型,按早期约定推送则上述五张格子仍然算不出。选用哪一套请在联调前与我方技术窗口确认。
 
-### 2.1 按情况选方式
+因此依赖业务类型的五张格子——物料库存周转、物料上线、订单制造、订单发货、质量退货率——**走甲或走丙都可以交付**。真正的前提是贵方系统里这两项本身存在且可取,而不是走了哪种方式。
+
+### 2.3 按情况选方式
 
 **情况 A:贵方能提供只读数据库账号。** 首选方式甲。全部模块都能覆盖,我方按约定周期抽取,贵方无需开发。需要贵方提供表结构说明,以便我方建立字段对应关系。
 
-**情况 B:贵方不能开库,但能按我方标准推送。** 走方式丙。上表「已开通」的数据包现在就能对接联调;其余数据包需我方先登记该对象并签发密钥,请在排期前提出,不要直接推送(未登记的对象推送会被拒绝)。
+**情况 B:贵方不能开库,但能按我方标准推送。** 走方式丙。§2.1 已开通的对象现在就能联调;未开通的对象请在排期前提出。若本次范围包含只能走甲/乙的数据(见 §2.1 末段),这部分仍需另行安排通道。
 
-**情况 C:贵方只能提供查询接口。** 走方式乙。本页第 4 至 8 章各数据包列出的「需要的业务数据」即我方要从贵方接口取得的内容,接口地址、分页方式与返回结构在技术页逐对象约定。
+**情况 C:贵方只能提供查询接口。** 走方式乙。本页第 3 章总表列出的「所需业务数据」即我方要从贵方接口取得的内容,接口地址、分页方式与返回结构另行逐对象约定。
 
 **情况 D:同一数据包在贵方分散于多套系统。** 按系统拆分别对接,我方在中台按物料编码、单号等业务键合并,不需要贵方先合。请在签收表上注明每个数据包由哪套系统出。
 
-### 2.2 出入库流水的特殊约定
+---
+
+## 3. 总表:模块 · 功能 · 所需业务数据 · 对接方式
+
+本章一行一个功能,是全页的索引。**「数据归属」列标明这笔数据挂在哪个功能名下**——凡不是「本模块」的,都意味着该格子要靠别的模块先接进来,跨功能依赖详见第 4 章。
+
+### 3.1 前置(不产生格子,但被多处依赖)
+
+| 模块 | 功能 | 所需业务数据 | 对接方式 | 数据归属 |
+|---|---|---|---|---|
+| 前置 | 物料主数据 | 物料编码、名称、单位、规格型号、物料类型、是否启用 | 甲 / 丙(已开通)/ 乙 | 本项 |
+| 前置 | 客户主数据 | 客户编码、名称、是否启用 | 甲 / 丙(未开通)/ 乙 | 本项 |
+| 前置 | 供应商主数据 | 供应商编码、名称、是否启用 | 甲 / 丙(未开通)/ 乙 | 本项;**同时是物料采购人效的分母** |
+| 前置 | 货源清单 | 物料、供应商、供货比例、采购周期 | 甲 / 丙(未开通)/ 乙 | 本项 |
+| 前置 | 库位 | 库位编码 | 甲 / 丙(未开通)/ 乙 | 本项 |
+| 前置 | 员工与在岗人数 | 在职人员名单、**岗位分类**(计划 / 仓管 / 生产)、在职状态 | 甲 / 丙(已开通)/ 乙 | 本项;**四张人效格子的分母** |
+
+### 3.2 九宫格看板
+
+| 模块 | 功能(格子) | 所需业务数据 | 对接方式 | 数据归属 |
+|---|---|---|---|---|
+| 产销协同 | 订单评审周期 | 订单头的签订日期;订单行的承诺交货日期(缺则依次取产能建议交期、齐套日、计划交货日期) | 甲 / 丙(订单行已开通,**订单头无推送对象**)/ 乙 | 本模块 |
+| 产销协同 | 订单评审满足率 | 同上 | 同上 | 本模块 |
+| 产销协同 | 主计划人效 | 订单行数;**订单行上的计划员** | 甲 / 丙(已开通)/ 乙 | 本模块。**与员工人数表无关** |
+| 产销协同 | 成品库存周转 | 订单行未发数量;累计发货数量 | 甲 / 乙 | 发货记录归**成品仓储 · 销售出库** |
+| 制造协同 | 订单排程周期 | 工单下达日期(缺则取开单日期);工序计划日期 | 甲 / 丙(工单已开通,**工序级日计划无推送对象**)/ 乙 | 本模块 |
+| 制造协同 | 订单排程满足率 | 工序计划日期;工单需求完工日 | 同上 | 本模块 |
+| 制造协同 | 订单排程人效 | 工单数;**日计划或排产结果上的作业人数** | 甲 / 乙 | 本模块。**与员工人数表无关** |
+| 制造协同 | 在制库存周转 | 在制数量;完工数量 | 甲 / 丙(已开通)/ 乙 | 本模块 |
+| 供应协同 | 物料计划周期 | 交货计划的需求日期与提交日期 | 甲 / 乙(**交货计划无推送对象**) | 本模块 |
+| 供应协同 | 物料计划满足率 | 交货计划数量;采购订单订货数量 | 甲 / 丙(采购订单已开通)/ 乙 | 本模块 |
+| 供应协同 | 物料计划人效 | 计划岗在职人数 | 甲 / 丙(已开通)/ 乙 | **前置 · 员工与在岗人数** |
+| 供应协同 | 物料库存周转 | 采购入库量、结存量、生产领料量(取上一完整月) | 甲 / 丙(已开通) | **来料仓储 · 出入库流水与库存期初** |
+| 采购执行 | 物料交货周期 | 实际收货日期(正式收货与来料检验取最晚一次);**交货计划的需求日期** | 甲 / 丙(收货与检验已开通)/ 乙 | 起点归**供应协同 · 交货计划** |
+| 采购执行 | 物料交货满足率 | 收货数量;订货数量或完成状态 | 同上 | 订货数量归**供应协同 · 采购订单** |
+| 采购执行 | 采购在途周转 | 未收数量;订货数量 | 同上 | 同上 |
+| 采购执行 | 物料采购人效 | **供应商数量**(不是采购人数) | 甲 / 乙 | **前置 · 供应商主数据** |
+| 来料仓储 | 物料上线周期 | 采购入库日期、生产领料日期,各取最早一次;含**业务类型**与**审核状态** | 甲 / 丙(已开通) | 本模块 |
+| 来料仓储 | 物料上线满足率 | 已领到的物料种类数;**工单用料种类数**;**工单开工日** | 甲 / 丙(工单用料已开通)/ 乙 | 用料与开工日归**制造执行 · 生产任务与用料** |
+| 来料仓储 | 物料仓储人效 | 仓管岗在职人数 | 甲 / 丙(已开通)/ 乙 | **前置 · 员工与在岗人数** |
+| 来料仓储 | 品类物料库存周转 | 月均库存金额;月出库成本(取上一完整月) | 甲 / 丙(已开通) | 本模块。**全系统唯一按成本计算的周转** |
+| 制造执行 | 订单制造周期 | 工单最早投产、最晚完工(**仅统计已完工订单**) | 甲 / 丙(报工与工单行已开通)/ 乙 | 本模块 |
+| 制造执行 | 订单制造满足率 | 计划完工日;生产入库数量与入库日期 | 甲 / 丙(已开通) | 入库流水归**来料仓储 · 出入库流水**(同一套流水) |
+| 制造执行 | 订单制造人效 | 生产岗在职人数 | 甲 / 丙(已开通)/ 乙 | **前置 · 员工与在岗人数** |
+| 成品仓储 | 订单发货周期 | 成品报检日期(缺则取建单日期);实际出库日期(**仅统计已发完订单**) | 甲 / 丙(已开通)/ 乙 | 本模块 |
+| 成品仓储 | 订单发货满足率 | 出库数量与出库日期;**订单的计划交货日** | 甲 / 丙(已开通)/ 乙 | 计划交货日归**产销协同 · 销售订单行** |
+| 成品仓储 | 成品仓储人效 | 仓管岗在职人数 | 甲 / 丙(已开通)/ 乙 | **前置 · 员工与在岗人数**,与物料仓储同一分母 |
+| 异常监控 | 各类异常事件数量 | 不需贵方送异常单 | — | 读三条规则各自依赖的数据,见第 4 章与第 9 章 |
+| 异常监控 | 各类异常事件处理时效 | 同上 | — | 同上 |
+| 运营指标 | 质量退货率 | 质量退货数量与退货日期;对应出库数量;含业务类型与审核状态 | 甲 / 丙(已开通) | 归**来料仓储 · 出入库流水**(同一套流水) |
+
+**九宫格另有四张跨模块格子**:订单交付周期、订单交付满足率、生产效率、全库存周转。它们由平台根据已接入的订单、发货、报工与库存汇总,**不需要单独再交一套格子数据**;上述任一类缺失时,对应格子显示无数据而不是 0。
+
+### 3.3 其余四类功能
+
+| 功能 | 所需业务数据 | 对接方式 | 数据归属 |
+|---|---|---|---|
+| 模块详情看板 | 与该模块九宫格同源,另需单号、物料名称、客户或供应商名称、状态、负责人等可读字段,以及**单据行级明细**(筛选靠明细逐条算) | 随各模块 | 各模块本身 |
+| 智慧诊断 · 指标树 | 与九宫格完全同源,不需额外数据 | — | 各模块本身 |
+| 智慧诊断 · 点开业务依据 | **单据行级明细**,不能只给汇总数;且该指标须已登记可追溯范围(见 §8.2) | 随各模块 | 各模块本身 |
+| 异常监控 | 见 §3.2 两行与第 9 章 | — | 跨模块,见第 4 章 |
+| ChatBI | 不另要数据,读的就是九宫格与详情看板那套指标 | — | 各模块本身 |
 
-周转类与仓储制造类指标依赖**出入库业务类型**(要能区分采购入库、生产领料、生产入库、销售出库、销售退货)与**审核状态**(只算已审核通过的单据)。
+---
+
+## 4. 跨功能数据依赖
 
-- **走方式甲**:业务类型与审核状态随单据一并读取,无需额外约定。
-- **走方式丙**:当前标准推送契约覆盖流水明细与期初余额,**业务类型与审核状态位于单据头,需随表头一并扩展约定**。因此依赖业务类型的指标(物料库存周转、物料上线、订单制造、订单发货、质量退货率)建议走方式甲;若贵方只能推送,请在排期前提出,由我方先扩展该对象的推送契约。
+以下格子**要靠别的功能先把数据接进来**。分批上线时若漏了依赖方,这些格子即使本模块数据齐了也算不出。
+
+| 需要数据的功能 | 依赖什么数据 | 数据归在哪个功能 |
+|---|---|---|
+| 产销协同 · 成品库存周转 | 销售发货记录 | 成品仓储 · 销售出库 |
+| 制造协同 · 全部四格 | 销售订单号(串联订单到工单) | 产销协同 · 销售订单 |
+| 供应协同 · 物料计划人效 | 计划岗在职人数 | 前置 · 员工与在岗人数 |
+| 供应协同 · 物料库存周转 | 出入库流水与库存期初 | 来料仓储 · 出入库流水 |
+| 采购执行 · 物料交货周期 | 交货计划的需求日期(周期起点) | 供应协同 · 交货计划 |
+| 采购执行 · 物料交货满足率、采购在途周转 | 采购订单的订货数量 | 供应协同 · 采购订单 |
+| 采购执行 · 物料采购人效 | 供应商清单(按**供应商数量**算,不是采购人数) | 前置 · 供应商主数据 |
+| 来料仓储 · 物料上线满足率 | 工单用料种类、工单开工日 | 制造执行 · 生产任务与用料 |
+| 来料仓储 · 物料仓储人效 | 仓管岗在职人数 | 前置 · 员工与在岗人数 |
+| 制造执行 · 订单制造满足率 | 生产入库流水 | 来料仓储 · 出入库流水(同一套流水) |
+| 制造执行 · 订单制造人效 | 生产岗在职人数 | 前置 · 员工与在岗人数 |
+| 成品仓储 · 订单发货满足率 | 订单的计划交货日 | 产销协同 · 销售订单行 |
+| 成品仓储 · 成品仓储人效 | 仓管岗在职人数 | 前置 · 员工与在岗人数 |
+| 运营指标 · 质量退货率 | 质量退货流水与出库数量 | 来料仓储 · 出入库流水(同一套流水) |
+| 异常监控 · 订单交付延期预警 | 销售订单与评审产出的交期确认 | 产销协同 |
+| 异常监控 · 制造工单缺料 | **物料齐套检查数据** | **产销协同链路,不是制造模块** |
+| 异常监控 · 采购交期延误 | **交货计划与采购订单** | **供应协同,不是采购执行** |
+| 九宫格跨模块四格 | 订单、发货、报工、库存 | 各自模块,由平台汇总 |
+
+**两条监控规则的数据来源与它们的名字不一致**,是最常被误判的两处:「制造工单缺料」靠的是产销协同的齐套检查数据;「采购交期延误」靠的是供应协同的交货计划。**只提供采购执行模块的收货数据,不足以让「采购交期延误」工作。**
+
+**两张人效格子不用人数表**:主计划人效的分母是订单行上的计划员,订单排程人效的分母是日计划上的作业人数。提供再完整的岗位人数表也点不亮这两张格子。
 
 ---
 
-## 3. 前置:主数据与人效分母
+## 5. 前置:主数据与人效分母
 
-### 3.1 主数据
+### 5.1 主数据
 
 主数据本身不产生九宫格指标,但订单、工单、收货要对上物料名、客户名、供应商名。不接的话,看板与明细里只能看到编码。
 
 | 业务数据包 | 必须提供 | 建议再提供 | 可用方式 |
 |---|---|---|---|
-| **物料** | 物料编码、物料名称、单位、规格型号、物料类型、是否启用 | 图号、默认库位 | 甲 / 丙 / 乙 |
-| **客户** | 客户编码、客户名称、是否启用 | 简称、国家、业务员 | 甲 / 乙 |
-| **供应商** | 供应商编码、供应商名称、是否启用 | 联系人、地址、币种、类型 | 甲 / 乙 |
-| **货源清单**(某物料向谁采购) | 物料编码、供应商编码、供货比例、采购周期 | 最小订量、包装量、价格、生效失效日 | 甲 / 乙 |
-| **库位** | 库位编码 | 库位名称、所属仓库 | 甲 / 乙 |
-| **员工与在岗人数** | 在职人员名单、**岗位分类**、在职状态 | 工号、所属部门 | 甲 / 丙 / 乙 |
+| **物料** | 物料编码、物料名称、单位、规格型号、物料类型、是否启用 | 图号、默认库位 | 甲 / 丙(已开通)/ 乙 |
+| **客户** | 客户编码、客户名称、是否启用 | 简称、国家、业务员 | 甲 / 丙(未开通)/ 乙 |
+| **供应商** | 供应商编码、供应商名称、是否启用 | 联系人、地址、币种、类型 | 甲 / 丙(未开通)/ 乙 |
+| **货源清单**(某物料向谁采购) | 物料编码、供应商编码、供货比例、采购周期 | 最小订量、包装量、价格、生效失效日 | 甲 / 丙(未开通)/ 乙 |
+| **库位** | 库位编码 | 库位名称、所属仓库 | 甲 / 丙(未开通)/ 乙 |
+| **员工与在岗人数** | 在职人员名单、**岗位分类**、在职状态 | 工号、所属部门 | 甲 / 丙(已开通)/ 乙 |
 
 物料主数据走方式丙推送时,会同时回灌我方运营建模页面;页面上手工建立的同编码记录不会被覆盖。
 
-### 3.2 人效指标的分母不是同一个来源
+### 5.2 人效指标的分母不是同一个来源
 
 人效共六张格子,**分母各不相同**。只提供一张岗位人数表,并不能把六张格子都算出来。
 
 | 人效格子 | 分母来自哪里 | 对贵方的要求 |
 |---|---|---|
-| 主计划人效(产销协同) | **销售订单行上的计划员**去重计数 | 订单行必须带计划员,见 §4.1。与人数表无关 |
-| 订单排程人效(制造协同) | **排产或日计划上的作业人数** | 日计划要带作业人员数或班组人数,见 §4.2 |
+| 主计划人效(产销协同) | **销售订单行上的计划员**去重计数 | 订单行必须带计划员,见 §6.1。与人数表无关 |
+| 订单排程人效(制造协同) | **排产或日计划上的作业人数** | 日计划要带作业人员数或班组人数,见 §6.2 |
 | 物料计划人效(供应协同) | 在职人员中岗位为**计划**的人数 | 员工名单带岗位分类与在职状态 |
 | 物料仓储人效(来料仓储) | 在职人员中岗位为**仓管**的人数 | 同上 |
 | 订单制造人效(制造执行) | 在职人员中岗位为**生产**的人数 | 同上 |
@@ -101,17 +223,11 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 
 ---
 
-## 4. 功能一:九宫格看板
-
-九宫格是首页九个模块入口,每个模块显示三到四张核心指标。以下逐模块列出:格子、口径、需要的业务数据、可用方式、缺数表现。
-
-### 4.0 先说一个会影响验收的表现
-
-**指标没有数据时,格子显示为「—」,同时状态标记为黄色「风险」。** 也就是说**没接数的格子,和业务真的有风险的格子,看起来是一样的**。
+## 6. 功能一:九宫格看板
 
-区分方法:数值为「—」即未取到数据,属对接范围问题;数值有数字而标黄,才是业务指标未达目标。首次验收时请按此判读,不要把大面积黄色误认为业务告警。
+九宫格是首页九个模块入口,每个模块显示三到四张核心指标。以下逐模块列出口径、需要的业务数据与缺数表现;总表见第 3 章。
 
-### 4.1 产销协同
+### 6.1 产销协同
 
 | 格子 | 业务口径 |
 |---|---|
@@ -135,13 +251,13 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 | 已通知发货数量、已发货数量 | 建议 | 交付进度 |
 | 订单状态、关闭标记 | 建议 | 排除作废单 |
 
-**订单头与订单行必须一起提供**:周期起点在头上、终点在行上,只给行会导致评审周期与满足率算不出来。
+**订单头与订单行必须一起提供**:周期起点在头上、终点在行上,只给行会导致评审周期与满足率算不出来。**订单行可走推送,订单头目前只能走方式甲或乙。**
 
-**销售发货**(成品库存周转必接,方式甲 / 乙):订单行、应发与实发数量、实际发货日期。
+**销售发货**(成品库存周转必接,方式甲 / 乙):订单行、应发与实发数量、实际发货日期。这笔数据归成品仓储,见 §6.7。
 
 **缺数表现**:缺签订日期或四个交期全缺,评审周期与满足率留空;缺计划员,主计划人效按 1 人计算;缺发货记录,成品库存周转留空。
 
-### 4.2 制造协同
+### 6.2 制造协同
 
 | 格子 | 业务口径 |
 |---|---|
@@ -150,12 +266,12 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 | 订单排程人效(行/人) | 工单数 ÷ 作业人数合计 |
 | 在制库存周转(天) | 在制数量 ÷ 完工数量 × 30,**按数量计算** |
 
-**生产工单**(必接,方式甲 / 乙)
+**生产工单**(必接,方式甲 / 丙 / 乙)
 
 | 数据项 | 必接 / 建议 | 说明 |
 |---|---|---|
 | 工单号 | 必接 | 排程与在制的主键 |
-| 销售订单号 | 必接 | 串联订单到工单 |
+| 销售订单号 | 必接 | 串联订单到工单,数据归产销协同 |
 | 工单数量、完工数量 | 必接 | 在制量与周转 |
 | 开单日期、下达日期、需求完工日 | 必接 | 周期起点与是否逾期 |
 | 物料编码与名称、工单状态、加急标记、产线 | 建议 | 详情与分布 |
@@ -170,9 +286,11 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 | 作业人数或班组人数 | 必接 | 订单排程人效的分母,缺了按 1 人计算 |
 | 工作中心、作业人员 | 建议 | 分布图与详情 |
 
+**工序级日计划与排产结果目前没有推送对象**,只能走方式甲或乙;工单本身可走推送。
+
 **缺数表现**:缺工序计划日期,排程周期与满足率留空;缺需求完工日,满足率按「未满足」计入,数值偏低。
 
-### 4.3 供应协同
+### 6.3 供应协同
 
 | 格子 | 业务口径 |
 |---|---|
@@ -184,34 +302,36 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 | 业务数据包 | 必须提供 | 建议再提供 | 可用方式 |
 |---|---|---|---|
 | **交货计划** | 交货计划号、对应采购订单行、物料、计划数量、**需求日期**、**提交日期**、已发数量、状态 | 剩余数量、退货数量、最近发货日 | 甲 / 乙 |
-| **采购订单** | 采购单号与行号、供应商、物料、订货数量、已收数量、订单日期、要货日期、状态 | 退货数量、采购员、关联工单 | 甲 / 乙 |
+| **采购订单** | 采购单号与行号、供应商、物料、订货数量、已收数量、订单日期、要货日期、状态 | 退货数量、采购员、关联工单 | 甲 / 丙(已开通)/ 乙 |
 | **物料需求计划** | 需求单号、物料、需求数量、需求日期 | 供应商、状态 | 甲 / 乙 |
 | **采购申请** | 申请单号、物料、申请数量、申请日期 | 发出日、到货日、状态 | 甲 / 乙 |
 | **齐套与现存量** | 物料、可用库存或库位存量 | 工单缺料数量、风险标记 | 甲 / 乙 |
-| **出入库流水与期初** | 见 §2.2 与 §4.5 | — | 甲(推荐)/ 丙 |
+| **出入库流水与期初** | 见 §6.5,数据归来料仓储 | — | 甲 / 丙(已开通) |
+
+**交货计划目前没有推送对象**,而它同时是本模块两张格子与采购执行三张格子的起点,只能走方式甲或乙。
 
 **缺数表现**:缺交货计划的需求日期或提交日期,物料计划周期留空;缺交货计划数量,满足率留空;缺出入库流水或领料记录,物料库存周转留空(不会显示 0)。
 
-### 4.4 采购执行
+### 6.4 采购执行
 
 | 格子 | 业务口径 |
 |---|---|
 | 物料交货周期(天) | 从**交货计划需求日期**到**实际收货日期**的平均天数,收货取正式收货与来料检验收货中最晚一次 |
 | 物料交货满足率(%) | **收货数量不少于订货数量,或状态已完成**的采购订单行占比(按数量与状态,不是按时间) |
-| 物料采购人效(颗/人) | 供应商数量为分母,见 §3.2 |
+| 物料采购人效(颗/人) | 供应商数量为分母,见 §5.2 |
 | 采购在途周转(天) | 未收数量 ÷ 订货数量 × 30 的平均值 |
 
 | 业务数据包 | 必须提供 | 建议再提供 | 可用方式 |
 |---|---|---|---|
-| **收货记录** | 收货日期、物料、收货数量、对应采购订单行 | 供应商、批次 | 甲 / 乙 |
-| **供应商发货执行** | 送货单号、物料、实发数量、发货日期 | 对应采购订单行 | 甲 / 乙 |
-| **来料检验收货行** | 收货数量、不合格数量、收货日期、物料 | 抽检数量、检验结论 | 甲 / 乙 |
-| **采购退货** | 退货数量、物料、对应采购订单 | 原因、状态 | 甲 / 乙 |
-| **欠料** | 工单、缺料物料、欠数 | 供应商、预计到货 | 甲 / 乙 |
+| **收货记录** | 收货日期、物料、收货数量、对应采购订单行 | 供应商、批次 | 甲 / 丙(已开通)/ 乙 |
+| **供应商发货执行** | 送货单号、物料、实发数量、发货日期 | 对应采购订单行 | 甲 / 丙(已开通)/ 乙 |
+| **来料检验收货行** | 收货数量、不合格数量、收货日期、物料 | 抽检数量、检验结论 | 甲 / 丙(已开通)/ 乙 |
+| **采购退货** | 退货数量、物料、对应采购订单 | 原因、状态 | 甲 / 丙(已开通)/ 乙 |
+| **欠料** | 工单、缺料物料、欠数 | 供应商、预计到货 | 甲 / 丙(已开通)/ 乙 |
 
-**特别说明**:采购执行这一模块目前没有现成的外部对接模板,需按贵方实际情况一次性约定。**情况 A**:采购与收货数据在贵方 ERP 只读库中,走方式甲,我方按贵方表结构建立字段对应。**情况 B**:采购数据由贵方系统主动推送,需我方先登记对象并签发密钥。**情况 C**:两者都不具备,则本模块四张格子不纳入本期范围。
+**本模块三张周期与满足率格子的起点数据不在本模块**:物料交货周期的起点是供应协同的交货计划需求日期,满足率与在途周转的分母是供应协同的采购订单订货数量。**只接本模块的收货与发货数据,这三张格子仍然算不出来。**
 
-### 4.5 来料仓储
+### 6.5 来料仓储
 
 | 格子 | 业务口径 |
 |---|---|
@@ -222,19 +342,22 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 
 | 业务数据包 | 必须提供 | 建议再提供 | 可用方式 |
 |---|---|---|---|
-| **出入库流水** | 物料、数量、出入库日期、**出入库业务类型**、**审核状态** | 库位、批次、单价或金额 | 甲(推荐)/ 丙(见 §2.2) |
-| **库存期初余额** | 物料、期初数量、起算日期 | 库位、批次 | 甲 / 丙 |
-| **生产领料与上线** | 领料单、工单、物料、实发数量、发到产线日期 | — | 甲 / 乙 |
-| **工单开工日** | 工单、实际开工日期 | — | 甲 / 乙 |
-| **采购收货** | 收货单号与行号、物料、实收数量、收货日期 | 供应商、采购订单、批次、库位 | 甲 / 乙 |
-| **来料检验结论** | 检验单号、结论、检验日期、物料 | 不合格数量 | 甲 / 乙 |
-| **委外发料、生产退料** | 单据号、物料、数量、日期 | — | 甲 / 乙 |
+| **出入库流水** | 物料、数量、出入库日期、**出入库业务类型**、**审核状态** | 库位、批次、单价或金额 | 甲 / 丙(已开通) |
+| **库存期初余额** | 物料、期初数量、起算日期 | 库位、批次 | 甲 / 丙(已开通) |
+| **月均库存金额** | 期间、月均库存金额 | — | 甲 / 丙(已开通) |
+| **生产领料与上线** | 领料单、工单、物料、实发数量、发到产线日期 | — | 甲 / 丙(属出入库流水的一种业务类型) |
+| **工单用料与开工日** | 工单、用料物料、需求数量、实际开工日期 | — | 甲 / 丙(已开通)/ 乙;数据归制造执行 |
+| **采购收货** | 收货单号与行号、物料、实收数量、收货日期 | 供应商、采购订单、批次、库位 | 甲 / 丙(已开通)/ 乙 |
+| **来料检验结论** | 检验单号、结论、检验日期、物料 | 不合格数量 | 甲 / 丙(已开通)/ 乙 |
+| **委外发料、生产退料** | 单据号、物料、数量、日期 | — | 甲 / 丙(属出入库流水的一种业务类型) |
+
+**出入库流水是全系统共用的一套数据**:供应协同的物料库存周转、制造执行的订单制造满足率、成品仓储的订单发货、运营指标的质量退货率,读的都是这一套流水,靠业务类型区分。接一次,四处都受益。
 
 **关于单价**:周转类指标提供**物料单价或出库成本**时按成本口径计算,不提供时按数量口径计算。两种口径的周转天数可能相差数倍,且看板上不作区分。若贵方需要与财务口径对账,请务必提供单价或成本。
 
 **缺数表现**:缺业务类型,物料上线与周转全部留空;缺工单开工日,上线满足率留空;缺单价,周转改按数量口径且不提示。
 
-### 4.6 制造执行
+### 6.6 制造执行
 
 | 格子 | 业务口径 |
 |---|---|
@@ -244,14 +367,16 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 
 | 业务数据包 | 必须提供 | 建议再提供 | 可用方式 |
 |---|---|---|---|
-| **生产任务与用料** | 工单号、物料、计划数量、计划完工日、完工标记与完工时间 | 产线、班组 | 甲 / 乙 |
-| **生产入库流水** | 工单、物料、入库数量、入库日期、审核状态 | 批次 | 甲(推荐)/ 丙(见 §2.2) |
-| **车间报工** | 工单或报工单号、开工与报工日期、报工数量 | 人员 | 甲 / 乙 |
+| **生产任务与用料** | 工单号、物料、计划数量、计划完工日、完工标记与完工时间、实际开工日期 | 产线、班组 | 甲 / 丙(已开通)/ 乙 |
+| **生产入库流水** | 工单、物料、入库数量、入库日期、审核状态 | 批次 | 甲 / 丙(已开通);数据归来料仓储的同一套流水 |
+| **车间报工** | 工单或报工单号、开工与报工日期、报工数量 | 人员 | 甲 / 丙(已开通)/ 乙 |
 | **过程检验** | 检验单号、工单、工序、结论、检验日期 | 物料、检验员、抽样数 | 甲 / 乙 |
 
+**本模块的生产任务与用料同时被来料仓储的物料上线满足率依赖**(提供工单用料种类与开工日),漏接会同时影响两个模块。
+
 **缺数表现**:未完工的订单不进入制造周期统计,因此若贵方只提供在制工单而无完工记录,这张格子会留空。
 
-### 4.7 成品仓储
+### 6.7 成品仓储
 
 | 格子 | 业务口径 |
 |---|---|
@@ -261,21 +386,23 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 
 | 业务数据包 | 必须提供 | 建议再提供 | 可用方式 |
 |---|---|---|---|
-| **销售订单与用料** | 订单号、物料、计划数量、计划交货日、完工标记 | — | 甲 / 乙 |
-| **销售出库流水** | 订单、物料、出库数量、出库日期、审核状态 | 批次、客户 | 甲(推荐)/ 丙(见 §2.2) |
-| **成品检验报检** | 报检日期(取订单行最早一次)、检验结论 | 检验单号、明细 | 甲 / 乙 |
+| **销售订单与用料** | 订单号、物料、计划数量、计划交货日、完工标记 | — | 甲 / 丙(已开通)/ 乙 |
+| **销售出库流水** | 订单、物料、出库数量、出库日期、审核状态 | 批次、客户 | 甲 / 丙(已开通);数据归来料仓储的同一套流水 |
+| **成品检验报检** | 报检日期(取订单行最早一次)、检验结论 | 检验单号、明细 | 甲 / 丙(已开通)/ 乙 |
 | **成品入库** | 入库单号、工单或订单、物料、入库数量、入库日期 | 部门、状态 | 甲 / 乙 |
-| **成品结存与期初** | 物料、数量、日期 | 库位 | 甲 / 丙 |
+| **成品结存与期初** | 物料、数量、日期 | 库位 | 甲 / 丙(已开通) |
+
+**订单发货满足率的计划交货日来自产销协同的销售订单行**,不接销售订单则这张格子算不出。本模块的销售出库流水同时是产销协同成品库存周转的来源。
 
-### 4.8 异常监控
+### 6.8 异常监控
 
 两张格子:**各类异常事件数量(个)**、**各类异常事件处理时效(小时)**。
 
-**不需要贵方提供异常单**,异常由我方监控规则读已入站数据自动生成,详见第 7 章。
+**不需要贵方提供异常单**,异常由我方监控规则读已入站数据自动生成,详见第 9 章。
 
 两张格子的空值表现不同:**异常数量在没有异常时显示 0**;**处理时效在没有已关闭异常时留空**。这是设计如此,不是故障。
 
-### 4.9 运营指标(全场)
+### 6.9 运营指标(全场)
 
 本期纳入一张格子:
 
@@ -283,13 +410,13 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 |---|---|
 | 质量退货率(PPM) | 质量退货数量 ÷ 出库数量 × 1,000,000。单位是百万分之一,**不是百分比** |
 
-需要提供:**质量退货流水**(因质量原因退回的数量、退货日期、对应物料,含业务类型与审核状态),以及对应的出库数量。可用方式:甲(推荐)/ 丙(见 §2.2)。
+需要提供:**质量退货流水**(因质量原因退回的数量、退货日期、对应物料,含业务类型与审核状态),以及对应的出库数量。可用方式:甲 / 丙(已开通)。这两项都来自来料仓储的同一套出入库流水,靠业务类型区分。
 
-**本期不纳入的四张格子**:订单交付周期、订单交付满足率、生产效率、全库存周转。这四项涉及跨模块合并口径与产线工时数据,将在后续版本提供,本期不要求贵方为此准备数据,也不列入验收范围。
+**九宫格另有四张跨模块格子**:订单交付周期、订单交付满足率、生产效率、全库存周转。它们由平台根据已接入的订单、发货、报工与库存汇总,不需要单独再交一套格子数据。订单、发货、报工或库存缺了,对应格子会显示无数据,而不是 0。
 
 ---
 
-## 5. 功能二:模块详情看板
+## 7. 功能二:模块详情看板
 
 每个模块的详情看板与该模块的九宫格**读同一套指标**,不额外要求新的数据包。区别在三处:
 
@@ -299,19 +426,19 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 
 **三,明细列表要可读字段。** 单号、物料名称、客户或供应商名称、状态、负责人,这些在九宫格里不需要,在详情列表里缺了就只能看到编码。
 
-按模块额外建议提供的明细,已在第 4 章各表的「建议再提供」列中标出。
+按模块额外建议提供的明细,已在第 6 章各表的「建议再提供」列中标出。
 
 ---
 
-## 6. 功能三:智慧诊断
+## 8. 功能三:智慧诊断
 
 诊断分两层能力,对数据的要求不同,请分别确认。
 
-### 6.1 指标树逐层下钻
+### 8.1 指标树逐层下钻
 
 指标树的取数与九宫格完全同源,**哪个模块的九宫格有数,该模块的指标树就能亮**,不需要额外数据。
 
-### 6.2 点开「业务依据」
+### 8.2 点开「业务依据」
 
 点开某个指标看它由哪些具体单据构成。这一层**只对已登记的指标开放**,未登记的指标点开会提示「尚未登记可追溯业务依据」。当前覆盖情况如下,请业务按此设定预期:
 
@@ -326,13 +453,13 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 | 成品仓储 | 三个指标 |
 | 运营指标 | 仅质量退货率 |
 
-点开依据要求提供**单据行级明细**,不能只给汇总数。哪些明细对应哪个模块,见第 4 章各表。
+点开依据要求提供**单据行级明细**,不能只给汇总数。哪些明细对应哪个模块,见第 6 章各表。
 
 ---
 
-## 7. 功能四:异常监控
+## 9. 功能四:异常监控
 
-### 7.1 不需要贵方送异常单
+### 9.1 不需要贵方送异常单
 
 异常由我方监控规则定时扫描已入站数据自动生成,贵方不需要提供异常台账。当前内置三条规则:
 
@@ -346,7 +473,7 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 
 供应协同、来料仓储、制造执行、成品仓储四个模块目前没有内置监控规则。
 
-### 7.2 部分对接时的表现
+### 9.2 部分对接时的表现
 
 规则能否工作取决于**它实际依赖的数据**,不取决于模块编号。常见情况如下:
 
@@ -357,19 +484,19 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 | 上述 + 交货计划与采购订单 | 可用 | 可用 | **可用** |
 | 只有采购与收货数据 | 不可用 | 不可用 | **不可用**(缺交货计划) |
 
-### 7.3 两个必须知晓的行为
+### 9.3 两个必须知晓的行为
 
 **一,缺数据是静默的。** 某条规则的来源数据为空时,规则照常运行、判定为「无异常」、**不报错也不告警**。因此「异常数为 0」有两种可能:真的没有异常,或者这条规则的数据没接进来。请在验收时逐条确认数据来源,不要仅凭 0 判断。
 
 **二,规则需要先配置责任人。** 每条规则要指定默认责任部门与处理人,未配置时即使判定出异常也不会生成待办。这属我方实施配置项,会在上线前完成,此处列出供贵方了解。
 
-### 7.4 订单全链路视图
+### 9.4 订单全链路视图
 
 异常监控模块另有一张订单全链路视图,按阶段展示一张订单从接单到交付的进展。当前**订单接收、设计核验、物料采购三个阶段有数据**,本体生产与总装两个阶段为预留,本期不展示实际数据。
 
 ---
 
-## 8. 功能五:ChatBI
+## 10. 功能五:ChatBI
 
 用日常语言提问指标,例如「上个月订单评审周期多少天」「哪个模块满足率最低」。
 
@@ -382,7 +509,7 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 
 ---
 
-## 9. 缺数影响对照表
+## 11. 缺数影响对照表
 
 按数据包查它缺失会影响什么。用于评估分批上线的取舍。
 
@@ -392,74 +519,73 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 | **客户、供应商主数据** | 详情列表与诊断标题只显示编码;供应商缺失还会影响物料采购人效的分母 |
 | **员工与岗位人数** | 物料计划、物料仓储、订单制造、成品仓储四张人效格子留空 |
 | **销售订单头** | 订单评审周期与满足率留空(周期起点在头上) |
-| **销售订单行** | 产销协同四张格子全部留空;订单交付延期预警失效;制造协同无法串联订单 |
+| **销售订单行** | 产销协同四张格子全部留空;订单交付延期预警失效;制造协同无法串联订单;成品仓储发货满足率留空 |
 | **订单行上的计划员** | 主计划人效按 1 人计算,数值明显偏大 |
 | **合同评审环节** | 九宫格不受影响;诊断「点开依据」在产销协同基本不可用 |
 | **销售发货记录** | 成品库存周转留空;成品仓储发货周期与满足率留空 |
 | **生产工单** | 制造协同四张格子全部留空 |
 | **日计划或排产结果** | 订单排程周期与满足率留空;排程人效按 1 人计算 |
-| **交货计划** | 物料计划周期与满足率留空;物料交货周期留空;**采购交期延误规则失效** |
+| **交货计划** | 物料计划周期与满足率留空;**采购执行的物料交货周期留空**;**采购交期延误规则失效** |
 | **采购订单** | 物料计划满足率、物料交货满足率、采购在途周转留空 |
 | **收货记录** | 物料交货周期与满足率留空 |
 | **物料齐套检查** | **制造工单缺料规则失效**;供应协同齐套类诊断依据不可用 |
 | **出入库流水的业务类型** | 物料库存周转、物料上线、订单制造、订单发货、质量退货率全部留空 |
 | **库存期初余额** | 全部周转类格子留空(仅有结存快照算不出周转) |
 | **物料单价或出库成本** | 周转类改按数量口径计算,与财务口径无法对账,且界面不提示 |
-| **工单开工日** | 物料上线满足率留空 |
+| **工单用料与开工日** | 物料上线满足率留空 |
 | **工单完工记录** | 订单制造周期留空(仅统计已完工订单) |
 | **成品报检记录** | 订单发货周期起点缺失,改取建单日期,周期偏长 |
 | **质量退货流水** | 质量退货率留空 |
 
 ---
 
-## 10. 全部接通后的功能可用性
+## 12. 全部接通后的功能可用性
 
-假设第 3 至第 4 章列出的「必须提供」全部到位:
+假设第 5 至第 6 章列出的「必须提供」全部到位:
 
 | 功能 | 状态 | 说明 |
 |---|---|---|
 | 九宫格看板 | **可用** | 八个模块的指标可出数;运营指标模块本期仅质量退货率 |
 | 模块详情看板 | **可用** | 含趋势、分层与按条件筛选 |
 | 智慧诊断指标树 | **可用** | 全模块逐层下钻 |
-| 智慧诊断点开依据 | **部分可用** | 覆盖范围见 §6.2,来料仓储、制造执行、成品仓储各仅两三个指标 |
+| 智慧诊断点开依据 | **部分可用** | 覆盖范围见 §8.2,来料仓储、制造执行、成品仓储各仅两三个指标 |
 | 异常监控 | **可用** | 三条内置规则均可工作 |
 | 订单全链路视图 | **部分可用** | 前三个阶段有数据,后两个阶段本期预留 |
 | ChatBI | **可用** | 随指标可用范围 |
 
-**本期明确不纳入**:订单交付周期、订单交付满足率、生产效率、全库存周转四张格子。
+**跨模块四格**:订单交付周期、订单交付满足率、生产效率、全库存周转随订单、发货、报工、库存数据到位后由平台计算。
 
 ---
 
-## 11. 分批上线建议
+## 13. 分批上线建议
+
+分批时请连同第 4 章的跨功能依赖一起看,避免某一批只接了格子所属模块、却漏了它依赖的上游数据。
 
 **第一批 · 主数据与订单。** 物料、客户、供应商、员工岗位人数、销售订单头与行、合同评审环节。上线后可点亮产销协同的评审周期、满足率与主计划人效,诊断可点开评审依据,ChatBI 可问这些指标,订单交付延期预警可启用。
 
-**第二批 · 计划与采购。** 生产工单、日计划、交货计划、采购订单、收货记录、物料齐套检查。上线后可点亮制造协同与供应协同、采购执行的周期与满足率,另外两条监控规则可启用。
+**第二批 · 计划与采购。** 生产工单、日计划、交货计划、采购订单、收货记录、物料齐套检查。上线后可点亮制造协同与供应协同、采购执行的周期与满足率,另外两条监控规则可启用。**交货计划是这一批的关键**:供应协同两张格子与采购执行三张格子都以它为起点。
 
-**第三批 · 库存与仓储制造。** 出入库流水(含业务类型与审核状态)、库存期初、领料上线、工单开工与完工、成品报检与出库、质量退货。上线后可点亮周转类格子与来料仓储、制造执行、成品仓储、质量退货率。若同时提供单价,周转类可按成本口径计算。
+**第三批 · 库存与仓储制造。** 出入库流水(含业务类型与审核状态)、库存期初、月均库存金额、领料上线、工单用料与开工、工单完工、成品报检与出库、质量退货。上线后可点亮周转类格子与来料仓储、制造执行、成品仓储、质量退货率。若同时提供单价,周转类可按成本口径计算。
 
 每批上线后可独立验收,不必等全部数据到齐。
 
 ---
 
-## 12. 业务签收表
+## 14. 业务签收表
 
 请业务对每个数据包逐项确认:
 
 | 确认项 | 说明 |
 |---|---|
 | 贵方有这笔业务 | 没有这笔业务的数据包直接标注「无此业务」,相关格子从验收范围移除 |
-| 能提供「必须提供」列 | 逐项核对第 3、4 章 |
+| 能提供「必须提供」列 | 逐项核对第 5、6 章 |
 | 由哪套系统出 | ERP / MES / WMS / QMS / SRM,可多套,我方按业务键合并 |
 | 用哪种方式提供 | 数据库同步 / 标准接口推送 / 接口拉取,见第 2 章 |
+| 本批是否已覆盖依赖项 | 对照第 4 章,确认本批格子依赖的上游数据也在范围内 |
 | 出入库流水能否带业务类型与审核状态 | 决定周转类与仓储制造类格子能否交付,见 §2.2 |
-| 能否提供物料单价或出库成本 | 决定周转按成本口径还是数量口径,见 §4.5 |
+| 能否提供物料单价或出库成本 | 决定周转按成本口径还是数量口径,见 §6.5 |
 | 能否提供库存期初余额 | 周转类必须「期初 + 流水」,只给结存快照算不出 |
 
-勾完交实施,按技术页逐对象对字段。
-
-本页「必接 / 建议」与技术页的「必 / 建」是同一套口径;两处若有出入,**以本页为准**。
-
 ---
 
 ## 变更记录
@@ -470,3 +596,4 @@ Ai-DOP 不直接连贵方系统出报表。所有对外数据先进入我方数
 | 2026-09-11 | 增补方式丙对接编码对照 |
 | 2026-09-22 | 按现行实现校正评审周期口径、人效分母、周转口径与开通状态 |
 | 2026-09-22 | **改版为按功能组织**:新增逐模块九宫格口径与可用方式、缺数影响对照表、功能可用性总表、分批上线建议、监控规则部分对接表现、三种方式的分情况应对;对接编码全部移至技术页,本页不再出现编码;明确本期不纳入的四张格子 |
+| 2026-09-27 | 新增第 3 章「模块 · 功能 · 所需业务数据 · 对接方式」总表与第 4 章「跨功能数据依赖」;方式丙已开通对象由 7 个更新为 20 个并改以业务名列出,补「仅能走甲/乙」的数据清单;**更正「出入库业务类型无推送通道」的旧结论**(现行推送约定中业务类型为必填、审核状态可选,无需我方扩展),相应删去「依赖业务类型的指标建议走方式甲」的建议;各模块章节补注数据归属;全页去除指向技术页的字段与编码说明 |

+ 167 - 13
doc/plan/系统集成/Ai-DOP第三方对接业务数据详表-技术页.md

@@ -5,14 +5,14 @@
 | 读者 | 第三方开发与实施、Ai-DOP 数据中台配置 |
 | 配套 | 业务含义见 [`Ai-DOP第三方对接业务数据详表-业务页.md`](./Ai-DOP第三方对接业务数据详表-业务页.md);接口协议见 [`Ai-DOP第三方系统集成指南.md`](./Ai-DOP第三方系统集成指南.md) |
 | 范围 | 三种入站方式的技术契约、各对象落表栏位、九宫格逐格计算口径、监控规则数据源、联调验收项 |
-| 日期 | 2026-09-22 |
+| 日期 | 2026-09-27(开通清单与契约字段按当日 `mdp_entity` 与代码契约核对) |
 
 ## 阅读约定
 
 - **源字段**:贵方库列名、拉取接口的 JSON 键,或推送接口的**对外契约字段**。
 - **贴源**:先入 `mdp_stg_*`(整行 JSON 留存);三种方式落同一张表。
 - **标准层 / 宽表**:转换写入的栏位,看板与诊断读这里。
-- **必 / 建**:与业务页「必接 / 建议」同一套口径;两处若有出入,**以业务页为准**。
+- **必 / 建**:与业务页「必接 / 建议」同一套口径。**指标口径以业务页为准,字段清单与开通状态以本页及 `GET .../schema` 为准**;业务页不出现表名与栏位名,不要拿它核对字段。
 
 ### 分层约束(不可绕过)
 
@@ -48,7 +48,53 @@
 
 推送入口:`POST /api/mdp/inbound/{entityCode}`(路径不含 `/v1/`)。取字段清单:`GET /api/mdp/inbound/{entityCode}/schema`。
 
-**开通制**:`entityCode` 为白名单,未开通的对象 `POST` 与 `GET .../schema` 均返回 403。当前**已开通 8 个对象**,契约字段如下(`必` = 协议必填,缺失则该行被拒)。
+**开通制**:`entityCode` 为白名单,未开通的对象 `POST` 与 `GET .../schema` 均返回 403。契约字段中 `必` = 协议必填,缺失则该行被拒。
+
+#### 1.2.1 对象开通清单
+
+开通需三件事同时成立,缺一都会 403:代码层有字段契约、`mdp_entity.inbound_enabled=1`、该租户在 `mdp_inbound_grant` 上有授权行。
+**`GET /api/mdp/inbound/{entityCode}/schema` 是字段清单与开通状态的权威来源**,本页表格与它不一致时以接口为准。
+
+下表「贴源表」「业务键」取自 `mdp_entity` 的实际登记值(核对日期见页首)。
+
+| entityCode | 业务对象 | 贴源表 | 业务键 | 状态 |
+|---|---|---|---|:---:|
+| `MDM_ITEM` | 物料主数据 | `mdp_stg_item` | `Domain,ItemNum` | 已开通 |
+| `MDM_EMPLOYEE_HEADCOUNT` | 员工与岗位人数 | `mdp_stg_employee` | `Domain,Employee` | 已开通 |
+| `S1_SALES_ORDER_ENTRY` | 销售订单行 | `mdp_stg_so` | `bill_no,entry_seq` | 已开通 |
+| `S2_WORK_ORDER_SCHEDULE` | 排程工单 | `mdp_stg_schedule` | `WorkOrder,ItemCode` | 已开通 |
+| `S3_PURCHASE_ORDER` | 采购订单 | `mdp_stg_purchase_order` | `PoNo,PoLine` | 已开通 |
+| `S3_PURCHASE_RECEIPT` | 采购收货 | `mdp_stg_purchase_receipt` | `Domain,Receiver,Line` | 已开通 |
+| `S4_SHIPMENT` | 供应商发货 | `mdp_stg_s4_shipment` | `ShipmentNo` | 已开通 |
+| `S4_IQC` | 来料检验 | `mdp_stg_s4_iqc` | `PoNo,PoLine,ItemCode` | 已开通 |
+| `S4_RETURN` | 采购退货 | `mdp_stg_s4_return` | `PoNo,PoLine,ItemCode` | 已开通 |
+| `S4_SHORTAGE` | 缺料 | `mdp_stg_s4_shortage` | `WorkOrder,ItemCode` | 已开通 |
+| `S5_INVENTORY_TXN` | 出入库流水 | `mdp_stg_inv_trans_inbound` | `Id` | 已开通 |
+| `S5_INVENTORY_OPENING_BALANCE` | 库存期初 | `mdp_stg_t8_kc_tz_list` | `Id` | 已开通 |
+| `S5_INVENTORY_BALANCE_MONTHLY` | 月度库存金额 | `mdp_stg_inv_bal_monthly` | `PeriodYm,ItemCode` | 已开通 |
+| `S5_WORK_ORDER_BOM` | 工单用料 | `mdp_stg_wo_bom` | `Domain,OrderNo,ItemCode` | 已开通 |
+| `S6_REPORT_TXN` | 车间报工 | `mdp_stg_s6_report` | `Domain,ReportId` | 已开通 |
+| `S6_WORK_ORDER_LINE` | 生产工单行 | `mdp_stg_wo_line` | `Domain,OrderNo` | 已开通 |
+| `S7_FINISHED_ONHAND` | 成品结存 | `mdp_stg_t8_kc_zj_list` | `Id` | 已开通 |
+| `S7_FINISHED_OPENING_BALANCE` | 成品期初 | `mdp_stg_t8_kc_zj_list` | `Id` | 已开通 |
+| `S7_FQC_TASK_TXN` | 成品检验任务 | `mdp_stg_fqc_pull` | `BillNo` | 已开通 |
+| `S7_SALES_ORDER_LINE` | 销售订单行(成品仓储口径) | `mdp_stg_wo_line` | `Domain,OrderNo,LineNo` | 已开通 |
+| `MDM_CUSTOMER` | 客户主数据 | 未登记 | — | 待登记 |
+| `MDM_SUPPLIER` | 供应商主数据 | 未登记 | — | 待登记 |
+| `MDM_LOCATION` | 库位 | 未登记 | — | 待登记 |
+| `MDM_SOURCE_LIST` | 货源清单 | 未登记 | — | 待登记 |
+| `S1_REQUIREMENT_EXAMINE_RESULT` | 齐套核验结果头 | 登记值待更正 | — | 待开通 |
+| `S1_REQUIREMENT_EXAMINE_DETAIL` | 齐套核验结果明细 | 登记值待更正 | — | 待开通 |
+
+**「待登记」与「待开通」的区别**:前四个对象代码层已有字段契约,但 `mdp_entity` 中没有对应行,开通需先登记实体与贴源表;后两个已有实体行但 `inbound_enabled=0`,且贴源表当前登记为 `mdp_stg_so`(占位值),开通前须一并更正。两类都请在排期前提出,由我方配置后再联调。
+
+**注意 `S5_INVENTORY_TXN` 的贴源表是 `mdp_stg_inv_trans_inbound`,不是 `mdp_stg_t8_kc_tz_list`。** 推送流水与方式甲抽取的 T8 流水落在不同贴源表,再由转换统一汇入标准层;两者不互相覆盖。`S7_SALES_ORDER_LINE` 与 `S6_WORK_ORDER_LINE` 共用 `mdp_stg_wo_line`,靠业务键区分。
+
+**没有推送契约的对象**(只能走方式甲或乙):销售订单头、合同评审环节、发货计划与发货单(ASN)、工序级日计划与排产结果、物料需求计划、采购申请、交货计划、现存量快照、过程检验、成品入库。
+
+另有历史 `entityCode` `S1_SEORDER_ENTRY` 在 `mdp_entity` 中仍为 `inbound_enabled=1`,但代码层无字段契约、也无授权行,**不要使用**,请统一用 `S1_SALES_ORDER_ENTRY`。
+
+#### 1.2.2 主数据与库存类契约
 
 **公共契约字段(所有对象)**
 
@@ -79,6 +125,8 @@
 |---|:---:|---|
 | `Domain` | 必 | 账套 |
 | `Employee` | 必 | 工号 |
+| `Position` | 必 | **岗位分类**,人效分母按此分「计划 / 仓管 / 生产」 |
+| `EmploymentStatus` | 必 | **在职状态**,只有在职才计入分母 |
 | `Name` | 建 | 姓名 |
 | `Department` | 建 | 部门 |
 
@@ -105,7 +153,7 @@
 
 **订单头未开通推送。** 评审周期起点(签订日期)在订单头上,因此**只推订单行无法算出评审周期与满足率**。当前可行方案:订单头走方式甲或方式乙,或申请开通订单头对象。
 
-**出入库流水** `S5_INVENTORY_TXN` → `mdp_stg_t8_kc_tz_list`
+**出入库流水** `S5_INVENTORY_TXN` → `mdp_stg_inv_trans_inbound`
 
 | 契约字段 | 必/建 | 业务含义 |
 |---|:---:|---|
@@ -122,7 +170,9 @@
 
 上表是 **v1**。**v2**(请求头 `X-Mdp-Contract-Version: v2`)改为中立字段:`Id`、`BizDocType`(必,须为已登记枚举,非法值整批 400),以及可选的 `TransType`(12 个中立阶段码之一,非法值整批 400)、`SrcTransType`(对方原始事务码,未能映射时进隔离表)、`Location`、`ItemCode`、`Qty`、`DocDate`、`TransTime`。不传 `TransType` 时只按 `BizDocType` 映射阶段码,不按库位推导。
 
-**v1 契约中不含业务类型。** 出入库业务类型与汇总、作废标记位于单据头。v2 用 `BizDocType` / `TransType` 表达。仅推 v1 流水时,按业务类型过滤的指标仍算不出。
+**v1 契约中不含业务类型**(业务类型与汇总、作废标记位于单据头),**v2 已用 `BizDocType` / `TransType` 表达且已开通**。
+
+也就是说,依赖业务类型的五个指标——物料库存周转、物料上线、订单制造、订单发货、质量退货率——**走方式丙即可交付,无需我方扩展契约,也无需改走方式甲**;前提是推送时声明 `X-Mdp-Contract-Version: v2` 并填 `BizDocType`。**不声明版本按 v1 处理**,这些指标仍算不出。
 
 **库存期初** `S5_INVENTORY_OPENING_BALANCE` → `mdp_stg_t8_kc_tz_list`(按来源标记隔离)
 
@@ -136,6 +186,106 @@
 
 结存快照**只作校验**,不能替代流水:所有周转指标由「期初 + 流水」自算结存。
 
+#### 1.2.3 业务单据类契约
+
+以下对象同样已开通,字段较少且语义直白,逐个列必填与可选项。除表内字段外,公共契约字段同 §1.2.2。
+
+**排程工单** `S2_WORK_ORDER_SCHEDULE` → `mdp_stg_schedule`
+
+必填:`WorkOrder`(工单号)、`ItemCode`(物料编码)、`QtyOrdered`(工单数量)。
+可选:`DocType`(单据类型)、`DueDate`(需求完工日)、`QtyCompleted`(完工数量)、`Status`(状态)、`ProdLine`(产线)。
+
+**生产工单行** `S6_WORK_ORDER_LINE` → `mdp_stg_wo_line`
+
+必填:`Domain`、`OrderNo`(工单号)、`ItemCode`、`QtyPlanned`(计划数量)。
+可选:`QtyCompleted`、`PlanFinishDate`(计划完工日)、`ReleaseTime`(下达时间)、`ClosedFlag`、`VoidFlag`。
+
+**指标必需**:`ReleaseTime` 是订单排程周期的起点、`PlanFinishDate` 是订单制造满足率的判定基准,两者协议可选但缺失则指标算不出。**工序级日计划与排产结果没有推送对象**,订单排程周期的终点(工序计划日期)与订单排程人效的分母(作业人数)只能走方式甲或乙。
+
+**销售订单行(成品仓储口径)** `S7_SALES_ORDER_LINE` → `mdp_stg_wo_line`
+
+必填:`Domain`、`OrderNo`、`LineNo`、`ItemCode`、`QtyPlanned`、`PlanFinishDate`。
+可选:`QtyCompleted`、`ReleaseTime`、`ClosedFlag`、`VoidFlag`。
+
+与 `S6_WORK_ORDER_LINE` 共用贴源表,靠业务键 `Domain,OrderNo,LineNo` 区分。本对象承载成品仓储「订单发货满足率」的计划交货日。
+
+**工单用料** `S5_WORK_ORDER_BOM` → `mdp_stg_wo_bom`
+
+必填:`Domain`、`OrderNo`、`ItemCode`、`QtyRequired`(需求数量)。可选:`LineNo`、`Unit`。
+
+来料仓储「物料上线满足率」的分母(工单用料种类数)取自本对象。
+
+**车间报工** `S6_REPORT_TXN` → `mdp_stg_s6_report`
+
+必填:`Domain`、`ReportId`(报工单号)、`WorkOrderNo`(工单号)、`ReportDate`(报工日期)、`ReportQty`(报工数量)。可选:`StartWorkDate`(开工日期)。
+
+**成品检验任务** `S7_FQC_TASK_TXN` → `mdp_stg_fqc_pull`
+
+必填:`BillNo`(报检单号)、`ProductionOrderNo`(生产订单号)、`MaterialCode`(物料编码)、`Qty`(数量)。可选:`SalesOrderNo`、`Domain`。
+
+成品仓储「订单发货周期」的起点取本对象订单行上最早的一次报检日期。
+
+**月度库存金额** `S5_INVENTORY_BALANCE_MONTHLY` → `mdp_stg_inv_bal_monthly`
+
+必填:`PeriodYm`(期间,`YYYYMM`)、`AvgBalanceAmount`(月均库存金额)。
+可选:`CategoryCode`(品类)、`WarehouseCode`(仓库)、`ItemCode`、`IssueCostAmount`(月出库成本)。
+
+来料仓储「品类物料库存周转」是全系统唯一按成本计算的周转,`IssueCostAmount` 协议可选但指标必需。
+
+**采购订单** `S3_PURCHASE_ORDER` → `mdp_stg_purchase_order`
+
+必填:`PoNo`、`PoLine`、`ItemCode`、`OrderQty`(订货数量)。
+可选:`SupplierCode`、`DueDate`(要货日期)、`OrderDate`(订单日期)、`Status`。
+
+**采购收货** `S3_PURCHASE_RECEIPT` → `mdp_stg_purchase_receipt`
+
+必填:`Domain`、`Receiver`(收货单号)、`Line`(行号)、`ItemCode`、`QtyReceived`(实收数量)。
+可选:`PoNo`、`SupplierCode`、`ReceiptDate`(收货日期)。
+
+**指标必需**:`ReceiptDate` 是物料交货周期的终点,`PoNo` 用于回连采购订单算交货满足率,两者协议可选但缺失则指标算不出。
+
+**供应商发货** `S4_SHIPMENT` → `mdp_stg_s4_shipment`
+
+必填:`ShipmentNo`(送货单号)、`PoNo`、`ItemCode`、`ShipQty`(实发数量)。
+可选:`PoLine`、`SupplierCode`、`ShipDate`(发货日期)。
+
+**来料检验** `S4_IQC` → `mdp_stg_s4_iqc`
+
+必填:`PoNo`、`PoLine`、`ItemCode`、`ReceiptQty`(收货数量)。
+可选:`SupplierCode`、`DefectQty`(不合格数量)、`QcResult`(检验结论)、`ReceiptDate`(收货日期)。
+
+物料交货周期取正式收货与来料检验收货中**最晚**一次,因此两个对象都推送时 `ReceiptDate` 必须准确。
+
+**采购退货** `S4_RETURN` → `mdp_stg_s4_return`
+
+必填:`PoNo`、`PoLine`、`ItemCode`、`ReturnQty`(退货数量)。
+可选:`SupplierCode`、`ReturnReason`、`ReturnStatus`。
+
+**缺料** `S4_SHORTAGE` → `mdp_stg_s4_shortage`
+
+必填:`WorkOrder`、`ItemCode`、`ShortageQty`(欠数)。
+可选:`SupplierCode`、`RiskLevel`、`NeedDate`。
+
+**本对象不驱动「制造工单缺料」监控规则。** 该规则读的是齐套核验结果(`S1_REQUIREMENT_EXAMINE_RESULT` / `_DETAIL`,当前待开通),推 `S4_SHORTAGE` 不能点亮它,见 §11.1。
+
+**齐套核验结果**(待开通)`S1_REQUIREMENT_EXAMINE_RESULT` / `S1_REQUIREMENT_EXAMINE_DETAIL`
+
+头必填:`Id`、`bill_no`、`morder_no`;可选 `sentry_id`、`create_time`、`IsDeleted`。
+明细必填:`Id`、`examine_id`、`item_number`、`lack_qty`、`level`;可选 `num`、`item_name`、`needCount`、`qty`、`is_use`。
+
+**这两张表是「制造工单缺料」规则的唯一数据源。** 齐套核验由我方资源检查作业产出,第三方推这两张结果才能点亮该规则;推物料主数据或库存现存量替代不了这次核验。两个对象当前 `inbound_enabled=0` 且贴源表登记值需更正,需要走推送请提前提出。
+
+**主数据补充契约**(待登记,代码层已有)
+
+| entityCode | 必填 | 可选 |
+|---|---|---|
+| `MDM_CUSTOMER` | `Cust`、`Name` | `Domain`、`Status` |
+| `MDM_SUPPLIER` | `Supp`、`Name` | `Domain`、`Status` |
+| `MDM_LOCATION` | `Location`、`Descr` | `Domain`、`Typed` |
+| `MDM_SOURCE_LIST` | `Supp`、`ItemNum` | `Domain`、`Status` |
+
+`MDM_SOURCE_LIST` 的契约不含供货比例与采购周期,若需要这两列请走方式甲或乙,见 §2.4。
+
 ### 1.3 方式乙 · 接口拉取
 
 我方按对象配置 `GET` 路径、返回数据路径、去重键、增量列,定时调用贵方接口。
@@ -155,17 +305,20 @@
 
 **情况 A · 贵方可提供只读库。** 全部对象走方式甲。表名列名与本页一致则直接配置;不一致则由我方按对象登记字段映射,或请贵方建视图对齐。这是覆盖最全、约束最少的方案。
 
-**情况 B · 贵方只能主动推送,且需要周转类与仓储制造类指标。** 已开通的 8 个对象可直接联调,但因流水的业务类型无推送通道(见 §1.2),须选其一:
+**情况 B · 贵方只能主动推送。** §1.2.1 中标「已开通」的 20 个对象可直接联调,**含周转类与仓储制造类指标所需的出入库流水**——推送时声明 `X-Mdp-Contract-Version: v2` 并填 `BizDocType` 即可,不需要我方扩展契约(见 §1.2.2)。
+
+本情况下真正需要另行安排的是两类:
+
+1. **标「待登记 / 待开通」的对象**(客户、供应商、库位、货源清单、齐套核验结果头与明细):代码层已有契约,需我方先登记实体与贴源表、签发授权,再联调;
+2. **没有推送契约的对象**(销售订单头、合同评审、交货计划、工序级日计划与排产结果等,清单见 §1.2.1 末段):只能改走方式甲或乙,否则相关指标不纳入本期范围。
 
-1. 出入库相关对象改走方式甲,其余走推送(推荐,无需等待开发);
-2. 由我方扩展单据头推送对象与契约字段,排期后再联调;
-3. 该批指标不纳入本期范围。
+第 2 类中**销售订单头与交货计划的影响面最大**:前者缺失则产销协同的评审周期与满足率算不出,后者缺失则供应协同两个指标、采购执行物料交货周期与「采购交期延误」监控规则同时失效。
 
 **情况 C · 贵方只能提供查询接口。** 走方式乙,按 §1.3 逐对象约定,并预留联调时间。
 
 **情况 D · 数据分散在多套系统。** 按系统分别对接,我方在中台按业务键合并。请在联调清单上注明每个对象由哪套系统出、走哪种方式。
 
-**情况 E · 某对象贵方完全没有。** 在联调清单上标注「无此业务」,对应指标从验收范围移除,见业务页第 9 章的缺数影响对照。
+**情况 E · 某对象贵方完全没有。** 在联调清单上标注「无此业务」,对应指标从验收范围移除,见业务页第 11 章的缺数影响对照。
 
 ---
 
@@ -455,7 +608,7 @@
 
 **物料交货周期的收货日期**取 `mdp_std_delivery_result.receipt_date` 与 `mdp_std_s4_iqc.receipt_date` 两者按采购订单行分组后的**最晚一次**,见 §10.4。两个来源提供其一即可,都提供则取较晚者。
 
-**注意**:本模块当前没有现成的外部对接模板,需按贵方实际情况一次性约定,见业务页 §4.4 与本页 §1.5。
+**注意**:本模块当前没有现成的外部对接模板,需按贵方实际情况一次性约定,见业务页 §6.4 与本页 §1.5。
 
 ---
 
@@ -672,7 +825,7 @@
 - 分母是**销售出库数量**,不是发货单的累计发货量,发货单数据不参与本指标。
 - 算式只按业务类型区分退货与出库,**没有「仅质量原因」的过滤条件**。若销售退货中混有非质量原因退货,该指标会偏高;需区分时须在源端用独立的业务类型取值或额外原因字段,并在实施时登记。
 
-**本期不纳入的四个指标**:订单交付周期、订单交付满足率、生产效率、全库存周转。当前无生产管道向指标日值表写入,不要列入验收范围,也不要向贵方索取相应数据。
+**跨模块四个指标**:订单交付周期、订单交付满足率、生产效率、全库存周转由平台按中立层订单、发货、报工与库存计算并写入指标日值。不要为这四格单独准备对象;底层对象缺数时格子为空,验收时按缺数说明,不要当成未实现。
 
 ---
 
@@ -718,7 +871,7 @@
 
 **一,无筛选与有筛选走不同的表。** 详情看板与九宫格在无筛选时只读指标日值表;一旦带上产品、订单、产线等业务维度,改读原子表或维度表,**且不回落到模块汇总值**。因此原子表无数据时,筛选结果为空,而不是显示未筛选的值。
 
-**二,业务依据按指标逐个登记。** 未登记的指标点开会提示尚未登记,覆盖范围见业务页 §6.2。新增可下钻指标需在我方登记,不是配置项。
+**二,业务依据按指标逐个登记。** 未登记的指标点开会提示尚未登记,覆盖范围见业务页 §8.2。新增可下钻指标需在我方登记,不是配置项。
 
 **禁止**让看板直连贵方接口或数据库。数据必须先入贴源、跑转换、生成指标。
 
@@ -748,3 +901,4 @@
 | 2026-09-11 | 增补推送通道对象编码、公共契约列、主数据镜像 |
 | 2026-09-22 | 校正评审周期口径、人效分母、库位与员工落表、齐套现存量、质量退货 |
 | 2026-09-22 | **改版为按功能组织**:新增三种入站方式的技术契约与八个已开通对象的完整契约字段、「协议必填与指标必需」区分、库存单据语义要求与业务类型取值表、各模块指标刷新窗口、**九宫格逐格计算口径(全模块)**、内置监控规则数据源与缺数行为、筛选走原子表的读表差异、按情况选择方式的分支;明确出入库业务类型无推送通道及其影响;明确本期不纳入的四个指标 |
+| 2026-09-27 | 新增 §1.2.1 对象开通清单(entityCode / 贴源表 / 业务键 / 状态,按 `mdp_entity` 实测)与 §1.2.3 业务单据类契约,**补齐此前完全未记载的 14 个已开通对象**;已开通数由 8 更正为 20;**更正 `S5_INVENTORY_TXN` 贴源表为 `mdp_stg_inv_trans_inbound`**;补 `MDM_EMPLOYEE_HEADCOUNT` 遗漏的必填项 `Position` / `EmploymentStatus`;**撤销「出入库业务类型无推送通道」的结论**(v2 契约已开通,声明版本头即可),据此重写 §1.5 情况 B;标注客户 / 供应商 / 库位 / 货源清单为「待登记」、齐套核验结果头与明细为「待开通且贴源表登记值需更正」;同步业务页改版后的章节号 |

+ 57 - 18
doc/plan/系统集成/Ai-DOP第三方系统集成指南.md

@@ -4,11 +4,23 @@
 |----|------|
 | 读者 | ERP / MES / WMS / QMS / SRM 等第三方系统厂商与实施人员 |
 | 目的 | 说明要对哪些**业务数据**对接;入站三种方式(库同步 / 我方拉取 / **对方推送我方标准 API**)与回写分别怎么做 |
-| 版本 | 2026-09-22(§4.6 增加 `entityCode` 开通状态;前版 2026-09-11) |
+| 版本 | 2026-09-27(§4.6 按代码契约与中台登记全量核对 `entityCode`,更正 6 个错误编码;前版 2026-09-22) |
 | 范围 | 入站(第三方 → Ai-DOP)与回写(Ai-DOP → 第三方) |
 
 本文只讲对接约定,不包含数据库密码、Token、连接串等凭据。凭据须走现场带外方式交付。
 
+## 0. 三种业务模式,没有模式开关
+
+同一套数据中台按租户登记“每个标准对象由哪个来源提供”。不存在“对接模式”配置项。
+
+| 模式 | 谁提供 S5–S7 | 其余模块 |
+|---|---|---|
+| 模式 1 | 165 MES/WMS | 本平台自有 |
+| 模式 2 | T8 | 本平台自有 |
+| 模式 3 | 第三方 | 第三方提供 S0–S7 |
+
+每种模式都可以用三种技术方式:库同步、我方读对方 API、对方推我方标准 API。看板、模块详情、智慧诊断、S8 监控、ChatBI 都只读本库中立层与指标日值,不直连对方库。
+
 ---
 
 ## 1. 先看懂三件事
@@ -373,35 +385,61 @@ HTTP/1.1 202 Accepted
 
 现场按白名单开通。未开通 → 403。规范字段以 `GET .../schema` 为准。目录 BOM(`MDM_BOM_*`)首期不开。
 
-**状态列先看**:`entityCode` 逐个开通。「**已开通**」=当前即可推送与取 schema;「**待登记**」=我方尚未在数据中台登记该对象并签发授权,现在 `POST` 与 `GET .../schema` **都会返回 403**。排联调计划前请先与我方确认本次要开通哪些对象;待登记对象在开通前可先走第 2、3 章的甲/乙入站,业务内容不变。
+**状态列先看**:`entityCode` 逐个开通。「**已开通**」=当前即可推送与取 schema;「**待登记**」=代码层已有字段契约,但我方尚未在数据中台登记该对象或签发授权,现在 `POST` 与 `GET .../schema` **都会返回 403**。排联调计划前请先与我方确认本次要开通哪些对象;待登记对象在开通前可先走第 2、3 章的甲/乙入站,业务内容不变。
+
+下表按 2026-09-27 的代码契约与 `mdp_entity` 登记状态核对。**编码请严格照抄,不要按业务名自行推断**——本节此前列过若干拼法相近但并不存在的编码,已在下方「已废弃编码」中列明。
 
 | entityCode | 业务对象 | 状态 | 说明 |
 |------------|----------|------|------|
 | `MDM_ITEM` | 物料 | 已开通 | 推送后回灌 S0 `ItemMaster`(手建同行不覆盖,记冲突) |
+| `MDM_EMPLOYEE_HEADCOUNT` | 雇员 / 岗位人数 | 已开通 | 回灌 `EmployeeMaster`;必填含 `Position`、`EmploymentStatus` |
 | `MDM_CUSTOMER` | 客户 | 待登记 | 回灌 `CustMaster` |
 | `MDM_SUPPLIER` | 供应商 | 待登记 | 回灌 `SuppMaster` |
 | `MDM_LOCATION` | 库位 | 待登记 | 回灌 `LocationMaster` |
-| `MDM_EMPLOYEE_HEADCOUNT` | 雇员 / 岗位人数 | 已开通 | 回灌 `EmployeeMaster`;贴源 `mdp_stg_employee` |
-| `MDM_SOURCE_LIST` | 货源清单 | 待登记 | 先于采购单据 |
-| `S1_SALES_ORDER` | 销售订单头 | 待登记 | 与行一起推,否则评审周期缺头日期 |
+| `MDM_SOURCE_LIST` | 货源清单 | 待登记 | 先于采购单据;契约不含供货比例与采购周期 |
 | `S1_SALES_ORDER_ENTRY` | 销售订单行 | 已开通 | 键:`bill_no` + `entry_seq` |
-| `S1_CONTRACT_REVIEW` | 合同评审(含环节起止) | 待登记 | 诊断依据;驱动 L2–L4 下钻,不是 L1 周期终点 |
-| `S1_SHIPPING_PLAN` / `S1_ASN_SHIPPER` | 发货计划 / ASN | 待登记 | |
-| `S2_WORK_ORDER` | 生产工单 | 待登记 | |
-| `S2_OPERATION_SCHEDULE` / `S2_SCHEDULE_RESULT` | 日计划 / 排产结果 | 待登记 | 须带作业人数,是排程人效分母 |
-| `S3_SUPPLY_DEMAND` / `S3_PURCHASE_REQUEST` / `S3_PURCHASE_ORDER` / `S3_DELIVERY_SCHEDULE` / `S3_INVENTORY_ONHAND` | 供应协同 | 待登记 | `S3_INVENTORY_ONHAND` 是现存量快照,不能替代流水算周转 |
-| `S4_RECEIPT_CONFIRM` / `S4_SUPPLIER_SHIPMENT` / `S4_PURCHASE_RETURN` / `S4_IQC_RECEIPT` / `S4_SHORTAGE` | 采购执行 | 待登记 | `S4_RECEIPT_CONFIRM` 为**入站**对象,与第 6 章同名的回写动作编码无关,见下方提示 |
-| `S5_PURCHASE_RECEIPT` / `S5_PRODUCTION_ISSUE` / `S5_PRODUCTION_RETURN` / `S5_OUTSOURCE_ISSUE` / `S5_IQC_RESULT` | 来料仓储 / IQC | 待登记 | |
-| `S5_INVENTORY_TXN` | 出入库流水 | 已开通 | T8 等价;拍平行落库存流水贴源 |
-| `S5_INVENTORY_OPENING_BALANCE` | 库存期初 | 已开通 | 与流水同分表、源表隔离 |
-| `S6_REPORT` / `S6_IPQC_INSPECTION` / `S6_LINE_RUNTIME` | 报工 / 过程检 / 产线 | 待登记 | |
-| `S7_PRODUCTION_RECEIPT` / `S7_FQC_RESULT` / `S7_QUALITY_RETURN` | 成品入库 / FQC / 质量退货 | 待登记 | |
+| `S1_REQUIREMENT_EXAMINE_RESULT` | 齐套核验结果头 | 待开通 | 「制造工单缺料」规则的唯一数据源 |
+| `S1_REQUIREMENT_EXAMINE_DETAIL` | 齐套核验结果明细 | 待开通 | 同上;两张一起推才有效 |
+| `S2_WORK_ORDER_SCHEDULE` | 排程工单 | 已开通 | 键:`WorkOrder` + `ItemCode` |
+| `S3_PURCHASE_ORDER` | 采购订单 | 已开通 | 键:`PoNo` + `PoLine` |
+| `S3_PURCHASE_RECEIPT` | 采购收货 | 已开通 | 键:`Domain` + `Receiver` + `Line` |
+| `S4_SHIPMENT` | 供应商发货 | 已开通 | 键:`ShipmentNo` |
+| `S4_IQC` | 来料检验 | 已开通 | 交货周期取正式收货与本对象中较晚一次 |
+| `S4_RETURN` | 采购退货 | 已开通 | 键:`PoNo` + `PoLine` + `ItemCode` |
+| `S4_SHORTAGE` | 缺料 | 已开通 | **不驱动「制造工单缺料」规则**,该规则读齐套核验结果 |
+| `S5_INVENTORY_TXN` | 出入库流水 | 已开通 | 贴源 `mdp_stg_inv_trans_inbound`;见下方版本头提示 |
+| `S5_INVENTORY_OPENING_BALANCE` | 库存期初 | 已开通 | 与流水源表隔离 |
+| `S5_INVENTORY_BALANCE_MONTHLY` | 月度库存金额 | 已开通 | 品类物料库存周转的唯一成本口径来源 |
+| `S5_WORK_ORDER_BOM` | 工单用料 | 已开通 | 物料上线满足率的分母 |
+| `S6_REPORT_TXN` | 车间报工 | 已开通 | 键:`Domain` + `ReportId` |
+| `S6_WORK_ORDER_LINE` | 生产工单行 | 已开通 | 键:`Domain` + `OrderNo` |
+| `S7_SALES_ORDER_LINE` | 销售订单行(成品仓储口径) | 已开通 | 与上一行共用贴源表,靠业务键区分 |
+| `S7_FQC_TASK_TXN` | 成品检验任务 | 已开通 | 订单发货周期的起点 |
 | `S7_FINISHED_ONHAND` | 成品结存 | 已开通 | 校验源;结存由期初+流水自算 |
 | `S7_FINISHED_OPENING_BALANCE` | 成品期初 | 已开通 | |
 
 主数据必须先于单据开通。物料更新不覆盖仓库侧共管字段(默认库位/货架/安全库存/批次策略)。
 
-> **注意 `S4_RECEIPT_CONFIRM` 一码两义**:它既是本节的**入站** `entityCode`(贵司推交货收货数据给我方),也是第 6 章的**回写动作编码**(我方把交货关闭/收货确认推给贵司)。两者方向相反、报文不同,请按所在章节区分,不要复用同一个接口实现。
+> **出入库流水必须声明版本头。** `S5_INVENTORY_TXN` 默认按不含业务类型的 v1 契约处理。带上请求头 `X-Mdp-Contract-Version: v2` 并填 `BizDocType` 后,业务类型与审核状态即可随流水推送,**无需我方扩展契约**;不声明版本则物料库存周转、物料上线、订单制造、订单发货、质量退货率五个指标算不出。
+
+#### 已废弃或从未存在的编码
+
+以下编码曾在本节列出,**均不是有效的推送 `entityCode`**,请勿使用:
+
+| 曾列编码 | 实际情况 |
+|---|---|
+| `S2_WORK_ORDER`、`S2_SCHEDULE_RESULT` | 无此推送对象。工单走 `S6_WORK_ORDER_LINE` 或 `S2_WORK_ORDER_SCHEDULE` |
+| `S4_SUPPLIER_SHIPMENT` | 应为 `S4_SHIPMENT`(已开通) |
+| `S4_PURCHASE_RETURN` | 应为 `S4_RETURN`(已开通) |
+| `S4_IQC_RECEIPT` | 应为 `S4_IQC`(已开通)。同名编码仅作为方式甲的抽取实体存在 |
+| `S6_REPORT` | 应为 `S6_REPORT_TXN`(已开通)。同名编码仅作为方式甲的抽取实体存在 |
+| `S7_FQC_RESULT` | 应为 `S7_FQC_TASK_TXN`(已开通)。同名编码仅作为方式甲的抽取实体存在 |
+| `S4_RECEIPT_CONFIRM` | 不是入站编码,只是第 6 章的**回写动作编码**(我方推给贵司) |
+| `S1_SALES_ORDER`、`S1_CONTRACT_REVIEW`、`S1_SHIPPING_PLAN`、`S1_ASN_SHIPPER`、`S2_OPERATION_SCHEDULE`、`S3_SUPPLY_DEMAND`、`S3_PURCHASE_REQUEST`、`S3_DELIVERY_SCHEDULE`、`S3_INVENTORY_ONHAND`、`S5_PURCHASE_RECEIPT`、`S5_PRODUCTION_ISSUE`、`S5_PRODUCTION_RETURN`、`S5_OUTSOURCE_ISSUE`、`S5_IQC_RESULT`、`S6_IPQC_INSPECTION`、`S6_LINE_RUNTIME`、`S7_PRODUCTION_RECEIPT`、`S7_QUALITY_RETURN` | 这些业务对象**没有推送契约**,只能走方式甲或乙。其中部分编码在中台存在同名的方式甲抽取实体,但不接受推送 |
+
+> **无推送通道的影响面**:销售订单头缺失则产销协同评审周期与满足率算不出;交货计划(`S3_DELIVERY_SCHEDULE`)缺失则供应协同两个指标、采购执行物料交货周期与「采购交期延误」监控规则同时失效;工序级日计划缺失则订单排程周期、满足率与排程人效算不出。这三项若在本期范围内,务必安排方式甲或乙。
+
+> **生产领料、生产入库、销售出库、质量退货不需要单独编码**:它们是出入库流水的业务类型,统一走 `S5_INVENTORY_TXN` 的 v2 契约,用 `BizDocType` 区分。
 
 **周转类指标的前置条件**:`S1_L1_004` / `S2_L1_004` / `S3_L1_004` / `S4_L1_004` / `S9_L1_005` 等周转格子由「期初 + 流水」在中台自算结存,结存快照(`S7_FINISHED_ONHAND` 等)只作校验。只推结存不推流水与期初时,这些格子算不出来。
 
@@ -591,7 +629,7 @@ python tools/mock/inbound/push_demo.py --base-url https://{aidop-host} --access-
 | 订单评审结果 | `S1_ORDER_STATUS_PUSH` | ERP / WMS | 销售订单评审通过 | `/outbox/S1_ORDER_STATUS_PUSH` |
 | 排程下达 | `S2_SCHEDULE_DISPATCH` | MES / WMS | 排程状态确认 | `/outbox/S2_SCHEDULE_DISPATCH` |
 | 采购申请外推 | `S3_PR_PUSH` | SRM / SAP / QAD | 采购申请需外发(高风险,默认未对真实 SAP 打开) | `/outbox/S3_PR_PUSH` |
-| 交货关闭 / 收货确认 | `S4_RECEIPT_CONFIRM`(**回写方向**,与 §4.6 同名的入站 `entityCode` 是两回事) | WMS / SRM | 供应商交货关闭 | `/outbox/S4_RECEIPT_CONFIRM` |
+| 交货关闭 / 收货确认 | `S4_RECEIPT_CONFIRM`(**仅回写方向**,入站侧无同名 `entityCode`,见 §4.6) | WMS / SRM | 供应商交货关闭 | `/outbox/S4_RECEIPT_CONFIRM` |
 | 来料检验结果 | `S5_IQC_RESULT_PUSH` | QMS / WMS | IQC 结论形成 | `/outbox/S5_IQC_RESULT_PUSH` |
 | 过程检验合格回写 | `S6_REPORT_PUSH` | QMS / MES | IPQC 整批合格结束 | `/outbox/S6_REPORT_PUSH` |
 | 成品检验结果 | `S7_FQC_RESULT_PUSH` | QMS / WMS | FQC 结论形成 | `/outbox/S7_FQC_RESULT_PUSH` |
@@ -699,3 +737,4 @@ python tools/mock/inbound/push_demo.py --base-url https://{aidop-host} --access-
 | 2026-09-04 | 初版:甲(库同步)+ 乙(我方拉取)+ 回写 |
 | 2026-09-11 | 按方式丙全部完成后的契约增补第 4 章(`API_INBOUND`);甲/乙改为迁移或对方不能推时的选项;T8 优先推流水/期初;选型与验收同步 |
 | 2026-09-22 | §4.6 增加「已开通 / 待登记」状态列与 403 提示;标注 `S4_RECEIPT_CONFIRM` 入站与回写一码两义;补周转类指标须推「期初 + 流水」;§7.3 验收清单增加开通状态自检 |
+| 2026-09-27 | §4.6 按代码契约与 `mdp_entity` 全量核对:已开通对象由 8 个补全为 20 个(新增排程工单、采购订单与收货、供应商发货、来料检验、采购退货、缺料、月度库存金额、工单用料、车间报工、生产工单行、销售订单行成品口径、成品检验任务);**更正 6 个错误编码**(`S4_SUPPLIER_SHIPMENT`→`S4_SHIPMENT`、`S4_PURCHASE_RETURN`→`S4_RETURN`、`S4_IQC_RECEIPT`→`S4_IQC`、`S6_REPORT`→`S6_REPORT_TXN`、`S7_FQC_RESULT`→`S7_FQC_TASK_TXN`、`S2_WORK_ORDER`→无此对象);新增「已废弃或从未存在的编码」对照表;撤销 `S4_RECEIPT_CONFIRM` 一码两义的说法(入站侧无此编码);补出入库流水 v2 版本头说明与「无推送通道对象」的影响面 |

File diff suppressed because it is too large
+ 0 - 1
doc/plan/系统集成/README.md


+ 19 - 0
doc/plan/项目验收/项目验收待办项.md

@@ -64,6 +64,25 @@ grep binlog_expire_logs_seconds /etc/my.cnf
 
 binlog 稳定后应停在大约一天的量(按当前写入速度约 15~25 GB),不应再线性涨到几十 GB。
 
+### 2.1 MySQL 缓冲池 / 连接数 / 错误日志轮转 / TimeoutStop(2026-09-26 补记)
+
+2026-09-24 的 1040 事故里,165 当时跑的是默认值:`innodb_buffer_pool_size` 128MB(`my.cnf` 里该行被注释)、`max_connections` 151。
+06:14 已临时抬到 4G / 500,但**只改运行时不算固化**——下次「只改 binlog」式的配置回滚会把它带回去。
+
+复核命令:
+
+```bash
+mysqld --print-defaults | tr ' ' '\n' | grep -E 'innodb_buffer_pool_size|max_connections|binlog_expire'
+ls -lh /var/log/mysqld.log
+systemctl show mysqld -p TimeoutStopUSec
+```
+
+期望:`innodb_buffer_pool_size` 为 4G、`max_connections` 为 500、`binlog_expire_logs_seconds` 为 86400;
+`mysqld.log` 有 logrotate 日切(保留 7 天,`copytruncate` 或 `postrotate systemctl reload mysqld`),不再靠人工 `truncate`;
+`TimeoutStopUSec` 不是 0(drop-in `TimeoutStopSec=90`),避免 SIGTERM 永久卡住。
+
+> 仓库里的 `docker/mysql/mysql.cnf` **不是**这台机器的生效文件,禁止 scp 上去覆盖现场。
+
 ### 3. 待交付
 
 #### 3.1 日志保留清理(应用侧)

+ 4 - 0
docker/mysql/mysql.cnf

@@ -1,3 +1,7 @@
+# 此文件只给 docker 开发套件。165(123.60.180.165)的生效文件是 /etc/my.cnf,
+# 禁止把本文件 scp 上去覆盖现场——现场还跑着 Gogs / SQL Server / 旧 Business,
+# 且本文件的 max_connections=1000、无缓冲池显式值,与现场护栏(4G / 500)不一致。
+
 [client]
 default-character-set=utf8
  

+ 46 - 0
server/Admin.NET.Core/SqlSugar/MdmMirrorWriteScope.cs

@@ -0,0 +1,46 @@
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 镜像写入方在范围内更新带外源标记的主数据。页面保存不进入该范围,因此会被只读守卫拒绝。
+/// </summary>
+public static class MdmMirrorWriteScope
+{
+    private static readonly AsyncLocal<int> Depth = new();
+
+    public static bool IsWriter => Depth.Value > 0;
+
+    /// <summary>
+    /// 页面更新或删除 S0 行时,外源行(source_system 非空且不是 AIDOP_NATIVE)拒绝。
+    /// 只认 Entity.S0 命名空间,避免中立层实体上的同名列被误拦。
+    /// </summary>
+    public static void RejectPageWrite(object? entity)
+    {
+        if (entity == null || IsWriter)
+            return;
+        var type = entity.GetType();
+        var ns = type.Namespace ?? string.Empty;
+        if (ns.IndexOf(".Entity.S0", StringComparison.Ordinal) < 0)
+            return;
+        var value = type.GetProperty("SourceSystem")?.GetValue(entity) as string;
+        if (string.IsNullOrWhiteSpace(value))
+            return;
+        if (string.Equals(value, "AIDOP_NATIVE", StringComparison.OrdinalIgnoreCase))
+            return;
+        throw new InvalidOperationException($"该记录来自 {value},只读");
+    }
+
+    public static IDisposable Enter()
+    {
+        Depth.Value++;
+        return new Popper();
+    }
+
+    private sealed class Popper : IDisposable
+    {
+        public void Dispose()
+        {
+            if (Depth.Value > 0)
+                Depth.Value--;
+        }
+    }
+}

+ 5 - 1
server/Admin.NET.Core/SqlSugar/SqlSugarSetup.cs

@@ -264,8 +264,12 @@ public static class SqlSugarSetup
                 }
             }
             // 编辑/更新
-            else if (entityInfo.OperationType == DataFilterType.UpdateByObject)
+            else if (entityInfo.OperationType == DataFilterType.UpdateByObject
+                     || entityInfo.OperationType == DataFilterType.DeleteByObject)
             {
+                MdmMirrorWriteScope.RejectPageWrite(entityInfo.EntityValue);
+                if (entityInfo.OperationType == DataFilterType.DeleteByObject)
+                    return;
                 if (entityInfo.PropertyName == nameof(EntityBase.UpdateTime))
                     entityInfo.SetValue(DateTime.Now);
                 else if (entityInfo.PropertyName == nameof(EntityBaseDel.DeleteTime))

+ 75 - 15
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.582</AssemblyVersion>
-    <FileVersion>1.0.582</FileVersion>
-    <Version>1.0.582</Version>
+    <AssemblyVersion>1.0.583</AssemblyVersion>
+    <FileVersion>1.0.583</FileVersion>
+    <Version>1.0.583</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -934,18 +934,78 @@
     <None Update="UpdateScripts\1.0.565.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
-    <None Update="UpdateScripts\1.0.581.sql">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
-    <None Update="UpdateScripts\1.0.581.verify.sql">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
-    <None Update="UpdateScripts\1.0.582.sql">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
-    <None Update="UpdateScripts\1.0.582.verify.sql">
-      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
-    </None>
+    <None Update="UpdateScripts\1.0.570.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.570.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.571.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.571.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.572.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.572.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.573.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.573.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.574.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.574.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.575.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.575.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.576.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.576.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.577.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.577.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.578.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.578.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.579.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.580.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.581.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.581.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.582.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.582.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="UpdateScripts\UAT-PLACEHOLDER-MENU-HIDE.ops.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>

+ 10 - 6
server/Admin.NET.Web.Entry/UpdateScripts/1.0.564.verify.sql

@@ -13,14 +13,18 @@ SELECT COUNT(*) = 1
    AND table_name = 'mdp_std_inv_trans_unmapped'
    AND column_name = 'unmapped_reason';
 
--- 角色取值只允许中立枚举
+-- 角色取值只允许中立枚举。
+-- 1.0.565 把 INSPECT/LINE/SCRAP 改成 MAT_INSPECT/MAT_LINE_LOC/REJECT_SCRAP 等。
+-- 表若已被 565 写过,本脚本的 INSERT IGNORE 不会改回旧码,故两套都接受,避免重跑卡启动。
 SELECT COUNT(*) = 0
   FROM mdp_location_role
- WHERE location_role NOT IN ('INSPECT','MATERIAL','SEMI','LINE','FG','SCRAP','OTHER','UNKNOWN');
+ WHERE location_role NOT IN (
+   'INSPECT','MATERIAL','SEMI','LINE','FG','SCRAP','OTHER','UNKNOWN',
+   'MAT_INSPECT','MAT_LINE_LOC','FG_INSPECT','FG_LINE_LOC','REJECT_SCRAP','VMI');
 
--- 待检仓口径到位(无 1000 库位时也算通过)
+-- 待检仓口径到位(无 1000 库位时也算通过)。1.0.565 将 INSPECT 改为 MAT_INSPECT,两者都算到位。
 SELECT (SELECT COUNT(*) FROM mdp_location_role WHERE location = '1000') = 0
-    OR (SELECT COUNT(*) FROM mdp_location_role WHERE location = '1000' AND location_role <> 'INSPECT' AND role_source <> 'MANUAL') = 0;
+    OR (SELECT COUNT(*) FROM mdp_location_role WHERE location = '1000' AND location_role NOT IN ('INSPECT','MAT_INSPECT') AND role_source <> 'MANUAL') = 0;
 
 -- trans_type 只允许 12 个中立阶段码或 NULL
 SELECT COUNT(*) = 0
@@ -33,10 +37,10 @@ SELECT COUNT(*) = 0
   FROM mdp_std_inv_trans_unmapped
  WHERE src_trans_type_raw IN ('inv-frz','inv-unfrz');
 
--- 隔离原因只允许三类
+-- 隔离原因只允许三类;1.0.565 追加 OUT_OF_SCOPE_ROLE,重跑时一并接受。
 SELECT COUNT(*) = 0
   FROM mdp_std_inv_trans_unmapped
- WHERE unmapped_reason NOT IN ('PENDING_VERIFY','LOCATION_ROLE_UNCONFIGURED','UNKNOWN_CODE');
+ WHERE unmapped_reason NOT IN ('PENDING_VERIFY','LOCATION_ROLE_UNCONFIGURED','UNKNOWN_CODE','OUT_OF_SCOPE_ROLE');
 
 -- 已映射成功的行不得留在隔离表
 SELECT COUNT(*) = 0

+ 19 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.570.sql

@@ -0,0 +1,19 @@
+-- 1.0.570 · KPI 基线快照表(G0-3)。幂等。快照行由 KpiBaselineSnapshotService 写入,不在本脚本灌数。
+
+CREATE TABLE IF NOT EXISTS ado_kpi_baseline_snapshot (
+  id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  snapshot_code VARCHAR(32) NOT NULL COMMENT '快照轮次:G0 / POST_F3 / ...',
+  tenant_id BIGINT NOT NULL COMMENT '租户',
+  factory_id BIGINT NOT NULL COMMENT '工厂;KPI 值表按 tenant+factory 键控',
+  module_code VARCHAR(16) NOT NULL COMMENT '模块 S5/S6/S7/S9',
+  metric_code VARCHAR(32) NOT NULL COMMENT '指标码',
+  lvl VARCHAR(4) NOT NULL COMMENT 'L1/L2/L3',
+  biz_date DATE NOT NULL COMMENT '业务日期',
+  metric_value DECIMAL(24,6) NULL COMMENT '指标值,NULL 表示无值',
+  target_value DECIMAL(24,6) NULL COMMENT '目标值',
+  result_status VARCHAR(24) NULL COMMENT '结果状态(E-4 落地后有值,之前留 NULL)',
+  taken_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '快照时刻',
+  PRIMARY KEY (id),
+  UNIQUE KEY uk_kpi_baseline_snapshot (snapshot_code, tenant_id, factory_id, module_code, metric_code, lvl, biz_date),
+  KEY idx_kpi_baseline_snapshot_code (snapshot_code)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KPI 基线快照(F-3 回归闸门)';

+ 20 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.570.verify.sql

@@ -0,0 +1,20 @@
+-- 1.0.570.verify.sql
+SELECT COUNT(*) = 1
+  FROM information_schema.tables
+ WHERE table_schema = DATABASE()
+   AND table_name = 'ado_kpi_baseline_snapshot';
+
+SELECT COUNT(*) = 8
+  FROM information_schema.columns
+ WHERE table_schema = DATABASE()
+   AND table_name = 'ado_kpi_baseline_snapshot'
+   AND column_name IN (
+     'snapshot_code','tenant_id','factory_id','module_code',
+     'metric_code','lvl','biz_date','metric_value');
+
+SELECT COUNT(*) = 1
+  FROM information_schema.statistics
+ WHERE table_schema = DATABASE()
+   AND table_name = 'ado_kpi_baseline_snapshot'
+   AND index_name = 'uk_kpi_baseline_snapshot'
+   AND column_name = 'factory_id';

+ 39 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.571.health.md

@@ -0,0 +1,39 @@
+# F-1 存量体检(只报不删)
+
+登记来源之外的中立行。删除由 F-2 在重建时执行。
+
+| 租户 | 对象 | 登记来源 | 表 | 非登记行数 |
+|---|---|---|---|---|
+| 797403760988229 | INV_TRANS | T8 | mdp_std_inv_trans | 405437 |
+| 838257186181189 | INV_SNAPSHOT | DOPDEMORQ_SQLSERVER | mdp_std_inventory | 5 |
+| | WO_LINE_PROD | | mdp_std_work_order_line | 0 |
+| | WO_LINE_SALES | | mdp_std_work_order_line | 0 |
+| | WO_BOM | | mdp_std_work_order_bom | 0 |
+| | EMPLOYEE | | mdp_std_employee | 0 |
+| | INV_BAL_MONTHLY | | mdp_std_inventory_balance_monthly | 0 |
+| 838257186181189 | S6_REPORT | DOPDEMORQ_SQLSERVER | mdp_std_s6_report | 4 |
+| 797403760988229 | FQC_TASK | T8 | mdp_std_fqc_task | 26 |
+| 838257186181189 | FQC_TASK | DOPDEMORQ_SQLSERVER | mdp_std_fqc_task | 17 |
+| | SO | | mdp_std_so | 0 |
+| | IQC | | mdp_std_iqc_inspection | 0 |
+| | IPQC | | mdp_std_ipqc_inspection | 0 |
+| 838257186181189 | SUPPLIER | AIDOP_NATIVE | mdp_std_supplier | 1 |
+| 797403760988229 | MASTER_ITEM | AIDOP_NATIVE | mdp_std_item | 27111 |
+| 838257186181189 | MASTER_ITEM | AIDOP_NATIVE | mdp_std_item | 14 |
+| | ACTION_EVENT | | mdp_std_action_event | 0 |
+
+## 仍为 AIDOP / AIDOPDEV_MYSQL 的行(唯一键冲突未迁)
+
+- `mdp_std_delivery_result` AIDOP 26
+- `mdp_std_material_readiness` AIDOP 71
+- `mdp_std_purchase_request` AIDOP 55
+- `mdp_std_supply_demand` AIDOP 89
+- `mdp_stg_delivery` AIDOP 164
+- `mdp_stg_delivery` AIDOPDEV_MYSQL 58
+- `mdp_stg_item` AIDOPDEV_MYSQL 7536
+- `mdp_stg_purchase_order` AIDOPDEV_MYSQL 56
+- `mdp_stg_receipt` AIDOPDEV_MYSQL 15
+- `mdp_stg_schedule` AIDOPDEV_MYSQL 386
+- `mdp_stg_ship_trans` AIDOP 1
+- `mdp_stg_so` AIDOP 24
+- `mdp_stg_supply_demand` AIDOP 1155

+ 2142 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.571.sql

@@ -0,0 +1,2142 @@
+-- 1.0.571 · F-1 来源登记、system_code、Q13 存量来源码迁移。幂等。不删行。
+
+CREATE TABLE IF NOT EXISTS mdp_tenant_std_source (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  tenant_id BIGINT NOT NULL,
+  std_object VARCHAR(48) NOT NULL COMMENT '附录 A 编码',
+  source_system VARCHAR(50) NOT NULL COMMENT 'AIDOP_NATIVE/T8/DOPDEMORQ_SQLSERVER/第三方 source_code',
+  source_scope VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'T8=账套 ztid;165=domain;自有与第三方可空',
+  remark VARCHAR(500) NULL,
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  UNIQUE KEY uk_tenant_std_source (tenant_id, std_object)
+) COMMENT='租户标准对象来源登记:一租户一对象一来源';
+
+CREATE TABLE IF NOT EXISTS mdp_tenant_std_source_log (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  tenant_id BIGINT NOT NULL,
+  std_object VARCHAR(48) NOT NULL,
+  old_source_system VARCHAR(50) NULL,
+  new_source_system VARCHAR(50) NOT NULL,
+  changed_by VARCHAR(64) NOT NULL,
+  changed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  rebuild_job_id VARCHAR(64) NULL,
+  KEY idx_tenant_obj (tenant_id, std_object, changed_at)
+);
+
+CREATE TABLE IF NOT EXISTS mdp_source_gate_log (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  tenant_id BIGINT NOT NULL,
+  std_object VARCHAR(48) NOT NULL,
+  source_system VARCHAR(50) NOT NULL,
+  gate_reason VARCHAR(32) NOT NULL COMMENT 'SOURCE_NOT_REGISTERED/SOURCE_MISMATCH/POSITION_UNMAPPED/WO_NOT_FOUND',
+  row_count INT NOT NULL,
+  sample_keys VARCHAR(1000) NULL,
+  sync_batch_id VARCHAR(64) NULL,
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  KEY idx_gate (tenant_id, std_object, create_time)
+);
+
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_source' AND COLUMN_NAME='system_code')=0, 'ALTER TABLE `mdp_source` ADD COLUMN `system_code` VARCHAR(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+UPDATE mdp_source SET system_code = CASE source_code
+  WHEN 'T8_V5_SQLSERVER' THEN 'T8'
+  WHEN 'DOPDEMORQ_SQLSERVER' THEN 'DOPDEMORQ_SQLSERVER'
+  WHEN 'AIDOPDEV_MYSQL' THEN 'AIDOP_NATIVE'
+  ELSE source_code END
+WHERE system_code IS NULL OR system_code = '';
+
+-- Q13 存量来源码迁移。同键已有目标码的行留下。
+
+UPDATE mdp_source_table_registry SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_action_event`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_std_action_event`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_std_action_event`.source_table AND clash.source_biz_key=`mdp_std_action_event`.source_biz_key
+);
+
+UPDATE `mdp_std_action_event`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_std_action_event`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_std_action_event`.source_table AND clash.source_biz_key=`mdp_std_action_event`.source_biz_key
+);
+
+UPDATE `mdp_std_delivery_exception` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_delivery_exception` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_delivery_exception` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_delivery_exception` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_delivery_result` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `factory_id`, `source_biz_key` FROM `mdp_std_delivery_result` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`factory_id` <=> a.`factory_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_delivery_result` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `factory_id`, `source_biz_key` FROM `mdp_std_delivery_result` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`factory_id` <=> a.`factory_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_delivery_schedule` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `factory_id`, `source_biz_key` FROM `mdp_std_delivery_schedule` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`factory_id` <=> a.`factory_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_delivery_schedule` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `factory_id`, `source_biz_key` FROM `mdp_std_delivery_schedule` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`factory_id` <=> a.`factory_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_employee` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `domain`, `employee_no` FROM `mdp_std_employee` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`domain` <=> a.`domain` AND c.`employee_no` <=> a.`employee_no`
+);
+
+UPDATE `mdp_std_employee` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `domain`, `employee_no` FROM `mdp_std_employee` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`domain` <=> a.`domain` AND c.`employee_no` <=> a.`employee_no`
+);
+
+UPDATE `mdp_std_fqc_result` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_fqc_task` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_row_id` FROM `mdp_std_fqc_task` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_row_id` <=> a.`source_row_id`
+);
+
+UPDATE `mdp_std_fqc_task` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_row_id` FROM `mdp_std_fqc_task` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_row_id` <=> a.`source_row_id`
+);
+
+UPDATE `mdp_std_inv_trans` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `src_rec_id` FROM `mdp_std_inv_trans` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`src_rec_id` <=> a.`src_rec_id`
+);
+
+UPDATE `mdp_std_inv_trans` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `src_rec_id` FROM `mdp_std_inv_trans` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`src_rec_id` <=> a.`src_rec_id`
+);
+
+UPDATE `mdp_std_inventory` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `domain`, `location`, `lot_serial`, `item_num`, `dimension1`, `dimension2`, `refs`, `site` FROM `mdp_std_inventory` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`domain` <=> a.`domain` AND c.`location` <=> a.`location` AND c.`lot_serial` <=> a.`lot_serial` AND c.`item_num` <=> a.`item_num` AND c.`dimension1` <=> a.`dimension1` AND c.`dimension2` <=> a.`dimension2` AND c.`refs` <=> a.`refs` AND c.`site` <=> a.`site`
+);
+
+UPDATE `mdp_std_inventory` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `domain`, `location`, `lot_serial`, `item_num`, `dimension1`, `dimension2`, `refs`, `site` FROM `mdp_std_inventory` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`domain` <=> a.`domain` AND c.`location` <=> a.`location` AND c.`lot_serial` <=> a.`lot_serial` AND c.`item_num` <=> a.`item_num` AND c.`dimension1` <=> a.`dimension1` AND c.`dimension2` <=> a.`dimension2` AND c.`refs` <=> a.`refs` AND c.`site` <=> a.`site`
+);
+
+UPDATE `mdp_std_inventory_balance_monthly` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `period_ym`, `category_code`, `warehouse_code`, `item_code` FROM `mdp_std_inventory_balance_monthly` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`period_ym` <=> a.`period_ym` AND c.`category_code` <=> a.`category_code` AND c.`warehouse_code` <=> a.`warehouse_code` AND c.`item_code` <=> a.`item_code`
+);
+
+UPDATE `mdp_std_inventory_balance_monthly` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `period_ym`, `category_code`, `warehouse_code`, `item_code` FROM `mdp_std_inventory_balance_monthly` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`period_ym` <=> a.`period_ym` AND c.`category_code` <=> a.`category_code` AND c.`warehouse_code` <=> a.`warehouse_code` AND c.`item_code` <=> a.`item_code`
+);
+
+UPDATE `mdp_std_ipqc_inspection` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_iqc_inspection` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_iqc_receipt_state` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_item` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_item` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_item` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_item` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_material_readiness` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `factory_id`, `source_biz_key` FROM `mdp_std_material_readiness` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`factory_id` <=> a.`factory_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_material_readiness` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `factory_id`, `source_biz_key` FROM `mdp_std_material_readiness` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`factory_id` <=> a.`factory_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_operation_schedule` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_outsource_issue` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_process_outsource_order` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `factory_id`, `source_biz_key` FROM `mdp_std_process_outsource_order` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`factory_id` <=> a.`factory_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_process_outsource_order` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `factory_id`, `source_biz_key` FROM `mdp_std_process_outsource_order` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`factory_id` <=> a.`factory_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_production_issue` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_production_receipt` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_production_return` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_purchase_order` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_purchase_order` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_purchase_order` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_purchase_order` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_purchase_order_completion` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_purchase_receipt` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_purchase_request` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_purchase_request` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_purchase_request` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_purchase_request` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_s4_iqc` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_s4_return` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_s4_shipment` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_s4_shortage` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_std_s6_report` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_s6_report` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_s6_report` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_s6_report` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_ship_trans`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_std_ship_trans`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_std_ship_trans`.source_table AND clash.source_biz_key=`mdp_std_ship_trans`.source_biz_key
+);
+
+UPDATE `mdp_std_ship_trans`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_std_ship_trans`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_std_ship_trans`.source_table AND clash.source_biz_key=`mdp_std_ship_trans`.source_biz_key
+);
+
+UPDATE `mdp_std_so`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_std_so`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_std_so`.source_table AND clash.source_biz_key=`mdp_std_so`.source_biz_key
+);
+
+UPDATE `mdp_std_so`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_std_so`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_std_so`.source_table AND clash.source_biz_key=`mdp_std_so`.source_biz_key
+);
+
+UPDATE `mdp_std_supplier` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_supplier` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_supplier` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_supplier` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_supplier_item` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_supplier_item` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_supplier_item` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_supplier_item` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_supply_demand` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_supply_demand` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_supply_demand` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_supply_demand` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_work_order_bom` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_work_order_bom` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_work_order_bom` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_work_order_bom` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_work_order_line` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOP'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_work_order_line` WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_work_order_line` a
+SET a.source_system='AIDOP_NATIVE'
+WHERE a.source_system='AIDOPDEV_MYSQL'
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT `tenant_id`, `source_biz_key` FROM `mdp_std_work_order_line` WHERE source_system IN ('AIDOP_NATIVE')
+  ) c
+  WHERE c.`tenant_id` <=> a.`tenant_id` AND c.`source_biz_key` <=> a.`source_biz_key`
+);
+
+UPDATE `mdp_std_work_order_schedule` SET source_system='AIDOP_NATIVE' WHERE source_system IN ('AIDOP','AIDOPDEV_MYSQL');
+
+UPDATE `mdp_stg_action_run`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_action_run`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_action_run`.source_table AND clash.source_biz_key=`mdp_stg_action_run`.source_biz_key
+);
+
+UPDATE `mdp_stg_action_run`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_action_run`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_action_run`.source_table AND clash.source_biz_key=`mdp_stg_action_run`.source_biz_key
+);
+
+UPDATE `mdp_stg_customer`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_customer`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_customer`.source_table AND clash.source_biz_key=`mdp_stg_customer`.source_biz_key
+);
+
+UPDATE `mdp_stg_customer`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_customer`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_customer`.source_table AND clash.source_biz_key=`mdp_stg_customer`.source_biz_key
+);
+
+UPDATE `mdp_stg_delivery`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_delivery`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_delivery`.source_table AND clash.source_biz_key=`mdp_stg_delivery`.source_biz_key
+);
+
+UPDATE `mdp_stg_delivery`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_delivery`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_delivery`.source_table AND clash.source_biz_key=`mdp_stg_delivery`.source_biz_key
+);
+
+UPDATE `mdp_stg_emp_duty`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_emp_duty`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_emp_duty`.source_table AND clash.source_biz_key=`mdp_stg_emp_duty`.source_biz_key
+);
+
+UPDATE `mdp_stg_emp_duty`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_emp_duty`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_emp_duty`.source_table AND clash.source_biz_key=`mdp_stg_emp_duty`.source_biz_key
+);
+
+UPDATE `mdp_stg_emp_work`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_emp_work`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_emp_work`.source_table AND clash.source_biz_key=`mdp_stg_emp_work`.source_biz_key
+);
+
+UPDATE `mdp_stg_emp_work`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_emp_work`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_emp_work`.source_table AND clash.source_biz_key=`mdp_stg_emp_work`.source_biz_key
+);
+
+UPDATE `mdp_stg_employee`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_employee`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_employee`.source_table AND clash.source_biz_key=`mdp_stg_employee`.source_biz_key
+);
+
+UPDATE `mdp_stg_employee`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_employee`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_employee`.source_table AND clash.source_biz_key=`mdp_stg_employee`.source_biz_key
+);
+
+UPDATE `mdp_stg_fqc_pull`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_fqc_pull`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_fqc_pull`.source_table AND clash.source_biz_key=`mdp_stg_fqc_pull`.source_biz_key
+);
+
+UPDATE `mdp_stg_fqc_pull`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_fqc_pull`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_fqc_pull`.source_table AND clash.source_biz_key=`mdp_stg_fqc_pull`.source_biz_key
+);
+
+UPDATE `mdp_stg_inv_trans`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_inv_trans`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_inv_trans`.source_table AND clash.source_biz_key=`mdp_stg_inv_trans`.source_biz_key
+);
+
+UPDATE `mdp_stg_inv_trans`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_inv_trans`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_inv_trans`.source_table AND clash.source_biz_key=`mdp_stg_inv_trans`.source_biz_key
+);
+
+UPDATE `mdp_stg_inventory`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_inventory`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_inventory`.source_table AND clash.source_biz_key=`mdp_stg_inventory`.source_biz_key
+);
+
+UPDATE `mdp_stg_inventory`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_inventory`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_inventory`.source_table AND clash.source_biz_key=`mdp_stg_inventory`.source_biz_key
+);
+
+UPDATE `mdp_stg_ipqc_inspection`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_ipqc_inspection`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_ipqc_inspection`.source_table AND clash.source_biz_key=`mdp_stg_ipqc_inspection`.source_biz_key
+);
+
+UPDATE `mdp_stg_ipqc_inspection`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_ipqc_inspection`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_ipqc_inspection`.source_table AND clash.source_biz_key=`mdp_stg_ipqc_inspection`.source_biz_key
+);
+
+UPDATE `mdp_stg_ipqc_inspection_detail`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_ipqc_inspection_detail`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_ipqc_inspection_detail`.source_table AND clash.source_biz_key=`mdp_stg_ipqc_inspection_detail`.source_biz_key
+);
+
+UPDATE `mdp_stg_ipqc_inspection_detail`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_ipqc_inspection_detail`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_ipqc_inspection_detail`.source_table AND clash.source_biz_key=`mdp_stg_ipqc_inspection_detail`.source_biz_key
+);
+
+UPDATE `mdp_stg_ipqc_pull`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_ipqc_pull`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_ipqc_pull`.source_table AND clash.source_biz_key=`mdp_stg_ipqc_pull`.source_biz_key
+);
+
+UPDATE `mdp_stg_ipqc_pull`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_ipqc_pull`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_ipqc_pull`.source_table AND clash.source_biz_key=`mdp_stg_ipqc_pull`.source_biz_key
+);
+
+UPDATE `mdp_stg_iqc_pull`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_iqc_pull`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_iqc_pull`.source_table AND clash.source_biz_key=`mdp_stg_iqc_pull`.source_biz_key
+);
+
+UPDATE `mdp_stg_iqc_pull`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_iqc_pull`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_iqc_pull`.source_table AND clash.source_biz_key=`mdp_stg_iqc_pull`.source_biz_key
+);
+
+UPDATE `mdp_stg_item`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_item`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_item`.source_table AND clash.source_biz_key=`mdp_stg_item`.source_biz_key
+);
+
+UPDATE `mdp_stg_item`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_item`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_item`.source_table AND clash.source_biz_key=`mdp_stg_item`.source_biz_key
+);
+
+UPDATE `mdp_stg_label`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_label`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_label`.source_table AND clash.source_biz_key=`mdp_stg_label`.source_biz_key
+);
+
+UPDATE `mdp_stg_label`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_label`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_label`.source_table AND clash.source_biz_key=`mdp_stg_label`.source_biz_key
+);
+
+UPDATE `mdp_stg_line`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_line`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_line`.source_table AND clash.source_biz_key=`mdp_stg_line`.source_biz_key
+);
+
+UPDATE `mdp_stg_line`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_line`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_line`.source_table AND clash.source_biz_key=`mdp_stg_line`.source_biz_key
+);
+
+UPDATE `mdp_stg_line_run_rest`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_line_run_rest`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_line_run_rest`.source_table AND clash.source_biz_key=`mdp_stg_line_run_rest`.source_biz_key
+);
+
+UPDATE `mdp_stg_line_run_rest`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_line_run_rest`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_line_run_rest`.source_table AND clash.source_biz_key=`mdp_stg_line_run_rest`.source_biz_key
+);
+
+UPDATE `mdp_stg_line_status`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_line_status`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_line_status`.source_table AND clash.source_biz_key=`mdp_stg_line_status`.source_biz_key
+);
+
+UPDATE `mdp_stg_line_status`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_line_status`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_line_status`.source_table AND clash.source_biz_key=`mdp_stg_line_status`.source_biz_key
+);
+
+UPDATE `mdp_stg_location`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_location`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_location`.source_table AND clash.source_biz_key=`mdp_stg_location`.source_biz_key
+);
+
+UPDATE `mdp_stg_location`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_location`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_location`.source_table AND clash.source_biz_key=`mdp_stg_location`.source_biz_key
+);
+
+UPDATE `mdp_stg_mobile_task`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_mobile_task`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_mobile_task`.source_table AND clash.source_biz_key=`mdp_stg_mobile_task`.source_biz_key
+);
+
+UPDATE `mdp_stg_mobile_task`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_mobile_task`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_mobile_task`.source_table AND clash.source_biz_key=`mdp_stg_mobile_task`.source_biz_key
+);
+
+UPDATE `mdp_stg_outsource_issue`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_outsource_issue`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_outsource_issue`.source_table AND clash.source_biz_key=`mdp_stg_outsource_issue`.source_biz_key
+);
+
+UPDATE `mdp_stg_outsource_issue`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_outsource_issue`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_outsource_issue`.source_table AND clash.source_biz_key=`mdp_stg_outsource_issue`.source_biz_key
+);
+
+UPDATE `mdp_stg_outsource_issue_detail`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_outsource_issue_detail`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_outsource_issue_detail`.source_table AND clash.source_biz_key=`mdp_stg_outsource_issue_detail`.source_biz_key
+);
+
+UPDATE `mdp_stg_outsource_issue_detail`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_outsource_issue_detail`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_outsource_issue_detail`.source_table AND clash.source_biz_key=`mdp_stg_outsource_issue_detail`.source_biz_key
+);
+
+UPDATE `mdp_stg_outsource_issue_pull`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_outsource_issue_pull`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_outsource_issue_pull`.source_table AND clash.source_biz_key=`mdp_stg_outsource_issue_pull`.source_biz_key
+);
+
+UPDATE `mdp_stg_outsource_issue_pull`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_outsource_issue_pull`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_outsource_issue_pull`.source_table AND clash.source_biz_key=`mdp_stg_outsource_issue_pull`.source_biz_key
+);
+
+UPDATE `mdp_stg_production_issue`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_production_issue`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_production_issue`.source_table AND clash.source_biz_key=`mdp_stg_production_issue`.source_biz_key
+);
+
+UPDATE `mdp_stg_production_issue`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_production_issue`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_production_issue`.source_table AND clash.source_biz_key=`mdp_stg_production_issue`.source_biz_key
+);
+
+UPDATE `mdp_stg_production_receipt`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_production_receipt`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_production_receipt`.source_table AND clash.source_biz_key=`mdp_stg_production_receipt`.source_biz_key
+);
+
+UPDATE `mdp_stg_production_receipt`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_production_receipt`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_production_receipt`.source_table AND clash.source_biz_key=`mdp_stg_production_receipt`.source_biz_key
+);
+
+UPDATE `mdp_stg_production_return`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_production_return`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_production_return`.source_table AND clash.source_biz_key=`mdp_stg_production_return`.source_biz_key
+);
+
+UPDATE `mdp_stg_production_return`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_production_return`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_production_return`.source_table AND clash.source_biz_key=`mdp_stg_production_return`.source_biz_key
+);
+
+UPDATE `mdp_stg_purchase_order`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_purchase_order`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_purchase_order`.source_table AND clash.source_biz_key=`mdp_stg_purchase_order`.source_biz_key
+);
+
+UPDATE `mdp_stg_purchase_order`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_purchase_order`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_purchase_order`.source_table AND clash.source_biz_key=`mdp_stg_purchase_order`.source_biz_key
+);
+
+UPDATE `mdp_stg_purchase_receipt`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_purchase_receipt`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_purchase_receipt`.source_table AND clash.source_biz_key=`mdp_stg_purchase_receipt`.source_biz_key
+);
+
+UPDATE `mdp_stg_purchase_receipt`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_purchase_receipt`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_purchase_receipt`.source_table AND clash.source_biz_key=`mdp_stg_purchase_receipt`.source_biz_key
+);
+
+UPDATE `mdp_stg_receipt`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_receipt`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_receipt`.source_table AND clash.source_biz_key=`mdp_stg_receipt`.source_biz_key
+);
+
+UPDATE `mdp_stg_receipt`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_receipt`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_receipt`.source_table AND clash.source_biz_key=`mdp_stg_receipt`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_customer`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_customer`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_customer`.source_table AND clash.source_biz_key=`mdp_stg_s0_customer`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_customer`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_customer`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_customer`.source_table AND clash.source_biz_key=`mdp_stg_s0_customer`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_department`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_department`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_department`.source_table AND clash.source_biz_key=`mdp_stg_s0_department`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_department`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_department`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_department`.source_table AND clash.source_biz_key=`mdp_stg_s0_department`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_emp_work_duty`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_emp_work_duty`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_emp_work_duty`.source_table AND clash.source_biz_key=`mdp_stg_s0_emp_work_duty`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_emp_work_duty`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_emp_work_duty`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_emp_work_duty`.source_table AND clash.source_biz_key=`mdp_stg_s0_emp_work_duty`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_employee`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_employee`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_employee`.source_table AND clash.source_biz_key=`mdp_stg_s0_employee`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_employee`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_employee`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_employee`.source_table AND clash.source_biz_key=`mdp_stg_s0_employee`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item`.source_table AND clash.source_biz_key=`mdp_stg_s0_item`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item`.source_table AND clash.source_biz_key=`mdp_stg_s0_item`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_bom`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_bom`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_bom`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_bom`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_bom`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_bom`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_bom`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_bom`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_op_condition`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_op_condition`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_op_condition`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_op_condition`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_op_condition`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_op_condition`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_op_condition`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_op_condition`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_op_image`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_op_image`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_op_image`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_op_image`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_op_image`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_op_image`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_op_image`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_op_image`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_op_line`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_op_line`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_op_line`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_op_line`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_op_line`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_op_line`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_op_line`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_op_line`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_routing_op`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_routing_op`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_routing_op`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_routing_op`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_routing_op`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_routing_op`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_routing_op`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_routing_op`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_substitute`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_substitute`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_substitute`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_substitute`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_item_substitute`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_item_substitute`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_item_substitute`.source_table AND clash.source_biz_key=`mdp_stg_s0_item_substitute`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_label_type`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_label_type`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_label_type`.source_table AND clash.source_biz_key=`mdp_stg_s0_label_type`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_label_type`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_label_type`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_label_type`.source_table AND clash.source_biz_key=`mdp_stg_s0_label_type`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_line`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_line`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_line`.source_table AND clash.source_biz_key=`mdp_stg_s0_line`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_line`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_line`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_line`.source_table AND clash.source_biz_key=`mdp_stg_s0_line`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_line_post`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_line_post`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_line_post`.source_table AND clash.source_biz_key=`mdp_stg_s0_line_post`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_line_post`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_line_post`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_line_post`.source_table AND clash.source_biz_key=`mdp_stg_s0_line_post`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_location`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_location`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_location`.source_table AND clash.source_biz_key=`mdp_stg_s0_location`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_location`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_location`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_location`.source_table AND clash.source_biz_key=`mdp_stg_s0_location`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_person_skill`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_person_skill`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_person_skill`.source_table AND clash.source_biz_key=`mdp_stg_s0_person_skill`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_person_skill`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_person_skill`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_person_skill`.source_table AND clash.source_biz_key=`mdp_stg_s0_person_skill`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_qms_dict`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_qms_dict`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_qms_dict`.source_table AND clash.source_biz_key=`mdp_stg_s0_qms_dict`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_qms_dict`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_qms_dict`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_qms_dict`.source_table AND clash.source_biz_key=`mdp_stg_s0_qms_dict`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_quality_base_type`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_quality_base_type`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_quality_base_type`.source_table AND clash.source_biz_key=`mdp_stg_s0_quality_base_type`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_quality_base_type`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_quality_base_type`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_quality_base_type`.source_table AND clash.source_biz_key=`mdp_stg_s0_quality_base_type`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_sop_file_type`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_sop_file_type`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_sop_file_type`.source_table AND clash.source_biz_key=`mdp_stg_s0_sop_file_type`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_sop_file_type`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_sop_file_type`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_sop_file_type`.source_table AND clash.source_biz_key=`mdp_stg_s0_sop_file_type`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_supplier`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_supplier`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_supplier`.source_table AND clash.source_biz_key=`mdp_stg_s0_supplier`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_supplier`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_supplier`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_supplier`.source_table AND clash.source_biz_key=`mdp_stg_s0_supplier`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_work_center`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_work_center`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_work_center`.source_table AND clash.source_biz_key=`mdp_stg_s0_work_center`.source_biz_key
+);
+
+UPDATE `mdp_stg_s0_work_center`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s0_work_center`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s0_work_center`.source_table AND clash.source_biz_key=`mdp_stg_s0_work_center`.source_biz_key
+);
+
+UPDATE `mdp_stg_s4_iqc`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s4_iqc`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s4_iqc`.source_table AND clash.source_biz_key=`mdp_stg_s4_iqc`.source_biz_key
+);
+
+UPDATE `mdp_stg_s4_iqc`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s4_iqc`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s4_iqc`.source_table AND clash.source_biz_key=`mdp_stg_s4_iqc`.source_biz_key
+);
+
+UPDATE `mdp_stg_s4_return`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s4_return`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s4_return`.source_table AND clash.source_biz_key=`mdp_stg_s4_return`.source_biz_key
+);
+
+UPDATE `mdp_stg_s4_return`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s4_return`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s4_return`.source_table AND clash.source_biz_key=`mdp_stg_s4_return`.source_biz_key
+);
+
+UPDATE `mdp_stg_s4_shipment`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s4_shipment`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s4_shipment`.source_table AND clash.source_biz_key=`mdp_stg_s4_shipment`.source_biz_key
+);
+
+UPDATE `mdp_stg_s4_shipment`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s4_shipment`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s4_shipment`.source_table AND clash.source_biz_key=`mdp_stg_s4_shipment`.source_biz_key
+);
+
+UPDATE `mdp_stg_s4_shortage`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s4_shortage`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s4_shortage`.source_table AND clash.source_biz_key=`mdp_stg_s4_shortage`.source_biz_key
+);
+
+UPDATE `mdp_stg_s4_shortage`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s4_shortage`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s4_shortage`.source_table AND clash.source_biz_key=`mdp_stg_s4_shortage`.source_biz_key
+);
+
+UPDATE `mdp_stg_s6_report`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s6_report`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s6_report`.source_table AND clash.source_biz_key=`mdp_stg_s6_report`.source_biz_key
+);
+
+UPDATE `mdp_stg_s6_report`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_s6_report`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_s6_report`.source_table AND clash.source_biz_key=`mdp_stg_s6_report`.source_biz_key
+);
+
+UPDATE `mdp_stg_schedule`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_schedule`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_schedule`.source_table AND clash.source_biz_key=`mdp_stg_schedule`.source_biz_key
+);
+
+UPDATE `mdp_stg_schedule`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_schedule`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_schedule`.source_table AND clash.source_biz_key=`mdp_stg_schedule`.source_biz_key
+);
+
+UPDATE `mdp_stg_ship_trans`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_ship_trans`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_ship_trans`.source_table AND clash.source_biz_key=`mdp_stg_ship_trans`.source_biz_key
+);
+
+UPDATE `mdp_stg_ship_trans`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_ship_trans`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_ship_trans`.source_table AND clash.source_biz_key=`mdp_stg_ship_trans`.source_biz_key
+);
+
+UPDATE `mdp_stg_so`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_so`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_so`.source_table AND clash.source_biz_key=`mdp_stg_so`.source_biz_key
+);
+
+UPDATE `mdp_stg_so`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_so`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_so`.source_table AND clash.source_biz_key=`mdp_stg_so`.source_biz_key
+);
+
+UPDATE `mdp_stg_source_list`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_source_list`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_source_list`.source_table AND clash.source_biz_key=`mdp_stg_source_list`.source_biz_key
+);
+
+UPDATE `mdp_stg_source_list`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_source_list`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_source_list`.source_table AND clash.source_biz_key=`mdp_stg_source_list`.source_biz_key
+);
+
+UPDATE `mdp_stg_supplier`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_supplier`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_supplier`.source_table AND clash.source_biz_key=`mdp_stg_supplier`.source_biz_key
+);
+
+UPDATE `mdp_stg_supplier`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_supplier`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_supplier`.source_table AND clash.source_biz_key=`mdp_stg_supplier`.source_biz_key
+);
+
+UPDATE `mdp_stg_supply_demand`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_supply_demand`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_supply_demand`.source_table AND clash.source_biz_key=`mdp_stg_supply_demand`.source_biz_key
+);
+
+UPDATE `mdp_stg_supply_demand`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_supply_demand`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_supply_demand`.source_table AND clash.source_biz_key=`mdp_stg_supply_demand`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_cj_bg_head_rep`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_cj_bg_head_rep`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_cj_bg_head_rep`.source_table AND clash.source_biz_key=`mdp_stg_t8_cj_bg_head_rep`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_cj_bg_head_rep`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_cj_bg_head_rep`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_cj_bg_head_rep`.source_table AND clash.source_biz_key=`mdp_stg_t8_cj_bg_head_rep`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_dd_head`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_dd_head`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_dd_head`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_dd_head`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_dd_head`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_dd_head`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_dd_head`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_dd_head`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_dd_list`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_dd_list`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_dd_list`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_dd_list`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_dd_list`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_dd_list`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_dd_list`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_dd_list`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_dd_list_cllist`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_dd_list_cllist`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_dd_list_cllist`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_dd_list_cllist`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_dd_list_cllist`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_dd_list_cllist`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_dd_list_cllist`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_dd_list_cllist`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_tz_head`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_tz_head`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_tz_head`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_tz_head`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_tz_head`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_tz_head`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_tz_head`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_tz_head`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_tz_list`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_tz_list`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_tz_list`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_tz_list`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_tz_list`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_tz_list`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_tz_list`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_tz_list`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_zj_list`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_zj_list`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_zj_list`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_zj_list`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_kc_zj_list`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_kc_zj_list`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_kc_zj_list`.source_table AND clash.source_biz_key=`mdp_stg_t8_kc_zj_list`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_sys_pelist`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_sys_pelist`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_sys_pelist`.source_table AND clash.source_biz_key=`mdp_stg_t8_sys_pelist`.source_biz_key
+);
+
+UPDATE `mdp_stg_t8_sys_pelist`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_t8_sys_pelist`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_t8_sys_pelist`.source_table AND clash.source_biz_key=`mdp_stg_t8_sys_pelist`.source_biz_key
+);
+
+UPDATE `mdp_stg_work_order_material`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system='AIDOP' AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_work_order_material`
+    WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_work_order_material`.source_table AND clash.source_biz_key=`mdp_stg_work_order_material`.source_biz_key
+);
+
+UPDATE `mdp_stg_work_order_material`
+SET source_system='AIDOP_NATIVE'
+WHERE source_system IN ('AIDOPDEV_MYSQL','AIDOP_NATIVE') AND source_biz_key IS NOT NULL
+AND NOT EXISTS (
+  SELECT 1 FROM (
+    SELECT source_table, source_biz_key FROM `mdp_stg_work_order_material`
+    WHERE source_system='AIDOP_NATIVE' AND source_biz_key IS NOT NULL
+  ) clash
+  WHERE clash.source_table=`mdp_stg_work_order_material`.source_table AND clash.source_biz_key=`mdp_stg_work_order_material`.source_biz_key
+);
+
+
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_work_order_line')>0 AND (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_work_order_line' AND COLUMN_NAME='written_by')=0, 'ALTER TABLE `mdp_std_work_order_line` ADD COLUMN `written_by` VARCHAR(32) NOT NULL DEFAULT ''LEGACY_UNVERIFIED''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_work_order_bom')>0 AND (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_work_order_bom' AND COLUMN_NAME='written_by')=0, 'ALTER TABLE `mdp_std_work_order_bom` ADD COLUMN `written_by` VARCHAR(32) NOT NULL DEFAULT ''LEGACY_UNVERIFIED''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_employee')>0 AND (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_employee' AND COLUMN_NAME='written_by')=0, 'ALTER TABLE `mdp_std_employee` ADD COLUMN `written_by` VARCHAR(32) NOT NULL DEFAULT ''LEGACY_UNVERIFIED''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_s6_report')>0 AND (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_s6_report' AND COLUMN_NAME='written_by')=0, 'ALTER TABLE `mdp_std_s6_report` ADD COLUMN `written_by` VARCHAR(32) NOT NULL DEFAULT ''LEGACY_UNVERIFIED''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_fqc_task')>0 AND (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_fqc_task' AND COLUMN_NAME='written_by')=0, 'ALTER TABLE `mdp_std_fqc_task` ADD COLUMN `written_by` VARCHAR(32) NOT NULL DEFAULT ''LEGACY_UNVERIFIED''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_inventory_balance_monthly')>0 AND (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_inventory_balance_monthly' AND COLUMN_NAME='written_by')=0, 'ALTER TABLE `mdp_std_inventory_balance_monthly` ADD COLUMN `written_by` VARCHAR(32) NOT NULL DEFAULT ''LEGACY_UNVERIFIED''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+
+INSERT IGNORE INTO mdp_tenant_std_source (tenant_id, std_object, source_system, source_scope, remark) VALUES
+(797403760988229,'INV_TRANS','T8','pbxfxp','Q5 模式二'),
+(797403760988229,'WO_LINE_PROD','T8','pbxfxp','Q5 模式二'),
+(797403760988229,'WO_LINE_SALES','T8','pbxfxp','Q5 模式二'),
+(797403760988229,'WO_BOM','T8','pbxfxp','Q5 模式二'),
+(797403760988229,'EMPLOYEE','T8','pbxfxp','Q5 模式二'),
+(797403760988229,'INV_BAL_MONTHLY','T8','pbxfxp','Q5 模式二'),
+(797403760988229,'S6_REPORT','T8','pbxfxp','Q5 模式二'),
+(797403760988229,'FQC_TASK','T8','pbxfxp','Q5 模式二'),
+(797403760988229,'SO','AIDOP_NATIVE','','Q5 模式二 S1 自有'),
+(797403760988229,'WO_SCHEDULE','AIDOP_NATIVE','','Q5 模式二 S2 自有'),
+(797403760988229,'SUPPLY_DEMAND','AIDOP_NATIVE','','Q5 模式二 S3 自有'),
+(797403760988229,'PURCHASE','AIDOP_NATIVE','','Q5 模式二 S3 自有'),
+(797403760988229,'SUPPLIER_DELIVERY','AIDOP_NATIVE','','Q5 模式二 S4 自有'),
+(797403760988229,'MASTER_ITEM','AIDOP_NATIVE','','Q5 模式二 S0 自有'),
+(838257186181189,'INV_TRANS','DOPDEMORQ_SQLSERVER','8010','Q12 模式一'),
+(838257186181189,'S6_REPORT','DOPDEMORQ_SQLSERVER','8010','Q12 模式一'),
+(838257186181189,'FQC_TASK','DOPDEMORQ_SQLSERVER','8010','Q12 模式一'),
+(838257186181189,'INV_BAL_MONTHLY','DOPDEMORQ_SQLSERVER','8010','Q12 模式一;A-6 不支持时仍保留登记'),
+(838257186181189,'WO_LINE_PROD','AIDOP_NATIVE','','Q12 模式一自有工单'),
+(838257186181189,'WO_LINE_SALES','AIDOP_NATIVE','','Q12 模式一自有订单'),
+(838257186181189,'WO_BOM','AIDOP_NATIVE','','Q12 模式一自有 BOM'),
+(838257186181189,'EMPLOYEE','AIDOP_NATIVE','','Q12 模式一员工权威在 S0'),
+(838257186181189,'SO','AIDOP_NATIVE','','Q12'),
+(838257186181189,'WO_SCHEDULE','AIDOP_NATIVE','','Q12'),
+(838257186181189,'SUPPLY_DEMAND','AIDOP_NATIVE','','Q12'),
+(838257186181189,'PURCHASE','AIDOP_NATIVE','','Q12'),
+(838257186181189,'SUPPLIER_DELIVERY','AIDOP_NATIVE','','Q12'),
+(838257186181189,'MASTER_ITEM','AIDOP_NATIVE','','Q12'),
+(797403760988229,'INV_SNAPSHOT','DOPDEMORQ_SQLSERVER','8010','未预置对象:现网主来源 53236 行 DOPDEMORQ,T8 快照 0 行'),
+(838257186181189,'INV_SNAPSHOT','DOPDEMORQ_SQLSERVER','8010','未预置对象:现网主来源 50364 行 DOPDEMORQ'),
+(797403760988229,'IQC','AIDOP_NATIVE','','未预置对象:现网 9 行 source_system=AIDOP,迁为 AIDOP_NATIVE'),
+(838257186181189,'IQC','AIDOP_NATIVE','','未预置对象:现网 11 行 source_system=AIDOP,迁为 AIDOP_NATIVE'),
+(797403760988229,'IPQC','AIDOP_NATIVE','','未预置对象:该租户现网 0 行,写入方为自有通道'),
+(838257186181189,'IPQC','AIDOP_NATIVE','','未预置对象:AIDOP 2 行 + AIDOPDEV_MYSQL 5 行,归一后均为自有'),
+(797403760988229,'PROD_DOC','AIDOP_NATIVE','','未预置对象:现网 0 行,生产单据投影写 AIDOP_NATIVE'),
+(838257186181189,'PROD_DOC','AIDOP_NATIVE','','未预置对象:现网 0 行,生产单据投影写 AIDOP_NATIVE'),
+(797403760988229,'OUTSOURCE','AIDOP_NATIVE','','未预置对象:现网 0 行,外协投影写自有通道'),
+(838257186181189,'OUTSOURCE','AIDOP_NATIVE','','未预置对象:现网 0 行,外协投影写自有通道'),
+(797403760988229,'ACTION_EVENT','AIDOP_NATIVE','','未预置对象:该租户现网 0 行,S1 自有'),
+(838257186181189,'ACTION_EVENT','AIDOP_NATIVE','','未预置对象:现网 75 行 AIDOPDEV_MYSQL,归一为自有'),
+(797403760988229,'SUPPLIER','DOPDEMORQ_SQLSERVER','8010','未预置对象:现网主来源 3185 行 DOPDEMORQ,自有 0 行'),
+(838257186181189,'SUPPLIER','AIDOP_NATIVE','','未预置对象:现网主来源 AIDOP 13 行,DOPDEMORQ 1 行');
+
+INSERT INTO mdp_tenant_std_source_log (tenant_id, std_object, old_source_system, new_source_system, changed_by)
+SELECT s.tenant_id, s.std_object, NULL, s.source_system, '1.0.571'
+FROM mdp_tenant_std_source s
+WHERE NOT EXISTS (
+  SELECT 1 FROM mdp_tenant_std_source_log l
+  WHERE l.tenant_id=s.tenant_id AND l.std_object=s.std_object AND l.changed_by='1.0.571'
+);

+ 53 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.571.verify.sql

@@ -0,0 +1,53 @@
+-- 1.0.571.verify.sql
+SELECT COUNT(*) = 3
+  FROM information_schema.tables
+ WHERE table_schema = DATABASE()
+   AND table_name IN ('mdp_tenant_std_source','mdp_tenant_std_source_log','mdp_source_gate_log');
+
+SELECT COUNT(*) = 1
+  FROM information_schema.statistics
+ WHERE table_schema = DATABASE()
+   AND table_name = 'mdp_tenant_std_source'
+   AND index_name = 'uk_tenant_std_source'
+   AND column_name = 'std_object';
+
+SELECT COUNT(*) = 1
+  FROM information_schema.columns
+ WHERE table_schema = DATABASE()
+   AND table_name = 'mdp_source'
+   AND column_name = 'system_code';
+
+SELECT COUNT(*) = 8
+  FROM mdp_tenant_std_source
+ WHERE tenant_id = 797403760988229
+   AND source_system = 'T8'
+   AND std_object IN ('INV_TRANS','WO_LINE_PROD','WO_LINE_SALES','WO_BOM','EMPLOYEE','INV_BAL_MONTHLY','S6_REPORT','FQC_TASK');
+
+SELECT COUNT(*) = 4
+  FROM mdp_tenant_std_source
+ WHERE tenant_id = 838257186181189
+   AND source_system = 'DOPDEMORQ_SQLSERVER'
+   AND std_object IN ('INV_TRANS','S6_REPORT','FQC_TASK','INV_BAL_MONTHLY');
+
+SELECT (SELECT system_code FROM mdp_source WHERE source_code = 'T8_V5_SQLSERVER') = 'T8';
+SELECT (SELECT system_code FROM mdp_source WHERE source_code = 'AIDOPDEV_MYSQL') = 'AIDOP_NATIVE';
+SELECT (SELECT system_code FROM mdp_source WHERE source_code = 'DOPDEMORQ_SQLSERVER') = 'DOPDEMORQ_SQLSERVER';
+SELECT (SELECT system_code FROM mdp_source WHERE source_code = 'QMS_API') = 'QMS_API';
+
+SELECT COUNT(*) = 0 FROM mdp_std_so WHERE source_system = 'AIDOP';
+
+SELECT COUNT(*) = 6
+  FROM information_schema.columns
+ WHERE table_schema = DATABASE()
+   AND column_name = 'written_by'
+   AND table_name IN (
+     'mdp_std_work_order_line','mdp_std_work_order_bom','mdp_std_employee',
+     'mdp_std_s6_report','mdp_std_fqc_task','mdp_std_inventory_balance_monthly');
+
+SELECT COUNT(*) = 42
+  FROM mdp_tenant_std_source
+ WHERE tenant_id IN (797403760988229, 838257186181189);
+
+SELECT COUNT(*) = 0
+  FROM mdp_tenant_std_source
+ WHERE tenant_id NOT IN (797403760988229, 838257186181189);

+ 127 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.572.sql

@@ -0,0 +1,127 @@
+-- 1.0.572 · N-3 岗位映射、E-4 指标结果状态与来源能力。幂等。不删行。
+
+CREATE TABLE IF NOT EXISTS mdp_employee_position_map (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  tenant_id BIGINT NOT NULL,
+  source_system VARCHAR(50) NOT NULL,
+  domain VARCHAR(50) NOT NULL DEFAULT '',
+  src_position_raw VARCHAR(200) NOT NULL COMMENT '源岗位原文',
+  src_position_field VARCHAR(32) NOT NULL COMMENT 'JobTitle/Post/WorkCtr/gw/Position',
+  position_code VARCHAR(24) NOT NULL DEFAULT 'UNKNOWN' COMMENT 'WAREHOUSE/PRODUCTION/PLANNER/OTHER/UNKNOWN',
+  role_source VARCHAR(24) NOT NULL DEFAULT 'AUTO_KEYWORD' COMMENT 'AUTO_KEYWORD/MANUAL',
+  remark VARCHAR(500) NULL,
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  UNIQUE KEY uk_emp_pos_map (tenant_id, source_system, domain, src_position_raw)
+);
+
+CREATE TABLE IF NOT EXISTS mdp_source_capability (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  source_system VARCHAR(50) NOT NULL,
+  semantic_code VARCHAR(48) NOT NULL COMMENT '附录 F 字段类语义码',
+  supported TINYINT NOT NULL,
+  reason VARCHAR(300) NOT NULL COMMENT '面向业务用户的原因文本',
+  evidence VARCHAR(500) NULL COMMENT '核实依据',
+  verified_at DATETIME NOT NULL,
+  UNIQUE KEY uk_source_capability (source_system, semantic_code)
+);
+
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_s9_kpi_value_l1_day' AND COLUMN_NAME='result_status')=0, 'ALTER TABLE `ado_s9_kpi_value_l1_day` ADD COLUMN `result_status` VARCHAR(24) NULL COMMENT ''OK/NO_DATA/SOURCE_UNSUPPORTED/FAILED''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_s9_kpi_value_l1_day' AND COLUMN_NAME='result_reason')=0, 'ALTER TABLE `ado_s9_kpi_value_l1_day` ADD COLUMN `result_reason` VARCHAR(300) NULL COMMENT ''非 OK 时的原因''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_s9_kpi_value_l2_day' AND COLUMN_NAME='result_status')=0, 'ALTER TABLE `ado_s9_kpi_value_l2_day` ADD COLUMN `result_status` VARCHAR(24) NULL COMMENT ''OK/NO_DATA/SOURCE_UNSUPPORTED/FAILED''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_s9_kpi_value_l2_day' AND COLUMN_NAME='result_reason')=0, 'ALTER TABLE `ado_s9_kpi_value_l2_day` ADD COLUMN `result_reason` VARCHAR(300) NULL COMMENT ''非 OK 时的原因''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_s9_kpi_value_l3_day' AND COLUMN_NAME='result_status')=0, 'ALTER TABLE `ado_s9_kpi_value_l3_day` ADD COLUMN `result_status` VARCHAR(24) NULL COMMENT ''OK/NO_DATA/SOURCE_UNSUPPORTED/FAILED''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_s9_kpi_value_l3_day' AND COLUMN_NAME='result_reason')=0, 'ALTER TABLE `ado_s9_kpi_value_l3_day` ADD COLUMN `result_reason` VARCHAR(300) NULL COMMENT ''非 OK 时的原因''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_s9_kpi_value_l4_day' AND COLUMN_NAME='result_status')=0, 'ALTER TABLE `ado_s9_kpi_value_l4_day` ADD COLUMN `result_status` VARCHAR(24) NULL COMMENT ''OK/NO_DATA/SOURCE_UNSUPPORTED/FAILED''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_s9_kpi_value_l4_day' AND COLUMN_NAME='result_reason')=0, 'ALTER TABLE `ado_s9_kpi_value_l4_day` ADD COLUMN `result_reason` VARCHAR(300) NULL COMMENT ''非 OK 时的原因''', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+UPDATE ado_s9_kpi_value_l1_day
+SET result_status=IF(metric_value IS NULL,'NO_DATA','OK'),
+    result_reason=IF(metric_value IS NULL,'窗口内无业务数据',NULL)
+WHERE result_status IS NULL;
+
+UPDATE ado_s9_kpi_value_l2_day
+SET result_status=IF(metric_value IS NULL,'NO_DATA','OK'),
+    result_reason=IF(metric_value IS NULL,'窗口内无业务数据',NULL)
+WHERE result_status IS NULL;
+
+UPDATE ado_s9_kpi_value_l3_day
+SET result_status=IF(metric_value IS NULL,'NO_DATA','OK'),
+    result_reason=IF(metric_value IS NULL,'窗口内无业务数据',NULL)
+WHERE result_status IS NULL;
+
+UPDATE ado_s9_kpi_value_l4_day
+SET result_status=IF(metric_value IS NULL,'NO_DATA','OK'),
+    result_reason=IF(metric_value IS NULL,'窗口内无业务数据',NULL)
+WHERE result_status IS NULL;
+
+INSERT INTO mdp_source_capability (source_system, semantic_code, supported, reason, evidence, verified_at)
+SELECT 'T8', 'FG_RECEIPT_TIME', 0, '源系统没有客户签收时间', 'Q2:T8 无客户签收时间字段', NOW()
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM mdp_source_capability WHERE source_system='T8' AND semantic_code='FG_RECEIPT_TIME');
+
+INSERT INTO mdp_source_capability (source_system, semantic_code, supported, reason, evidence, verified_at)
+SELECT 'DOPDEMORQ_SQLSERVER', 'FG_RECEIPT_TIME', 0, '源系统没有客户签收时间', 'Q2:165 无客户签收时间字段', NOW()
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM mdp_source_capability WHERE source_system='DOPDEMORQ_SQLSERVER' AND semantic_code='FG_RECEIPT_TIME');
+
+INSERT INTO mdp_source_capability (source_system, semantic_code, supported, reason, evidence, verified_at)
+SELECT 'DOPDEMORQ_SQLSERVER', 'DOC_QTY_CLOSE', 0, '165 工单行没有关闭标志来源列', 'R9.4:165 无对应列,不把状态码猜成关闭', NOW()
+FROM DUAL
+WHERE NOT EXISTS (SELECT 1 FROM mdp_source_capability WHERE source_system='DOPDEMORQ_SQLSERVER' AND semantic_code='DOC_QTY_CLOSE');
+
+INSERT INTO mdp_employee_position_map
+  (tenant_id, source_system, domain, src_position_raw, src_position_field, position_code, role_source)
+SELECT p.tenant_id, 'T8', IFNULL(p.ztid,''), p.gw, 'gw',
+  CASE
+    WHEN p.gw='计划' THEN 'PLANNER'
+    WHEN p.gw='仓管' THEN 'WAREHOUSE'
+    WHEN p.gw='生产' THEN 'PRODUCTION'
+    WHEN p.gw LIKE '%仓%' OR p.gw LIKE '%库管%' OR p.gw LIKE '%保管%' OR p.gw LIKE '%物料员%' OR p.gw LIKE '%WMS%' THEN 'WAREHOUSE'
+    WHEN p.gw LIKE '%生产%' OR p.gw LIKE '%操作%' OR p.gw LIKE '%装配%' OR p.gw LIKE '%车间%' OR p.gw LIKE '%班组%' OR p.gw LIKE '%产线%' OR p.gw LIKE '%工段%' THEN 'PRODUCTION'
+    WHEN p.gw LIKE '%计划%' OR p.gw LIKE '%PMC%' OR p.gw LIKE '%调度%' THEN 'PLANNER'
+    ELSE 'UNKNOWN' END,
+  'AUTO_KEYWORD'
+FROM (
+  SELECT DISTINCT tenant_id, IFNULL(ztid,'') AS ztid, gw
+  FROM mdp_std_t8_sys_pelist
+  WHERE gw IS NOT NULL AND gw<>''
+) p
+ON DUPLICATE KEY UPDATE
+  position_code=IF(role_source='MANUAL', position_code, VALUES(position_code)),
+  src_position_field=IF(role_source='MANUAL', src_position_field, VALUES(src_position_field));
+
+INSERT INTO mdp_employee_position_map
+  (tenant_id, source_system, domain, src_position_raw, src_position_field, position_code, role_source)
+SELECT e.tenant_id, 'AIDOP_NATIVE', IFNULL(e.Domain,''), e.raw_pos, 'JobTitle',
+  CASE
+    WHEN e.raw_pos='计划' THEN 'PLANNER'
+    WHEN e.raw_pos='仓管' THEN 'WAREHOUSE'
+    WHEN e.raw_pos='生产' THEN 'PRODUCTION'
+    WHEN e.raw_pos LIKE '%仓%' OR e.raw_pos LIKE '%库管%' OR e.raw_pos LIKE '%保管%' OR e.raw_pos LIKE '%物料员%' OR e.raw_pos LIKE '%WMS%' THEN 'WAREHOUSE'
+    WHEN e.raw_pos LIKE '%生产%' OR e.raw_pos LIKE '%操作%' OR e.raw_pos LIKE '%装配%' OR e.raw_pos LIKE '%车间%' OR e.raw_pos LIKE '%班组%' OR e.raw_pos LIKE '%产线%' OR e.raw_pos LIKE '%工段%' THEN 'PRODUCTION'
+    WHEN e.raw_pos LIKE '%计划%' OR e.raw_pos LIKE '%PMC%' OR e.raw_pos LIKE '%调度%' THEN 'PLANNER'
+    ELSE 'UNKNOWN' END,
+  'AUTO_KEYWORD'
+FROM (
+  SELECT DISTINCT tenant_id, Domain, COALESCE(NULLIF(JobTitle,''), NULLIF(WorkCtr,'')) AS raw_pos
+  FROM EmployeeMaster
+  WHERE COALESCE(NULLIF(JobTitle,''), NULLIF(WorkCtr,'')) IS NOT NULL
+) e
+ON DUPLICATE KEY UPDATE
+  position_code=IF(role_source='MANUAL', position_code, VALUES(position_code)),
+  src_position_field=IF(role_source='MANUAL', src_position_field, VALUES(src_position_field));
+
+ALTER TABLE mdp_std_employee
+  MODIFY COLUMN position_code VARCHAR(32) NOT NULL DEFAULT 'OTHER' COMMENT 'WAREHOUSE/PRODUCTION/PLANNER/OTHER/UNKNOWN';

+ 51 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.572.verify.sql

@@ -0,0 +1,51 @@
+-- 1.0.572.verify.sql
+SELECT COUNT(*) = 2
+  FROM information_schema.tables
+ WHERE table_schema = DATABASE()
+   AND table_name IN ('mdp_employee_position_map','mdp_source_capability');
+
+SELECT COUNT(*) = 1
+  FROM information_schema.statistics
+ WHERE table_schema = DATABASE()
+   AND table_name = 'mdp_employee_position_map'
+   AND index_name = 'uk_emp_pos_map'
+   AND column_name = 'src_position_raw';
+
+SELECT COUNT(*) = 8
+  FROM information_schema.columns
+ WHERE table_schema = DATABASE()
+   AND table_name IN ('ado_s9_kpi_value_l1_day','ado_s9_kpi_value_l2_day','ado_s9_kpi_value_l3_day','ado_s9_kpi_value_l4_day')
+   AND column_name IN ('result_status','result_reason');
+
+SELECT COUNT(*) = 0
+  FROM ado_s9_kpi_value_l1_day
+ WHERE result_status IS NULL;
+
+SELECT COUNT(*) = 0
+  FROM ado_s9_kpi_value_l2_day
+ WHERE result_status IS NULL;
+
+SELECT COUNT(*) = 0
+  FROM ado_s9_kpi_value_l3_day
+ WHERE result_status IS NULL;
+
+SELECT COUNT(*) = 0
+  FROM ado_s9_kpi_value_l4_day
+ WHERE result_status IS NULL;
+
+SELECT COUNT(*) = 2
+  FROM mdp_source_capability
+ WHERE semantic_code = 'FG_RECEIPT_TIME' AND supported = 0
+   AND source_system IN ('T8','DOPDEMORQ_SQLSERVER');
+
+SELECT COUNT(*) = 1
+  FROM mdp_source_capability
+ WHERE source_system = 'DOPDEMORQ_SQLSERVER' AND semantic_code = 'DOC_QTY_CLOSE' AND supported = 0;
+
+SELECT COUNT(*) = 0
+  FROM mdp_source_capability
+ WHERE source_system = 'T8' AND semantic_code = 'INV_LOCATION';
+
+SELECT COUNT(*) = 0
+  FROM mdp_source_capability
+ WHERE semantic_code = 'INV_BAL_AMOUNT' AND supported = 0;

+ 436 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.573.sql

@@ -0,0 +1,436 @@
+-- 1.0.573 B-2 / A-3 / A-4 / A-5 / C-4 staging / D-4 source columns
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_t8_kc_tz_head' AND COLUMN_NAME='zbrname')=0, 'ALTER TABLE `mdp_std_t8_kc_tz_head` ADD COLUMN `zbrname` varchar(100) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_t8_kc_tz_list' AND COLUMN_NAME='hw')=0, 'ALTER TABLE `mdp_std_t8_kc_tz_list` ADD COLUMN `hw` varchar(80) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_t8_kc_tz_list' AND COLUMN_NAME='ckcode')=0, 'ALTER TABLE `mdp_std_t8_kc_tz_list` ADD COLUMN `ckcode` varchar(80) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_t8_kc_tz_list' AND COLUMN_NAME='pcnoid')=0, 'ALTER TABLE `mdp_std_t8_kc_tz_list` ADD COLUMN `pcnoid` varchar(80) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_t8_kc_tz_list' AND COLUMN_NAME='kcsl')=0, 'ALTER TABLE `mdp_std_t8_kc_tz_list` ADD COLUMN `kcsl` decimal(18,6) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_s6_report' AND COLUMN_NAME='start_work_date')=0, 'ALTER TABLE `mdp_std_s6_report` ADD COLUMN `start_work_date` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+CREATE TABLE IF NOT EXISTS mdp_stg_inv_trans_inbound (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  tenant_id BIGINT NOT NULL DEFAULT 0,
+  source_system VARCHAR(50) NULL,
+  source_table VARCHAR(200) NULL,
+  source_row_id VARCHAR(200) NULL,
+  source_biz_key VARCHAR(300) NULL,
+  raw_data JSON NULL,
+  sync_batch_id VARCHAR(100) NULL,
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  sync_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
+  process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
+  process_message VARCHAR(500) NULL,
+  update_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  UNIQUE KEY uk_source_key (tenant_id, source_system, source_table, source_biz_key),
+  KEY idx_batch (sync_batch_id),
+  KEY idx_src (source_table, source_row_id)
+);
+
+CREATE TABLE IF NOT EXISTS mdp_stg_wo_line (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  tenant_id BIGINT NOT NULL DEFAULT 0,
+  source_system VARCHAR(50) NULL,
+  source_table VARCHAR(200) NULL,
+  source_row_id VARCHAR(200) NULL,
+  source_biz_key VARCHAR(300) NULL,
+  raw_data JSON NULL,
+  sync_batch_id VARCHAR(100) NULL,
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  sync_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
+  process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
+  process_message VARCHAR(500) NULL,
+  update_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  UNIQUE KEY uk_source_key (tenant_id, source_system, source_table, source_biz_key),
+  KEY idx_batch (sync_batch_id),
+  KEY idx_src (source_table, source_row_id)
+);
+
+CREATE TABLE IF NOT EXISTS mdp_stg_wo_bom (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  tenant_id BIGINT NOT NULL DEFAULT 0,
+  source_system VARCHAR(50) NULL,
+  source_table VARCHAR(200) NULL,
+  source_row_id VARCHAR(200) NULL,
+  source_biz_key VARCHAR(300) NULL,
+  raw_data JSON NULL,
+  sync_batch_id VARCHAR(100) NULL,
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  sync_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
+  process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
+  process_message VARCHAR(500) NULL,
+  update_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  UNIQUE KEY uk_source_key (tenant_id, source_system, source_table, source_biz_key),
+  KEY idx_batch (sync_batch_id),
+  KEY idx_src (source_table, source_row_id)
+);
+
+CREATE TABLE IF NOT EXISTS mdp_stg_inv_bal_monthly (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  tenant_id BIGINT NOT NULL DEFAULT 0,
+  source_system VARCHAR(50) NULL,
+  source_table VARCHAR(200) NULL,
+  source_row_id VARCHAR(200) NULL,
+  source_biz_key VARCHAR(300) NULL,
+  raw_data JSON NULL,
+  sync_batch_id VARCHAR(100) NULL,
+  create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  sync_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
+  process_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
+  process_message VARCHAR(500) NULL,
+  update_time DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  UNIQUE KEY uk_source_key (tenant_id, source_system, source_table, source_biz_key),
+  KEY idx_batch (sync_batch_id),
+  KEY idx_src (source_table, source_row_id)
+);
+
+INSERT INTO mdp_stg_inv_trans_inbound
+  (tenant_id, source_system, source_table, source_row_id, source_biz_key, raw_data, sync_batch_id, process_status, process_message, create_time, sync_time, update_time)
+SELECT tenant_id, source_system, source_table, source_row_id, source_biz_key, raw_data, sync_batch_id, process_status, process_message, create_time, sync_time, update_time
+FROM mdp_stg_t8_kc_tz_list
+WHERE IFNULL(source_system,'') NOT IN ('T8','T8_V5_SQLSERVER')
+  AND NOT EXISTS (
+    SELECT 1 FROM mdp_stg_inv_trans_inbound n
+    WHERE n.tenant_id=mdp_stg_t8_kc_tz_list.tenant_id
+      AND IFNULL(n.source_system,'')=IFNULL(mdp_stg_t8_kc_tz_list.source_system,'')
+      AND IFNULL(n.source_table,'')=IFNULL(mdp_stg_t8_kc_tz_list.source_table,'')
+      AND n.source_biz_key=mdp_stg_t8_kc_tz_list.source_biz_key
+  );
+
+DELETE FROM mdp_stg_t8_kc_tz_list WHERE IFNULL(source_system,'') NOT IN ('T8','T8_V5_SQLSERVER');
+
+UPDATE mdp_entity
+SET target_table_name='mdp_stg_inv_trans_inbound', update_time=NOW()
+WHERE entity_code='S5_INVENTORY_TXN' AND target_table_name<>'mdp_stg_inv_trans_inbound';
+
+CREATE TABLE IF NOT EXISTS mdp_ship_recon_diff (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  tenant_id BIGINT NOT NULL,
+  source_system VARCHAR(50) NOT NULL,
+  domain VARCHAR(50) NOT NULL,
+  ord_nbr VARCHAR(120) NOT NULL,
+  ord_line VARCHAR(50) NOT NULL,
+  biz_date DATE NOT NULL,
+  asn_qty DECIMAL(18,6) NOT NULL DEFAULT 0,
+  inv_iss_so_qty DECIMAL(18,6) NOT NULL DEFAULT 0,
+  diff_qty DECIMAL(18,6) NOT NULL,
+  detected_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  UNIQUE KEY uk_ship_recon (tenant_id, source_system, domain, ord_nbr, ord_line, biz_date)
+);
+
+UPDATE mdp_std_inv_trans
+SET biz_doc_type='OTHER'
+WHERE source_system='DOPDEMORQ_SQLSERVER' AND src_trans_type_raw='iss-so' AND biz_doc_type='SALES_SHIP';
+
+UPDATE mdp_entity SET status=1, update_time=NOW()
+WHERE entity_code IN ('S7_FQC_TASK_SQLSERVER','S7_FQC_RESULT_SQLSERVER','S7_FQC_RESULT_DETAIL_SQLSERVER')
+  AND status=0;
+INSERT INTO mdp_entity (`tenant_id`, `source_id`, `entity_code`, `entity_name`, `entity_type`, `source_table_name`, `source_api_path`, `api_config_id`, `target_table_name`, `sync_mode`, `incr_column`, `batch_size`, `response_data_path`, `dedup_key_path`, `last_cursor`, `last_sync_to`, `job_id`, `status`, `remark`, `create_time`, `update_time`, `TenantId`, `SourceId`, `EntityCode`, `EntityName`, `EntityType`, `SourceTableName`, `SourceApiPath`, `ApiConfigId`, `TargetTableName`, `SyncMode`, `IncrColumn`, `BatchSize`, `ResponseDataPath`, `DedupKeyPath`, `LastCursor`, `biz_key_expr`, `file_config_json`, `is_public_template`, `inbound_enabled`, `inbound_max_rows`, `stale_guard_enabled`, `silence_calendar`, `silence_alert_hours`)
+SELECT `tenant_id`, `source_id`, 'S6_EMP_WORK_ORD_TRANS_SQLSERVER', '165报工流水', `entity_type`, 'EmpWorkOrdTransHist', `source_api_path`, `api_config_id`, 'mdp_stg_s6_report', `sync_mode`, `incr_column`, `batch_size`, `response_data_path`, `dedup_key_path`, `last_cursor`, `last_sync_to`, `job_id`, 1, 'S6_REPORT', NOW(), NOW(), `TenantId`, `SourceId`, `EntityCode`, `EntityName`, `EntityType`, `SourceTableName`, `SourceApiPath`, `ApiConfigId`, `TargetTableName`, `SyncMode`, `IncrColumn`, `BatchSize`, `ResponseDataPath`, `DedupKeyPath`, `LastCursor`, 'Domain,Trans,RecID', `file_config_json`, `is_public_template`, `inbound_enabled`, `inbound_max_rows`, `stale_guard_enabled`, `silence_calendar`, `silence_alert_hours`
+FROM mdp_entity
+WHERE entity_code='S7_FQC_TASK_SQLSERVER'
+  AND NOT EXISTS (SELECT 1 FROM mdp_entity e2 WHERE e2.entity_code='S6_EMP_WORK_ORD_TRANS_SQLSERVER');
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ItemMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ItemMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ItemMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ItemMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='CustMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `CustMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='CustMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `CustMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='SuppMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `SuppMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='SuppMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `SuppMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LocationMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `LocationMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LocationMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `LocationMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='EmployeeMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `EmployeeMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='EmployeeMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `EmployeeMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='DepartmentMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `DepartmentMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='DepartmentMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `DepartmentMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='srm_purchase' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `srm_purchase` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='srm_purchase' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `srm_purchase` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ProductStructureMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ProductStructureMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ProductStructureMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ProductStructureMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ProductStructureOp' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ProductStructureOp` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ProductStructureOp' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ProductStructureOp` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='RoutingOpDetail' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `RoutingOpDetail` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='RoutingOpDetail' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `RoutingOpDetail` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='StdOpMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `StdOpMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='StdOpMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `StdOpMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='WorkCtrMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `WorkCtrMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='WorkCtrMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `WorkCtrMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='WorkCenter' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `WorkCenter` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='WorkCenter' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `WorkCenter` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LineMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `LineMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LineMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `LineMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ProdLineDetail' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ProdLineDetail` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ProdLineDetail' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ProdLineDetail` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LineSkillMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `LineSkillMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LineSkillMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `LineSkillMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LineSkillDetail' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `LineSkillDetail` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LineSkillDetail' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `LineSkillDetail` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LinePost' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `LinePost` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LinePost' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `LinePost` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='PersonSkill' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `PersonSkill` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='PersonSkill' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `PersonSkill` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='EmpSkills' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `EmpSkills` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='EmpSkills' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `EmpSkills` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='PersonSkillAssignment' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `PersonSkillAssignment` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='PersonSkillAssignment' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `PersonSkillAssignment` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectioncrit' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_inspectioncrit` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectioncrit' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_inspectioncrit` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspeccrit_entry' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_inspeccrit_entry` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspeccrit_entry' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_inspeccrit_entry` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectionfreq' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_inspectionfreq` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectionfreq' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_inspectionfreq` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectioninstru' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_inspectioninstru` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectioninstru' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_inspectioninstru` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectionitems' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_inspectionitems` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectionitems' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_inspectionitems` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspection_method' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_inspection_method` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspection_method' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_inspection_method` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectionstd' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_inspectionstd` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectionstd' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_inspectionstd` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectionstdentry' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_inspectionstdentry` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectionstdentry' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_inspectionstdentry` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_sampscheme' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_sampscheme` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_sampscheme' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_sampscheme` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectpro' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_inspectpro` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspectpro' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_inspectpro` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspro_ent' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_inspro_ent` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_inspro_ent' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_inspro_ent` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_quality_base_type' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_quality_base_type` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_quality_base_type' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_quality_base_type` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_jygf' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_jygf` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_jygf' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_jygf` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_jygfzb' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_jygfzb` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_jygfzb' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_jygfzb` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_gcjygf' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_gcjygf` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_gcjygf' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_gcjygf` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_gcjygfzb' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_gcjygfzb` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_gcjygfzb' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_gcjygfzb` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_fqcjygf' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_fqcjygf` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_fqcjygf' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_fqcjygf` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_fqcjygfzb' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_fqcjygfzb` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_fqcjygfzb' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_fqcjygfzb` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_oqcjygf' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_oqcjygf` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_oqcjygf' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_oqcjygf` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_oqcjygfzb' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_oqcjygfzb` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_oqcjygfzb' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_oqcjygfzb` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_ccjygf' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_ccjygf` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_ccjygf' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_ccjygf` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_ccjygfzb' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_ccjygfzb` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_ccjygfzb' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_ccjygfzb` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_lymjbmd' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `qms_lymjbmd` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='qms_lymjbmd' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `qms_lymjbmd` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ItemPackMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ItemPackMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ItemPackMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ItemPackMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LocationShelfMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `LocationShelfMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='LocationShelfMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `LocationShelfMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='NbrControl' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `NbrControl` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='NbrControl' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `NbrControl` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='NbrTypeMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `NbrTypeMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='NbrTypeMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `NbrTypeMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='BarCodeType' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `BarCodeType` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='BarCodeType' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `BarCodeType` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='BarCodeNbr' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `BarCodeNbr` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='BarCodeNbr' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `BarCodeNbr` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='CostCtrMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `CostCtrMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='CostCtrMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `CostCtrMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='EmpWorkDutyMaster' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `EmpWorkDutyMaster` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='EmpWorkDutyMaster' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `EmpWorkDutyMaster` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='wms_rwzp' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `wms_rwzp` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='wms_rwzp' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `wms_rwzp` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0OrderReviewCycle' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `S0OrderReviewCycle` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0OrderReviewCycle' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `S0OrderReviewCycle` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0ProductDesignCycle' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `S0ProductDesignCycle` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0ProductDesignCycle' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `S0ProductDesignCycle` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0ContractReviewCycle' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `S0ContractReviewCycle` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0ContractReviewCycle' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `S0ContractReviewCycle` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0ContractReviewCycleBreakdown' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `S0ContractReviewCycleBreakdown` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0ContractReviewCycleBreakdown' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `S0ContractReviewCycleBreakdown` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='PriorityCode' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `PriorityCode` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='PriorityCode' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `PriorityCode` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_s0_sales_order_priority_rule' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ado_s0_sales_order_priority_rule` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_s0_sales_order_priority_rule' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ado_s0_sales_order_priority_rule` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0CategoryLeadTime' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `S0CategoryLeadTime` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0CategoryLeadTime' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `S0CategoryLeadTime` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0MaterialPlanCycle' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `S0MaterialPlanCycle` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0MaterialPlanCycle' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `S0MaterialPlanCycle` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0OrderScheduleCycle' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `S0OrderScheduleCycle` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0OrderScheduleCycle' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `S0OrderScheduleCycle` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='WorkOrdControl' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `WorkOrdControl` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='WorkOrdControl' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `WorkOrdControl` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ItemSubstituteDetail' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ItemSubstituteDetail` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ItemSubstituteDetail' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ItemSubstituteDetail` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ic_substitute' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ic_substitute` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ic_substitute' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ic_substitute` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_kitting_exclude_rule' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ado_kitting_exclude_rule` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_kitting_exclude_rule' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ado_kitting_exclude_rule` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='PreprocessElement' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `PreprocessElement` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='PreprocessElement' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `PreprocessElement` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='PreprocessElementParam' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `PreprocessElementParam` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='PreprocessElementParam' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `PreprocessElementParam` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ElementParam' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ElementParam` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ElementParam' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ElementParam` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0ProcessFlowCard' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `S0ProcessFlowCard` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='S0ProcessFlowCard' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `S0ProcessFlowCard` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='MaterialProcessElement' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `MaterialProcessElement` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='MaterialProcessElement' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `MaterialProcessElement` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ItemOpCondition' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ItemOpCondition` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ItemOpCondition' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ItemOpCondition` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ImageType' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `ImageType` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ImageType' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `ImageType` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='QualitySegmentImage' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `QualitySegmentImage` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='QualitySegmentImage' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `QualitySegmentImage` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='DevMonitor' AND COLUMN_NAME='source_system')=0, 'ALTER TABLE `DevMonitor` ADD COLUMN `source_system` varchar(50) NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+SET @sql := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='DevMonitor' AND COLUMN_NAME='source_updated_at')=0, 'ALTER TABLE `DevMonitor` ADD COLUMN `source_updated_at` datetime NULL', 'DO 0');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;

+ 35 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.573.verify.sql

@@ -0,0 +1,35 @@
+-- 1.0.573.verify.sql
+SELECT COUNT(*) = 4
+  FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_t8_kc_tz_list'
+   AND COLUMN_NAME IN ('hw','ckcode','pcnoid','kcsl');
+
+SELECT COUNT(*) = 1
+  FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_std_t8_kc_tz_head' AND COLUMN_NAME='zbrname';
+
+SELECT COUNT(*) = 1
+  FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_ship_recon_diff';
+
+SELECT COUNT(*) = 4
+  FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA=DATABASE()
+   AND TABLE_NAME IN ('mdp_stg_inv_trans_inbound','mdp_stg_wo_line','mdp_stg_wo_bom','mdp_stg_inv_bal_monthly');
+
+SELECT COUNT(*) = 0
+  FROM mdp_entity
+ WHERE entity_code IN ('S7_FQC_TASK_SQLSERVER','S7_FQC_RESULT_SQLSERVER','S7_FQC_RESULT_DETAIL_SQLSERVER')
+   AND status=0;
+
+SELECT COUNT(*) = 1
+  FROM mdp_entity
+ WHERE entity_code='S6_EMP_WORK_ORD_TRANS_SQLSERVER' AND status=1;
+
+SELECT COUNT(*) = 0
+  FROM mdp_std_inv_trans
+ WHERE source_system='DOPDEMORQ_SQLSERVER' AND src_trans_type_raw='iss-so' AND biz_doc_type='SALES_SHIP';
+
+SELECT COUNT(*) = 0
+  FROM mdp_stg_t8_kc_tz_list
+ WHERE IFNULL(source_system,'') NOT IN ('T8','T8_V5_SQLSERVER');

+ 80 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.574.sql

@@ -0,0 +1,80 @@
+-- 1.0.574 开通 S1–S4 / C-4 推送实体与授权。
+-- 只新增登记。不删除行,不改已有实体的来源、蛇形贴源表和游标,不改 mdp_tenant_std_source。
+-- 唯一的既有行改动:把报工实体的 Pascal 列改成与蛇形列一致。
+-- 模板取现有 S5_INVENTORY_TXN 的最早一行;没有模板则本脚本不插入,避免造出一个没有来源的实体。
+
+INSERT INTO mdp_entity (
+  `tenant_id`, `source_id`, `entity_code`, `entity_name`, `entity_type`, `source_table_name`, `source_api_path`, `api_config_id`, `target_table_name`,
+  `sync_mode`, `incr_column`, `batch_size`, `response_data_path`, `dedup_key_path`, `last_cursor`, `last_sync_to`, `job_id`, `status`, `remark`, `create_time`, `update_time`,
+  `TenantId`, `SourceId`, `EntityCode`, `EntityName`, `EntityType`, `SourceTableName`, `SourceApiPath`, `ApiConfigId`, `TargetTableName`,
+  `SyncMode`, `IncrColumn`, `BatchSize`, `ResponseDataPath`, `DedupKeyPath`, `LastCursor`,
+  `biz_key_expr`, `file_config_json`, `is_public_template`, `inbound_enabled`, `inbound_max_rows`, `stale_guard_enabled`, `silence_calendar`, `silence_alert_hours`)
+SELECT
+  src.tenant_id, src.source_id, d.entity_code, d.entity_name, src.entity_type, d.entity_code, src.source_api_path, src.api_config_id, d.target_table,
+  src.sync_mode, src.incr_column, src.batch_size, src.response_data_path, src.dedup_key_path, NULL, NULL, NULL, 1, d.remark, NOW(), NOW(),
+  src.TenantId, src.SourceId, d.entity_code, d.entity_name, src.EntityType, d.entity_code, src.SourceApiPath, src.ApiConfigId, d.target_table,
+  src.SyncMode, src.IncrColumn, src.BatchSize, src.ResponseDataPath, src.DedupKeyPath, NULL,
+  d.biz_key, src.file_config_json, src.is_public_template, 1, 500, 1, NULL, NULL
+FROM (
+  SELECT * FROM (
+    SELECT * FROM mdp_entity WHERE entity_code='S5_INVENTORY_TXN' ORDER BY id LIMIT 1
+  ) tmpl
+) src
+JOIN (
+  SELECT 'S2_WORK_ORDER_SCHEDULE' AS entity_code, '排程工单' AS entity_name, 'mdp_stg_schedule' AS target_table, 'WorkOrder,ItemCode' AS biz_key, '推送契约,不改既有来源登记' AS remark
+  UNION ALL SELECT 'S3_PURCHASE_ORDER', '采购订单', 'mdp_stg_purchase_order', 'PoNo,PoLine', '推送契约,不改既有来源登记'
+  UNION ALL SELECT 'S3_PURCHASE_RECEIPT', '采购收货', 'mdp_stg_purchase_receipt', 'Domain,Receiver,Line', '推送契约,不改既有来源登记'
+  UNION ALL SELECT 'S4_SHIPMENT', '供应商发货', 'mdp_stg_s4_shipment', 'ShipmentNo', '推送契约,不改既有来源登记'
+  UNION ALL SELECT 'S4_IQC', '来料检验', 'mdp_stg_s4_iqc', 'PoNo,PoLine,ItemCode', '推送契约,不改既有来源登记'
+  UNION ALL SELECT 'S4_RETURN', '采购退货', 'mdp_stg_s4_return', 'PoNo,PoLine,ItemCode', '推送契约,不改既有来源登记'
+  UNION ALL SELECT 'S4_SHORTAGE', '缺料', 'mdp_stg_s4_shortage', 'WorkOrder,ItemCode', '推送契约,不改既有来源登记'
+  UNION ALL SELECT 'S6_WORK_ORDER_LINE', '生产工单行', 'mdp_stg_wo_line', 'Domain,OrderNo', '推送契约,不改既有来源登记'
+  UNION ALL SELECT 'S7_SALES_ORDER_LINE', '销售订单行', 'mdp_stg_wo_line', 'Domain,OrderNo,LineNo', '推送契约,不改既有来源登记'
+  UNION ALL SELECT 'S5_WORK_ORDER_BOM', '工单用料', 'mdp_stg_wo_bom', 'Domain,OrderNo,ItemCode', '推送契约,不改既有来源登记'
+  UNION ALL SELECT 'S6_REPORT_TXN', '报工', 'mdp_stg_s6_report', 'Domain,ReportId', '推送契约,不改既有来源登记'
+  UNION ALL SELECT 'S7_FQC_TASK_TXN', 'FQC任务', 'mdp_stg_fqc_pull', 'BillNo', '推送契约,不改既有来源登记'
+  UNION ALL SELECT 'S5_INVENTORY_BALANCE_MONTHLY', '月度库存金额', 'mdp_stg_inv_bal_monthly', 'PeriodYm,ItemCode', '推送契约,不改既有来源登记'
+) d
+WHERE NOT EXISTS (SELECT 1 FROM mdp_entity e WHERE e.entity_code = d.entity_code);
+
+INSERT INTO mdp_inbound_grant
+  (tenant_id, access_key, entity_code, factory_id, source_code, rate_limit_per_min, batch_row_limit, status, remark, create_time, update_time)
+SELECT g.tenant_id, g.access_key, d.entity_code, g.factory_id, g.source_code, g.rate_limit_per_min, g.batch_row_limit, 1, '沿用已有入站密钥', NOW(), NOW()
+FROM mdp_inbound_grant g
+JOIN (
+  SELECT 'S2_WORK_ORDER_SCHEDULE' AS entity_code
+  UNION ALL SELECT 'S3_PURCHASE_ORDER'
+  UNION ALL SELECT 'S3_PURCHASE_RECEIPT'
+  UNION ALL SELECT 'S4_SHIPMENT'
+  UNION ALL SELECT 'S4_IQC'
+  UNION ALL SELECT 'S4_RETURN'
+  UNION ALL SELECT 'S4_SHORTAGE'
+  UNION ALL SELECT 'S6_WORK_ORDER_LINE'
+  UNION ALL SELECT 'S7_SALES_ORDER_LINE'
+  UNION ALL SELECT 'S5_WORK_ORDER_BOM'
+  UNION ALL SELECT 'S6_REPORT_TXN'
+  UNION ALL SELECT 'S7_FQC_TASK_TXN'
+  UNION ALL SELECT 'S5_INVENTORY_BALANCE_MONTHLY'
+) d
+WHERE g.entity_code='S5_INVENTORY_TXN' AND g.status=1
+  AND NOT EXISTS (
+    SELECT 1 FROM mdp_inbound_grant x
+    WHERE x.access_key=g.access_key AND x.entity_code=d.entity_code);
+
+UPDATE mdp_entity
+SET inbound_enabled=1, update_time=NOW()
+WHERE entity_code IN ('MDM_CUSTOMER', 'MDM_SUPPLIER', 'MDM_LOCATION')
+  AND inbound_enabled=0;
+
+UPDATE mdp_entity
+SET EntityCode = entity_code,
+    EntityName = entity_name,
+    SourceTableName = source_table_name,
+    TargetTableName = target_table_name,
+    update_time = NOW()
+WHERE entity_code = 'S6_EMP_WORK_ORD_TRANS_SQLSERVER'
+  AND (
+    IFNULL(EntityCode, '') <> entity_code
+    OR IFNULL(SourceTableName, '') <> IFNULL(source_table_name, '')
+    OR IFNULL(TargetTableName, '') <> IFNULL(target_table_name, '')
+  );

+ 12 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.574.verify.sql

@@ -0,0 +1,12 @@
+-- 有库存入站模板时,13 个推送实体必须已开通。没有模板时本脚本不插入,验收放行。
+SELECT
+  (SELECT COUNT(*) FROM mdp_entity WHERE entity_code='S5_INVENTORY_TXN') = 0
+  OR (
+    SELECT COUNT(*) FROM mdp_entity
+    WHERE entity_code IN (
+      'S2_WORK_ORDER_SCHEDULE','S3_PURCHASE_ORDER','S3_PURCHASE_RECEIPT',
+      'S4_SHIPMENT','S4_IQC','S4_RETURN','S4_SHORTAGE',
+      'S6_WORK_ORDER_LINE','S7_SALES_ORDER_LINE','S5_WORK_ORDER_BOM',
+      'S6_REPORT_TXN','S7_FQC_TASK_TXN','S5_INVENTORY_BALANCE_MONTHLY')
+      AND status=1 AND inbound_enabled=1
+  ) = 13;

+ 17 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.575.sql

@@ -0,0 +1,17 @@
+-- 1.0.575 槽位 0a(§0.7.1 / §13 Q32、Q27)
+-- 修正 1.0.571 已写死、且 hash 锁死改不了的两行登记。必须在任何 AIDOP S5 重算之前执行。
+--
+-- 幂等说明:WHERE 带 source_system='T8',已改过的行不会再改。
+-- 这不是「跑几次结果都一样」:B-2 把 INV_TRANS 改回 T8 之后,若有人在老环境上手工重放本脚本,
+-- 会把它又改回 165。正常迁移按 version 在 sys_db_migration_log 只执行一次;
+-- 全新环境初始化时 B-2 尚未做过,重放结果仍然正确。不要在已切回 T8 的库上手工重放。
+
+UPDATE mdp_tenant_std_source
+   SET source_system='DOPDEMORQ_SQLSERVER', source_scope='8010',
+       remark='Q32:T8 库存投影未出数,登记随实际数据;B-2 完成后改回 T8'
+ WHERE tenant_id=797403760988229 AND std_object='INV_TRANS' AND source_system='T8';
+
+UPDATE mdp_tenant_std_source
+   SET source_system='AIDOP_NATIVE', source_scope='',
+       remark='Q27:本库自建 FQC 归自有通道'
+ WHERE tenant_id=797403760988229 AND std_object='FQC_TASK' AND source_system='T8';

+ 14 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.575.verify.sql

@@ -0,0 +1,14 @@
+-- 表尚未建成时放行(1.0.571 会先建表;本脚本排在其后)。
+SELECT
+  (SELECT COUNT(*) FROM information_schema.TABLES
+    WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='mdp_tenant_std_source') = 0
+  OR (
+    (SELECT source_system FROM mdp_tenant_std_source
+      WHERE tenant_id=797403760988229 AND std_object='INV_TRANS' LIMIT 1) = 'DOPDEMORQ_SQLSERVER'
+    AND
+    (SELECT IFNULL(source_scope,'') FROM mdp_tenant_std_source
+      WHERE tenant_id=797403760988229 AND std_object='INV_TRANS' LIMIT 1) = '8010'
+    AND
+    (SELECT source_system FROM mdp_tenant_std_source
+      WHERE tenant_id=797403760988229 AND std_object='FQC_TASK' LIMIT 1) = 'AIDOP_NATIVE'
+  );

Some files were not shown because too many files changed in this diff