|
|
@@ -713,6 +713,374 @@ public static class S0DimCatalog
|
|
|
new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
|
|
|
]);
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 物料-工序-产线产能。源 <c>ProdLineDetail</c>,父 = <see cref="Item"/>。
|
|
|
+ ///
|
|
|
+ /// <para><b>身份 = tenant + Part + Op + Line</b>(实测租户内重复 0 组)。
|
|
|
+ /// <c>Line</c> **不可省**:去掉后 <c>tenant+Part+Op</c> 有 <b>197 组 / 394 行</b>重复。
|
|
|
+ /// <c>Domain</c> 每租户单值(797→8010、824585161322565→2410、838257186181189→8010),
|
|
|
+ /// 零判别力 ⇒ LEGACY_SCOPE,不入身份(D-025)。</para>
|
|
|
+ ///
|
|
|
+ /// <para>🔴 <b>dual-column 陷阱</b>:真列是 <c>InternalEquipmentCode</c>(2430/10646 有值),
|
|
|
+ /// 同表的 <c>internal_equipment_type_code</c> 是 <b>0/10646 的死影子列</b>。取错不报错、只会全 NULL。</para>
|
|
|
+ ///
|
|
|
+ /// <para>🔴 <c>RunCrew</c> 在本表是 <c>varchar(8)</c>,而 <see cref="LinePost"/> 的同名列源自
|
|
|
+ /// <c>LineSkillMaster</c> 是 <c>decimal(10,3)</c> —— <b>同名不同型</b>,照抄会当场类型出错。</para>
|
|
|
+ ///
|
|
|
+ /// <para><c>Line</c> 有 <b>11 行带尾随空格</b>(<c>A01 </c>×2 / <c>BA01 </c>×6 / <c>DC01-1 </c>×1 /
|
|
|
+ /// <c>S01 </c>×1 / <c>S02 </c>×1)。<c>utf8mb4_0900_ai_ci</c> 是 NO PAD,这是真实业务键差异,
|
|
|
+ /// **原样透传,禁止 TRIM**。</para>
|
|
|
+ ///
|
|
|
+ /// <para><c>Line</c> / <c>Site</c> 都只作软引用**不设父**:<c>Line→dim_line</c> 同租户孤儿
|
|
|
+ /// 8848/10646(租户 824585161322565 的 <c>LineMaster</c> 整体 0 行),<c>Site</c> 更有 5823/10646 为 NULL;
|
|
|
+ /// 且 CRUD 写入路径对二者**零存在性校验**(<c>_lineMasterRep</c>/<c>_workCtrRep</c> 只用于列表回显)。
|
|
|
+ /// 与 <see cref="LinePost"/> 不挂 <c>dim_line</c> 同源。</para>
|
|
|
+ ///
|
|
|
+ /// <para>⚠️ <c>Op</c> 是本仓**首个 <c>Int</c> 类型业务键**。三侧渲染已验证一致:
|
|
|
+ /// C# <c>((int)1002).ToString()</c> / 源侧 <c>CONCAT_WS(…, a.Op)</c> / dim 侧同 —— 均为 <c>'1002'</c>。</para>
|
|
|
+ ///
|
|
|
+ /// <para>租户分布 797=5817 / 824585161322565=4823 / 838257186181189=6,
|
|
|
+ /// 故 <c>batch_size</c> 取 <b>20000</b>(3.4× 头寸;默认 5000 会被
|
|
|
+ /// <c>AssertNoSourceDuplicateAsync</c> 的 <c>sourceCount >= batchSize</c> 直接判失败)。</para>
|
|
|
+ /// </summary>
|
|
|
+ public static readonly S0DimDefinition ItemOpLine = new(
|
|
|
+ Key: "ITEM_OP_LINE",
|
|
|
+ EntityCode: "S0_ITEM_OP_LINE",
|
|
|
+ SourceTable: "ProdLineDetail",
|
|
|
+ SourceSystem: LocalSourceSystem,
|
|
|
+ StagingTable: "mdp_stg_s0_item_op_line",
|
|
|
+ DimTable: "bridge_item_op_line",
|
|
|
+ BusinessKeyColumns: ["tenant_id", "item_code", "operation_no", "line_code"],
|
|
|
+ SourceBizKeyColumns: ["Part", "Op", "Line"],
|
|
|
+ ParentDimTable: "dim_item",
|
|
|
+ ParentKeyColumns: ["item_code"],
|
|
|
+ Columns:
|
|
|
+ [
|
|
|
+ new("tenant_id", S0DimValueKind.TenantIdColumn),
|
|
|
+ new("item_code", S0DimValueKind.Str, "Part", Required: true),
|
|
|
+ new("operation_no", S0DimValueKind.Int, "Op", Required: true),
|
|
|
+ new("line_code", S0DimValueKind.Str, "Line", Required: true),
|
|
|
+ new("domain_code", S0DimValueKind.Str, "Domain", Required: true),
|
|
|
+ new("work_center_code", S0DimValueKind.Str, "Site"),
|
|
|
+ new("rate", S0DimValueKind.Dec, "Rate", Precision: 18, Scale: 6),
|
|
|
+ new("setup_time", S0DimValueKind.Dec, "SetupTime", Precision: 18, Scale: 6),
|
|
|
+ new("overlap_time", S0DimValueKind.Dec, "OverlapTime", Precision: 18, Scale: 6),
|
|
|
+ new("sequence_no", S0DimValueKind.Dec, "Sequence", Precision: 18, Scale: 6),
|
|
|
+ new("standard_staff_count", S0DimValueKind.Dec, "StandardStaffCount", Precision: 18, Scale: 6),
|
|
|
+ new("skill_code", S0DimValueKind.Str, "SkillNo"),
|
|
|
+ // 🔴 真列,**不是**同表 0/10646 的影子列 internal_equipment_type_code
|
|
|
+ new("internal_equipment_code", S0DimValueKind.Str, "InternalEquipmentCode"),
|
|
|
+ new("mold_type_code", S0DimValueKind.Str, "MoldTypeCode"),
|
|
|
+ new("start_marker", S0DimValueKind.Int, "Start"),
|
|
|
+ new("is_active", S0DimValueKind.BoolTrue, "IsActive"),
|
|
|
+ new("is_confirmed", S0DimValueKind.BoolTrue, "IsConfirm"),
|
|
|
+ // 🔴 源 varchar(8),与 dim_line_post.run_crew 的 decimal(10,3) 同名不同型
|
|
|
+ new("run_crew", S0DimValueKind.Str, "RunCrew"),
|
|
|
+ new("op_type", S0DimValueKind.Str, "OpType"),
|
|
|
+ new("bom_code", S0DimValueKind.Str, "Bom"),
|
|
|
+ new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
|
|
|
+ ]);
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 物料工艺路线工序。源 <c>RoutingOpDetail</c>(38806 行,S0 最大的桥),父 = <see cref="Item"/>。
|
|
|
+ ///
|
|
|
+ /// <para><b>身份 = tenant + MaterialCode + OperationCode</b>(实测重复 0 组)。
|
|
|
+ /// <c>RouteCode</c> 不必要:每租户 <c>COUNT(DISTINCT RouteCode) == COUNT(DISTINCT MaterialCode)</c>
|
|
|
+ /// 且两列逐值相同 38805/38806 行。<c>SortNo</c> 也不必要(近乎常量)。
|
|
|
+ /// 去掉 <c>OperationCode</c> 则塌陷成 <b>2879 组 / 38677 行</b>重复。</para>
|
|
|
+ ///
|
|
|
+ /// <para>🔴 <b>本表的时间列叫 <c>UpdatedAt</c> / <c>CreatedAt</c>,不是全仓通用的 <c>UpdateTime</c></b>。
|
|
|
+ /// 照抄其它 definition 写 <c>UpdateTime</c> 会**静默取到恒 NULL**(JSON 键不存在 →
|
|
|
+ /// <c>JSON_EXTRACT</c> 返回 SQL NULL,不报错)。</para>
|
|
|
+ ///
|
|
|
+ /// <para>🔴 <b>六组新旧列近重复,取错不会得到 NULL 而是「看起来合法的错值」</b>
|
|
|
+ /// (<c>RouteCode</c>/<c>RoutingCode</c>/<c>MaterialCode</c>/<c>RouteName</c> 四列 38805/38806 同值)。
|
|
|
+ /// 真列判据 = CRUD 写入路径:<c>RouteCode</c>(非 <c>RoutingCode</c>)·
|
|
|
+ /// <c>OperationCode</c>(非旧列 <c>Op</c>)· <c>OperationDescription</c>(非 <c>Descr</c>)。</para>
|
|
|
+ ///
|
|
|
+ /// <para><b><c>StdOp</c> 不设父</b>:对 <c>StdOpMaster</c> 孤儿 <b>37112/38746</b>(95.8%),
|
|
|
+ /// <c>AdoS0ReferenceChecker</c> 无 StdOp 校验、CRUD 的 <c>ApplyUpsert</c> 根本不写该列、UI 无下拉,
|
|
|
+ /// 且值域不同构(本表 <c>0010</c>/<c>0020</c> vs <c>StdOpMaster.StdOp</c> 的 <c>1-1/包装</c>,
|
|
|
+ /// 仅 1634/38806 偶然命中)。**它与 item 孤儿是两回事** —— item 孤儿只有 <b>44</b>。</para>
|
|
|
+ ///
|
|
|
+ /// <para>父孤儿 <b>44 行 = ALLOW + WARNING</b>(框架原生:孤儿只告警不阻断):全在租户 797,
|
|
|
+ /// 仅 2 个物料码 <c>A198CABA</c>×18 / <c>918B0238</c>×26,忽略租户后仍是 44
|
|
|
+ /// ⇒ 是被引用主数据**全库整体缺失**,不是跨租户错挂。</para>
|
|
|
+ ///
|
|
|
+ /// <para><b>本轮无法物化的两个业务语义</b>(数据侧问题,非建模问题):
|
|
|
+ /// 「报工工序」—— CRUD 唯一写入的 <c>MilestoneOperation</c> 是 <b>0/38806</b>,
|
|
|
+ /// 数据全在 CRUD 从不写的旧列 <c>MilestoneOp</c>,而后者是**可空 <c>tinyint(1)</c>**(1 行 NULL);
|
|
|
+ /// 「启用状态」—— <c>IsEnabled</c> distinct=1 零信息,<c>IsActive</c> 同为可空 <c>tinyint(1)</c>。
|
|
|
+ /// 可空 tinyint 用 <c>BoolTrue</c> 会滤掉 NULL 行导致行数对账 FAIL、用 <c>Int</c> 会静默写 0,
|
|
|
+ /// 故一律不物化(与 <see cref="Item"/> 排除 <c>DEAControlled</c> 同因)。</para>
|
|
|
+ ///
|
|
|
+ /// <para><c>Ufld2</c>…<c>Ufld9</c>(8 列)· <c>OverlapUnits</c> · <c>PackingQty</c> ·
|
|
|
+ /// <c>ProcessOutDay</c> 全库 distinct=1,零信息,不物化。</para>
|
|
|
+ ///
|
|
|
+ /// <para>租户分布 797=32309 / 824585161322565=4824 / 838257237606469=1398 /
|
|
|
+ /// 838257186181189=156 / 838257212780613=119,故 <c>batch_size</c> 取 <b>100000</b>(3.1× 头寸)。</para>
|
|
|
+ /// </summary>
|
|
|
+ public static readonly S0DimDefinition ItemRoutingOp = new(
|
|
|
+ Key: "ITEM_ROUTING_OP",
|
|
|
+ EntityCode: "S0_ITEM_ROUTING_OP",
|
|
|
+ SourceTable: "RoutingOpDetail",
|
|
|
+ SourceSystem: LocalSourceSystem,
|
|
|
+ StagingTable: "mdp_stg_s0_item_routing_op",
|
|
|
+ DimTable: "bridge_item_routing_op",
|
|
|
+ BusinessKeyColumns: ["tenant_id", "item_code", "operation_code"],
|
|
|
+ SourceBizKeyColumns: ["MaterialCode", "OperationCode"],
|
|
|
+ ParentDimTable: "dim_item",
|
|
|
+ ParentKeyColumns: ["item_code"],
|
|
|
+ Columns:
|
|
|
+ [
|
|
|
+ new("tenant_id", S0DimValueKind.TenantIdColumn),
|
|
|
+ new("item_code", S0DimValueKind.Str, "MaterialCode", Required: true),
|
|
|
+ // 🔴 新列;旧列 Op(int) 是 38805/38806 的近重复,不得取
|
|
|
+ new("operation_code", S0DimValueKind.Str, "OperationCode", Required: true),
|
|
|
+ new("route_code", S0DimValueKind.Str, "RouteCode"), // 非旧列 RoutingCode
|
|
|
+ new("route_name", S0DimValueKind.Str, "RouteName"), // 实存码值而非名称
|
|
|
+ new("operation_description", S0DimValueKind.Str, "OperationDescription"), // 非旧列 Descr
|
|
|
+ new("work_center_code", S0DimValueKind.Str, "WorkCenterCode"),
|
|
|
+ new("std_op", S0DimValueKind.Str, "StdOp"), // NO PARENT,见类型注释
|
|
|
+ // Domain 有 1 行 NULL(RecID=106435 的 TEST 行),故不设 Required,否则整行被滤 → 行数对账 FAIL
|
|
|
+ new("domain_code", S0DimValueKind.Str, "Domain"),
|
|
|
+ new("company_id", S0DimValueKind.Int, "CompanyRefId", Required: true),
|
|
|
+ new("factory_id", S0DimValueKind.Int, "FactoryRefId", Required: true),
|
|
|
+ new("ufld1", S0DimValueKind.Str, "Ufld1"),
|
|
|
+ new("udeci1", S0DimValueKind.Dec, "UDeci1", Precision: 18, Scale: 5),
|
|
|
+ new("udeci2", S0DimValueKind.Dec, "UDeci2", Precision: 18, Scale: 5),
|
|
|
+ new("udeci3", S0DimValueKind.Dec, "UDeci3", Precision: 18, Scale: 5),
|
|
|
+ new("udeci4", S0DimValueKind.Dec, "UDeci4", Precision: 18, Scale: 5),
|
|
|
+ new("udeci5", S0DimValueKind.Dec, "UDeci5", Precision: 18, Scale: 5),
|
|
|
+ // 🔴 源是 (18,6),与 UDeci* 的 (18,5) 不同,不得统一
|
|
|
+ new("run_time", S0DimValueKind.Dec, "RunTime", Precision: 18, Scale: 6),
|
|
|
+ new("outsourced_lead_time", S0DimValueKind.Int, "OutsourcedLeadTime"),
|
|
|
+ new("sort_no", S0DimValueKind.Int, "SortNo"),
|
|
|
+ new("comment_index", S0DimValueKind.Int, "CommentIndex"),
|
|
|
+ new("parent_op", S0DimValueKind.Int, "ParentOp"),
|
|
|
+ // 本表唯一可安全物化的布尔:NOT NULL 且 distinct=2
|
|
|
+ new("is_confirmed", S0DimValueKind.BoolTrue, "IsConfirm"),
|
|
|
+ // 🔴 源列名是 UpdatedAt,不是 UpdateTime
|
|
|
+ new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdatedAt")
|
|
|
+ ]);
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 物料工序生产要素。源 <c>ItemOpCondition</c>,父 = <see cref="Item"/>。
|
|
|
+ ///
|
|
|
+ /// <para><b>身份 = tenant + ItemNum + CodeType</b>(实测重复 0 组)。</para>
|
|
|
+ ///
|
|
|
+ /// <para>🔴 <b><c>IsMain</c> 不入身份 —— 但原因不是框架禁令,而是更下游且无法绕过的一层</b>:
|
|
|
+ /// 源列是 <c>tinyint(1) NOT NULL</c>,在 <c>raw_data</c> 里是 JSON <b>BOOLEAN</b>(非 1/0)。
|
|
|
+ /// 即便放开 <c>Validate()</c> 允许布尔入键仍然会挂 —— staging 的 <c>source_biz_key</c> 由
|
|
|
+ /// <c>mdp_entity.biz_key_expr</c> 驱动、走 C# <c>bool.ToString()</c> 产出 <c>"True"</c>/<c>"False"</c>,
|
|
|
+ /// 而源侧 checksum 渲染的是 <c>'1'</c>/<c>'0'</c>,四向集合比对必然全量不等价。
|
|
|
+ /// 修它要动全实体共享的 <c>MdpStagingWriter.BuildBizKey</c>。
|
|
|
+ /// 旁证:全部 <c>mdp_stg_s0_*</c> 的 <c>source_biz_key</c> 中匹配 <c>True|False</c> 的行数 = <b>0</b>,
|
|
|
+ /// 布尔入键在本仓从未发生过。故 <c>IsMain</c> 落 <b>属性</b>,且**必须用 <c>BoolTrue</c>** ——
|
|
|
+ /// 用 <c>Int</c> 会把 <c>true</c>/<c>false</c> 双双静默压成 0(比 bit(1) 地雷更毒,不报错不为 NULL),
|
|
|
+ /// 用 <c>Str</c> 会得到 <c>'true'</c> 而源侧是 <c>'1'</c>,校验和必挂。</para>
|
|
|
+ ///
|
|
|
+ /// <para><c>CodeType</c> 实测单值 <c>'Prod'</c> 且控制器硬编码写死、无视 DTO 入参,零区分力;
|
|
|
+ /// 保留系防不可逆键收缩(同 <see cref="LinePost"/> 的 <c>job_no</c> 先例),不是因为它有用。</para>
|
|
|
+ ///
|
|
|
+ /// <para>🔴 <c>ItemCode</c>(→ <c>element_code</c>)是**要素编码**,与 <c>ItemNum</c>(物料编码)
|
|
|
+ /// **语义不同**:实测两列相等的行数 = <b>0</b>,且 5/10 为空。任何把它当物料码 join 的代码都是 bug。</para>
|
|
|
+ ///
|
|
|
+ /// <para><c>Op</c> 虽 10/10 填充且 <c>(ItemNum,Op)→RoutingOpDetail</c> 零孤儿,但不在源 UK 内、
|
|
|
+ /// DTO 无 <c>[Required]</c> ⇒ 不入键。**不要因为表名叫 ItemOpCondition 就把 Op 塞进身份。**</para>
|
|
|
+ ///
|
|
|
+ /// <para>⚠️ dim 唯一键严格弱于源 UK(源含 <c>Domain</c> 与 <c>IsMain</c>),且两个控制器
|
|
|
+ /// 均无查重守卫,业务**可以**合法插出同 <c>(tenant,ItemNum,'Prod')</c> 的 IsMain=0/1 两行。
|
|
|
+ /// 届时 <c>AssertNoSourceDuplicateAsync</c>(在 purge **之前**)会显式失败、零数据被触碰 ——
|
|
|
+ /// 失败可见、可定位、不静默。当前实测 0 例。</para>
|
|
|
+ /// </summary>
|
|
|
+ public static readonly S0DimDefinition ItemOpCondition = new(
|
|
|
+ Key: "ITEM_OP_CONDITION",
|
|
|
+ EntityCode: "S0_ITEM_OP_CONDITION",
|
|
|
+ SourceTable: "ItemOpCondition",
|
|
|
+ SourceSystem: LocalSourceSystem,
|
|
|
+ StagingTable: "mdp_stg_s0_item_op_condition",
|
|
|
+ DimTable: "bridge_item_op_condition",
|
|
|
+ BusinessKeyColumns: ["tenant_id", "item_code", "code_type"],
|
|
|
+ SourceBizKeyColumns: ["ItemNum", "CodeType"],
|
|
|
+ ParentDimTable: "dim_item",
|
|
|
+ ParentKeyColumns: ["item_code"],
|
|
|
+ Columns:
|
|
|
+ [
|
|
|
+ new("tenant_id", S0DimValueKind.TenantIdColumn),
|
|
|
+ new("item_code", S0DimValueKind.Str, "ItemNum", Required: true),
|
|
|
+ new("code_type", S0DimValueKind.Str, "CodeType", Required: true),
|
|
|
+ // 🔴 必须 BoolTrue:Int 会把 true/false 双双静默压成 0,Str 会得到 'true' 而源侧是 '1'
|
|
|
+ new("is_main", S0DimValueKind.BoolTrue, "IsMain"),
|
|
|
+ new("op_code", S0DimValueKind.Str, "Op"),
|
|
|
+ new("line_code", S0DimValueKind.Str, "Line"),
|
|
|
+ new("typed_code", S0DimValueKind.Str, "Typed"),
|
|
|
+ // 🔴 要素编码,**不是** ItemNum(实测两列 0 行相等)
|
|
|
+ new("element_code", S0DimValueKind.Str, "ItemCode"),
|
|
|
+ new("descr", S0DimValueKind.Str, "Descr"),
|
|
|
+ new("domain_code", S0DimValueKind.Str, "Domain"),
|
|
|
+ new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
|
|
|
+ ]);
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 物料工序 SOP 文件。源 <c>QualitySegmentImage</c>,父 = <see cref="Item"/>。
|
|
|
+ ///
|
|
|
+ /// <para><b>身份 = tenant + item_num + op + image_type_id</b>(实测重复 0 组)。
|
|
|
+ /// 源 UNIQUE 还含 <c>domain</c>,但它跨租户口径异构(797 用组织雪花号 <c>797403761152069</c>、
|
|
|
+ /// 838257186181189 用 legacy <c>8010</c>/<c>2410</c>),坐实 LEGACY_SCOPE,不入身份。</para>
|
|
|
+ ///
|
|
|
+ /// <para>🔴 <b>源表没有任何时间列</b>(11 列全清单实测),故**不声明 <c>source_updated_at</c>** ——
|
|
|
+ /// 与 <see cref="SopFileType"/> / <see cref="LabelType"/> 同型。**不得为字段齐整而回填。**</para>
|
|
|
+ ///
|
|
|
+ /// <para>⚠️ 源表那个叫 <c>uk_QualitySegmentImage_scope</c> 的索引**不是唯一索引**
|
|
|
+ /// (<c>IsUnique=false</c>);真正的唯一键是 <c>uk_QualitySegmentImage_scope_unique</c>。**别被名字骗了。**</para>
|
|
|
+ ///
|
|
|
+ /// <para><c>image_type_id</c> 软引用 <see cref="SopFileType"/> 的 <c>file_type_code</c>(实测 0 孤儿),
|
|
|
+ /// 但**不设父** —— 框架只能挂一个父,而本桥的主语是「某物料某工序的 SOP 文件」,
|
|
|
+ /// <c>image_type_id</c> 只是该文件的分类(闭集字典)。挂 <c>dim_item</c> 才让
|
|
|
+ /// 「S0 ITEM AUTHORITY = dim_item」在一处可强制、可审计;且 <c>ref_sop_file_type</c>
|
|
|
+ /// 当前只物化了租户 797,选它作父会让别的租户产出纯刷新顺序噪声的 WARN。
|
|
|
+ /// 同 <see cref="LinePostSkill"/> 的 <c>skill_code</c> 先例。</para>
|
|
|
+ ///
|
|
|
+ /// <para>⚠️ <c>item_num</c> / <c>op</c> / <c>image_type_id</c> 在源 DDL 里**均可空**,
|
|
|
+ /// 且 upsert DTO 只有 <c>Domain</c> 带 <c>[Required]</c> ⇒ 业务可合法建 NULL 行 →
|
|
|
+ /// 被 <c>RowGuards</c> 整行过滤 → <c>dim_count < stg_count</c> → 阻断 FAIL。
|
|
|
+ /// 与 <see cref="EmpWorkDuty"/> 同风险类。当前实测 0 NULL / 0 空串。</para>
|
|
|
+ /// </summary>
|
|
|
+ public static readonly S0DimDefinition ItemOpImage = new(
|
|
|
+ Key: "ITEM_OP_IMAGE",
|
|
|
+ EntityCode: "S0_ITEM_OP_IMAGE",
|
|
|
+ SourceTable: "QualitySegmentImage",
|
|
|
+ SourceSystem: LocalSourceSystem,
|
|
|
+ StagingTable: "mdp_stg_s0_item_op_image",
|
|
|
+ DimTable: "bridge_item_op_image",
|
|
|
+ BusinessKeyColumns: ["tenant_id", "item_code", "op_code", "file_type_code"],
|
|
|
+ SourceBizKeyColumns: ["item_num", "op", "image_type_id"],
|
|
|
+ ParentDimTable: "dim_item",
|
|
|
+ ParentKeyColumns: ["item_code"],
|
|
|
+ Columns:
|
|
|
+ [
|
|
|
+ new("tenant_id", S0DimValueKind.TenantIdColumn),
|
|
|
+ new("item_code", S0DimValueKind.Str, "item_num", Required: true),
|
|
|
+ new("op_code", S0DimValueKind.Str, "op", Required: true),
|
|
|
+ new("file_type_code", S0DimValueKind.Str, "image_type_id", Required: true),
|
|
|
+ new("line_code", S0DimValueKind.Str, "line"),
|
|
|
+ new("file_path", S0DimValueKind.Str, "path"),
|
|
|
+ new("descr", S0DimValueKind.Str, "descr"),
|
|
|
+ new("rev", S0DimValueKind.Str, "rev"),
|
|
|
+ // 源是真 int(非 tinyint(1)),走 Int 安全
|
|
|
+ new("sort_id", S0DimValueKind.Int, "sort_id"),
|
|
|
+ new("domain_code", S0DimValueKind.Str, "domain")
|
|
|
+ // 🔴 刻意无 source_updated_at:源表零时间列,不得回填
|
|
|
+ ]);
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// BOM 结构(父件-子件用量)。源 <c>ProductStructureMaster</c>(62036 行,S0 最大的表),
|
|
|
+ /// 父 = <see cref="Item"/>。
|
|
|
+ ///
|
|
|
+ /// <para><b>身份 = tenant + ParentItem + ComponentItem</b>(实测重复 0 组)。
|
|
|
+ /// <c>LineOp</c> / <c>Op</c> / <c>SequenceNum</c> 加进去**零增益**(仍是 0 组),故不入身份;
|
|
|
+ /// 去掉 <c>ComponentItem</c> 则塌陷成 <b>4341 组 / 60494 行</b>重复。
|
|
|
+ /// 旧 UK 含 <c>CompanyRefId</c>+<c>FactoryRefId</c>,不予继承 —— 身份不加它们已然唯一(D-025)。</para>
|
|
|
+ ///
|
|
|
+ /// <para>🔴🔴 <b>legacy 代理键 <c>ComponentMaterialId</c> / <c>ParentMaterialId</c> 是已实证的跨租户泄漏向量</b>
|
|
|
+ /// (2026-09-08 实测):
|
|
|
+ /// <list type="bullet">
|
|
|
+ /// <item>同租户解析 <c>ComponentMaterialId → ItemMaster.RecID</c>:<b>170 行解析失败</b>;</item>
|
|
|
+ /// <item>忽略租户解析:<b>0 行失败</b> —— 看起来「完美」;</item>
|
|
|
+ /// <item>而这 <b>170 行恰恰全部挂到了别的租户的 <c>RecID</c> 上</b>。</item>
|
|
|
+ /// </list>
|
|
|
+ /// <c>ParentMaterialId</c> 同款,量更大(同租户失败 <b>2060</b>、忽略租户 0)。
|
|
|
+ /// 也就是说:**去掉租户条件不会报错,只会静默串租户**。
|
|
|
+ /// 而同租户能解析的 61866 行里,代理键解出的码与 <c>ComponentItem</c> <b>61866/61866 完全一致</b>
|
|
|
+ /// ⇒ **自然键完全可靠,代理键只带来风险**。故本桥一律走自然键
|
|
|
+ /// <c>item_code</c> / <c>component_item_code</c>,两个 id 仅作 legacy 技术属性留痕,
|
|
|
+ /// **任何消费方解析它们时必须带 <c>tenant_id</c>**。</para>
|
|
|
+ ///
|
|
|
+ /// <para>父连接:<c>ParentItem</c> 与 <c>ComponentItem</c> 对 <c>ItemMaster</c> 的孤儿
|
|
|
+ /// **同租户与忽略租户都是 0/62036**。框架单父,取 <c>ParentItem</c> 作 <c>item_code</c>
|
|
|
+ /// (下游「查某物料的 BOM」天然按父件过滤);<c>ComponentItem</c> 落
|
|
|
+ /// <c>component_item_code</c>,同样零孤儿但不设父。</para>
|
|
|
+ ///
|
|
|
+ /// <para>🔴 <c>IsActive</c> 是 <b><c>bit(1)</c> 且含 1 行 NULL</b> —— 双重地雷,**不物化**:
|
|
|
+ /// 用 <see cref="S0DimValueKind.Int"/> 会让对账每轮必挂(<c>CAST(b'1' AS CHAR)</c> 是裸字节),
|
|
|
+ /// 用 <see cref="S0DimValueKind.BoolTrue"/> 会把那 1 行 NULL 整行滤掉 → 行数对账 FAIL。
|
|
|
+ /// 同理 <c>IsEnabled</c>(distinct=1 零信息)、<c>SequenceNum</c>(distinct=1)、
|
|
|
+ /// <c>StructureType</c>(7.2% 填充但 distinct=1)、<c>LineOp</c>/<c>Remark</c>/<c>StartEff</c>/<c>EndEff</c>
|
|
|
+ /// (全库 0 填充)一并不物化。</para>
|
|
|
+ ///
|
|
|
+ /// <para>⚠️ 单租户最大 <b>57199</b> 行(797),<c>batch_size</c> 取 <b>200000</b>(3.5× 头寸)。</para>
|
|
|
+ /// </summary>
|
|
|
+ public static readonly S0DimDefinition ItemBom = new(
|
|
|
+ Key: "ITEM_BOM",
|
|
|
+ EntityCode: "S0_ITEM_BOM",
|
|
|
+ SourceTable: "ProductStructureMaster",
|
|
|
+ SourceSystem: LocalSourceSystem,
|
|
|
+ StagingTable: "mdp_stg_s0_item_bom",
|
|
|
+ DimTable: "bridge_item_bom",
|
|
|
+ BusinessKeyColumns: ["tenant_id", "item_code", "component_item_code"],
|
|
|
+ SourceBizKeyColumns: ["ParentItem", "ComponentItem"],
|
|
|
+ ParentDimTable: "dim_item",
|
|
|
+ ParentKeyColumns: ["item_code"],
|
|
|
+ Columns:
|
|
|
+ [
|
|
|
+ new("tenant_id", S0DimValueKind.TenantIdColumn),
|
|
|
+ new("item_code", S0DimValueKind.Str, "ParentItem", Required: true),
|
|
|
+ new("component_item_code", S0DimValueKind.Str, "ComponentItem", Required: true),
|
|
|
+ new("qty", S0DimValueKind.Dec, "Qty", Precision: 18, Scale: 5),
|
|
|
+ new("unit", S0DimValueKind.Str, "UM"),
|
|
|
+ new("scrap", S0DimValueKind.Dec, "Scrap", Precision: 18, Scale: 5),
|
|
|
+ new("qty_consumed", S0DimValueKind.Dec, "QtyConsumed", Precision: 18, Scale: 5),
|
|
|
+ new("qty_exchanged", S0DimValueKind.Dec, "QtyExchd", Precision: 18, Scale: 5),
|
|
|
+ new("refs", S0DimValueKind.Str, "Refs"),
|
|
|
+ new("operation_no", S0DimValueKind.Int, "Op"),
|
|
|
+ new("company_id", S0DimValueKind.Int, "CompanyRefId", Required: true),
|
|
|
+ new("factory_id", S0DimValueKind.Int, "FactoryRefId", Required: true),
|
|
|
+ // 🔴 legacy 技术属性,解析时**必须带 tenant_id**(见类型注释的跨租户实证)
|
|
|
+ new("legacy_parent_item_id", S0DimValueKind.Int, "ParentMaterialId", Required: true),
|
|
|
+ new("legacy_component_item_id", S0DimValueKind.Int, "ComponentMaterialId", Required: true),
|
|
|
+ new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdatedAt")
|
|
|
+ ]);
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 替代料。源 <c>ItemSubstituteDetail</c>,父 = <see cref="Item"/>。
|
|
|
+ ///
|
|
|
+ /// <para><b>身份 = tenant + ParentItem + ItemNum + SubstituteItem</b>(实测重复 0 组)。
|
|
|
+ /// 虽然 <c>tenant+ItemNum+SubstituteItem</c> 当前也是 0 组,但全表仅 <b>16 行</b>,
|
|
|
+ /// 这点样本证明不了 <c>ParentItem</c> 可去;而实测 <c>ParentItem</c> 与 <c>ItemNum</c>
|
|
|
+ /// <b>16 行无一相等</b>(distinct 9 vs 14),是**真实不同的信息**
|
|
|
+ /// —— 去掉即不可逆的键收缩,故保留(同 <see cref="LinePost"/> 的 <c>job_no</c> 先例)。
|
|
|
+ /// <c>Domain</c> 在旧 UK 内,按 D-025 落 LEGACY_SCOPE 不入身份。</para>
|
|
|
+ ///
|
|
|
+ /// <para>三个 item 引用(<c>ParentItem</c> / <c>ItemNum</c> / <c>SubstituteItem</c>)
|
|
|
+ /// **全部统一到 <c>dim_item</c> 的自然键 <c>tenant + item_code</c>**,实测孤儿各 0/16。
|
|
|
+ /// 框架单父,取被替代的 <c>ItemNum</c> 作 <c>item_code</c>(本行的主语是「这个料有哪些替代」);
|
|
|
+ /// 另两个落 <c>parent_item_code</c> / <c>substitute_item_code</c>,同样零孤儿但不设父。</para>
|
|
|
+ /// </summary>
|
|
|
+ public static readonly S0DimDefinition ItemSubstitute = new(
|
|
|
+ Key: "ITEM_SUBSTITUTE",
|
|
|
+ EntityCode: "S0_ITEM_SUBSTITUTE",
|
|
|
+ SourceTable: "ItemSubstituteDetail",
|
|
|
+ SourceSystem: LocalSourceSystem,
|
|
|
+ StagingTable: "mdp_stg_s0_item_substitute",
|
|
|
+ DimTable: "bridge_item_substitute",
|
|
|
+ BusinessKeyColumns: ["tenant_id", "parent_item_code", "item_code", "substitute_item_code"],
|
|
|
+ SourceBizKeyColumns: ["ParentItem", "ItemNum", "SubstituteItem"],
|
|
|
+ ParentDimTable: "dim_item",
|
|
|
+ ParentKeyColumns: ["item_code"],
|
|
|
+ Columns:
|
|
|
+ [
|
|
|
+ new("tenant_id", S0DimValueKind.TenantIdColumn),
|
|
|
+ new("parent_item_code", S0DimValueKind.Str, "ParentItem", Required: true),
|
|
|
+ new("item_code", S0DimValueKind.Str, "ItemNum", Required: true),
|
|
|
+ new("substitute_item_code", S0DimValueKind.Str, "SubstituteItem", Required: true),
|
|
|
+ new("substitute_qty", S0DimValueKind.Dec, "SubstituteQty", Precision: 18, Scale: 6),
|
|
|
+ new("substitute_type", S0DimValueKind.Str, "SubstituteType"),
|
|
|
+ new("remark", S0DimValueKind.Str, "Remark"),
|
|
|
+ new("domain_code", S0DimValueKind.Str, "Domain", Required: true),
|
|
|
+ new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
|
|
|
+ ]);
|
|
|
+
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
// C3 · 关系 / 明细桥接(bridge_*)
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
@@ -872,7 +1240,13 @@ public static class S0DimCatalog
|
|
|
// C1 扩展:物料(S0 Item 权威;后续 6 张 C3 都要挂它作父,故必须排在 C3 之前)
|
|
|
Item,
|
|
|
// C3 关系桥(EmpWorkDuty 的父 dim_employee 在前;LinePostSkill 的父 LinePost 紧邻其前)
|
|
|
- EmpWorkDuty, LinePost, LinePostSkill
|
|
|
+ EmpWorkDuty, LinePost, LinePostSkill,
|
|
|
+ // C3-A 物料桥(父 dim_item,必须排在 Item 之后)
|
|
|
+ ItemOpLine, ItemRoutingOp,
|
|
|
+ // C3-C 物料桥(父 dim_item)
|
|
|
+ ItemOpCondition, ItemOpImage,
|
|
|
+ // C3-B 物料桥(父 dim_item)
|
|
|
+ ItemBom, ItemSubstitute
|
|
|
];
|
|
|
|
|
|
/// <summary>按 Key 取维度(大小写不敏感);不存在返回 null。</summary>
|