namespace Admin.NET.Plugin.AiDOP.DataPlatform.S0Dim; /// /// S0 主数据维度清单 —— **本模块唯一的业务知识落点**。 /// /// 新增一个维度 = 在此追加一个 , /// 不得在 Materializer / Reconciler 中出现按 Key 分支的逻辑。 /// /// 🔴 JsonPath 一律是**源表物理列名**,不是实体属性名。改动前先核对 SHOW CREATE TABLE。 /// public static class S0DimCatalog { /// 本库样板源(mdp_source.source_code),入站为只读 SELECT。 public const string LocalSourceSystem = "AIDOPDEV_MYSQL"; /// /// 工作中心。源 WorkCtrMaster(无 company/factory 列,故 dim 也不设 —— 不为统一而发明数据)。 /// public static readonly S0DimDefinition WorkCenter = new( Key: "WORK_CENTER", EntityCode: "S0_WORK_CENTER", SourceTable: "WorkCtrMaster", SourceSystem: LocalSourceSystem, StagingTable: "mdp_stg_s0_work_center", DimTable: "dim_work_center", BusinessKeyColumns: ["tenant_id", "domain_code", "work_center_code"], SourceBizKeyColumns: ["Domain", "WorkCtr"], Columns: [ new("tenant_id", S0DimValueKind.TenantIdColumn), new("domain_code", S0DimValueKind.Str, "Domain", Required: true), new("work_center_code", S0DimValueKind.Str, "WorkCtr", Required: true), new("work_center_name", S0DimValueKind.Str, "Descr"), new("department_code", S0DimValueKind.Str, "Department"), new("is_active", S0DimValueKind.BoolTrue, "IsActive"), new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime") ]); /// /// 部门。源 DepartmentMaster。 /// company/factory 仅作属性透传:实测同一 (tenant, Domain) 下 factory 同时存在 legacy 数值与 SysOrg 雪花两种值, /// 与身份正交,本阶段不做组织域治理。 /// public static readonly S0DimDefinition Department = new( Key: "DEPARTMENT", EntityCode: "S0_DEPARTMENT", SourceTable: "DepartmentMaster", SourceSystem: LocalSourceSystem, StagingTable: "mdp_stg_s0_department", DimTable: "dim_department", BusinessKeyColumns: ["tenant_id", "domain_code", "department_code"], SourceBizKeyColumns: ["Domain", "Department"], Columns: [ new("tenant_id", S0DimValueKind.TenantIdColumn), new("company_id", S0DimValueKind.Int, "company_ref_id", Required: true), new("factory_id", S0DimValueKind.Int, "factory_ref_id", Required: true), new("domain_code", S0DimValueKind.Str, "Domain", Required: true), new("department_code", S0DimValueKind.Str, "Department", Required: true), new("department_name", S0DimValueKind.Str, "Descr"), new("is_active", S0DimValueKind.BoolTrue, "IsActive"), // 源侧 238/239 非空:UATDEMO 的「未分配」行 UpdateTime 恒 NULL。 // 本 definition 走 FULL(mdp_entity.incr_column = NULL),不生成 "UpdateTime > cursor" 谓词, // 因此该行每轮都能进 stg 与 dim;切 INCR 前必须先落 keyset NullTimePhase 两阶段。 new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime") ]); /// /// 库位。源 LocationMaster —— **dual-column 复刻表**。 /// /// 🔴 以下 JsonPath 全部是 PascalCase 真列,同表存在同名 snake_case 影子列且**恒空/已发散**, /// 取错即静默得到空值: /// Domain(真) vs domain_code(132 行全空串) · /// IsActive(真) vs is_active(132 行中仅 1 行与真列相等) · /// PhysicalAddress(42 非空) vs physical_address(0 非空) · /// CreateUser(119 非空) vs create_user(0 非空) · /// RecID(真主键) vs rec_id(恒 0)。 /// /// 身份取 (tenant, domain_code, location_code):与子表父引用一致,且在源侧索引口径变化时仍成立; /// 另用 镜像当前源 UK (tenant_id, location)。 /// public static readonly S0DimDefinition Location = new( Key: "LOCATION", EntityCode: "S0_LOCATION", SourceTable: "LocationMaster", SourceSystem: LocalSourceSystem, StagingTable: "mdp_stg_s0_location", DimTable: "dim_location", BusinessKeyColumns: ["tenant_id", "domain_code", "location_code"], SourceBizKeyColumns: ["Domain", "location"], MirrorUniqueColumns: ["tenant_id", "location_code"], Columns: [ new("tenant_id", S0DimValueKind.TenantIdColumn), new("company_id", S0DimValueKind.Int, "company_ref_id", Required: true), new("factory_id", S0DimValueKind.Int, "factory_ref_id", Required: true), new("domain_code", S0DimValueKind.Str, "Domain", Required: true), new("location_code", S0DimValueKind.Str, "location", Required: true), new("location_name", S0DimValueKind.Str, "descr"), new("location_type", S0DimValueKind.Str, "typed"), new("storer", S0DimValueKind.Str, "storer"), new("physical_address", S0DimValueKind.Str, "PhysicalAddress"), new("is_active", S0DimValueKind.BoolTrue, "IsActive"), new("source_updated_at", S0DimValueKind.DateTimeSec, "UpdateTime") ]); /// /// 货架(库位子维度)。源 LocationShelfMaster。 /// /// 与 共用 mdp_stg_s0_location,靠 source_table 隔离, /// 两者是**两个独立 definition**,绝不合并成一次转换。 /// /// 本表**全列 snake_case,没有影子列** —— 不可套用 LocationMaster 的禁用规则。 /// 另:源 update_time 0/30 非空(无水位)、主键 rec_id 不在通用行标识候选列表中 /// 且每次父表 FULL Replace 都会变,故物化只认 sync_batch_id,不依赖 source_row_id 稳定性。 /// 无 is_active —— 源表没有该列,不发明。 /// public static readonly S0DimDefinition LocationShelf = new( Key: "LOCATION_SHELF", EntityCode: "S0_LOCATION_SHELF", SourceTable: "LocationShelfMaster", SourceSystem: LocalSourceSystem, StagingTable: "mdp_stg_s0_location", DimTable: "dim_location_shelf", BusinessKeyColumns: ["tenant_id", "domain_code", "location_code", "shelf_code"], SourceBizKeyColumns: ["domain_code", "location", "inv_shelf"], ParentDimTable: "dim_location", ParentKeyColumns: ["domain_code", "location_code"], Columns: [ new("tenant_id", S0DimValueKind.TenantIdColumn), new("company_id", S0DimValueKind.Int, "company_ref_id", Required: true), new("factory_id", S0DimValueKind.Int, "factory_ref_id", Required: true), new("domain_code", S0DimValueKind.Str, "domain_code", Required: true), new("location_code", S0DimValueKind.Str, "location", Required: true), new("shelf_code", S0DimValueKind.Str, "inv_shelf", Required: true), new("shelf_name", S0DimValueKind.Str, "descr"), new("area", S0DimValueKind.Str, "area"), // 源恒 NULL(0/30),保留列以便源将来启用;不得为了“看起来有值”而回填 new("source_updated_at", S0DimValueKind.DateTimeSec, "update_time") ]); /// 全部维度,按依赖顺序(父在前,子在后 —— 孤儿判定需要父维度先就绪)。 public static readonly IReadOnlyList All = [WorkCenter, Department, Location, LocationShelf]; /// 按 Key 取维度(大小写不敏感);不存在返回 null。 public static S0DimDefinition? Find(string? key) => string.IsNullOrWhiteSpace(key) ? null : All.FirstOrDefault(d => string.Equals(d.Key, key.Trim(), StringComparison.OrdinalIgnoreCase)); /// 校验全部声明;任一不合法即抛出(供启动自检与单测使用)。 public static void ValidateAll() { foreach (var d in All) d.Validate(); var dupDim = All.GroupBy(d => d.DimTable, StringComparer.Ordinal).FirstOrDefault(g => g.Count() > 1); if (dupDim is not null) throw new InvalidOperationException( $"dim 表被多个 definition 共用:{dupDim.Key} —— FULL Replace 按 tenant 整表删除,会互相清空"); var dupEntity = All.GroupBy(d => d.EntityCode, StringComparer.Ordinal).FirstOrDefault(g => g.Count() > 1); if (dupEntity is not null) throw new InvalidOperationException($"entity_code 重复:{dupEntity.Key}"); // 同一 staging 被多个 definition 共用时,source_table 必须互异,否则 purge/物化会互相污染 foreach (var g in All.GroupBy(d => d.StagingTable, StringComparer.Ordinal).Where(g => g.Count() > 1)) { var tables = g.Select(d => d.SourceTable).ToList(); if (tables.Distinct(StringComparer.Ordinal).Count() != tables.Count) throw new InvalidOperationException($"staging {g.Key} 被多个 definition 共用且 source_table 重复"); } } }