Plan Features(套餐控制)+ Feature Flags(灰度发布)· 最后更新 2026-03-28
Celoria 有两套独立的 feature 控制系统,解决不同问题:
按套餐控制租户能用什么功能。基于数据库表 plan_features + tenant_features。
日常使用 管理客户功能权限
管理入口:/en/platform/plan-features
灰度/金丝雀发布,控制新功能推出比例。基于数据库表 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 — 矩阵里取消勾选 |
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"
| Feature | Trial | Standard | Professional | Enterprise |
|---|---|---|---|---|
| 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 管理页面 可随时修改矩阵。
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]
| 层 | 文件 | 作用 | 未通过时 |
|---|---|---|---|
| API 路由 | server.js → requireTenantFeature() |
阻止 API 请求 | 返回 403 |
| 后台任务 | jobs/index.js → shouldRunJob() |
跳过 cron 执行 | 日志记录 skip |
| 前端菜单 | Sidebar.tsx → MENU_FEATURE_MAP |
隐藏菜单项 | 不可见 |
| 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 |
| Feature | Job |
|---|---|
| loyalty | pointsExpiration, membershipDowngrade, membershipPeriodReset, packageExpiration, subscriptionExpiration, subscriptionReminder, referralRewardRetry |
| gift_cards | giftCardExpiration |
| marketing | automationExecution, campaignSchedule, hybridFallback, eventListenerTimeout |
| advanced_reports | benchmarkCalculation |
| time_clock | clockReminder |
不守卫的核心 Job:appointmentReminder, weeklySchedule, campaignCleanup, auditAlert, auditCleanup
feature_flags 表
┌─────────────────────┬─────────┬──────────────────┬─────────────┐
│ flag_key │ enabled │ target_tenants │ rollout_% │
├─────────────────────┼─────────┼──────────────────┼─────────────┤
│ new-checkout-flow │ true │ {qqnails} │ 0 │
│ dark-mode │ true │ {} │ 30 │
│ experimental-ai │ false │ {} │ 0 │
└─────────────────────┴─────────┴──────────────────┴─────────────┘
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
// 后端:在路由 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); // 老流程
});
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 分支
方法 A:调整 Plan — 在 Plan & Features 页面的 Tenant 列表,用下拉框改 plan。改完立刻生效。
方法 B:单独开某个功能 — 点 "Manage Features" → toggle 开关。写入 tenant_features 表,不影响 plan。
features 表注册新 feature code(via migration)requireTenantFeature('new_feature') 守卫在 Plan & Features 矩阵页面,勾选目标 plan 的 checkbox → 该 plan 的所有租户立刻能用。
// 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', // ← 加这行
};
-- SQL: 从所有 plan 移除
UPDATE public.plan_features SET is_enabled = false WHERE feature_code = 'voice_bot';
-- 或在管理页面取消勾选
# .env
ENABLE_TENANT_FEATURES=false
# 重启后端 — 所有 feature guard 失效,等同于没加守卫
| 文件 | 作用 |
|---|---|
backend/middleware/tenant-feature-middleware.js | requireTenantFeature() 中间件 |
backend/middleware/feature-flags.js | isFeatureOn() 灰度判断 |
backend/config/features.js | 全局 feature 开关(env var 驱动) |
backend/services/tenant-feature-service.js | Plan/Tenant feature 业务逻辑 |
backend/jobs/feature-guard-map.js | Job → Feature 映射表 |
backend/jobs/index.js | JobScheduler + shouldRunJob() |
backend/server.js | 路由级 requireTenantFeature() 注册 |
backend/api/platform/plan-features.js | Plan 矩阵 CRUD API |
backend/api/platform/tenant-features-admin.js | 单租户 grant/revoke API |
backend/api/platform/feature-flags.js | Feature Flag CRUD API |
frontend/.../hooks/useTenantFeatures.ts | 前端 tenant feature hook |
frontend/.../components/ui/Sidebar.tsx | 菜单 feature 过滤 |
frontend/.../platform/plan-features/page.tsx | Plan & Features 管理页面 |
frontend/.../platform/feature-flags/page.tsx | Feature Flags 管理页面 |
backend/database/migrations/1772000000004_seed_features_data.js | 原始 12 个 feature 种子 |
backend/database/migrations/1833100000001_seed_additional_features.js | 新增 8 个 feature 种子 |
| 缓存位置 | TTL | 清除时机 |
|---|---|---|
| Plan/Tenant features (后端内存) | 无固定 TTL | 每次 add/remove/grant/revoke 操作后 cache.clear() |
| Feature flags (后端内存) | 30 秒 | API 修改 flag 后 clearCache() |
| 前端 useTenantFeatures | 页面生命周期 | 组件 mount 时 fetch 一次 |
结论:管理页面操作后,后端立即生效(缓存已清除),前端需刷新页面。