Numbering 实现了 Framework 的 ISequenceNumberGenerator,从三张租户化表选择规则、组装字符串并写入生成事实。它不生成数据库主键,也没有规则写入用例或管理后台;但它有一个只读治理查询入口(见 §12),供运营查看规则与计数事实。
1. 当前产品面
模块本地除 Infrastructure 外,已有 Contracts 与 Application 项目用于暴露只读治理查询(见 §12)。公开 Port/纯算法在 BitzOrcas.Application.Numbering,API Host 通过 AddBitzOrcasNumberingPlatform() 注册实现。
2. 输入与输出
// ① TableName + Field 是规则绑定键,不是任意动态 SQL 表名。var request = new SequenceNumberRequest( TableName: "DemoCase", Field: "SerialId", EntityValues: new Dictionary<string, object?> { ["OfficeId"] = "SH", ["CaseType"] = "Civil" }, IsEnabled: true, ParentSequenceNumber: null, BusinessSerialContext: null);
// ② 通过共享 Port 调用;结果同时返回字符串、数值和前缀。Result<SequenceNumberResult> result = await sequenceNumbers.GenerateAsync(request, cancellationToken);请求没有 TenantId 字段,租户来自底层 IEntitySet<T> 的当前租户上下文。
3. 三张 owner 表
| 表 | 责任 |
|---|---|
SysSequenceNumberRule | TableName/Field 下的条件、优先级、分隔符与补偿开关 |
SysSequenceNumberRuleSetting | RuleId 下按 Sort 排列的分段 |
SysSequenceNumberGenerateRecord | 已分配字符串、数值、前缀、激活与父号事实 |
三者都继承 BizEntityBase,声明 IsTenant=true 和软删除。它们是规则/计数事实例外,不是恢复一表一 Entity/Mapper 模式的样板。
4. 规则选择
Generator 查询同 TableName/Field、IsActive && IsEnabled 的规则,按 Priority 升序交给 SequenceRuleSelector。条件支持 eq、ne、in、contains;字段从 EntityValues 读取。
空条件会在主循环中直接视为 true,因此它必须放在最大 Priority。否则它会提前吞掉后续条件规则。
5. 六种分段
| SectionType | 输出 |
|---|---|
| Constant | Value 常量 |
| Date | 当前时间按 .NET 格式;可切固定四月财年逻辑 |
| FiscalYear | 两位财年 |
| Branch | FieldSource 查 JSON 映射,失败回退 Value/FD |
| BusinessField | EntityValues 字段,缺失回退 Value |
| SequenceNumber | 当前数值按 Length 左补零,默认 6 位 |
未知 SectionType 会被静默当 Constant,而不是返回配置错误。
6. 当前交付规则
种子只有两个演示规则:
DemoCase.SerialId→CASE-{yy}-{seq6};DemoInvoice.SerialId→INV-{yyyy}-{seq6}。
两条规则和六条设置都写入 TenantId 1000001。它们是示例基线,不是面向任意业务表自动生效的通用规则。
7. 计数与前缀
前缀是所有非 SequenceNumber 段按 Separator 拼接的字符串。例如 CASE-26。Generator 按 Tenant + TableName + Field + Prefix 查询所有事实的最大 SequenceNumber,再加一。
Prefix 变化自然形成新计数区间,所以 Date/FiscalYear/Branch/BusinessField 都可能触发“重置”。源码没有独立的日/月/年 reset policy 字段。
8. 唯一性边界
数据库唯一索引是 TenantId + TableName + Field + SequenceNumberStr。两个并发调用可能同时读到同一个 MAX;一个插入成功,另一个依靠数据库异常字符串识别冲突并重试。
异常识别只匹配若干英文文本和 SQL Server 编号,属于脆弱的 provider 兼容层;非匹配异常会直接抛出。
9. 业务表接续
可选 IBusinessSerialContext 解决两类迁移问题:事实表为空时从业务表 MAX 接续;生成后检查完整字符串是否已存在。
该回调由业务 owner 实现,Numbering 不拼接动态表名 SQL。当前源码仓库没有生产业务模块调用 ISequenceNumberGenerator 的证据,主要消费证据来自测试。
10. 断号补偿
规则 IsCompensate=true 时,第一次尝试优先查同前缀、IsActivated=false 的最小数值,把它更新为请求的激活状态并返回。找不到跳号不会让生成失败,而是继续分配新号。
补偿路径不调用业务表 ExistsInBusinessTableAsync,也没有并发 claim 条件;两个调用可能读到同一未激活行。
11. 当前保证与不保证
| 可以依赖 | 不可假设 |
|---|---|
| 三张租户/软删元数据表 | 已有管理 API、权限或审计 |
| 条件规则与六类分段算法 | Feature 已在 GenerateAsync 强制 |
双 ORM IEntitySet 实现 | 数据库原子 increment/分布式锁 |
| 字符串唯一索引兜底 | StartValue 已生效 |
| 可选业务表接续/判重 | 补偿是并发安全 claim |
| 三次唯一冲突重试 | 所有 provider 异常都可识别 |
12. 治理查询入口
GET /api/numbering/rules(资源 numbering/rules、动作 Read)是一个 owner-local 只读治理报告。查询参数:Search(可选)、Status(可选,取值 all/active/inactive/conditional,默认 all)。
返回 NumberingGovernanceReport:
Rules:最多 200 条规则摘要(超出置Truncated=true),每条NumberingRuleSummary给出RuleKey(形如{Table}-{Field}-{RuleName})、Compensate、Conditional、Condition(受控,不含运行时业务值)、Priority、ConditionScope,以及人读Template(如{date:yyyyMMdd}、{fiscalYear}、{branch}、{field:...}、{seq:n})和每段详情(Segments,含SubstitutionConfigured标志,替换体本身不返回);- 全局计数:
TotalRules、ActiveRules、ConditionalRules、CompensatingRules; - 计数事实:
GeneratedRecords、ActivatedRecords、HeldRecords(已生成但未激活); GeneratedAt。
读存储 NumberingGovernanceReadStore 在 Infrastructure 层实现 INumberingGovernanceReadStore,刻意只返回能力统计与规则模板,不返回已生成的业务流水号、父级流水号或分支映射体。未注册时降级为 fail-closed 的 UnavailableNumberingGovernanceReadStore。错误码:Numbering.Governance.StoreUnavailable(ServiceUnavailable)、Numbering.Governance.InvalidQuery(Validation)。
与 MasterData 治理查询同属一类 owner-local 只读投影:回答”有哪些规则、生成了多少”,不回答”规则是否正确、序列是否无重复”。
13. 文档导航
14. 源码核查
# Generator、规则选择与分段算法。rg -n "GenerateAsync|SelectRule|AssembleFull" src/Platform/Numbering src/Framework/BitzOrcas.Application/Numbering -g '*.cs'
# 治理查询入口与只读报告。rg -n "GetNumberingGovernance|NumberingGovernanceReport|NumberingGovernanceReadStore" \ src/Platform/Numbering -g '*.cs'
# 当前生产消费面;除注册外应无业务调用。rg -n "ISequenceNumberGenerator|SequenceNumberRequest" src -g '*.cs' --glob '!**/bin/**' --glob '!**/obj/**'