Kaynağa Gözat

feat(s0): 新增八个参考字典与三个关系桥,维度总数达十九

Phase 2 的 C2(参考字典)与 C3(关系桥)在同一套通用框架上落地,
十一个新对象全部只新增 S0DimDefinition,专用业务服务仍为零。

C2 参考字典(ref_*)八个:
- 独立字典三张:BarCodeType / ImageType / qms_quality_base_type
- qms 字典族五张共用 mdp_stg_s0_qms_dict,靠 source_table 隔离

C3 关系桥三个:
- EmpWorkDutyMaster -> bridge_employee_work_duty,父 dim_employee
- LineSkillMaster   -> dim_line_post
- LineSkillDetail   -> bridge_line_post_skill,父 dim_line_post 走自然键

身份沿用 D-025:一律 tenant_id + 业务码,Domain/company/factory 均为
LEGACY_SCOPE 不入唯一键。但 EmpWorkDuty 的 location_code 是例外并保留在
身份里,因为实测 tenant+Employee+Duty 有一组重复:同一员工同一串职责下
Location 1001 与 1000 是两条不同职责、物料也不同。

本轮实测出的三个框架地雷,已分别写进类型文档、目录注释与测试:
- bit(1) 源列若用 Int,对账每轮必挂:CAST(b'1' AS CHAR) 是 0x01 而
  CAST(1 AS CHAR) 是 0x31。PriorityCode.OrderBy 正是 bit(1),整表缓做
- qms 的 FSTATUS/FENABLE 是 varchar 且值域含中文与字母,用 BoolTrue 会让
  四张表整表归零,而 qms_inspectionfreq.FENABLE 恰好存活八行丢五行,
  是部分静默丢失,最难发现。十个状态列一律 Str 透传
- LineSkillDetail 是本仓第四张 dual-column 表,四对双列里三对是有值但
  发散型(影子列 250/261 非空但与真列无一行相等),取错不报错而是得到
  看起来合法的错值;且 bridge 的目标列 effective_date 其源物理列是
  StartDate,与源表那个恒空的同名影子列极易混淆

父连接必须走自然键:代理键 LineSkillRecID 同租户孤儿 210/261,
PersonSkillId 更有 250/261 属于别的租户;自然键三租户全零孤儿。
两个 legacy id 仅作留痕列,禁止 join。

EmpSkills 不在本批:源侧 tenant+Employee+SkillNo 有两组重复,
需业务侧经 CRUD 删除接口清理后才能接入。

migration 1.0.508 / 1.0.509,各带 36 条与 18 条结构断言的 verify,
经 AutoVersionUpdate 真路径验证:检测 -> 执行 -> verify Success -> 记账。
YY968XX 1 gün önce
ebeveyn
işleme
bf6095fadb

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

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.508</AssemblyVersion>
-    <FileVersion>1.0.508</FileVersion>
-    <Version>1.0.508</Version>
+    <AssemblyVersion>1.0.509</AssemblyVersion>
+    <FileVersion>1.0.509</FileVersion>
+    <Version>1.0.509</Version>
   </PropertyGroup>
 
   <ItemGroup>
@@ -727,6 +727,18 @@
     <None Update="UpdateScripts\1.0.507.verify.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>
+    <None Update="UpdateScripts\1.0.508.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.508.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.509.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
+    <None Update="UpdateScripts\1.0.509.verify.sql">
+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+    </None>
     <None Update="UpdateScripts\UAT-PLACEHOLDER-MENU-HIDE.ops.sql">
       <CopyToOutputDirectory>Always</CopyToOutputDirectory>
     </None>

+ 377 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.508.sql

