Feature 控制系统

Plan Features(套餐控制)+ Feature Flags(灰度发布)· 最后更新 2026-03-28

1. 双系统总览

Celoria 有两套独立的 feature 控制系统,解决不同问题:

🅰️ Plan Features — "有没有"

按套餐控制租户能用什么功能。基于数据库表 plan_features + tenant_features

日常使用 管理客户功能权限

管理入口:
/en/platform/plan-features

🅱️ Feature Flags — "用哪个版本"

灰度/金丝雀发布,控制新功能推出比例。基于数据库表 feature_flags

偶尔使用 灰度发布新功能

管理入口:
/en/platform/feature-flags

什么时候用哪个?

场景用哪个
客户 A 买了高级套餐,客户 B 没买Plan Features — 改 plan 或矩阵
新功能写完了,想先内部测试Plan Features — 不加到任何 plan,给自己的租户单独 grant
新功能有两个版本,想 A/B 测试Feature Flags — isFeatureOn() 做代码分支
重构了支付流程,想灰度 10% 上线Feature Flags — rollout_percent = 10
紧急关掉某个功能Plan Features — 矩阵里取消勾选

2. Plan Features 系统(System A)

2.1 数据模型

erDiagram
    features {
        varchar code PK "gift_cards, loyalty, marketing..."
        varchar name "显示名称"
        varchar category "core, crm, marketing, hr..."
        boolean is_active "是否启用"
    }
    plan_features {
        varchar plan_type "trial, standard, professional, enterprise"
        varchar feature_code FK "→ features.code"
        boolean is_enabled "是否包含在该 plan"
    }
    tenant_features {
        uuid tenant_id FK "→ tenants.id"
        varchar feature_code FK "→ features.code"
        boolean is_enabled "单独开关"
        timestamp expires_at "过期时间 (可选)"
    }
    tenants {
        uuid id PK
        varchar short_id "qqnails, ppnails..."
        varchar plan_type "当前套餐"
    }
    features ||--o{ plan_features : "belongs to plans"
    features ||--o{ tenant_features : "granted to tenants"
    tenants ||--o{ tenant_features : "has overrides"

2.2 套餐矩阵(当前配置)

FeatureTrialStandardProfessionalEnterprise
basic_appointments
basic_reports
checkin
cash_drawer
marketing
email_marketing
gift_cards
time_clock
guest_tagging
sms_marketing
advanced_reports
loyalty
payroll
research
voice_bot
api_access
agent_mode
white_label
custom_integrations
AI_INSIGHTS_ENABLED
priority_support

Plan & Features 管理页面 可随时修改矩阵。

2.3 权限判断链路

flowchart TD
    A[租户请求 API] --> B{requireTenantFeature 中间件}
    B --> C{ENABLE_TENANT_FEATURES=false?}
    C -->|是| D[直接放行 next]
    C -->|否| E{解析 tenantId}
    E -->|无 tenantId| D
    E -->|有| F{查 plan_features 表}
    F --> G{该 plan 包含此 feature?}
    G -->|是| D
    G -->|否| H{查 tenant_features 表}
    H --> I{有单独 grant 且未过期?}
    I -->|是| D
    I -->|否| J[返回 403 FEATURE_NOT_ENABLED]

2.4 三层保护

文件作用未通过时
API 路由 server.jsrequireTenantFeature() 阻止 API 请求 返回 403
后台任务 jobs/index.jsshouldRunJob() 跳过 cron 执行 日志记录 skip
前端菜单 Sidebar.tsxMENU_FEATURE_MAP 隐藏菜单项 不可见

2.5 受守卫的路由 & 任务

API 路由守卫(29 个)
Feature Code路由
gift_cards/api/gift-cards, /api/admin/gift-cards
loyalty/api/loyalty
marketing/api/marketing, /api/promotion, /api/admin/ipad-showcase
email_marketing/api/email-templates, /api/email-designs, /api/templates/*
advanced_reports/api/reports, /api/income-expense
research/api/research
voice_bot/api/voice-bot, /api/voice
payroll/api/payroll, /api/commission
time_clock/api/time-clock
checkin/api/checkin, /api/checkin-config
agent_mode/api/agent-mode, /api/agent/*
cash_drawer/api/cash-drawer, /api/day-end-closeout
guest_tagging/api/tags
后台任务守卫(14 个)
FeatureJob
loyaltypointsExpiration, membershipDowngrade, membershipPeriodReset, packageExpiration, subscriptionExpiration, subscriptionReminder, referralRewardRetry
gift_cardsgiftCardExpiration
marketingautomationExecution, campaignSchedule, hybridFallback, eventListenerTimeout
advanced_reportsbenchmarkCalculation
time_clockclockReminder

不守卫的核心 Job:appointmentReminder, weeklySchedule, campaignCleanup, auditAlert, auditCleanup

3. Feature Flags 系统(System B)

3.1 数据模型

feature_flags 表
┌─────────────────────┬─────────┬──────────────────┬─────────────┐
│ flag_key            │ enabled │ target_tenants   │ rollout_%   │
├─────────────────────┼─────────┼──────────────────┼─────────────┤
│ new-checkout-flow   │ true    │ {qqnails}        │ 0           │
│ dark-mode           │ true    │ {}               │ 30          │
│ experimental-ai     │ false   │ {}               │ 0           │
└─────────────────────┴─────────┴──────────────────┴─────────────┘

3.2 判断逻辑(3 层过滤)

flowchart TD
    A[isFeatureOn req, flagKey] --> B{flag 存在?}
    B -->|否| Z[return false]
    B -->|是| C{enabled = false?}
    C -->|是| Z
    C -->|否| D{target_tenants 不为空?}
    D -->|是| E{当前租户在列表中?}
    E -->|是| Y[return true]
    E -->|否| Z
    D -->|否| F{rollout_percent >= 100?}
    F -->|是| Y
    F -->|否| G{hash tenantId mod 100 < rollout?}
    G -->|是| Y
    G -->|否| Z

3.3 代码中使用

// 后端:在路由 handler 里做条件分支
const { isFeatureOn } = require('../middleware/feature-flags');

router.get('/checkout', async (req, res) => {
  if (await isFeatureOn(req, 'new-checkout-flow')) {
    return processCheckoutV2(req, res);  // 新流程
  }
  return processCheckoutV1(req, res);    // 老流程
});

3.4 灰度发布流程

1. 在 /platform/feature-flags 创建 flag: new-checkout-flow
2. 设置 target_tenants: ['qqnails']  → 只有 qqnails 能看到
3. 测试通过后,改为 rollout_percent: 10  → 10% 租户
4. 观察指标,逐步提高: 10% → 25% → 50% → 100%
5. 100% 稳定后,删掉 flag,代码里去掉 if 分支

4. 日常操作指南

4.1 新客户上线,控制他能用什么

方法 A:调整 Plan — 在 Plan & Features 页面的 Tenant 列表,用下拉框改 plan。改完立刻生效。

方法 B:单独开某个功能 — 点 "Manage Features" → toggle 开关。写入 tenant_features 表,不影响 plan。

4.2 写了新功能,想先不上线

  1. features 表注册新 feature code(via migration)
  2. 在代码里加 requireTenantFeature('new_feature') 守卫
  3. 不要把它加到任何 plan 的矩阵里
  4. 想给自己试 → 在管理页面单独 grant 给你的租户

4.3 新功能稳定了,正式发布

在 Plan & Features 矩阵页面,勾选目标 plan 的 checkbox → 该 plan 的所有租户立刻能用。

4.4 开发者:给新功能加守卫

// 1. 后端路由守卫 — 在 server.js 的 app.use() 加一行
app.use('/api/new-feature', requireTenantFeature('new_feature'), newFeatureRouter);

// 2. 后台任务守卫 — 在 jobs/feature-guard-map.js 加映射
const JOB_FEATURE_MAP = {
  ...
  newFeatureJob: 'new_feature',  // ← 加这行
};

// 3. 前端菜单 — 在 Sidebar.tsx 的 MENU_FEATURE_MAP 加映射
const MENU_FEATURE_MAP: Record<string, string> = {
  ...
  'new-feature': 'new_feature',  // ← 加这行
};

5. 紧急操作

⚠️ 紧急关掉某个功能
-- SQL: 从所有 plan 移除
UPDATE public.plan_features SET is_enabled = false WHERE feature_code = 'voice_bot';
-- 或在管理页面取消勾选
⚠️ 全局关掉所有 feature 检查(紧急逃生)
# .env
ENABLE_TENANT_FEATURES=false
# 重启后端 — 所有 feature guard 失效,等同于没加守卫

6. 关键文件索引

文件作用
backend/middleware/tenant-feature-middleware.jsrequireTenantFeature() 中间件
backend/middleware/feature-flags.jsisFeatureOn() 灰度判断
backend/config/features.js全局 feature 开关(env var 驱动)
backend/services/tenant-feature-service.jsPlan/Tenant feature 业务逻辑
backend/jobs/feature-guard-map.jsJob → Feature 映射表
backend/jobs/index.jsJobScheduler + shouldRunJob()
backend/server.js路由级 requireTenantFeature() 注册
backend/api/platform/plan-features.jsPlan 矩阵 CRUD API
backend/api/platform/tenant-features-admin.js单租户 grant/revoke API
backend/api/platform/feature-flags.jsFeature Flag CRUD API
frontend/.../hooks/useTenantFeatures.ts前端 tenant feature hook
frontend/.../components/ui/Sidebar.tsx菜单 feature 过滤
frontend/.../platform/plan-features/page.tsxPlan & Features 管理页面
frontend/.../platform/feature-flags/page.tsxFeature Flags 管理页面
backend/database/migrations/1772000000004_seed_features_data.js原始 12 个 feature 种子
backend/database/migrations/1833100000001_seed_additional_features.js新增 8 个 feature 种子

7. 缓存行为

缓存位置TTL清除时机
Plan/Tenant features (后端内存)无固定 TTL每次 add/remove/grant/revoke 操作后 cache.clear()
Feature flags (后端内存)30 秒API 修改 flag 后 clearCache()
前端 useTenantFeatures页面生命周期组件 mount 时 fetch 一次

结论:管理页面操作后,后端立即生效(缓存已清除),前端需刷新页面。