BitzOrcas 已实现数据库驱动的 Feature 管理。Feature 不是散落在代码里的布尔配置,而是由全局定义目录、租户覆盖、授权求值器和缓存共同组成的运行时能力。
决策模型
Feature code ↓租户是否存在覆盖? ── 是 → 使用 OverrideState │ 否 └──────────────→ 使用全局 DefaultStateFeatureStore 从 SysFeatureDefinition 读取全局定义,从 SysFeatureOverride 读取租户覆盖。定义不存在或存储查询失败时返回禁用,避免基础设施故障意外打开能力。
定义 Feature
模块在自己的契约边界声明稳定功能码,并用 FeatureDefinition 元数据描述它。源生成器在编译期收集这些定义,避免运行时扫描。
// ① 先看契约与控制流;校验、取消和类型化错误都要显式保留。[FeatureCatalog]public static class TicketFeatures{ [FeatureDefinition("工单管理", DefaultEnabled = false)] public const string Ticketing = "platform.tickets";}功能码一旦进入套餐、租户覆盖或审计记录,就应视为兼容性契约。重命名需要数据迁移和消费者迁移,不能只改常量。
在授权管线中求值
FeaturePolicyEvaluator 根据资源所属模块映射到功能码,并通过 IFeatureDecisionProvider 查询当前租户状态。启用返回 Allow,禁用返回 Deny;没有 Feature 约束的资源返回 Neutral,继续由其他授权求值器决定。
不要在每个 Handler 里重复 if (featureEnabled)。入口授权管线更容易保持一致,也能避免某个端点漏检。
当前覆盖范围
必须注意:当前 FeaturePolicyEvaluator.ModuleFeatureMap 只映射三项:
| Resource module | Feature code | 禁用时 |
|---|---|---|
tickets | platform.tickets | Deny |
chat | platform.chat | Deny |
workflow | platform.workflow | Deny |
| 其他模块 | 无映射 | Neutral |
仓库中 Website、Documents、Webhooks、Search 等模块已经声明 Feature,不表示它们自动进入通用授权 evaluator。新增声明后还要明确 enforcement point:扩展映射、模块专用授权策略、Endpoint group filter、Job/Consumer guard,或其他可测试边界。
与套餐权益和发布开关的关系
PlatformBilling.EntitlementResolver 可以消费 FeatureStore 与套餐映射,但当前 FeatureStore 自身只有“租户覆盖优先、否则全局默认”两层。不要把套餐 JSON、License Edition、灰度百分比或用户 cohort 描述成 FeatureStore 已自动合并的层次。
商业套餐/许可证 → 决定租户应拥有哪些能力Feature 管理 → 保存全局默认与租户覆盖授权 evaluator → 对已映射资源执行 Allow/Deny发布开关 → 控制新代码的渐进暴露与回滚同一个功能码可以承担商业权益,但长期发布实验应有 owner、结束日期和清理计划。若套餐同步到租户覆盖,需要幂等同步、撤销和审计,不能靠人工长期维护两套事实。
管理状态
管理端提供 Feature 查询和更新用例。更新逻辑取决于定义的作用域:
- 租户级 Feature:写入或更新当前租户的覆盖行,不修改全局默认值。
- 平台级 Feature:更新全局
DefaultState。
更新成功后发布 FeatureChangedIntegrationEvent,并失效当前租户的 Feature 缓存。覆盖行只保存变化字段;删除覆盖后自然回到全局默认值。
对于平台级 Feature,当前更新事件与缓存失效仍携带操作者当前租户。采用全局状态时应验证所有受影响租户的缓存是否被正确失效;只失效当前租户会让其他租户保留旧状态直到 TTL。这个场景必须由合同测试证明,不能从事件名称推断。
缓存与故障
Evaluator 使用 auth:feature:{tenantId}:{featureCode},缓存 Medium TTL,并按租户 Feature tag 失效。启用缓存命中返回 Allow,禁用返回 Deny;未映射模块 Neutral。
// 关闭状态是明确 Deny;未知/未映射资源才是 Neutral。var decision = await evaluator.EvaluateAsync( currentUser, resource, action, cancellationToken);
// 测试应断言 decision kind,而不只看最终 HTTP 状态。decision.Evaluator.ShouldBe(nameof(FeaturePolicyEvaluator));FeatureStore 捕获非取消异常并返回 false;Unavailable Provider 也保持关闭。缓存故障遵循缓存端口策略,但事实源故障不能变成 enabled。
测试矩阵
- 无覆盖时使用 DefaultState,覆盖 Enabled/Disabled 均优先;
- 定义不存在、存储异常和 unavailable adapter 返回关闭;
- tickets/chat/workflow 的禁用状态明确 Deny;
- 未映射模块返回 Neutral,并由其他策略决定;
- 租户 A 更新不改变租户 B;全局更新正确处理全部缓存;
- API、后台任务和异步消费者对关闭状态保持一致;
- FeatureChanged 事件重复消费安全,审计保留操作者与旧/新状态。
发布策略
Feature 适合控制可回退、可观测的发布过程,例如先为内部租户开放新模块,再逐步扩大范围。它不适合长期保留两套业务模型。稳定后应清理旧分支,并决定功能码是否继续承担商业套餐边界。
上线前至少验证:
- 功能码已进入定义目录和种子数据。
- 默认状态符合最小授权原则。
- 租户 A 的覆盖不会影响租户 B。
- 关闭 Feature 时,API、后台任务和异步消费者的行为一致。
- 变更事件、缓存失效和审计记录可观测。
本地与测试环境
缺少生产持久化适配器时,UnavailableFeatureStore 与 UnavailableFeatureDecisionProvider 会保持关闭语义。测试应显式提供预期状态,不要依赖“环境里刚好启用”。至少覆盖默认启用、默认禁用、租户覆盖和存储失败四类路径。