@@ -0,0 +1,377 @@
+-- =====================================================================================
+-- 1.0.508  ·  S0 MASTER DATA PLATFORM · PHASE 2 · C2 参考字典(8 个 ref_*)
+--
+-- C2-1 独立字典(各自 staging):
+--   BarCodeType           → mdp_stg_s0_label_type        → ref_label_type
+--   ImageType             → mdp_stg_s0_sop_file_type     → ref_sop_file_type
+--   qms_quality_base_type → mdp_stg_s0_quality_base_type → ref_quality_base_type
+-- C2-2 qms 字典族(**共用 mdp_stg_s0_qms_dict**,靠 source_table 隔离,同 Location/LocationShelf 范式):
+--   qms_sampscheme / qms_inspectionitems / qms_inspection_method
+--   qms_inspectionfreq / qms_inspectioninstru → ref_*
+--
+-- 身份口径(沿用 D-025):一律 tenant_id + 业务码;Domain/company/factory 均 LEGACY_SCOPE,不入唯一键。
+--
+-- 🔴 本组落地时实测出的两个框架地雷(写在这里免得下次重踩):
+--   ① bit(1) 源列若用 Int,对账每轮必挂:CAST(b'1' AS CHAR)=0x01 而 CAST(1 AS CHAR)=0x31。
+--      PriorityCode.OrderBy 正是 bit(1) —— 故 PriorityCode 整表本批缓做。
+--   ② qms 的 FSTATUS/FENABLE 是 varchar,值域含 `已审核`/`已启用`/`A`/`ENABLED`/`INVALID_STATUS`。
+--      用 BoolTrue 会命中可辨识性守卫:4 张表整表归零;qms_inspectionfreq.FENABLE 恰好存活 8/13
+--      —— 部分静默丢失,最难发现。⇒ 10 个状态列一律 Str 原样透传。
+--
+-- 边界:不动源表、不动 S0 CRUD、不动前端、不碰 mdp_std_*、不复用任何 DOPDEMORQ staging。
+-- =====================================================================================
+
+-- ── 一、贴源表(4 张;UK 含 source_system,D-022 的结构性防串源前提)──
+
+CREATE TABLE IF NOT EXISTS `mdp_stg_s0_label_type` (
+  `id`              bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`       bigint       NOT NULL DEFAULT 0,
+  `factory_id`      bigint       DEFAULT NULL,
+  `source_system`   varchar(50)  NOT NULL DEFAULT 'AIDOPDEV_MYSQL',
+  `source_table`    varchar(100) NOT NULL,
+  `source_row_id`   varchar(100) NOT NULL,
+  `source_biz_key`  varchar(300) DEFAULT NULL,
+  `sync_batch_id`   varchar(100) NOT NULL,
+  `sync_time`       datetime     NOT NULL,
+  `process_status`  varchar(20)  NOT NULL DEFAULT 'PENDING',
+  `raw_data`        json         NOT NULL,
+  `process_message` varchar(500) DEFAULT NULL,
+  `update_time`     datetime     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `create_time`     datetime     DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_stg_s0_label_type` (`tenant_id`,`source_system`,`source_table`,`source_row_id`),
+  KEY `idx_stg_s0_label_type_biz` (`source_biz_key`),
+  KEY `idx_stg_s0_label_type_batch` (`tenant_id`,`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 贴源:标签类型(源 BarCodeType)';
+
+CREATE TABLE IF NOT EXISTS `mdp_stg_s0_sop_file_type` (
+  `id`              bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`       bigint       NOT NULL DEFAULT 0,
+  `factory_id`      bigint       DEFAULT NULL,
+  `source_system`   varchar(50)  NOT NULL DEFAULT 'AIDOPDEV_MYSQL',
+  `source_table`    varchar(100) NOT NULL,
+  `source_row_id`   varchar(100) NOT NULL,
+  `source_biz_key`  varchar(300) DEFAULT NULL,
+  `sync_batch_id`   varchar(100) NOT NULL,
+  `sync_time`       datetime     NOT NULL,
+  `process_status`  varchar(20)  NOT NULL DEFAULT 'PENDING',
+  `raw_data`        json         NOT NULL,
+  `process_message` varchar(500) DEFAULT NULL,
+  `update_time`     datetime     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `create_time`     datetime     DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_stg_s0_sop_file_type` (`tenant_id`,`source_system`,`source_table`,`source_row_id`),
+  KEY `idx_stg_s0_sop_file_type_biz` (`source_biz_key`),
+  KEY `idx_stg_s0_sop_file_type_batch` (`tenant_id`,`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 贴源:SOP 文件类型(源 ImageType)';
+
+CREATE TABLE IF NOT EXISTS `mdp_stg_s0_quality_base_type` (
+  `id`              bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`       bigint       NOT NULL DEFAULT 0,
+  `factory_id`      bigint       DEFAULT NULL,
+  `source_system`   varchar(50)  NOT NULL DEFAULT 'AIDOPDEV_MYSQL',
+  `source_table`    varchar(100) NOT NULL,
+  `source_row_id`   varchar(100) NOT NULL,
+  `source_biz_key`  varchar(300) DEFAULT NULL,
+  `sync_batch_id`   varchar(100) NOT NULL,
+  `sync_time`       datetime     NOT NULL,
+  `process_status`  varchar(20)  NOT NULL DEFAULT 'PENDING',
+  `raw_data`        json         NOT NULL,
+  `process_message` varchar(500) DEFAULT NULL,
+  `update_time`     datetime     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `create_time`     datetime     DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_stg_s0_quality_base_type` (`tenant_id`,`source_system`,`source_table`,`source_row_id`),
+  KEY `idx_stg_s0_quality_base_type_biz` (`source_biz_key`),
+  KEY `idx_stg_s0_quality_base_type_batch` (`tenant_id`,`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 贴源:质量基础类型(源 qms_quality_base_type)';
+
+CREATE TABLE IF NOT EXISTS `mdp_stg_s0_qms_dict` (
+  `id`              bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`       bigint       NOT NULL DEFAULT 0,
+  `factory_id`      bigint       DEFAULT NULL,
+  `source_system`   varchar(50)  NOT NULL DEFAULT 'AIDOPDEV_MYSQL',
+  `source_table`    varchar(100) NOT NULL,
+  `source_row_id`   varchar(100) NOT NULL,
+  `source_biz_key`  varchar(300) DEFAULT NULL,
+  `sync_batch_id`   varchar(100) NOT NULL,
+  `sync_time`       datetime     NOT NULL,
+  `process_status`  varchar(20)  NOT NULL DEFAULT 'PENDING',
+  `raw_data`        json         NOT NULL,
+  `process_message` varchar(500) DEFAULT NULL,
+  `update_time`     datetime     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `create_time`     datetime     DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_stg_s0_qms_dict` (`tenant_id`,`source_system`,`source_table`,`source_row_id`),
+  KEY `idx_stg_s0_qms_dict_biz` (`source_biz_key`),
+  KEY `idx_stg_s0_qms_dict_batch` (`tenant_id`,`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 贴源:qms 字典族(5 个源表共用,靠 source_table 隔离)';
+
+-- ── 二、参考表(8 张)──
+
+CREATE TABLE IF NOT EXISTS `ref_label_type` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`         bigint       NOT NULL,
+  `label_type_code`   varchar(100) NOT NULL COMMENT '标签类型码(**唯一业务键**)← $.bar_type',
+  `label_class`       varchar(50) DEFAULT NULL COMMENT '← $.class',
+  `input_string`      varchar(1000) DEFAULT NULL COMMENT '← $.input_string',
+  `domain_code`       varchar(50) DEFAULT NULL COMMENT 'LEGACY_SCOPE ← $.domain_code;**不是身份**',
+  `company_id`        bigint      DEFAULT NULL COMMENT 'LEGACY_SCOPE ← $.company_ref_id',
+  `factory_id`        bigint      DEFAULT NULL COMMENT 'LEGACY_SCOPE ← $.factory_ref_id',
+  `source_updated_at` datetime    DEFAULT NULL COMMENT '← $.update_time',
+  `source_system`     varchar(50)  NOT NULL,
+  `source_biz_key`    varchar(300) NOT NULL,
+  `sync_batch_id`     varchar(64)  NOT NULL,
+  `sync_time`         datetime     NOT NULL,
+  `create_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_ref_label_type` (`tenant_id`,`label_type_code`),
+  KEY `idx_ref_label_type_batch` (`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 参考字典:标签类型。源 BarCodeType,FULL REPLACE。';
+
+CREATE TABLE IF NOT EXISTS `ref_sop_file_type` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`         bigint       NOT NULL,
+  `file_type_code`    varchar(100) NOT NULL COMMENT 'SOP 文件类型码(**唯一业务键**)← $.image_type_id',
+  `file_type_name`    varchar(255) DEFAULT NULL COMMENT '← $.descr',
+  `domain_code`       varchar(50) DEFAULT NULL COMMENT 'LEGACY_SCOPE ← $.domain(注意源物理列叫 domain)',
+  `source_system`     varchar(50)  NOT NULL,
+  `source_biz_key`    varchar(300) NOT NULL,
+  `sync_batch_id`     varchar(64)  NOT NULL,
+  `sync_time`         datetime     NOT NULL,
+  `create_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_ref_sop_file_type` (`tenant_id`,`file_type_code`),
+  KEY `idx_ref_sop_file_type_batch` (`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 参考字典:SOP 文件类型。源 ImageType。**源表无任何时间列,故无 source_updated_at,不得回填**。';
+
+CREATE TABLE IF NOT EXISTS `ref_quality_base_type` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`         bigint       NOT NULL,
+  `type_category`     varchar(100) NOT NULL COMMENT '类别(**业务键第一段**)← $.type_category',
+  `type_code`         varchar(100) NOT NULL COMMENT '编码(**业务键第二段**)← $.type_code',
+  `short_name`        varchar(200) DEFAULT NULL COMMENT '← $.short_name',
+  `full_name`         varchar(255) DEFAULT NULL COMMENT '← $.full_name',
+  `remark`            varchar(500) DEFAULT NULL COMMENT '← $.remark',
+  `is_active`         tinyint(1)  NOT NULL COMMENT '← $.is_active',
+  `source_updated_at` datetime    DEFAULT NULL COMMENT '← $.modify_time',
+  `source_system`     varchar(50)  NOT NULL,
+  `source_biz_key`    varchar(300) NOT NULL,
+  `sync_batch_id`     varchar(64)  NOT NULL,
+  `sync_time`         datetime     NOT NULL,
+  `create_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_ref_quality_base_type` (`tenant_id`,`type_category`,`type_code`),
+  KEY `idx_ref_quality_base_type_batch` (`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 参考字典:质量基础类型。源 qms_quality_base_type。键为两段(CRUD 查重即按 category+code)。';
+
+CREATE TABLE IF NOT EXISTS `ref_sampling_scheme` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`         bigint       NOT NULL,
+  `scheme_code`       varchar(255) NOT NULL COMMENT '抽样方案码(**唯一业务键**)← $.FNUMBER',
+  `scheme_name`       varchar(255) DEFAULT NULL COMMENT '← $.FNAME',
+  `sampling_type`     varchar(255) DEFAULT NULL COMMENT '← $.FSAMPLINGTYPE',
+  `inspection_level`  varchar(255) DEFAULT NULL COMMENT '← $.FINSPECTIONLEVEL',
+  `strictness`        varchar(255) DEFAULT NULL COMMENT '← $.FSTRICTNESS',
+  `aql_value`         varchar(255) DEFAULT NULL COMMENT '← $.FAQLVALUE(源为 varchar,保持 Str)',
+  `inspection_type`   varchar(255) DEFAULT NULL COMMENT '← $.FINSPECTIONTYPE',
+  `status_code`       varchar(255) DEFAULT NULL COMMENT '🔴 Str 透传 ← $.FSTATUS;值域含中文,禁 BoolTrue',
+  `enable_code`       varchar(255) DEFAULT NULL COMMENT '🔴 Str 透传 ← $.FENABLE;同上',
+  `remark`            varchar(255) DEFAULT NULL COMMENT '← $.FCOMMENT',
+  `source_updated_at` datetime    DEFAULT NULL COMMENT '← $.FMODIFYTIME',
+  `source_system`     varchar(50)  NOT NULL,
+  `source_biz_key`    varchar(300) NOT NULL,
+  `sync_batch_id`     varchar(64)  NOT NULL,
+  `sync_time`         datetime     NOT NULL,
+  `create_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_ref_sampling_scheme` (`tenant_id`,`scheme_code`),
+  KEY `idx_ref_sampling_scheme_batch` (`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 参考字典:抽样方案。源 qms_sampscheme。';
+
+CREATE TABLE IF NOT EXISTS `ref_inspection_item` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`         bigint       NOT NULL,
+  `item_code`         varchar(255) NOT NULL COMMENT '检验项目码(**唯一业务键**)← $.FNUMBER',
+  `item_name`         varchar(255) DEFAULT NULL COMMENT '← $.FNAME',
+  `check_method_id`   bigint      DEFAULT NULL COMMENT '← $.FCHECKMETHOD;**源侧代理主键 id,非业务键**,不可用于 dim 解析',
+  `check_basis_id`    bigint      DEFAULT NULL COMMENT '← $.FCHECKBASIS;同上',
+  `status_code`       varchar(255) DEFAULT NULL COMMENT '🔴 Str 透传 ← $.FSTATUS',
+  `enable_code`       varchar(255) DEFAULT NULL COMMENT '🔴 Str 透传 ← $.FENABLE',
+  `remark`            varchar(255) DEFAULT NULL COMMENT '← $.FCOMMENT',
+  `source_updated_at` datetime    DEFAULT NULL COMMENT '← $.FMODIFYTIME',
+  `source_system`     varchar(50)  NOT NULL,
+  `source_biz_key`    varchar(300) NOT NULL,
+  `sync_batch_id`     varchar(64)  NOT NULL,
+  `sync_time`         datetime     NOT NULL,
+  `create_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_ref_inspection_item` (`tenant_id`,`item_code`),
+  KEY `idx_ref_inspection_item_batch` (`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 参考字典:检验项目。源 qms_inspectionitems。';
+
+CREATE TABLE IF NOT EXISTS `ref_inspection_method` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`         bigint       NOT NULL,
+  `method_code`       varchar(255) NOT NULL COMMENT '检验方法码(**唯一业务键**)← $.FNUMBER',
+  `method_name`       varchar(255) DEFAULT NULL COMMENT '← $.FNAME',
+  `control_strategy`  varchar(255) DEFAULT NULL COMMENT '← $.FCTRLSTRATEGY',
+  `status_code`       varchar(255) DEFAULT NULL COMMENT '🔴 Str 透传 ← $.FSTATUS',
+  `enable_code`       varchar(255) DEFAULT NULL COMMENT '🔴 Str 透传 ← $.FENABLE',
+  `remark`            varchar(255) DEFAULT NULL COMMENT '← $.FCOMMENT',
+  `source_updated_at` datetime    DEFAULT NULL COMMENT '← $.FMODIFYTIME',
+  `source_system`     varchar(50)  NOT NULL,
+  `source_biz_key`    varchar(300) NOT NULL,
+  `sync_batch_id`     varchar(64)  NOT NULL,
+  `sync_time`         datetime     NOT NULL,
+  `create_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_ref_inspection_method` (`tenant_id`,`method_code`),
+  KEY `idx_ref_inspection_method_batch` (`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 参考字典:检验方法。源 qms_inspection_method。';
+
+CREATE TABLE IF NOT EXISTS `ref_inspection_frequency` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`         bigint       NOT NULL,
+  `frequency_code`    varchar(255) NOT NULL COMMENT '检验频次码(**唯一业务键**)← $.FNUMBER',
+  `frequency_name`    varchar(255) DEFAULT NULL COMMENT '← $.FNAME',
+  `remark`            varchar(255) DEFAULT NULL COMMENT '🔴 ← $.FREMARK(本表叫 FREMARK,**不是** FCOMMENT)',
+  `status_code`       varchar(255) DEFAULT NULL COMMENT '🔴 Str 透传 ← $.FSTATUS;值域 A/ENABLED',
+  `enable_code`       varchar(255) DEFAULT NULL COMMENT '🔴 Str 透传 ← $.FENABLE;值域 1/ENABLED —— BoolTrue 会存活 8 丢 5,最阴',
+  `source_updated_at` datetime    DEFAULT NULL COMMENT '← $.FMODIFYTIME',
+  `source_system`     varchar(50)  NOT NULL,
+  `source_biz_key`    varchar(300) NOT NULL,
+  `sync_batch_id`     varchar(64)  NOT NULL,
+  `sync_time`         datetime     NOT NULL,
+  `create_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_ref_inspection_frequency` (`tenant_id`,`frequency_code`),
+  KEY `idx_ref_inspection_frequency_batch` (`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 参考字典:检验频次。源 qms_inspectionfreq。';
+
+CREATE TABLE IF NOT EXISTS `ref_inspection_instrument` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`         bigint       NOT NULL,
+  `instrument_code`   varchar(255) NOT NULL COMMENT '量检具码(**唯一业务键**)← $.FNUMBER',
+  `instrument_name`   varchar(255) DEFAULT NULL COMMENT '← $.FNAME',
+  `model`             varchar(255) DEFAULT NULL COMMENT '← $.FMODEL',
+  `specification`     varchar(255) DEFAULT NULL COMMENT '← $.FSPECIFICATION',
+  `manufacturer`      varchar(255) DEFAULT NULL COMMENT '← $.FMANUFACTURER',
+  `gauge_category`    varchar(255) DEFAULT NULL COMMENT '← $.gauge_category(源自有 snake_case 列)',
+  `calibration_cycle_days` int         DEFAULT NULL COMMENT '← $.calibration_cycle_days',
+  `next_calibration_date` varchar(50) DEFAULT NULL COMMENT '← $.next_calibration_date;源是 varchar 存日期串,**Str 透传不用 DateTimeSec**',
+  `status_code`       varchar(255) DEFAULT NULL COMMENT '🔴 Str 透传 ← $.FSTATUS',
+  `enable_code`       varchar(255) DEFAULT NULL COMMENT '🔴 Str 透传 ← $.FENABLE',
+  `remark`            varchar(255) DEFAULT NULL COMMENT '← $.FCOMMENT',
+  `source_updated_at` datetime    DEFAULT NULL COMMENT '← $.FMODIFYTIME',
+  `source_system`     varchar(50)  NOT NULL,
+  `source_biz_key`    varchar(300) NOT NULL,
+  `sync_batch_id`     varchar(64)  NOT NULL,
+  `sync_time`         datetime     NOT NULL,
+  `create_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_ref_inspection_instrument` (`tenant_id`,`instrument_code`),
+  KEY `idx_ref_inspection_instrument_batch` (`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 参考字典:量检具。源 qms_inspectioninstru。';
+
+-- ── 三、mdp_entity 入站配置(8 条,全 FULL / incr_column=NULL)──
+-- biz_key_expr 必须与 Definition.SourceBizKeyColumns 逐字一致,否则 AssertEntityContractAsync 当场报漂移。
+
+SET @src_id = (SELECT `id` FROM `mdp_source` WHERE `source_code` = 'AIDOPDEV_MYSQL' LIMIT 1);
+
+INSERT INTO `mdp_entity`
+  (`tenant_id`,`source_id`,`entity_code`,`entity_name`,`entity_type`,
+   `source_table_name`,`target_table_name`,`sync_mode`,`incr_column`,`batch_size`,
+   `biz_key_expr`,`status`,`create_time`,`update_time`)
+SELECT 0, @src_id, 'S0_LABEL_TYPE', 'S0 标签类型字典入站', 'TABLE',
+       'BarCodeType', 'mdp_stg_s0_label_type', 'FULL', NULL, 5000,
+       'bar_type', 1, NOW(), NOW()
+WHERE @src_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `mdp_entity` WHERE `entity_code` = 'S0_LABEL_TYPE');
+
+INSERT INTO `mdp_entity`
+  (`tenant_id`,`source_id`,`entity_code`,`entity_name`,`entity_type`,
+   `source_table_name`,`target_table_name`,`sync_mode`,`incr_column`,`batch_size`,
+   `biz_key_expr`,`status`,`create_time`,`update_time`)
+SELECT 0, @src_id, 'S0_SOP_FILE_TYPE', 'S0 SOP 文件类型字典入站', 'TABLE',
+       'ImageType', 'mdp_stg_s0_sop_file_type', 'FULL', NULL, 5000,
+       'image_type_id', 1, NOW(), NOW()
+WHERE @src_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `mdp_entity` WHERE `entity_code` = 'S0_SOP_FILE_TYPE');
+
+INSERT INTO `mdp_entity`
+  (`tenant_id`,`source_id`,`entity_code`,`entity_name`,`entity_type`,
+   `source_table_name`,`target_table_name`,`sync_mode`,`incr_column`,`batch_size`,
+   `biz_key_expr`,`status`,`create_time`,`update_time`)
+SELECT 0, @src_id, 'S0_QUALITY_BASE_TYPE', 'S0 质量基础类型字典入站', 'TABLE',
+       'qms_quality_base_type', 'mdp_stg_s0_quality_base_type', 'FULL', NULL, 5000,
+       'type_category,type_code', 1, NOW(), NOW()
+WHERE @src_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `mdp_entity` WHERE `entity_code` = 'S0_QUALITY_BASE_TYPE');
+
+INSERT INTO `mdp_entity`
+  (`tenant_id`,`source_id`,`entity_code`,`entity_name`,`entity_type`,
+   `source_table_name`,`target_table_name`,`sync_mode`,`incr_column`,`batch_size`,
+   `biz_key_expr`,`status`,`create_time`,`update_time`)
+SELECT 0, @src_id, 'S0_SAMPLING_SCHEME', 'S0 抽样方案字典入站', 'TABLE',
+       'qms_sampscheme', 'mdp_stg_s0_qms_dict', 'FULL', NULL, 5000,
+       'FNUMBER', 1, NOW(), NOW()
+WHERE @src_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `mdp_entity` WHERE `entity_code` = 'S0_SAMPLING_SCHEME');
+
+INSERT INTO `mdp_entity`
+  (`tenant_id`,`source_id`,`entity_code`,`entity_name`,`entity_type`,
+   `source_table_name`,`target_table_name`,`sync_mode`,`incr_column`,`batch_size`,
+   `biz_key_expr`,`status`,`create_time`,`update_time`)
+SELECT 0, @src_id, 'S0_INSPECTION_ITEM', 'S0 检验项目字典入站', 'TABLE',
+       'qms_inspectionitems', 'mdp_stg_s0_qms_dict', 'FULL', NULL, 5000,
+       'FNUMBER', 1, NOW(), NOW()
+WHERE @src_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `mdp_entity` WHERE `entity_code` = 'S0_INSPECTION_ITEM');
+
+INSERT INTO `mdp_entity`
+  (`tenant_id`,`source_id`,`entity_code`,`entity_name`,`entity_type`,
+   `source_table_name`,`target_table_name`,`sync_mode`,`incr_column`,`batch_size`,
+   `biz_key_expr`,`status`,`create_time`,`update_time`)
+SELECT 0, @src_id, 'S0_INSPECTION_METHOD', 'S0 检验方法字典入站', 'TABLE',
+       'qms_inspection_method', 'mdp_stg_s0_qms_dict', 'FULL', NULL, 5000,
+       'FNUMBER', 1, NOW(), NOW()
+WHERE @src_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `mdp_entity` WHERE `entity_code` = 'S0_INSPECTION_METHOD');
+
+INSERT INTO `mdp_entity`
+  (`tenant_id`,`source_id`,`entity_code`,`entity_name`,`entity_type`,
+   `source_table_name`,`target_table_name`,`sync_mode`,`incr_column`,`batch_size`,
+   `biz_key_expr`,`status`,`create_time`,`update_time`)
+SELECT 0, @src_id, 'S0_INSPECTION_FREQUENCY', 'S0 检验频次字典入站', 'TABLE',
+       'qms_inspectionfreq', 'mdp_stg_s0_qms_dict', 'FULL', NULL, 5000,
+       'FNUMBER', 1, NOW(), NOW()
+WHERE @src_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `mdp_entity` WHERE `entity_code` = 'S0_INSPECTION_FREQUENCY');
+
+INSERT INTO `mdp_entity`
+  (`tenant_id`,`source_id`,`entity_code`,`entity_name`,`entity_type`,
+   `source_table_name`,`target_table_name`,`sync_mode`,`incr_column`,`batch_size`,
+   `biz_key_expr`,`status`,`create_time`,`update_time`)
+SELECT 0, @src_id, 'S0_INSPECTION_INSTRUMENT', 'S0 量检具字典入站', 'TABLE',
+       'qms_inspectioninstru', 'mdp_stg_s0_qms_dict', 'FULL', NULL, 5000,
+       'FNUMBER', 1, NOW(), NOW()
+WHERE @src_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `mdp_entity` WHERE `entity_code` = 'S0_INSPECTION_INSTRUMENT');
+
+-- ── 回滚(注释,非可执行)──
+-- DROP TABLE IF EXISTS `ref_inspection_instrument`,`ref_inspection_frequency`,`ref_inspection_method`,
+--   `ref_inspection_item`,`ref_sampling_scheme`,`ref_quality_base_type`,`ref_sop_file_type`,`ref_label_type`;
+-- DROP TABLE IF EXISTS `mdp_stg_s0_qms_dict`,`mdp_stg_s0_quality_base_type`,
+--   `mdp_stg_s0_sop_file_type`,`mdp_stg_s0_label_type`;
+-- DELETE FROM `mdp_entity` WHERE `tenant_id`=0 AND `entity_code` IN
+--   ('S0_LABEL_TYPE','S0_SOP_FILE_TYPE','S0_QUALITY_BASE_TYPE','S0_SAMPLING_SCHEME',
+--    'S0_INSPECTION_ITEM','S0_INSPECTION_METHOD','S0_INSPECTION_FREQUENCY','S0_INSPECTION_INSTRUMENT');

