Skip to content
bitzorcas
中EN

Concept

功能开关

使用全局 Feature 定义、租户覆盖、授权求值与缓存失效管理功能发布边界。

Last updated

BitzOrcas 已实现数据库驱动的 Feature 管理。Feature 不是散落在代码里的布尔配置,而是由全局定义目录、租户覆盖、授权求值器和缓存共同组成的运行时能力。

决策模型

Feature code
↓
租户是否存在覆盖? ── 是 → 使用 OverrideState
│ 否
└──────────────→ 使用全局 DefaultState

FeatureStore 从 SysFeatureDefinition 读取全局定义,从 SysFeatureOverride 读取租户覆盖。定义不存在或存储查询失败时返回禁用,避免基础设施故障意外打开能力。

是否

模块 Feature 声明

全局定义 DefaultState

租户覆盖存在?

OverrideState

DefaultState

授权 Feature evaluator

Allow / Deny / Neutral

定义 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 moduleFeature code禁用时
ticketsplatform.ticketsDeny
chatplatform.chatDeny
workflowplatform.workflowDeny
其他模块无映射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 适合控制可回退、可观测的发布过程,例如先为内部租户开放新模块,再逐步扩大范围。它不适合长期保留两套业务模型。稳定后应清理旧分支,并决定功能码是否继续承担商业套餐边界。

上线前至少验证:

  1. 功能码已进入定义目录和种子数据。
  2. 默认状态符合最小授权原则。
  3. 租户 A 的覆盖不会影响租户 B。
  4. 关闭 Feature 时,API、后台任务和异步消费者的行为一致。
  5. 变更事件、缓存失效和审计记录可观测。

本地与测试环境

缺少生产持久化适配器时,UnavailableFeatureStore 与 UnavailableFeatureDecisionProvider 会保持关闭语义。测试应显式提供预期状态,不要依赖“环境里刚好启用”。至少覆盖默认启用、默认禁用、租户覆盖和存储失败四类路径。

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%