规则 DSL 决定“为哪个业务字段、在什么条件下、按什么段生成字符串”。当前 DSL 轻量且可测试,但错误配置多采用静默回退。
1. 候选规则加载
查询条件是 TableName、Field、IsActive、IsEnabled。Tenant 与软删由生成的 persistence adapter 过滤。Priority 越小越先求值。
Selector 的末尾虽有“回退空条件”代码,但空条件在前面的循环已经恒真。默认规则只有放在最后才符合预期。
2. RuleId 真实形状
运行时 RuleId 是:
{TableName}-{Field}-{RuleName}设置表必须使用完全相同的字符串。模型注释中仍有只写 TableName+Field 的遗留表述,不能据此建资产。
3. JSON 条件
{ "field": "CaseType", "op": "Eq", "value": "Civil"}JSON enum 使用 JsonStringEnumConverter。字段名在 EntityValues 中按字典 comparer 语义查找;一般字典默认大小写敏感,而值比较不区分大小写。
4. 四个操作符
| 操作符 | 行为 |
|---|---|
| Eq | actual == value,忽略大小写 |
| Ne | 不相等,忽略大小写 |
| In | value 按逗号分割、trim 后匹配 |
| Contains | actual 是否含 value,忽略大小写 |
属性不存在返回 false。属性值 null 转为空字符串,因此 Eq "" 可以命中 null。
5. 条件错误语义
非法 JSON 返回 false,不记录日志;反序列化结果为 null 时却返回 true。空白 JSON 也返回 true。ConditionScope 只持久化和 seed copy,当前选择器/缓存没有使用它。
public static class NumberingErrors{ public static readonly Error ConditionInvalid = Error.Validation("Numbering.Condition.Invalid", "Invalid condition");}
// ① 生产资产应先反序列化并验证字段与操作符。var condition = JsonSerializer.Deserialize<SequenceRuleCondition>(json);if (condition is null || string.IsNullOrWhiteSpace(condition.Field)) return Result.Failure(NumberingErrors.ConditionInvalid);
// ② 用真实业务快照做命中测试,避免默认规则提前吞掉。var matched = condition.IsSatisfiedBy(sampleEntityValues);
// ③ 默认空条件必须使用最大的 Priority。EnsureDefaultRuleIsLast(rules);这是规则发布前应增加的校验层;当前模块没有管理入口执行它。
6. Constant
Constant 原样返回 Value。空值会成为空段,但 string.Join 仍可能留下分隔符。未知 SectionType 也被 ParseSegmentType 映射为 Constant,可能把拼写错误变成看似成功的输出。
7. Date
Date 默认格式 yy,显式支持 yy、yyyy、yyyy-MM、yyyyMM,其他值直接传给 DateTime.ToString(format, InvariantCulture)。
格式错误可能抛出 FormatException。没有时区配置;使用 IAppClock.Now 的 DateTime 语义。
8. FiscalYear
财政年固定从 4 月开始:4–12 月取当前年份后两位,1–3 月取上一年份后两位。Date 段若 IsFiscalYear=true 也返回两位财年,忽略 DateFormat。
源码技术债务已指出起始月应配置化,目前不能声称支持任意企业财年。
9. Branch
Branch 从 FieldSource 获取 key,再用 SubstitutionValue JSON 字典映射。缺 FieldSource、映射 JSON 非法、key 缺失或未命中时,返回 Value;Value 为空则返回 FD。
所有失败都静默回退,可能把多个真实分支折叠进同一前缀。
{ "SH": "SHA", "BJ": "BJA", "GZ": "GZA"}10. BusinessField
BusinessField 读取 FieldSource 对应值并调用 ToString()。字段不存在回退 Value;字段存在但值为 null 时输出空字符串。它没有格式、长度、字符集或敏感数据校验。
不要把客户姓名、证件号等敏感字段直接编入业务流水号。
11. SequenceNumber
SequenceNumber 用 D{Length} 左补零;Length <= 0 时默认 6。StartValue 虽存在于模型、配置和映射中,但 GetNextSequenceNumberAsync 未读取它,空前缀事实始终从 1 开始。
多个 SequenceNumber 段会输出同一个数值多次;当前没有校验“恰好一个计数段”。
12. Separator 与 Prefix
Assembler 对每个段按 Sort 遍历:非序号段同时进入 prefix/full,序号段只进入 full。最后分别 string.Join(separator, ...)。
// ① 固定时钟让 Date 段可测试。var clock = new FakeClock(new DateTime(2026, 7, 15));
// ② CASE + yy + 六位序号,分隔符为 "-"。var (full, prefix) = SequenceSegmentAssembler.AssembleFull( segments, new Dictionary<string, object?>(), clock, sequenceNumber: 1, separator: "-");
// ③ full = CASE-26-000001;prefix = CASE-26。13. 规则发布门禁
发布前应拒绝:重复/负 Sort、无 SequenceNumber、多 SequenceNumber、未知类型、非法日期格式、非法 Branch JSON、空业务字段名、默认规则优先级不最大、重复优先级歧义、前缀超 128 或完整串超 128。
当前 CSV reader 与运行时都没有完整执行这些门禁。
14. 核查命令
# 条件与分段的全部纯算法。sed -n '1,260p' src/Framework/BitzOrcas.Application/Numbering/SequenceRuleSelector.cssed -n '1,300p' src/Framework/BitzOrcas.Application/Numbering/SequenceSegmentAssembler.cs
# StartValue/ConditionScope 当前仅被携带,没有生成语义。rg -n "StartValue|ConditionScope" src/Platform/Numbering src/Framework/BitzOrcas.Application/Numbering -g '*.cs'