+ 196 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.508.verify.sql

@@ -0,0 +1,196 @@
+-- 1.0.508.verify.sql
+-- 契约(CURRENT.md > F-11):禁 SET @var · 每条返回 1/0 · 一条一断言 · 末条为真 SELECT。
+-- 断言强度沿用 Phase 1 Final Review:UK 断言**精确列序**(CREATE TABLE IF NOT EXISTS 会静默 no-op,
+-- 只数列数会放过结构漂移);字符串等值一律 COLLATE utf8mb4_bin(biz_key_expr 大小写是致命维度)。
+
+
+SELECT (SELECT COUNT(*) FROM information_schema.TABLES
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('mdp_stg_s0_label_type','mdp_stg_s0_sop_file_type','mdp_stg_s0_quality_base_type','mdp_stg_s0_qms_dict')) = 4;
+
+SELECT (SELECT COUNT(*) FROM information_schema.TABLES
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('ref_label_type','ref_sop_file_type','ref_quality_base_type','ref_sampling_scheme','ref_inspection_item','ref_inspection_method','ref_inspection_frequency','ref_inspection_instrument')) = 8;
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mdp_stg_s0_label_type'
+                 AND INDEX_NAME = 'uk_stg_s0_label_type' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,source_system,source_table,source_row_id';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mdp_stg_s0_sop_file_type'
+                 AND INDEX_NAME = 'uk_stg_s0_sop_file_type' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,source_system,source_table,source_row_id';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mdp_stg_s0_quality_base_type'
+                 AND INDEX_NAME = 'uk_stg_s0_quality_base_type' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,source_system,source_table,source_row_id';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mdp_stg_s0_qms_dict'
+                 AND INDEX_NAME = 'uk_stg_s0_qms_dict' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,source_system,source_table,source_row_id';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_label_type'
+                 AND INDEX_NAME = 'uk_ref_label_type' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,label_type_code';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_sop_file_type'
+                 AND INDEX_NAME = 'uk_ref_sop_file_type' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,file_type_code';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_quality_base_type'
+                 AND INDEX_NAME = 'uk_ref_quality_base_type' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,type_category,type_code';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_sampling_scheme'
+                 AND INDEX_NAME = 'uk_ref_sampling_scheme' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,scheme_code';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_inspection_item'
+                 AND INDEX_NAME = 'uk_ref_inspection_item' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,item_code';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_inspection_method'
+                 AND INDEX_NAME = 'uk_ref_inspection_method' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,method_code';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_inspection_frequency'
+                 AND INDEX_NAME = 'uk_ref_inspection_frequency' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,frequency_code';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_inspection_instrument'
+                 AND INDEX_NAME = 'uk_ref_inspection_instrument' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,instrument_code';
+
+SELECT (SELECT COUNT(*) FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('ref_label_type','ref_sop_file_type','ref_quality_base_type','ref_sampling_scheme','ref_inspection_item','ref_inspection_method','ref_inspection_frequency','ref_inspection_instrument')
+          AND COLUMN_NAME = 'tenant_id' AND IS_NULLABLE = 'NO') = 8;
+
+SELECT (SELECT COUNT(*) FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('mdp_stg_s0_label_type','mdp_stg_s0_sop_file_type','mdp_stg_s0_quality_base_type','mdp_stg_s0_qms_dict')
+          AND COLUMN_NAME = 'tenant_id' AND IS_NULLABLE = 'NO') = 4;
+
+SELECT (SELECT COUNT(*) FROM information_schema.STATISTICS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_label_type'
+          AND INDEX_NAME = 'uk_ref_label_type' AND COLUMN_NAME IN ('domain_code','company_id','factory_id')) = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.STATISTICS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_sop_file_type'
+          AND INDEX_NAME = 'uk_ref_sop_file_type' AND COLUMN_NAME IN ('domain_code','company_id','factory_id')) = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.STATISTICS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_quality_base_type'
+          AND INDEX_NAME = 'uk_ref_quality_base_type' AND COLUMN_NAME IN ('domain_code','company_id','factory_id')) = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.STATISTICS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_sampling_scheme'
+          AND INDEX_NAME = 'uk_ref_sampling_scheme' AND COLUMN_NAME IN ('domain_code','company_id','factory_id')) = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.STATISTICS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_inspection_item'
+          AND INDEX_NAME = 'uk_ref_inspection_item' AND COLUMN_NAME IN ('domain_code','company_id','factory_id')) = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.STATISTICS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_inspection_method'
+          AND INDEX_NAME = 'uk_ref_inspection_method' AND COLUMN_NAME IN ('domain_code','company_id','factory_id')) = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.STATISTICS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_inspection_frequency'
+          AND INDEX_NAME = 'uk_ref_inspection_frequency' AND COLUMN_NAME IN ('domain_code','company_id','factory_id')) = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.STATISTICS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_inspection_instrument'
+          AND INDEX_NAME = 'uk_ref_inspection_instrument' AND COLUMN_NAME IN ('domain_code','company_id','factory_id')) = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_sop_file_type'
+          AND COLUMN_NAME = 'source_updated_at') = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE()
+          AND TABLE_NAME IN ('ref_sampling_scheme','ref_inspection_item','ref_inspection_method',
+                             'ref_inspection_frequency','ref_inspection_instrument')
+          AND COLUMN_NAME IN ('status_code','enable_code') AND DATA_TYPE = 'varchar') = 10;
+
+SELECT (SELECT COUNT(*) FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ref_inspection_frequency'
+          AND COLUMN_NAME = 'remark') = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity`
+        WHERE `entity_code` = 'S0_LABEL_TYPE'
+          AND `biz_key_expr` COLLATE utf8mb4_bin = 'bar_type'
+          AND `source_table_name` COLLATE utf8mb4_bin = 'BarCodeType'
+          AND `target_table_name` COLLATE utf8mb4_bin = 'mdp_stg_s0_label_type'
+          AND `status` = 1 AND `sync_mode` = 'FULL' AND `incr_column` IS NULL) = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity`
+        WHERE `entity_code` = 'S0_SOP_FILE_TYPE'
+          AND `biz_key_expr` COLLATE utf8mb4_bin = 'image_type_id'
+          AND `source_table_name` COLLATE utf8mb4_bin = 'ImageType'
+          AND `target_table_name` COLLATE utf8mb4_bin = 'mdp_stg_s0_sop_file_type'
+          AND `status` = 1 AND `sync_mode` = 'FULL' AND `incr_column` IS NULL) = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity`
+        WHERE `entity_code` = 'S0_QUALITY_BASE_TYPE'
+          AND `biz_key_expr` COLLATE utf8mb4_bin = 'type_category,type_code'
+          AND `source_table_name` COLLATE utf8mb4_bin = 'qms_quality_base_type'
+          AND `target_table_name` COLLATE utf8mb4_bin = 'mdp_stg_s0_quality_base_type'
+          AND `status` = 1 AND `sync_mode` = 'FULL' AND `incr_column` IS NULL) = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity`
+        WHERE `entity_code` = 'S0_SAMPLING_SCHEME'
+          AND `biz_key_expr` COLLATE utf8mb4_bin = 'FNUMBER'
+          AND `source_table_name` COLLATE utf8mb4_bin = 'qms_sampscheme'
+          AND `target_table_name` COLLATE utf8mb4_bin = 'mdp_stg_s0_qms_dict'
+          AND `status` = 1 AND `sync_mode` = 'FULL' AND `incr_column` IS NULL) = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity`
+        WHERE `entity_code` = 'S0_INSPECTION_ITEM'
+          AND `biz_key_expr` COLLATE utf8mb4_bin = 'FNUMBER'
+          AND `source_table_name` COLLATE utf8mb4_bin = 'qms_inspectionitems'
+          AND `target_table_name` COLLATE utf8mb4_bin = 'mdp_stg_s0_qms_dict'
+          AND `status` = 1 AND `sync_mode` = 'FULL' AND `incr_column` IS NULL) = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity`
+        WHERE `entity_code` = 'S0_INSPECTION_METHOD'
+          AND `biz_key_expr` COLLATE utf8mb4_bin = 'FNUMBER'
+          AND `source_table_name` COLLATE utf8mb4_bin = 'qms_inspection_method'
+          AND `target_table_name` COLLATE utf8mb4_bin = 'mdp_stg_s0_qms_dict'
+          AND `status` = 1 AND `sync_mode` = 'FULL' AND `incr_column` IS NULL) = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity`
+        WHERE `entity_code` = 'S0_INSPECTION_FREQUENCY'
+          AND `biz_key_expr` COLLATE utf8mb4_bin = 'FNUMBER'
+          AND `source_table_name` COLLATE utf8mb4_bin = 'qms_inspectionfreq'
+          AND `target_table_name` COLLATE utf8mb4_bin = 'mdp_stg_s0_qms_dict'
+          AND `status` = 1 AND `sync_mode` = 'FULL' AND `incr_column` IS NULL) = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity`
+        WHERE `entity_code` = 'S0_INSPECTION_INSTRUMENT'
+          AND `biz_key_expr` COLLATE utf8mb4_bin = 'FNUMBER'
+          AND `source_table_name` COLLATE utf8mb4_bin = 'qms_inspectioninstru'
+          AND `target_table_name` COLLATE utf8mb4_bin = 'mdp_stg_s0_qms_dict'
+          AND `status` = 1 AND `sync_mode` = 'FULL' AND `incr_column` IS NULL) = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity` e JOIN `mdp_source` s ON s.`id` = e.`source_id`
+        WHERE e.`entity_code` LIKE 'S0\_%' AND s.`source_code` <> 'AIDOPDEV_MYSQL') = 0;

+ 187 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.509.sql

@@ -0,0 +1,187 @@
+-- =====================================================================================
+-- 1.0.509  ·  S0 MASTER DATA PLATFORM · PHASE 2 · C3 关系桥(3 个)
+--
+--   EmpWorkDutyMaster → mdp_stg_s0_emp_work_duty → bridge_employee_work_duty
+--   LineSkillMaster   → mdp_stg_s0_line_post     → dim_line_post
+--   LineSkillDetail   → mdp_stg_s0_line_post     → bridge_line_post_skill   (父子共用 staging)
+--
+-- 身份口径(沿用 D-025):tenant_id + 业务码;Domain/company/factory 均 LEGACY_SCOPE。
+-- 但**有一处例外经实测确立**:EmpWorkDuty 的 location_code **必须留在身份里** ——
+--   tenant+Employee+Duty 实测有 1 组重复(797…229 的 G00308 同一串 Duty 下
+--   Location=1001 与 1000 是两条不同职责、物料也不同)。库位是真实区分维,不是 legacy scope。
+--
+-- 🔴 LineSkillDetail 是本仓第 4 张 dual-column 表,且最危险:四对双列里三对是「有值但发散」型
+--    (影子列 250/261 非空,与真列**没有一行相等**)—— 取错不报错,得到看起来合法的错值。
+--    尤其 bridge 的目标列 effective_date 其源物理列是 **StartDate**,
+--    而源表另有一个字面叫 effective_date 的 0/261 影子列。已加契约测试钉死。
+--
+-- 🔴 父连接必须走自然键:代理键 LineSkillRecID 同租户孤儿 210/261、忽略租户后 175 行落到别的租户;
+--    PersonSkillId 更甚,250/261 属于别的租户。自然键 (tenant, ProdLine, JOBNo) 三租户全 0 孤儿。
+--    两个 legacy id 仅作留痕列物化,任何 join 都不得使用。
+--
+-- EmpSkills **不在本批**:源侧 tenant+Employee+SkillNo 有 2 组重复(RecID 4848/4943 与 5156/4945),
+--   需业务 owner 经 CRUD 删除接口清理后才能接入(见 KNOWN-ISSUES)。
+-- =====================================================================================
+
+-- ── 一、贴源表(2 张)──
+
+CREATE TABLE IF NOT EXISTS `mdp_stg_s0_emp_work_duty` (
+  `id`              bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`       bigint       NOT NULL DEFAULT 0,
+  `factory_id`      bigint       DEFAULT NULL,
+  `source_system`   varchar(50)  NOT NULL DEFAULT 'AIDOPDEV_MYSQL',
+  `source_table`    varchar(100) NOT NULL,
+  `source_row_id`   varchar(100) NOT NULL,
+  `source_biz_key`  varchar(300) DEFAULT NULL,
+  `sync_batch_id`   varchar(100) NOT NULL,
+  `sync_time`       datetime     NOT NULL,
+  `process_status`  varchar(20)  NOT NULL DEFAULT 'PENDING',
+  `raw_data`        json         NOT NULL,
+  `process_message` varchar(500) DEFAULT NULL,
+  `update_time`     datetime     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `create_time`     datetime     DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_stg_s0_emp_work_duty` (`tenant_id`,`source_system`,`source_table`,`source_row_id`),
+  KEY `idx_stg_s0_emp_work_duty_biz` (`source_biz_key`),
+  KEY `idx_stg_s0_emp_work_duty_batch` (`tenant_id`,`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 贴源:员工物料职责(源 EmpWorkDutyMaster)';
+
+CREATE TABLE IF NOT EXISTS `mdp_stg_s0_line_post` (
+  `id`              bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`       bigint       NOT NULL DEFAULT 0,
+  `factory_id`      bigint       DEFAULT NULL,
+  `source_system`   varchar(50)  NOT NULL DEFAULT 'AIDOPDEV_MYSQL',
+  `source_table`    varchar(100) NOT NULL,
+  `source_row_id`   varchar(100) NOT NULL,
+  `source_biz_key`  varchar(300) DEFAULT NULL,
+  `sync_batch_id`   varchar(100) NOT NULL,
+  `sync_time`       datetime     NOT NULL,
+  `process_status`  varchar(20)  NOT NULL DEFAULT 'PENDING',
+  `raw_data`        json         NOT NULL,
+  `process_message` varchar(500) DEFAULT NULL,
+  `update_time`     datetime     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `create_time`     datetime     DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_stg_s0_line_post` (`tenant_id`,`source_system`,`source_table`,`source_row_id`),
+  KEY `idx_stg_s0_line_post_biz` (`source_biz_key`),
+  KEY `idx_stg_s0_line_post_batch` (`tenant_id`,`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 贴源:产线岗位与岗位技能(LineSkillMaster + LineSkillDetail 共用,靠 source_table 隔离)';
+
+-- ── 二、桥接 / 维度表(3 张)──
+
+CREATE TABLE IF NOT EXISTS `bridge_employee_work_duty` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`         bigint       NOT NULL,
+  `employee_code`     varchar(100) NOT NULL COMMENT '工号(业务键)← $.Employee;按业务键引用 dim_employee',
+  `duty`              varchar(500) NOT NULL COMMENT '职责串(业务键)← $.Duty',
+  `location_code`     varchar(100) NOT NULL COMMENT '库位(**业务键,不可省**)← $.Location;实测同 Employee+Duty 下有两个不同 Location',
+  `domain_code`       varchar(50)  NOT NULL COMMENT 'LEGACY_SCOPE ← $.Domain',
+  `company_id`        bigint       NOT NULL COMMENT 'LEGACY_SCOPE ← $.company_ref_id',
+  `factory_id`        bigint       NOT NULL COMMENT 'LEGACY_SCOPE ← $.factory_ref_id',
+  `line_code`         varchar(100) DEFAULT NULL COMMENT '← $.ProdLine;软引用 dim_line,无 FK',
+  `item_num`          varchar(100) DEFAULT NULL COMMENT '← $.ItemNum',
+  `item_num_from`     varchar(100) DEFAULT NULL COMMENT '← $.ItemNum1',
+  `item_num_to`       varchar(100) DEFAULT NULL COMMENT '← $.ItemNum2',
+  `emp_type`          varchar(50)  DEFAULT NULL COMMENT '← $.EmpType',
+  `source_system`     varchar(50)  NOT NULL,
+  `source_biz_key`    varchar(300) NOT NULL,
+  `source_updated_at` datetime     DEFAULT NULL COMMENT '← $.UpdateTime(源 10/25,允许 NULL)',
+  `sync_batch_id`     varchar(64)  NOT NULL,
+  `sync_time`         datetime     NOT NULL,
+  `create_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  -- 索引字节 = 8 + 100*4 + 500*4 + 100*4 = 2808 < 3072(DYNAMIC/utf8mb4)。任一列加宽都会越界,改前必须重算。
+  UNIQUE KEY `uk_bridge_employee_work_duty` (`tenant_id`,`employee_code`,`duty`,`location_code`),
+  KEY `idx_bridge_ewd_batch` (`sync_batch_id`),
+  KEY `idx_bridge_ewd_emp` (`tenant_id`,`employee_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 关系桥:员工物料职责。源 EmpWorkDutyMaster,FULL REPLACE。父 = dim_employee。';
+
+CREATE TABLE IF NOT EXISTS `dim_line_post` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`         bigint       NOT NULL,
+  `line_code`         varchar(100) NOT NULL COMMENT '产线码(业务键)← $.ProdLine;软引用 dim_line,28/57 对不上故**不设父**',
+  `job_no`            varchar(100) NOT NULL COMMENT '岗位码(业务键)← $.JOBNo;当前每租户单值,保留以防不可逆键收缩',
+  `domain_code`       varchar(50)  NOT NULL COMMENT 'LEGACY_SCOPE ← $.Domain',
+  `job_name`          varchar(255) DEFAULT NULL COMMENT '← $.JOBDescr',
+  `site`              varchar(12)  DEFAULT NULL COMMENT '← $.Site(源 0/57 全 NULL);保留可见性——它在源 UK 里,丢了将来看不出身份为何被破坏',
+  `order_by`          int          DEFAULT NULL COMMENT '← $.Orderby',
+  `run_crew`          decimal(10,3) DEFAULT NULL COMMENT '← $.RunCrew;**scale 必须与源 decimal(10,3) 逐位相同**,否则 CAST AS CHAR 后 0.000≠0.0,校验和 FAIL',
+  `source_system`     varchar(50)  NOT NULL,
+  `source_biz_key`    varchar(300) NOT NULL,
+  `source_updated_at` datetime     DEFAULT NULL COMMENT '← $.UpdateTime',
+  `sync_batch_id`     varchar(64)  NOT NULL,
+  `sync_time`         datetime     NOT NULL,
+  `create_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time`       datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_dim_line_post` (`tenant_id`,`line_code`,`job_no`),
+  KEY `idx_dim_line_post_batch` (`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 维度:产线岗位。源 LineSkillMaster。源侧 UK 含全 NULL 的 Site 故形同虚设,本表 UK 是第一道真守卫。';
+
+CREATE TABLE IF NOT EXISTS `bridge_line_post_skill` (
+  `id`                        bigint       NOT NULL AUTO_INCREMENT,
+  `tenant_id`                 bigint       NOT NULL,
+  `line_code`                 varchar(100) NOT NULL COMMENT '业务键 + 父键 ← $.ProdLine',
+  `job_no`                    varchar(100) NOT NULL COMMENT '业务键 + 父键 ← $.JOBNo',
+  `skill_code`                varchar(100) NOT NULL COMMENT '业务键 ← $.SkillNo;软引用 dim_person_skill(210/261 对不上,**不设父**)',
+  `domain_code`               varchar(50)  DEFAULT NULL COMMENT 'LEGACY_SCOPE ← $.Domain(源可空,故不设 Required)',
+  `site`                      varchar(12)  DEFAULT NULL COMMENT '← $.Site(源 0/261)',
+  `effective_date`            datetime     DEFAULT NULL COMMENT '🔴 源物理列是 **$.StartDate**,不是同表那个 0/261 的影子列 effective_date',
+  `required_level`            varchar(50)  DEFAULT NULL COMMENT '← $.required_level',
+  `remark`                    varchar(500) DEFAULT NULL COMMENT '← $.remark',
+  `is_enabled`                tinyint(1)   NOT NULL COMMENT '← $.is_enabled',
+  `legacy_line_skill_rec_id`  bigint       DEFAULT NULL COMMENT '⚠️ 仅留痕 ← $.LineSkillRecID;同租户孤儿 210/261,**禁止 join**',
+  `legacy_person_skill_id`    bigint       DEFAULT NULL COMMENT '⚠️ 仅留痕 ← $.PersonSkillId;250/261 属于别的租户,**禁止 join**',
+  `source_system`             varchar(50)  NOT NULL,
+  `source_biz_key`            varchar(300) NOT NULL,
+  `source_updated_at`         datetime     DEFAULT NULL COMMENT '← $.UpdateTime',
+  `sync_batch_id`             varchar(64)  NOT NULL,
+  `sync_time`                 datetime     NOT NULL,
+  `create_time`               datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `update_time`               datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_bridge_line_post_skill` (`tenant_id`,`line_code`,`job_no`,`skill_code`),
+  KEY `idx_bls_parent` (`tenant_id`,`line_code`,`job_no`),
+  KEY `idx_bls_batch` (`sync_batch_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='S0 关系桥:产线岗位技能。源 LineSkillDetail,FULL REPLACE。父 = dim_line_post,**走自然键**。';
+
+-- ── 三、mdp_entity 入站配置(3 条,全 FULL / incr_column=NULL)──
+
+SET @src_id = (SELECT `id` FROM `mdp_source` WHERE `source_code` = 'AIDOPDEV_MYSQL' LIMIT 1);
+
+INSERT INTO `mdp_entity`
+  (`tenant_id`,`source_id`,`entity_code`,`entity_name`,`entity_type`,
+   `source_table_name`,`target_table_name`,`sync_mode`,`incr_column`,`batch_size`,
+   `biz_key_expr`,`status`,`create_time`,`update_time`)
+SELECT 0, @src_id, 'S0_EMP_WORK_DUTY', 'S0 员工物料职责入站', 'TABLE',
+       'EmpWorkDutyMaster', 'mdp_stg_s0_emp_work_duty', 'FULL', NULL, 5000,
+       'Employee,Duty,Location', 1, NOW(), NOW()
+WHERE @src_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `mdp_entity` WHERE `entity_code` = 'S0_EMP_WORK_DUTY');
+
+INSERT INTO `mdp_entity`
+  (`tenant_id`,`source_id`,`entity_code`,`entity_name`,`entity_type`,
+   `source_table_name`,`target_table_name`,`sync_mode`,`incr_column`,`batch_size`,
+   `biz_key_expr`,`status`,`create_time`,`update_time`)
+SELECT 0, @src_id, 'S0_LINE_POST', 'S0 产线岗位入站', 'TABLE',
+       'LineSkillMaster', 'mdp_stg_s0_line_post', 'FULL', NULL, 5000,
+       'ProdLine,JOBNo', 1, NOW(), NOW()
+WHERE @src_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `mdp_entity` WHERE `entity_code` = 'S0_LINE_POST');
+
+INSERT INTO `mdp_entity`
+  (`tenant_id`,`source_id`,`entity_code`,`entity_name`,`entity_type`,
+   `source_table_name`,`target_table_name`,`sync_mode`,`incr_column`,`batch_size`,
+   `biz_key_expr`,`status`,`create_time`,`update_time`)
+SELECT 0, @src_id, 'S0_LINE_POST_SKILL', 'S0 产线岗位技能入站', 'TABLE',
+       'LineSkillDetail', 'mdp_stg_s0_line_post', 'FULL', NULL, 5000,
+       'ProdLine,JOBNo,SkillNo', 1, NOW(), NOW()
+WHERE @src_id IS NOT NULL
+  AND NOT EXISTS (SELECT 1 FROM `mdp_entity` WHERE `entity_code` = 'S0_LINE_POST_SKILL');
+
+-- ── 回滚(注释,非可执行)──
+-- DROP TABLE IF EXISTS `bridge_line_post_skill`,`dim_line_post`,`bridge_employee_work_duty`;
+-- DROP TABLE IF EXISTS `mdp_stg_s0_line_post`,`mdp_stg_s0_emp_work_duty`;
+-- DELETE FROM `mdp_entity` WHERE `tenant_id`=0 AND `entity_code` IN
+--   ('S0_EMP_WORK_DUTY','S0_LINE_POST','S0_LINE_POST_SKILL');

+ 93 - 0
server/Admin.NET.Web.Entry/UpdateScripts/1.0.509.verify.sql

@@ -0,0 +1,93 @@
+-- 1.0.509.verify.sql
+-- 契约(CURRENT.md > F-11):禁 SET @var · 每条返回 1/0 · 一条一断言 · 末条为真 SELECT。
+-- 断言强度沿用 Phase 1 Final Review:UK 断言**精确列序**(CREATE TABLE IF NOT EXISTS 会静默 no-op,
+-- 只数列数会放过结构漂移);字符串等值一律 COLLATE utf8mb4_bin(biz_key_expr 大小写是致命维度)。
+
+
+SELECT (SELECT COUNT(*) FROM information_schema.TABLES
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('mdp_stg_s0_emp_work_duty','mdp_stg_s0_line_post')) = 2;
+
+SELECT (SELECT COUNT(*) FROM information_schema.TABLES
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('bridge_employee_work_duty','dim_line_post','bridge_line_post_skill')) = 3;
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mdp_stg_s0_emp_work_duty'
+                 AND INDEX_NAME = 'uk_stg_s0_emp_work_duty' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,source_system,source_table,source_row_id';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mdp_stg_s0_line_post'
+                 AND INDEX_NAME = 'uk_stg_s0_line_post' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,source_system,source_table,source_row_id';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'bridge_employee_work_duty'
+                 AND INDEX_NAME = 'uk_bridge_employee_work_duty' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,employee_code,duty,location_code';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dim_line_post'
+                 AND INDEX_NAME = 'uk_dim_line_post' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,line_code,job_no';
+
+SELECT IFNULL((SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)
+               FROM information_schema.STATISTICS
+               WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'bridge_line_post_skill'
+                 AND INDEX_NAME = 'uk_bridge_line_post_skill' AND NON_UNIQUE = 0), '')
+       = 'tenant_id,line_code,job_no,skill_code';
+
+SELECT (SELECT COUNT(*) FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('bridge_employee_work_duty','dim_line_post','bridge_line_post_skill')
+          AND COLUMN_NAME = 'tenant_id' AND IS_NULLABLE = 'NO') = 3;
+
+SELECT (SELECT COUNT(*) FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ('mdp_stg_s0_emp_work_duty','mdp_stg_s0_line_post')
+          AND COLUMN_NAME = 'tenant_id' AND IS_NULLABLE = 'NO') = 2;
+
+SELECT (SELECT COUNT(*) FROM information_schema.STATISTICS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dim_line_post'
+          AND INDEX_NAME = 'uk_dim_line_post' AND COLUMN_NAME IN ('domain_code','company_id','factory_id')) = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.STATISTICS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'bridge_line_post_skill'
+          AND INDEX_NAME = 'uk_bridge_line_post_skill' AND COLUMN_NAME IN ('domain_code','company_id','factory_id')) = 0;
+
+SELECT (SELECT COUNT(*) FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dim_line_post'
+          AND COLUMN_NAME = 'run_crew' AND NUMERIC_PRECISION = 10 AND NUMERIC_SCALE = 3) = 1;
+
+SELECT (SELECT COUNT(*) FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'bridge_line_post_skill'
+          AND COLUMN_NAME = 'effective_date' AND IS_NULLABLE = 'YES') = 1;
+
+SELECT (SELECT COUNT(*) FROM information_schema.STATISTICS
+        WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'bridge_line_post_skill'
+          AND NON_UNIQUE = 0
+          AND COLUMN_NAME IN ('legacy_line_skill_rec_id','legacy_person_skill_id')) = 0;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity`
+        WHERE `entity_code` = 'S0_EMP_WORK_DUTY'
+          AND `biz_key_expr` COLLATE utf8mb4_bin = 'Employee,Duty,Location'
+          AND `source_table_name` COLLATE utf8mb4_bin = 'EmpWorkDutyMaster'
+          AND `target_table_name` COLLATE utf8mb4_bin = 'mdp_stg_s0_emp_work_duty'
+          AND `status` = 1 AND `sync_mode` = 'FULL' AND `incr_column` IS NULL) = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity`
+        WHERE `entity_code` = 'S0_LINE_POST'
+          AND `biz_key_expr` COLLATE utf8mb4_bin = 'ProdLine,JOBNo'
+          AND `source_table_name` COLLATE utf8mb4_bin = 'LineSkillMaster'
+          AND `target_table_name` COLLATE utf8mb4_bin = 'mdp_stg_s0_line_post'
+          AND `status` = 1 AND `sync_mode` = 'FULL' AND `incr_column` IS NULL) = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity`
+        WHERE `entity_code` = 'S0_LINE_POST_SKILL'
+          AND `biz_key_expr` COLLATE utf8mb4_bin = 'ProdLine,JOBNo,SkillNo'
+          AND `source_table_name` COLLATE utf8mb4_bin = 'LineSkillDetail'
+          AND `target_table_name` COLLATE utf8mb4_bin = 'mdp_stg_s0_line_post'
+          AND `status` = 1 AND `sync_mode` = 'FULL' AND `incr_column` IS NULL) = 1;
+
+SELECT (SELECT COUNT(*) FROM `mdp_entity` WHERE `source_table_name` = 'EmpSkills') = 0;

+ 119 - 1
server/Plugins/Admin.NET.Plugin.AiDOP.Tests/S0/Dim/S0DimContractTests.cs

@@ -26,7 +26,17 @@ public class S0DimContractTests
         // 断言内容而非数量:数量断言每加一个维度就要改一次,且改完什么也没守住。
         // 这里守的是「目录里恰好是这些维度、每个都能被 Find 命中、源系统统一」。
         Assert.Equal(
-            ["WORK_CENTER", "DEPARTMENT", "LOCATION", "LOCATION_SHELF", "EMPLOYEE", "LINE", "CUSTOMER", "PERSON_SKILL"],
+            [
+                // C1
+                "WORK_CENTER", "DEPARTMENT", "LOCATION", "LOCATION_SHELF",
+                "EMPLOYEE", "LINE", "CUSTOMER", "PERSON_SKILL",
+                // C2
+                "LABEL_TYPE", "SOP_FILE_TYPE", "QUALITY_BASE_TYPE",
+                "SAMPLING_SCHEME", "INSPECTION_ITEM", "INSPECTION_METHOD",
+                "INSPECTION_FREQUENCY", "INSPECTION_INSTRUMENT",
+                // C3
+                "EMP_WORK_DUTY", "LINE_POST", "LINE_POST_SKILL"
+            ],
             S0DimCatalog.All.Select(d => d.Key).ToArray());
         Assert.All(S0DimCatalog.All, d => Assert.Same(d, S0DimCatalog.Find(d.Key)));
         Assert.All(S0DimCatalog.All, d => Assert.Equal(S0DimCatalog.LocalSourceSystem, d.SourceSystem));
@@ -580,6 +590,114 @@ public class S0DimContractTests
             S0DimCatalog.PersonSkill.Columns.Select(c => c.JsonPath ?? ""));
     }
 
