MasterData 的“模型”是九种 owner-local persistence record,不是领域聚合。它们保留旧表兼容,同时通过编译期元数据服务不同 ORM。
1. 为什么叫 CatalogRecord
*CatalogRecord 明确表达它是参考目录持久化行,并避免恢复旧的一表一 *Entity + mapper 模式。所有记录直接携带 [BitzTable]、[BitzColumn] 与 [BitzIndex],Generator 在编译期生成表模型。
MasterData 程序集本身不引用两个 adapter。
2. 共同基线
九种行都继承 BizEntityBase,继承 ID、TenantId、OfficeId、审计、启用、软删、内部标记等字段;所有表都声明租户过滤和软删除。
这有两个后果:
- “平台标准数据”也以 tenant 行呈现,当前资产多使用 TenantId=0;
- 查询必须依赖 adapter 正确应用租户/软删过滤,不能绕过
IEntitySet<T>裸查。
3. 自然键与索引
| 表 | 关键索引/自然键 | 注意事项 |
|---|---|---|
| SysLanguage | Tenant + Code unique | 应验证每租户最多一个默认语言 |
| SysCountry | Tenant + Alpha2 unique | Alpha2 属性却可 null |
| SysExchangeRate | Tenant + base + target + ModifyTimee unique | 字段拼写与 CSV 不一致 |
| SysGeneralCodeGroup | Tenant + Code unique | 层级无 FK/环约束 |
| SysGeneralCode | Tenant + Code + Class unique | Resolver 按 Group 读取 |
| SysGeneralCodeText | Tenant + Code + Class + Language index | 该索引未标 unique |
| SysIndustrySetting | Tenant + ParentCode index | Code 没有 unique 索引 |
| SysPublicHoliday | Tenant + Country + Year index | DayDate 自然键仅在 seeder 中 |
| SysTranslation | Key + Lang + Scope + Tenant + Office unique | 由 I18n 使用 |
数据库约束与 seed matcher 并不完全等价,尤其 code text 和 holiday。
4. 元数据示例
// ① 表级元数据在 owner 源码声明,Generator 编译期收集。[BitzTable("SysLanguage", IsTenant = true, IsSoftDelete = true)][BitzIndex("UX_SysLanguage_Tenant_Code", "TenantId", "Code", IsUnique = true)]public sealed class SysLanguageCatalogRecord : BizEntityBase{ // ② BCP-47 code 是稳定业务键;显示名可以更新。 [BitzColumn(Length = 20, IsRequired = true)] public string Code { get; set; } = string.Empty;
[BitzColumn(Length = 120)] public string? DisplayName { get; set; }}示例裁剪了同一源码类型的非关键字段,完整定义以源码为准。
5. ORM 中立读取
MasterData 服务只注入 IEntitySet<T>:
// ① 表达式必须同时被当前支持的 ORM 翻译。var entries = await codes.ListAsync( row => row.Group == groupKey && row.IsActive && !row.IsDeleted, cancellationToken);
// ② 排序在内存完成;大目录需评估是否改为可翻译排序端口。var ordered = entries.OrderBy(row => row.Sort).ToList();return ordered;源码还显式写 !IsDeleted,即使表元数据已有软删过滤。这是防御性重复,而不是跨租户权限证明。
6. 三种“唯一”必须一致
一个可重复导入的目录至少涉及:
- 数据库 unique index;
- seed step
WhereColumns/Match; - 读取/缓存映射键。
当前 SysGeneralCodeText 三者不一致:索引含 Class,seed matcher 只有 Code+Language,Resolver 也只按 Code 组图。
7. SysTranslation 与 I18n
MasterData 物理拥有 Translation/Language 行;I18n 拥有翻译应用契约、Repository、作用域覆盖和查询 API。依赖方向不是通过 MasterData Contracts 表达,因为 MasterData 没有 Contracts 项目;I18n Infrastructure 直接引用 MasterData Infrastructure。
这是一项已记录的物理耦合。未来若拆包独立交付,应提取窄的 persistence contracts/owner model 包,而不是复制表类型。
8. 目录模型不提供的规则
当前类型没有实现:
- 国家/币种/行业标准版本和有效期;
- 汇率来源、报价时间、精度与反向规则;
- 节假日调休/工作日和发布机构;
- 字典组 FK、树环与孤儿防护;
- 默认语言唯一性;
- 保护记录的写入阻断;
- 乐观并发以外的发布审批和历史版本。
这些不是列属性可以自动补全的业务规则。
9. 新增目录的判断
新增数据前先问:
- 是否跨多个模块共享且需要持久稳定 code?
- 是否有权威来源、版本和更新责任人?
- 是租户可覆盖,还是平台全局唯一?
- 是否需要 I18n、有效期和历史回放?
- 是否应属于某个业务模块而非 MasterData?
模块私有状态枚举通常留在归属模块;不要为“方便下拉框”就创建共享表。
10. 新行类型示例
// ① 使用 owner-local record,并明确租户和软删语义。[BitzTable("SysExampleCatalog", IsTenant = true, IsSoftDelete = true)][BitzIndex("UX_SysExample_Tenant_Code", "TenantId", "Code", IsUnique = true)]public sealed class SysExampleCatalogRecord : BizEntityBase{ // ② 自然键必填,长度与权威标准一致。 [BitzColumn(Length = 32, IsRequired = true)] public string Code { get; set; } = string.Empty;
[BitzColumn(Length = 160, IsRequired = true)] public string DisplayName { get; set; } = string.Empty;}随后必须补 metadata gate、双 ORM parity、seed matcher、资产完整性和文档,而不仅是建类。
11. 测试证据
MasterDataInfrastructureArchitectureTests 固定:旧 Framework/Entity 路径删除、九行/八资产存在、程序集 ORM 中立、seed 基类、Host 组合、Resolver 的 IEntitySet 依赖和九种生成元数据。跨 ORM 集成测试注册这些类型,验证 adapter parity 与 seed 端到端。
这些测试没有证明目录业务规则、生产数据新鲜度或管理权限。
12. 核查命令
# 表与索引定义。rg -n "BitzTable|BitzIndex|class .*CatalogRecord" src/Platform/MasterData -g '*.cs'
# 模块不应引用具体 ORM。rg -n "SqlSugar|EntityFrameworkCore|Infrastructure.EfCore" src/Platform/MasterData -g '*.cs' -g '*.csproj'
# 架构门禁中的九行清单必须同步新增类型。rg -n "Owner_Assembly_Should_Emit_Metadata" tests/BitzOrcas.Architecture.Tests/MasterDataInfrastructureArchitectureTests.cs