S0DimCatalog.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. namespace Admin.NET.Plugin.AiDOP.DataPlatform.S0Dim;
  2. /// <summary>
  3. /// S0 主数据维度清单 —— **本模块唯一的业务知识落点**。
  4. ///
  5. /// 新增一个维度 = 在此追加一个 <see cref="S0DimDefinition"/>,
  6. /// 不得在 Materializer / Reconciler 中出现按 Key 分支的逻辑。
  7. ///
  8. /// 🔴 JsonPath 一律是**源表物理列名**,不是实体属性名。改动前先核对 <c>SHOW CREATE TABLE</c>。
  9. /// </summary>
  10. public static class S0DimCatalog
  11. {
  12. /// <summary>本库样板源(<c>mdp_source.source_code</c>),入站为只读 SELECT。</summary>
  13. public const string LocalSourceSystem = "AIDOPDEV_MYSQL";
  14. /// <summary>
  15. /// 工作中心。源 <c>WorkCtrMaster</c>(无 company/factory 列,故 dim 也不设 —— 不为统一而发明数据)。
  16. /// </summary>
  17. public static readonly S0DimDefinition WorkCenter = new(
  18. Key: "WORK_CENTER",
  19. EntityCode: "S0_WORK_CENTER",
  20. SourceTable: "WorkCtrMaster",
  21. SourceSystem: LocalSourceSystem,
  22. StagingTable: "mdp_stg_s0_work_center",
  23. DimTable: "dim_work_center",
  24. BusinessKeyColumns: ["tenant_id", "domain_code", "work_center_code"],
  25. SourceBizKeyColumns: ["Domain", "WorkCtr"],
  26. Columns:
  27. [
  28. new("tenant_id", S0DimValueKind.TenantIdColumn),
  29. new("domain_code", S0DimValueKind.Str, "Domain", Required: true),
  30. new("work_center_code", S0DimValueKind.Str, "WorkCtr", Required: true),
  31. new("work_center_name", S0DimValueKind.Str, "Descr"),
  32. new("department_code", S0DimValueKind.Str, "Department"),
  33. new("is_active", S0DimValueKind.BoolTrue, "IsActive"),
  34. new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
  35. ]);
  36. /// <summary>
  37. /// 部门。源 <c>DepartmentMaster</c>。
  38. /// company/factory 仅作属性透传:实测同一 (tenant, Domain) 下 factory 同时存在 legacy 数值与 SysOrg 雪花两种值,
  39. /// 与身份正交,本阶段不做组织域治理。
  40. /// </summary>
  41. public static readonly S0DimDefinition Department = new(
  42. Key: "DEPARTMENT",
  43. EntityCode: "S0_DEPARTMENT",
  44. SourceTable: "DepartmentMaster",
  45. SourceSystem: LocalSourceSystem,
  46. StagingTable: "mdp_stg_s0_department",
  47. DimTable: "dim_department",
  48. BusinessKeyColumns: ["tenant_id", "domain_code", "department_code"],
  49. SourceBizKeyColumns: ["Domain", "Department"],
  50. Columns:
  51. [
  52. new("tenant_id", S0DimValueKind.TenantIdColumn),
  53. new("company_id", S0DimValueKind.Int, "company_ref_id", Required: true),
  54. new("factory_id", S0DimValueKind.Int, "factory_ref_id", Required: true),
  55. new("domain_code", S0DimValueKind.Str, "Domain", Required: true),
  56. new("department_code", S0DimValueKind.Str, "Department", Required: true),
  57. new("department_name", S0DimValueKind.Str, "Descr"),
  58. new("is_active", S0DimValueKind.BoolTrue, "IsActive"),
  59. // 源侧 238/239 非空:UATDEMO 的「未分配」行 UpdateTime 恒 NULL。
  60. // 本 definition 走 FULL(mdp_entity.incr_column = NULL),不生成 "UpdateTime > cursor" 谓词,
  61. // 因此该行每轮都能进 stg 与 dim;切 INCR 前必须先落 keyset NullTimePhase 两阶段。
  62. new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
  63. ]);
  64. /// <summary>
  65. /// 库位。源 <c>LocationMaster</c> —— **dual-column 复刻表**。
  66. ///
  67. /// 🔴 以下 JsonPath 全部是 PascalCase 真列,同表存在同名 snake_case 影子列且**恒空/已发散**,
  68. /// 取错即静默得到空值:
  69. /// <c>Domain</c>(真) vs <c>domain_code</c>(132 行全空串) ·
  70. /// <c>IsActive</c>(真) vs <c>is_active</c>(132 行中仅 1 行与真列相等) ·
  71. /// <c>PhysicalAddress</c>(42 非空) vs <c>physical_address</c>(0 非空) ·
  72. /// <c>CreateUser</c>(119 非空) vs <c>create_user</c>(0 非空) ·
  73. /// <c>RecID</c>(真主键) vs <c>rec_id</c>(恒 0)。
  74. ///
  75. /// 身份取 (tenant, domain_code, location_code):与子表父引用一致,且在源侧索引口径变化时仍成立;
  76. /// 另用 <see cref="S0DimDefinition.MirrorUniqueColumns"/> 镜像当前源 UK (tenant_id, location)。
  77. /// </summary>
  78. public static readonly S0DimDefinition Location = new(
  79. Key: "LOCATION",
  80. EntityCode: "S0_LOCATION",
  81. SourceTable: "LocationMaster",
  82. SourceSystem: LocalSourceSystem,
  83. StagingTable: "mdp_stg_s0_location",
  84. DimTable: "dim_location",
  85. BusinessKeyColumns: ["tenant_id", "domain_code", "location_code"],
  86. SourceBizKeyColumns: ["Domain", "location"],
  87. MirrorUniqueColumns: ["tenant_id", "location_code"],
  88. Columns:
  89. [
  90. new("tenant_id", S0DimValueKind.TenantIdColumn),
  91. new("company_id", S0DimValueKind.Int, "company_ref_id", Required: true),
  92. new("factory_id", S0DimValueKind.Int, "factory_ref_id", Required: true),
  93. new("domain_code", S0DimValueKind.Str, "Domain", Required: true),
  94. new("location_code", S0DimValueKind.Str, "location", Required: true),
  95. new("location_name", S0DimValueKind.Str, "descr"),
  96. new("location_type", S0DimValueKind.Str, "typed"),
  97. new("storer", S0DimValueKind.Str, "storer"),
  98. new("physical_address", S0DimValueKind.Str, "PhysicalAddress"),
  99. new("is_active", S0DimValueKind.BoolTrue, "IsActive"),
  100. new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime")
  101. ]);
  102. /// <summary>
  103. /// 货架(库位子维度)。源 <c>LocationShelfMaster</c>。
  104. ///
  105. /// 与 <see cref="Location"/> 共用 <c>mdp_stg_s0_location</c>,靠 <c>source_table</c> 隔离,
  106. /// 两者是**两个独立 definition**,绝不合并成一次转换。
  107. ///
  108. /// 本表**全列 snake_case,没有影子列** —— 不可套用 LocationMaster 的禁用规则。
  109. /// 另:源 <c>update_time</c> 0/30 非空(无水位)、主键 <c>rec_id</c> 不在通用行标识候选列表中
  110. /// 且每次父表 FULL Replace 都会变,故物化只认 <c>sync_batch_id</c>,不依赖 source_row_id 稳定性。
  111. /// 无 is_active —— 源表没有该列,不发明。
  112. /// </summary>
  113. public static readonly S0DimDefinition LocationShelf = new(
  114. Key: "LOCATION_SHELF",
  115. EntityCode: "S0_LOCATION_SHELF",
  116. SourceTable: "LocationShelfMaster",
  117. SourceSystem: LocalSourceSystem,
  118. StagingTable: "mdp_stg_s0_location",
  119. DimTable: "dim_location_shelf",
  120. BusinessKeyColumns: ["tenant_id", "domain_code", "location_code", "shelf_code"],
  121. SourceBizKeyColumns: ["domain_code", "location", "inv_shelf"],
  122. ParentDimTable: "dim_location",
  123. ParentKeyColumns: ["domain_code", "location_code"],
  124. Columns:
  125. [
  126. new("tenant_id", S0DimValueKind.TenantIdColumn),
  127. new("company_id", S0DimValueKind.Int, "company_ref_id", Required: true),
  128. new("factory_id", S0DimValueKind.Int, "factory_ref_id", Required: true),
  129. new("domain_code", S0DimValueKind.Str, "domain_code", Required: true),
  130. new("location_code", S0DimValueKind.Str, "location", Required: true),
  131. new("shelf_code", S0DimValueKind.Str, "inv_shelf", Required: true),
  132. new("shelf_name", S0DimValueKind.Str, "descr"),
  133. new("area", S0DimValueKind.Str, "area"),
  134. // 源恒 NULL(0/30),保留列以便源将来启用;不得为了“看起来有值”而回填
  135. new("source_updated_at", S0DimValueKind.DateTimeSec, "update_time")
  136. ]);
  137. /// <summary>全部维度,按依赖顺序(父在前,子在后 —— 孤儿判定需要父维度先就绪)。</summary>
  138. public static readonly IReadOnlyList<S0DimDefinition> All = [WorkCenter, Department, Location, LocationShelf];
  139. /// <summary>按 Key 取维度(大小写不敏感);不存在返回 null。</summary>
  140. public static S0DimDefinition? Find(string? key) =>
  141. string.IsNullOrWhiteSpace(key)
  142. ? null
  143. : All.FirstOrDefault(d => string.Equals(d.Key, key.Trim(), StringComparison.OrdinalIgnoreCase));
  144. /// <summary>校验全部声明;任一不合法即抛出(供启动自检与单测使用)。</summary>
  145. public static void ValidateAll()
  146. {
  147. foreach (var d in All) d.Validate();
  148. var dupDim = All.GroupBy(d => d.DimTable, StringComparer.Ordinal).FirstOrDefault(g => g.Count() > 1);
  149. if (dupDim is not null)
  150. throw new InvalidOperationException(
  151. $"dim 表被多个 definition 共用:{dupDim.Key} —— FULL Replace 按 tenant 整表删除,会互相清空");
  152. var dupEntity = All.GroupBy(d => d.EntityCode, StringComparer.Ordinal).FirstOrDefault(g => g.Count() > 1);
  153. if (dupEntity is not null)
  154. throw new InvalidOperationException($"entity_code 重复:{dupEntity.Key}");
  155. // 同一 staging 被多个 definition 共用时,source_table 必须互异,否则 purge/物化会互相污染
  156. foreach (var g in All.GroupBy(d => d.StagingTable, StringComparer.Ordinal).Where(g => g.Count() > 1))
  157. {
  158. var tables = g.Select(d => d.SourceTable).ToList();
  159. if (tables.Distinct(StringComparer.Ordinal).Count() != tables.Count)
  160. throw new InvalidOperationException($"staging {g.Key} 被多个 definition 共用且 source_table 重复");
  161. }
  162. }
  163. }