+    // ────────────────────────────────────────────────────────────────
+    // C2 / C3:本轮实测出的框架地雷,钉在测试里
+    // ────────────────────────────────────────────────────────────────
+
+    [Fact]
+    public void Qms_status_columns_must_be_str_never_booltrue()
+    {
+        // 实测值域含 `已审核` / `已启用` / `A` / `ENABLED` / `INVALID_STATUS`。
+        // 用 BoolTrue 会命中 RowGuards 的可辨识性守卫:4 张表整表归零,
+        // 而 qms_inspectionfreq.FENABLE 恰好存活 8/13 —— 部分静默丢失,最难发现。
+        foreach (var key in new[] { "SAMPLING_SCHEME", "INSPECTION_ITEM", "INSPECTION_METHOD",
+                                    "INSPECTION_FREQUENCY", "INSPECTION_INSTRUMENT" })
+        {
+            var d = S0DimCatalog.Find(key)!;
+            foreach (var name in new[] { "status_code", "enable_code" })
+            {
+                var col = d.Columns.FirstOrDefault(c => c.TargetColumn == name);
+                if (col is null) continue;
+                Assert.Equal(S0DimValueKind.Str, col.Kind);
+            }
+        }
+    }
+
+    [Fact]
+    public void InspectionFrequency_remark_must_map_FREMARK_not_FCOMMENT()
+    {
+        // 兄弟表都叫 FCOMMENT,唯独本表叫 FREMARK。照抄会取到不存在的列。
+        var col = S0DimCatalog.InspectionFrequency.Columns.Single(c => c.TargetColumn == "remark");
+        Assert.Equal("FREMARK", col.JsonPath);
+    }
+
+    [Fact]
+    public void SopFileType_must_not_declare_watermark_it_has_no_time_column()
+    {
+        // 源 ImageType 只有 5 列、无任何时间列。不得为"标准字段齐整"而回填。
+        Assert.DoesNotContain("source_updated_at",
+            S0DimCatalog.SopFileType.Columns.Select(c => c.TargetColumn));
+    }
+
+    [Fact]
+    public void LinePostSkill_effective_date_must_read_StartDate_not_the_shadow_column()
+    {
+        // 🔴 目标列名 effective_date 与源表那个 0/261 的影子列同名,是本批最易取错的一处。
+        var col = S0DimCatalog.LinePostSkill.Columns.Single(c => c.TargetColumn == "effective_date");
+        Assert.Equal("StartDate", col.JsonPath);
+
+        var insert = S0DimSqlBuilder.BuildInsertSql(S0DimCatalog.LinePostSkill);
+        Assert.Contains("$.StartDate", insert, Ord);
+        Assert.DoesNotContain("$.effective_date", insert, Ord);
+
+        var srcChk = S0DimSqlBuilder.BuildSourceChecksumSql(S0DimCatalog.LinePostSkill);
+        Assert.Contains($"{S0DimSqlBuilder.SrcAlias}.`StartDate`", srcChk, Ord);
+        Assert.DoesNotContain($"{S0DimSqlBuilder.SrcAlias}.`effective_date`", srcChk, Ord);
+        // 另三对发散型影子列同样禁用
+        foreach (var shadow in new[] { "id", "line_skill_master_id", "person_skill_id" })
+            Assert.DoesNotContain($"{S0DimSqlBuilder.SrcAlias}.`{shadow}`", srcChk, Ord);
+    }
+
+    [Fact]
+    public void LinePostSkill_parent_must_use_natural_key_not_surrogate()
+    {
+        // 代理键已污染:LineSkillRecID 同租户孤儿 210/261,PersonSkillId 250/261 跨租户。
+        var d = S0DimCatalog.LinePostSkill;
+        Assert.Equal("dim_line_post", d.ParentDimTable);
+        Assert.Equal(["line_code", "job_no"], d.ParentKeyColumns);
+        // 父键必须与 dim_line_post 业务键(去 tenant)逐字一致
+        Assert.Equal(S0DimCatalog.LinePost.BusinessKeyColumns.Skip(1), d.ParentKeyColumns);
+        // legacy id 只能是普通属性,绝不入键
+        foreach (var legacy in new[] { "legacy_line_skill_rec_id", "legacy_person_skill_id" })
+            Assert.DoesNotContain(legacy, d.BusinessKeyColumns);
+    }
+
+    [Fact]
+    public void EmpWorkDuty_identity_must_keep_location()
+    {
+        // 实测 tenant+Employee+Duty 有 1 组重复(G00308 同 Duty 下 Location 1001 vs 1000)。
+        // location 是真实区分维,不是 legacy scope,不可简化掉。
+        var d = S0DimCatalog.EmpWorkDuty;
+        Assert.Equal(["tenant_id", "employee_code", "duty", "location_code"], d.BusinessKeyColumns);
+        Assert.Equal(["Employee", "Duty", "Location"], d.SourceBizKeyColumns);
+        Assert.Equal("dim_employee", d.ParentDimTable);
+        Assert.Equal(["employee_code"], d.ParentKeyColumns);
+    }
+
+    [Fact]
+    public void LinePost_run_crew_precision_must_match_source_decimal_10_3()
+    {
+        // 对账两侧都走 CAST(expr AS CHAR):scale 不同会让 '0.000' 与 '0.0' 不等 → 校验和 FAIL。
+        var col = S0DimCatalog.LinePost.Columns.Single(c => c.TargetColumn == "run_crew");
+        Assert.Equal(S0DimValueKind.Dec, col.Kind);
+        Assert.Equal(10, col.Precision);
+        Assert.Equal(3, col.Scale);
+    }
+
+    [Fact]
+    public void C2_and_C3_identities_must_not_contain_legacy_scope()
+    {
+        foreach (var key in new[] { "LABEL_TYPE", "SOP_FILE_TYPE", "QUALITY_BASE_TYPE",
+                                    "SAMPLING_SCHEME", "INSPECTION_ITEM", "INSPECTION_METHOD",
+                                    "INSPECTION_FREQUENCY", "INSPECTION_INSTRUMENT",
+                                    "LINE_POST", "LINE_POST_SKILL" })
+        {
+            var d = S0DimCatalog.Find(key)!;
+            foreach (var legacy in new[] { "domain_code", "company_id", "factory_id" })
+                Assert.DoesNotContain(legacy, d.BusinessKeyColumns);
+        }
+    }
+
     [Fact]
     public void Batch4_must_not_reuse_dopdemorq_staging_tables()
     {

+ 411 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/S0Dim/S0DimCatalog.cs

@@ -358,9 +358,419 @@ public static class S0DimCatalog
             new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdatedAt")
         ]);
 
+    // ════════════════════════════════════════════════════════════════════════════
+    // C2 · 参考/配置字典(ref_*)
+    //
+    // 与 C1 维度同用一套机制,只是语义是「字典」而非「主数据实体」,故目标表用 ref_ 前缀。
+    // 身份口径与 C1 完全一致:tenant_id + 业务码;Domain / company / factory 一律 LEGACY_SCOPE。
+    //
+    // 🔴 本组落地时实测出的两个框架地雷,写在这里免得下次重踩:
+    //   ① bit(1) 源列用 Int 会让对账每轮必挂(详见 S0DimValueKind.Int 的说明)。
+    //   ② qms 字典的 FSTATUS / FENABLE 是 varchar 且值域含中文与字母
+    //      (`已审核` / `已启用` / `A` / `ENABLED` / `INVALID_STATUS`),
+    //      用 BoolTrue 会命中可辨识性守卫:4 张表**整表归零**,
+    //      而 qms_inspectionfreq.FENABLE 恰好存活 8/13 —— 部分静默丢失,最难发现。
+    //      ⇒ 状态列一律 Str 原样透传,语义归一留给下游。
+    // ════════════════════════════════════════════════════════════════════════════
+
+    /// <summary>
+    /// 标签类型字典。源 <c>BarCodeType</c>。
+    ///
+    /// <para>身份 = <c>tenant_id + bar_type</c>。CRUD 查重
+    /// (<c>AdoS0LabelTypesController</c>)= <c>tenant + FactoryRefId + BarType</c>,
+    /// 而 <c>FactoryRefId</c> 由 <c>AdoS0OrgScope</c> 仅按租户解析并强制 stamp ⇒ 等价 <c>tenant + bar_type</c>,
+    /// **不含 Domain**。实测 <c>bar_type</c> 跨租户碰撞 6 个编码 ⇒ 租户必须入键。</para>
+    ///
+    /// <para>⚠️ 本表主键叫 <c>rec_id</c>(带下划线),不在 <c>MdpDbPullExecutor.ResolveSourceRowId</c>
+    /// 的候选列表内(<c>rec_id ≠ recid</c>),且本表无 <c>id</c> 列 ⇒ <c>source_row_id</c> 落 GUID 兜底。
+    /// 与 <c>LocationShelfMaster</c> 同型,**安全**:物化只认 <c>sync_batch_id</c>,
+    /// 且 purge 先于 pull,不依赖 source_row_id 稳定性。</para>
+    /// </summary>
+    public static readonly S0DimDefinition LabelType = new(
+        Key: "LABEL_TYPE",
+        EntityCode: "S0_LABEL_TYPE",
+        SourceTable: "BarCodeType",
+        SourceSystem: LocalSourceSystem,
+        StagingTable: "mdp_stg_s0_label_type",
+        DimTable: "ref_label_type",
+        BusinessKeyColumns: ["tenant_id", "label_type_code"],
+        SourceBizKeyColumns: ["bar_type"],
+        Columns:
+        [
+            new("tenant_id",         S0DimValueKind.TenantIdColumn),
+            new("label_type_code",   S0DimValueKind.Str,         "bar_type",       Required: true),
+            new("label_class",       S0DimValueKind.Str,         "class"),
+            new("input_string",      S0DimValueKind.Str,         "input_string"),
+            new("domain_code",       S0DimValueKind.Str,         "domain_code"),
+            new("company_id",        S0DimValueKind.Int,         "company_ref_id", Required: true),
+            new("factory_id",        S0DimValueKind.Int,         "factory_ref_id", Required: true),
+            new("source_updated_at", S0DimValueKind.DateTimeSec, "update_time")
+        ]);
+
+    /// <summary>
+    /// SOP 文件类型字典。源 <c>ImageType</c>(**只有 5 列**)。
+    ///
+    /// <para>身份 = <c>tenant_id + image_type_id</c>。控制器
+    /// <c>AdoS0MfgSopFileTypesController</c> 无查重守卫,定位走 <c>ByIdScopedAsync(id, tenantId)</c>,
+    /// 且已有显式「去 domain 化」注释(S0-DOMAIN-FILTER-REMOVAL-READONLY-1)。</para>
+    ///
+    /// <para>🔴 <b>本表没有任何时间列</b>(实测 5 列 = id/tenant_id/image_type_id/descr/domain),
+    /// 故**不声明 <c>source_updated_at</c>**,ref 表里也不建该列。
+    /// 不得为了「标准字段齐整」而回填 —— JsonPath 必须是真实物理列。</para>
+    ///
+    /// <para>注意物理列名是 <c>domain</c> 而非 <c>domain_code</c>;且其取值在三个租户间口径异构
+    /// (<c>S0-F001</c> / 雪花号 / <c>8010</c>),更加坐实它是 LEGACY_SCOPE。</para>
+    /// </summary>
+    public static readonly S0DimDefinition SopFileType = new(
+        Key: "SOP_FILE_TYPE",
+        EntityCode: "S0_SOP_FILE_TYPE",
+        SourceTable: "ImageType",
+        SourceSystem: LocalSourceSystem,
+        StagingTable: "mdp_stg_s0_sop_file_type",
+        DimTable: "ref_sop_file_type",
+        BusinessKeyColumns: ["tenant_id", "file_type_code"],
+        SourceBizKeyColumns: ["image_type_id"],
+        Columns:
+        [
+            new("tenant_id",      S0DimValueKind.TenantIdColumn),
+            new("file_type_code", S0DimValueKind.Str, "image_type_id", Required: true),
+            new("file_type_name", S0DimValueKind.Str, "descr"),
+            new("domain_code",    S0DimValueKind.Str, "domain")
+        ]);
+
+    /// <summary>
+    /// 质量基础类型字典。源 <c>qms_quality_base_type</c>。
+    ///
+    /// <para>身份 = <c>tenant_id + type_category + type_code</c>,**两段业务码**。
+    /// <c>type_code</c> 单列在当前数据里也唯一,但那是运气 —— 代码硬证据:
+    /// <c>AdoS0QualityBaseDataControllers</c> 的 Create/Update 两处查重均按
+    /// <c>(TypeCategory, TypeCode)</c>,注释亦写明「(TypeCategory, TypeCode) 全局唯一」。
+    /// 按「当前业务证据优先」取两段。源表无任何 UK。</para>
+    ///
+    /// <para>本表无 Domain / company / factory 列 —— 不设、不发明(同 <c>WorkCtrMaster</c> 先例)。
+    /// <c>is_active</c> 是 <c>tinyint(1) NOT NULL</c>,在 raw_data 里是 JSON BOOLEAN,可安全走 BoolTrue。</para>
+    /// </summary>
+    public static readonly S0DimDefinition QualityBaseType = new(
+        Key: "QUALITY_BASE_TYPE",
+        EntityCode: "S0_QUALITY_BASE_TYPE",
+        SourceTable: "qms_quality_base_type",
+        SourceSystem: LocalSourceSystem,
+        StagingTable: "mdp_stg_s0_quality_base_type",
+        DimTable: "ref_quality_base_type",
+        BusinessKeyColumns: ["tenant_id", "type_category", "type_code"],
+        SourceBizKeyColumns: ["type_category", "type_code"],
+        Columns:
+        [
+            new("tenant_id",         S0DimValueKind.TenantIdColumn),
+            new("type_category",     S0DimValueKind.Str,         "type_category", Required: true),
+            new("type_code",         S0DimValueKind.Str,         "type_code",     Required: true),
+            new("short_name",        S0DimValueKind.Str,         "short_name"),
+            new("full_name",         S0DimValueKind.Str,         "full_name"),
+            new("remark",            S0DimValueKind.Str,         "remark"),
+            new("is_active",         S0DimValueKind.BoolTrue,    "is_active"),
+            new("source_updated_at", S0DimValueKind.DateTimeSec, "modify_time")
+        ]);
+
+    /// <summary>
+    /// 抽样方案字典。源 <c>qms_sampscheme</c>。C2-2 qms 字典族共用 <c>mdp_stg_s0_qms_dict</c>,靠 <c>source_table</c> 隔离。
+    /// <para>🔴 <c>FSTATUS</c>/<c>FENABLE</c> 必须 <c>Str</c>:实测值域 <c>已审核×8 · DISABLED×1 · INVALID_STATUS×1</c>
+    /// 与 <c>已启用×8 · ENABLED×2</c>,用 BoolTrue 会**整表归零**。</para>
+    /// </summary>
+    public static readonly S0DimDefinition SamplingScheme = new(
+        Key: "SAMPLING_SCHEME",
+        EntityCode: "S0_SAMPLING_SCHEME",
+        SourceTable: "qms_sampscheme",
+        SourceSystem: LocalSourceSystem,
+        StagingTable: "mdp_stg_s0_qms_dict",
+        DimTable: "ref_sampling_scheme",
+        BusinessKeyColumns: ["tenant_id", "scheme_code"],
+        SourceBizKeyColumns: ["FNUMBER"],
+        Columns:
+        [
+            new("tenant_id",         S0DimValueKind.TenantIdColumn),
+            new("scheme_code",       S0DimValueKind.Str,         "FNUMBER", Required: true),
+            new("scheme_name",       S0DimValueKind.Str,         "FNAME"),
+            new("sampling_type",     S0DimValueKind.Str,         "FSAMPLINGTYPE"),
+            new("inspection_level",  S0DimValueKind.Str,         "FINSPECTIONLEVEL"),
+            new("strictness",        S0DimValueKind.Str,         "FSTRICTNESS"),
+            new("aql_value",         S0DimValueKind.Str,         "FAQLVALUE"),
+            new("inspection_type",   S0DimValueKind.Str,         "FINSPECTIONTYPE"),
+            new("status_code",       S0DimValueKind.Str,         "FSTATUS"),
+            new("enable_code",       S0DimValueKind.Str,         "FENABLE"),
+            new("remark",            S0DimValueKind.Str,         "FCOMMENT"),
+            new("source_updated_at", S0DimValueKind.DateTimeSec, "FMODIFYTIME")
+        ]);
+
+    /// <summary>
+    /// 检验项目字典。源 <c>qms_inspectionitems</c>。
+    /// <para>⚠️ <c>FCHECKMETHOD</c>/<c>FCHECKBASIS</c> 指向的是**源表代理主键 <c>id</c>**,不是业务键,
+    /// 故只作普通 Int 属性透传,**不声明父维度**(框架的孤儿判定按业务键列做 join,对不上)。
+    /// 同 <c>dim_employee.sys_user_id</c> 先例。</para>
+    /// </summary>
+    public static readonly S0DimDefinition InspectionItem = new(
+        Key: "INSPECTION_ITEM",
+        EntityCode: "S0_INSPECTION_ITEM",
+        SourceTable: "qms_inspectionitems",
+        SourceSystem: LocalSourceSystem,
+        StagingTable: "mdp_stg_s0_qms_dict",
+        DimTable: "ref_inspection_item",
+        BusinessKeyColumns: ["tenant_id", "item_code"],
+        SourceBizKeyColumns: ["FNUMBER"],
+        Columns:
+        [
+            new("tenant_id",         S0DimValueKind.TenantIdColumn),
+            new("item_code",         S0DimValueKind.Str,         "FNUMBER", Required: true),
+            new("item_name",         S0DimValueKind.Str,         "FNAME"),
+            new("check_method_id",   S0DimValueKind.Int,         "FCHECKMETHOD"),
+            new("check_basis_id",    S0DimValueKind.Int,         "FCHECKBASIS"),
+            new("status_code",       S0DimValueKind.Str,         "FSTATUS"),
+            new("enable_code",       S0DimValueKind.Str,         "FENABLE"),
+            new("remark",            S0DimValueKind.Str,         "FCOMMENT"),
+            new("source_updated_at", S0DimValueKind.DateTimeSec, "FMODIFYTIME")
+        ]);
+
+    /// <summary>检验方法字典。源 <c>qms_inspection_method</c>。状态列同族一律 <c>Str</c>。</summary>
+    public static readonly S0DimDefinition InspectionMethod = new(
+        Key: "INSPECTION_METHOD",
+        EntityCode: "S0_INSPECTION_METHOD",
+        SourceTable: "qms_inspection_method",
+        SourceSystem: LocalSourceSystem,
+        StagingTable: "mdp_stg_s0_qms_dict",
+        DimTable: "ref_inspection_method",
+        BusinessKeyColumns: ["tenant_id", "method_code"],
+        SourceBizKeyColumns: ["FNUMBER"],
+        Columns:
+        [
+            new("tenant_id",         S0DimValueKind.TenantIdColumn),
+            new("method_code",       S0DimValueKind.Str,         "FNUMBER", Required: true),
+            new("method_name",       S0DimValueKind.Str,         "FNAME"),
+            new("control_strategy",  S0DimValueKind.Str,         "FCTRLSTRATEGY"),
+            new("status_code",       S0DimValueKind.Str,         "FSTATUS"),
+            new("enable_code",       S0DimValueKind.Str,         "FENABLE"),
+            new("remark",            S0DimValueKind.Str,         "FCOMMENT"),
+            new("source_updated_at", S0DimValueKind.DateTimeSec, "FMODIFYTIME")
+        ]);
+
+    /// <summary>
+    /// 检验频次字典。源 <c>qms_inspectionfreq</c>。
+    /// <para>🔴 <b>本表与兄弟表有两处不同,照抄会出错</b>:
+    /// ① 备注列叫 <c>FREMARK</c>,**不是** <c>FCOMMENT</c>;
+    /// ② <c>FENABLE</c> 的值域是 <c>'1'×8 · ENABLED×5</c> —— 用 BoolTrue 会**存活 8 行、静默丢 5 行**,
+    /// 是全族唯一不会整表归零、因而最容易被误判成「通过」的一张。</para>
+    /// </summary>
+    public static readonly S0DimDefinition InspectionFrequency = new(
+        Key: "INSPECTION_FREQUENCY",
+        EntityCode: "S0_INSPECTION_FREQUENCY",
+        SourceTable: "qms_inspectionfreq",
+        SourceSystem: LocalSourceSystem,
+        StagingTable: "mdp_stg_s0_qms_dict",
+        DimTable: "ref_inspection_frequency",
+        BusinessKeyColumns: ["tenant_id", "frequency_code"],
+        SourceBizKeyColumns: ["FNUMBER"],
+        Columns:
+        [
+            new("tenant_id",         S0DimValueKind.TenantIdColumn),
+            new("frequency_code",    S0DimValueKind.Str,         "FNUMBER", Required: true),
+            new("frequency_name",    S0DimValueKind.Str,         "FNAME"),
+            new("remark",            S0DimValueKind.Str,         "FREMARK"),
+            new("status_code",       S0DimValueKind.Str,         "FSTATUS"),
+            new("enable_code",       S0DimValueKind.Str,         "FENABLE"),
+            new("source_updated_at", S0DimValueKind.DateTimeSec, "FMODIFYTIME")
+        ]);
+
+    /// <summary>
+    /// 量检具字典。源 <c>qms_inspectioninstru</c>。
+    /// <para>三个运维列是源表自有的 snake_case 列(非 F 前缀):<c>gauge_category</c> ·
+    /// <c>calibration_cycle_days</c>(int) · <c>next_calibration_date</c>。</para>
+    /// <para>⚠️ <c>next_calibration_date</c> 源是 <b>varchar(50)</b> 存日期串(如 <c>2027-02-25</c>),
+    /// 用 <c>Str</c> 忠实透传而非 <c>DateTimeSec</c>:后者对非 ISO 脏值会让**两侧同时变 NULL**,
+    /// checksum 照样相等而数据静默丢失。</para>
+    /// </summary>
+    public static readonly S0DimDefinition InspectionInstrument = new(
+        Key: "INSPECTION_INSTRUMENT",
+        EntityCode: "S0_INSPECTION_INSTRUMENT",
+        SourceTable: "qms_inspectioninstru",
+        SourceSystem: LocalSourceSystem,
+        StagingTable: "mdp_stg_s0_qms_dict",
+        DimTable: "ref_inspection_instrument",
+        BusinessKeyColumns: ["tenant_id", "instrument_code"],
+        SourceBizKeyColumns: ["FNUMBER"],
+        Columns:
+        [
+            new("tenant_id",              S0DimValueKind.TenantIdColumn),
+            new("instrument_code",        S0DimValueKind.Str,         "FNUMBER", Required: true),
+            new("instrument_name",        S0DimValueKind.Str,         "FNAME"),
+            new("model",                  S0DimValueKind.Str,         "FMODEL"),
+            new("specification",          S0DimValueKind.Str,         "FSPECIFICATION"),
+            new("manufacturer",           S0DimValueKind.Str,         "FMANUFACTURER"),
+            new("gauge_category",         S0DimValueKind.Str,         "gauge_category"),
+            new("calibration_cycle_days", S0DimValueKind.Int,         "calibration_cycle_days"),
+            new("next_calibration_date",  S0DimValueKind.Str,         "next_calibration_date"),
+            new("status_code",            S0DimValueKind.Str,         "FSTATUS"),
+            new("enable_code",            S0DimValueKind.Str,         "FENABLE"),
+            new("remark",                 S0DimValueKind.Str,         "FCOMMENT"),
+            new("source_updated_at",      S0DimValueKind.DateTimeSec, "FMODIFYTIME")
+        ]);
+
+    // ════════════════════════════════════════════════════════════════════════════
+    // C3 · 关系 / 明细桥接(bridge_*)
+    // ════════════════════════════════════════════════════════════════════════════
+
+    /// <summary>
+    /// 员工物料职责桥。源 <c>EmpWorkDutyMaster</c>。
+    ///
+    /// <para>身份 = <c>tenant_id + Employee + Duty + Location</c>。
+    /// <b><c>location_code</c> 不可省</b> —— 实测 <c>tenant+Employee+Duty</c> 有 1 组重复:
+    /// 租户 797…229 的 <c>G00308</c> 在同一串 Duty 下有两行,分别是 <c>Location=1001</c>
+    /// 与 <c>Location=1000</c>(物料也不同)⇒ 库位是真实的区分维,不是 legacy scope。
+    /// 加上 Location 后租户内重复 = 0。</para>
+    ///
+    /// <para>挂父有业务依据:本表是少数**真正强制父存在性**的 CRUD ——
+    /// <c>AdoS0EmpWorkDutiesController</c> 写入前调 <c>EmployeeExistsInTenantAsync</c>。
+    /// 实测父孤儿 0/25。父键取 <c>["employee_code"]</c>,与 <c>dim_employee</c> 身份对齐。</para>
+    ///
+    /// <para>⚠️ CRUD **无查重守卫**且 DTO 的 <c>Duty</c>/<c>Location</c> 是 <c>string?</c> 无 <c>[Required]</c>
+    /// ⇒ 业务可以合法建出 NULL 行,而它们是 Required 业务键,会被 RowGuards 整行过滤导致对账 FAIL。
+    /// 当前实测 0 NULL / 0 空串,verify 与运行验收都显式断言这一点。</para>
+    /// </summary>
+    public static readonly S0DimDefinition EmpWorkDuty = new(
+        Key: "EMP_WORK_DUTY",
+        EntityCode: "S0_EMP_WORK_DUTY",
+        SourceTable: "EmpWorkDutyMaster",
+        SourceSystem: LocalSourceSystem,
+        StagingTable: "mdp_stg_s0_emp_work_duty",
+        DimTable: "bridge_employee_work_duty",
+        BusinessKeyColumns: ["tenant_id", "employee_code", "duty", "location_code"],
+        SourceBizKeyColumns: ["Employee", "Duty", "Location"],
+        ParentDimTable: "dim_employee",
+        ParentKeyColumns: ["employee_code"],
+        Columns:
+        [
+            new("tenant_id",         S0DimValueKind.TenantIdColumn),
+            new("employee_code",     S0DimValueKind.Str,         "Employee",       Required: true),
+            new("duty",              S0DimValueKind.Str,         "Duty",           Required: true),
+            new("location_code",     S0DimValueKind.Str,         "Location",       Required: true),
+            new("domain_code",       S0DimValueKind.Str,         "Domain",         Required: true),
+            new("company_id",        S0DimValueKind.Int,         "company_ref_id", Required: true),
+            new("factory_id",        S0DimValueKind.Int,         "factory_ref_id", Required: true),
+            new("line_code",         S0DimValueKind.Str,         "ProdLine"),
+            new("item_num",          S0DimValueKind.Str,         "ItemNum"),
+            new("item_num_from",     S0DimValueKind.Str,         "ItemNum1"),
+            new("item_num_to",       S0DimValueKind.Str,         "ItemNum2"),
+            new("emp_type",          S0DimValueKind.Str,         "EmpType"),
+            new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
+        ]);
+
+    /// <summary>
+    /// 产线岗位。源 <c>LineSkillMaster</c>。与 <see cref="LinePostSkill"/> 共用
+    /// <c>mdp_stg_s0_line_post</c>,靠 <c>source_table</c> 隔离(同 Location/LocationShelf 范式)。
+    ///
+    /// <para>身份 = <c>tenant_id + ProdLine + JOBNo</c>,实测租户内重复 0;
+    /// <c>Domain</c> 每租户单值 ⇒ LEGACY_SCOPE,不入身份。
+    /// <c>JOBNo</c> 当前每租户只有一个取值(<c>102</c>),零区分力,但它是 CRUD 必填的岗位码,
+    /// **保留在身份中** —— 去掉会造成不可逆的键收缩,将来多岗位时无法恢复。</para>
+    ///
+    /// <para><b>不挂父维度</b>,三条理由:① <c>ProdLine → LineMaster.Line</c> 同租户孤儿 28/57,
+    /// 且这 28 个码在**任何租户**都不存在(是被引用主数据整体缺失,不是租户错挂);
+    /// ② CRUD 对 <c>ProdLine</c> 不做任何存在性校验(对比 EmpWorkDuty 有 <c>EmployeeExistsInTenantAsync</c>);
+    /// ③ 与既有范式一致 —— <c>dim_line.location_code</c> / <c>dim_employee.work_center_code</c>
+    /// 都是同名软引用、不声明父。挂父只会每轮产出无人能闭环的 WARN。</para>
+    ///
+    /// <para>🔴 源侧 <c>IX_LineSkillMaster</c> 含 <c>Site</c> 而 Site **0/57 全 NULL**,
+    /// MySQL UNIQUE 对 NULL 永不判等 ⇒ **源侧当前对本表没有任何唯一性约束**,
+    /// 观测到的 0 重复是无守卫的巧合。dim 的唯一键将是第一道真守卫。</para>
+    ///
+    /// <para>⚠️ 3 行 <c>ProdLine</c> 带尾随空格(<c>'A01 '</c>/<c>'S01 '</c>/<c>'S02 '</c>)。
+    /// <c>utf8mb4_0900_ai_ci</c> 是 NO PAD,尾空格参与比较,是真实业务键差异 —— **原样透传,禁止 TRIM**。</para>
+    /// </summary>
+    public static readonly S0DimDefinition LinePost = new(
+        Key: "LINE_POST",
+        EntityCode: "S0_LINE_POST",
+        SourceTable: "LineSkillMaster",
+        SourceSystem: LocalSourceSystem,
+        StagingTable: "mdp_stg_s0_line_post",
+        DimTable: "dim_line_post",
+        BusinessKeyColumns: ["tenant_id", "line_code", "job_no"],
+        SourceBizKeyColumns: ["ProdLine", "JOBNo"],
+        Columns:
+        [
+            new("tenant_id",         S0DimValueKind.TenantIdColumn),
+            new("line_code",         S0DimValueKind.Str,         "ProdLine", Required: true),
+            new("job_no",            S0DimValueKind.Str,         "JOBNo",    Required: true),
+            new("domain_code",       S0DimValueKind.Str,         "Domain",   Required: true),
+            new("job_name",          S0DimValueKind.Str,         "JOBDescr"),
+            // 0/57 全空,但它在源 UK 里 —— 保留可见性,否则将来身份被破坏时看不出原因
+            new("site",              S0DimValueKind.Str,         "Site"),
+            new("order_by",          S0DimValueKind.Int,         "Orderby"),
+            // Precision/Scale 必须与源 decimal(10,3) 逐位相同:对账两侧都走 CAST(expr AS CHAR),
+            // scale 不同会让 '0.000' 与 '0.0' 不等,属性校验和当场 FAIL。
+            new("run_crew",          S0DimValueKind.Dec,         "RunCrew", Precision: 10, Scale: 3),
+            new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
+        ]);
+
+    /// <summary>
+    /// 产线岗位技能桥。源 <c>LineSkillDetail</c> —— **本仓第 4 张 dual-column 表,且是最危险的一张**。
+    ///
+    /// <para>🔴 它的四对双列里有三对是「有值但发散」型(影子列 250/261 非空,
+    /// 但与真列**没有一行相等**)—— 取错不会得到 NULL,会得到**看起来合法的错值**:
+    /// <c>RecID</c>(真) vs <c>id</c>(影) · <c>LineSkillRecID</c>(真) vs <c>line_skill_master_id</c>(影) ·
+    /// <c>PersonSkillId</c>(真) vs <c>person_skill_id</c>(影)。
+    /// 第四对是恒空型:<c>StartDate</c>(真, 250/261) vs <c>effective_date</c>(影, **0/261**)。
+    /// ⚠️ 本 dim 的目标列**取名** <c>effective_date</c>,而它的源物理列是 <c>StartDate</c> ——
+    /// 名字与源表那个影子列同名,是本批最容易取错的一处,已加契约测试钉死。</para>
+    ///
+    /// <para><b>父连接必须走自然键</b>:代理键已被污染 ——
+    /// <c>LineSkillRecID → LineSkillMaster.RecID</c> 同租户孤儿 **210/261**,忽略租户后
+    /// **175 行解析到别的租户**;<c>PersonSkillId</c> 更甚,**250/261 属于别的租户**。
+    /// 自然键 <c>(tenant, ProdLine, JOBNo)</c> 连父三租户**全 0 孤儿**。
+    /// 两个 legacy id 只作留痕列物化,**任何 join 都不得使用**。</para>
+    ///
+    /// <para><c>SkillNo</c> 同租户在 <c>PersonSkill</c> 不命中 210/261 ——
+    /// **不因为都叫 Skill 就建父子关系**(与 <see cref="PersonSkill"/> 已有的同款否决一致)。</para>
+    /// </summary>
+    public static readonly S0DimDefinition LinePostSkill = new(
+        Key: "LINE_POST_SKILL",
+        EntityCode: "S0_LINE_POST_SKILL",
+        SourceTable: "LineSkillDetail",
+        SourceSystem: LocalSourceSystem,
+        StagingTable: "mdp_stg_s0_line_post",
+        DimTable: "bridge_line_post_skill",
+        BusinessKeyColumns: ["tenant_id", "line_code", "job_no", "skill_code"],
+        SourceBizKeyColumns: ["ProdLine", "JOBNo", "SkillNo"],
+        ParentDimTable: "dim_line_post",
+        ParentKeyColumns: ["line_code", "job_no"],
+        Columns:
+        [
+            new("tenant_id",                S0DimValueKind.TenantIdColumn),
+            new("line_code",                S0DimValueKind.Str,         "ProdLine", Required: true),
+            new("job_no",                   S0DimValueKind.Str,         "JOBNo",    Required: true),
+            new("skill_code",               S0DimValueKind.Str,         "SkillNo",  Required: true),
+            // 源可空(虽然 261/261 有值):设 Required 会让未来的 NULL 行被静默滤掉并引发全批 FAIL
+            new("domain_code",              S0DimValueKind.Str,         "Domain"),
+            new("site",                     S0DimValueKind.Str,         "Site"),
+            // 🔴 源物理列是 StartDate,**不是**同表那个 0/261 的影子列 effective_date
+            new("effective_date",           S0DimValueKind.DateTimeSec, "StartDate"),
+            new("required_level",           S0DimValueKind.Str,         "required_level"),
+            new("remark",                   S0DimValueKind.Str,         "remark"),
+            new("is_enabled",               S0DimValueKind.BoolTrue,    "is_enabled"),
+            // 仅留痕,禁止 join —— 见类型注释
+            new("legacy_line_skill_rec_id", S0DimValueKind.Int,         "LineSkillRecID"),
+            new("legacy_person_skill_id",   S0DimValueKind.Int,         "PersonSkillId"),
+            new("source_updated_at",        S0DimValueKind.DateTimeSec, "UpdateTime")
+        ]);
+
     /// <summary>全部维度,按依赖顺序(父在前,子在后 —— 孤儿判定需要父维度先就绪)。</summary>
     public static readonly IReadOnlyList<S0DimDefinition> All =
-        [WorkCenter, Department, Location, LocationShelf, Employee, Line, Customer, PersonSkill];
+    [
+        // C1 主数据
+        WorkCenter, Department, Location, LocationShelf, Employee, Line, Customer, PersonSkill,
+        // C2-1 独立字典
+        LabelType, SopFileType, QualityBaseType,
+        // C2-2 qms 字典族(共用 mdp_stg_s0_qms_dict)
+        SamplingScheme, InspectionItem, InspectionMethod, InspectionFrequency, InspectionInstrument,
+        // C3 关系桥(EmpWorkDuty 的父 dim_employee 在前;LinePostSkill 的父 LinePost 紧邻其前)
+        EmpWorkDuty, LinePost, LinePostSkill
+    ];
 
     /// <summary>按 Key 取维度(大小写不敏感);不存在返回 null。</summary>
     public static S0DimDefinition? Find(string? key) =>

+ 16 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/DataPlatform/S0Dim/S0DimValueKind.cs

@@ -13,7 +13,22 @@ public enum S0DimValueKind
     /// <summary>可空字符串。</summary>
     Str,
 
-    /// <summary>可空整数(SIGNED)。</summary>
+    /// <summary>
+    /// 可空整数(SIGNED)。
+    ///
+    /// <para>🔴 <b>源列若是 MySQL 的 <c>bit(1)</c>,绝不能用本 Kind</b>(2026-09-07 实测):
+    /// 对账两侧都走 <c>IFNULL(CAST(expr AS CHAR), 哨兵)</c>,而
+    /// <c>CAST(b'1' AS CHAR)</c> 返回的是**裸字节** <c>0x01</c>,
+    /// dim 侧的 <c>CAST(1 AS CHAR)</c> 是字符 <c>'1'</c> = <c>0x31</c> —— 两者不等,
+    /// 于是 <b>source_chk ≠ dim_chk,每一轮刷新都会被阻断对账打回</b>,
+    /// 而数据本身其实是对的,排查时极易怀疑错方向。</para>
+    ///
+    /// <para>正确做法:<c>bit(1)</c> 用 <see cref="BoolTrue"/>(其 Norm 是双层
+    /// <c>CAST(CAST(… AS UNSIGNED) AS CHAR)</c>,能把 bit 正规化成 <c>'1'</c>/<c>'0'</c>)。
+    /// 但要先确认该列**不会为 NULL** —— <c>BoolTrue</c> 的可辨识性守卫要求原值
+    /// ∈ <c>{1,0,true,false}</c>,NULL 会让整行被过滤、行数对账 FAIL。
+    /// 两个条件都不满足时,本阶段的结论是**不物化该列**。</para>
+    /// </summary>
     Int,
 
     /// <summary>可空 DECIMAL(Precision, Scale)。</summary>