2025年2月19日·阅读约2分钟

支持升级与附加项的计划与权限数据库模式

一个支持升级、附加项、试用和撤销的计划与权限(entitlements)数据库模式,通过清晰的表与时间窗口避免写死规则。

支持升级与附加项的计划与权限数据库模式

为什么计划和功能会很快变得混乱

定价页上的计划看起来很简单:Basic、Pro、Enterprise。真正的混乱始于你试图把这些名称变成应用内的访问规则时。

把功能检查写死(比如 if plan = Pro then allow X)在第一个版本时可行。随后定价变化了。某个功能从 Pro 移到了 Basic,新附加项出现,或者销售把定制包绑进了交易。很快你会在 API、UI、移动端和后台任务里看到相同的规则被复制多处。你改了一个地方却忘了另一个。用户会发现异常。

第二个问题是时间。订阅不是一个静态标签;它会在计费周期中改变。有人今天升级,下个月降级,暂停或在余费未用完时取消。如果你的数据库只存储“当前计划”,你就丢失了时间线,之后无法回答一些基本问题:上周二他们有什么访问权限?为什么客服批准了退款?

附加项让情况更糟,因为它们跨越计划。附加项可能解锁额外席位、移除限制或启用特定功能。用户可以在任何计划上购买、之后移除,或在降级后仍保留。如果规则嵌在代码里,你最终会得到越来越多的特例处理。

下面是常会打破天真的设计的几类情况:

  • 周期中升级:访问应立即变化,计费按比例可能有不同规则。
  • 定时降级:访问可能保持“更高”的状态直到已付周期结束。
  • 保留旧制(Grandfathering):老客户保留新用户无法获得的功能。
  • 定制交易:某个账户获得功能 A 但没有功能 B,即便他们共享同一计划名。
  • 审计需求:支持、财务或合规问“他们什么时候具体启用了什么?”

目标很简单:一个灵活的访问控制模型,能随定价变化而变化,而不必每次重写业务逻辑。你要有一个地方可以问“他们能做这个吗?”,并且数据库有一条时间线能解释答案。

在本文结尾,你会得到一个可复制的模式:把计划与附加项当作输入,把权限(entitlements)作为功能访问的单一事实来源。相同的方法也适用于像 AppMaster 这种无代码构建器,因为你可以把规则保存在数据中,并在后端、Web 与移动端一致地查询它们。

关键术语:计划、附加项、权限与访问

许多订阅问题源于词汇混用。如果大家用同一个词指不同事,你的模式就会演化成特例。

在计划与权限数据库模式里,值得分清的术语如下:

  • Plan(计划):订阅时用户默认获得的捆绑(例如 Basic 或 Pro)。计划通常设定基线限制和包含的功能。
  • Add-on(附加项):可选购买,用以改变基线(例如“额外席位”或“高级报表”)。附加项应可挂载与移除,而不改变计划本身。
  • Entitlement(权限/准入):最终计算出的“他们现在拥有什么”,合并了 plan + add-ons + 覆盖项后得到的结果。这应该是应用查询的对象。
  • Permission(权限/能力):具体的可执行动作(例如“导出数据”或“管理计费”)。权限通常依赖角色加上 entitlements。
  • Access(访问):应用强制规则后的现实结果(界面显示或隐藏某功能、API 调用被允许或阻止、限制被执行)。

功能开关(feature flags)相关但不同。feature flag 通常是你控制的产品开关(用于灰度发布、实验或在故障时快速关停)。entitlement 是基于客户付费或你授予的、针对具体客户的访问。想要不涉及计费就改变行为时用 flag;当访问需和订阅、发票或合同一致时用 entitlement。

作用域也是混淆来源之一。把这些概念分清:

  • User(用户):一个人。适用于角色(管理员 vs 成员)和个人限额。
  • Account(账户/客户):付费实体。适于保存计费信息与订阅归属。
  • Workspace(工作区/项目/团队):工作的发生地。许多产品在这里应用 entitlements(席位、存储、启用模块)。

时间很重要,因为访问会变化。直接建模时间:

  • 开始与结束:权限可以仅在一个时间窗内生效(试用、促销、年度合同)。
  • 定时变更:升级可以立即生效;降级常在下次续费生效。
  • 宽限与取消:付款失败后可能允许有限访问,但必须有明确结束日期。

举例:某公司在 Pro 计划上,中途新增“高级报表”并安排在下个周期降级到 Basic。计划随后变化,附加项立即开始,权限层仍然是回答“这个工作区今天能看高级报表吗?”的单一来源。

一个针对计划与功能的简单核心模式

好的计划与权限数据库模式从小处开始:把你卖的东西(计划与附加项)和用户能做的事(功能)分开。如果把这两者分清,升级与新附加项只需改数据,而不是重写代码。

下面是一组实用的核心表,适用于大多数订阅产品:

  • products:可售条目(基础计划、团队计划、额外席位附加包、优先支持附加包)。
  • plans:可选,如果你希望计划是产品的一种特殊类型并有专用字段(计费周期、公开显示顺序)。很多团队把计划放在 products 里并用 product_type 列区分。
  • features:功能目录(API 访问、最大项目数、导出、SSO、短信额度)。
  • product_features(或如果拆分则为 plan_features):关联表,表示某产品包含哪些功能,通常带有一个值字段。

关联表是大多数能力所在。功能很少只是开/关。一个计划可能包含 max_projects = 10,而附加项可能是 +5。因此 product_features 应至少支持:

  • feature_value(数字、文本、JSON,或拆成多列)
  • value_type(boolean、integer、enum、json)
  • grant_mode(替代还是累加),以便附加项能“增加 5 个席位”而不是覆盖基线限制

把附加项也建模为产品。唯一区别是购买方式。基础计划产品通常为“每账户仅一份”,而附加项可能允许数量多份。但两者都以相同方式映射到功能上。这样可以避免把“如果附加项 X 则启用功能 Y”这样的特例散布在代码里。

功能应为数据而非代码常量。如果你在多个服务里写死功能检查,最终会出现不一致(Web 端说可以,移动端说不可以,后端又不同意)。当功能保存在数据库时,应用可以问一个统一的问题,修改行就能下发变更。

命名比很多人想象的重要。使用稳定的标识符,即便营销名称改变也不要改它们:

  • feature_key,如 max_projectsssopriority_support
  • product_code,如 plan_starter_monthlyaddon_extra_seats

把显示标签(feature_nameproduct_name)独立管理。如果你用 AppMaster 的 Data Designer 和 PostgreSQL,把这些 key 作为唯一字段会立刻见效:你可以在保持集成与报表稳定的同时安全地更新展示名。

权限层:一个询问“他们能吗?”的地方

大多数订阅系统出问题,是因为“他们买了什么”存一处,而“他们能做什么”在五个不同的代码路径里计算。解决办法是权限层:一张表(或视图)代表在某个时间点某主体的有效访问。

如果你想要一个能应对升级、降级、试用和一次性授权的计划与权限数据库模式,这一层是让一切可预测的关键部分。

一个实用的 entitlements 表

把每行当作一条声明:"这个主体在某段时间内以这个来源拥有这个功能及其值"。常见字段形态如下:

  • subject_type(例如 accountuserorg)与 subject_id
  • feature_id
  • value(该功能的有效值)
  • source(来源:directplanaddondefault
  • starts_at 与 ends_at(nullable 的 ends_at 表示持续有效)

你可以使用几种方式表示 value:单个文本/JSON 列加 value_type,或把 value_bool、value_int、value_text 拆列。保持简单且便于查询。

覆盖大多数产品的值类型

功能并非总是开/关。这些值类型通常能覆盖真实的计费与访问需求:

  • Boolean:启用/禁用(can_export = true)
  • 配额数值:限制(seats = 10、api_calls = 100000)
  • 等级:档位(support_tier = 2)
  • 字符串:模式或变体(data_retention = 90_days

优先级:冲突如何解决

冲突是正常的。某用户可能在计划里有 5 个席位,买了一个附加包再加 10 个席位,又被客服人工授权额外席位。

要设定明确规则并在所有地方一致执行:

  1. 直接授予优先于计划
  2. 其次为附加项
  3. 最后为默认值

一个简单做法是保存所有候选行(来自计划、附加项、直接授予),并通过按来源优先级排序再按最新 starts_at 来为每个 subject_id + feature_id 计算最终“胜出者”。

举个实际场景:客户今天降级,但他们已付的某个附加项持续到月底。有了 starts_at/ends_at,计划相关功能的降级可立即生效,而附加项在其 ends_at 前仍然有效。你的应用只需一个查询就能回答“他们能吗?”,无需特例逻辑。

订阅、条目与有时间界限的访问

掌控你的部署选项
部署到你自己的云,或在需要完全控制时导出代码库。
导出源码

你的计划目录(plans、add-ons、features)是“有什么”。订阅是“谁在什么时候拥有什么”。把两者分开,升级与取消就不再可怕。

一个实用模式是:每个账户一条 subscription,再在其下有多条 subscription_items(一个基础计划加零或多条附加项)。在计划与权限数据库模式中,这提供了在不改写访问规则的情况下记录时间线的清晰位置。

建模购买时间线的核心表

可以用两张易于查询的表保持简单:

  • subscriptions:id、account_id、status(active、trialing、canceled、past_due)、started_at、current_period_start、current_period_end、canceled_at(nullable)
  • subscription_items:id、subscription_id、item_type(plan、addon)、plan_id/addon_id、quantity、started_at、ends_at(nullable)、source(stripe、manual、promo)

一个常见细节:为每个 item 存各自的日期。这样你可以授予一个仅 30 天的附加项,或允许计划在客户取消后继续运行到已付周期末。

不要把计费与访问混在一起

计费中的换算、发票和重试是计费问题。功能访问是权限问题。不要试图从发票行来“算访问”。

相反,让计费事件更新订阅记录(例如延长 current_period_end、创建新的 subscription_item 行或设置 ends_at)。应用随后从订阅时间线(或之后的权限层)回答访问问题,而不是依赖计费计算。

无惊喜的定时变更

升级与降级常在特定时刻生效:

  • 在 subscriptions 表上添加 pending_plan_idchange_at 来处理单个定时计划变更。
  • 或者如果你需要历史与多个未来变更,使用 subscription_changes 表(subscription_id、effective_at、from_plan_id、to_plan_id、reason)。

这样可以防止把“降级在周期末生效”这种规则硬编码到代码的随机角落:调度本身就是数据。

试用如何适配

试用只是有时间界限的访问且来源不同。两种干净的选项:

  • 把试用当作订阅状态(trialing),并带有 trial_start/trial_end 日期。
  • 或者为试用创建授予的条目/权限,带有 started_at/ends_at 且 source = trial。

如果在 AppMaster 中实现,这些表能整齐映射到 Data Designer(PostgreSQL),日期使得“现在哪些在生效”变得无需特例即可查询。

逐步实现该模式

在所有地方使用一条访问规则
使用一个由 Web 与移动端共用的 Business Process 创建单一的“他们能否做到”检查。
开始构建

好的计划与权限数据库模式起于一条承诺:功能逻辑由数据驱动,而不是散落的代码路径。你的应用应该只问一个问题——“现在的有效权限是什么?”——并得到明确答案。

1) 用稳定键定义功能

创建一张 feature 表,含一个稳定且可读的 key,永远不要重命名(即便 UI 标签变了)。合适的 key 如 export_csvapi_calls_per_monthseats

添加类型,使系统知道如何处理该值:boolean(开/关)或 numeric(限制/配额)。保持简单与一致。

2) 将计划与附加项映射到权限上

现在你需要两个事实来源:计划包含什么,以及每个附加项授予什么。

一个简单可行的顺序:

  • 把所有功能放在一张 feature 表里,带稳定键与值类型。
  • 创建 planplan_entitlement,每行授予一个功能值(例如 seats = 5export_csv = true)。
  • 创建 addonaddon_entitlement 授予额外值(例如 seats + 10api_calls_per_month + 50000priority_support = true)。
  • 决定如何合并值:布尔通常用 OR,数字限制常用 MAX(取更高者),席位类数量通常用 SUM。
  • 记录权限的开始和结束时间,这样升级、取消与按比例计费不会破坏访问检查。

如果你在 AppMaster 构建,可以在 Data Designer 中模型化这些表,并把合并规则放在一个小的“策略”表或枚举里供 Business Process 使用。

3) 产出“有效权限”

你有两个选项:读取时计算(每次查询并合并)或在变更时生成缓存快照。对于大多数应用,快照更容易推理并且在负载下更快。

常见做法是建立一张 account_entitlement 表来存储每个功能的最终结果,并带有 valid_fromvalid_to

4) 用一条检查强制访问

不要把规则分散到屏幕、端点与后台任务里。在应用代码中放一条函数来读取有效权限并决策。

can(account_id, feature_key, needed_value=1):
  ent = get_effective_entitlement(account_id, feature_key, now)
  if ent.type == "bool": return ent.value == true
  if ent.type == "number": return ent.value >= needed_value

一旦所有地方都调用 can(...),升级与附加项就变成了数据更新,而不是需要重写的逻辑。

示例场景:升级加附加包,没有意外

一个 6 人的客服团队在 Starter 计划上。Starter 包含 3 个座席和每月 1000 条短信。他们在月中扩展到 6 人并想要额外的 5000 条短信包。你希望这在没有特例代码的情况下正常工作。

第 1 天:他们在 Starter 上开始

为该账户创建一条 subscription 并设定计费周期(例如 1 月 1 日到 1 月 31 日)。然后添加一条代表计划的 subscription_item

在结账时(或通过夜间任务),你为该周期写入权限授予:

  • entitlement_grantagent_seats,值 3,开始 1 月 1 日,结束 1 月 31 日
  • entitlement_grantsms_messages,值 1000,开始 1 月 1 日,结束 1 月 31 日

你的应用从不问“他们在什么计划?”,而是问“现在他们的有效权限是什么?”并得到 seats = 3,SMS = 1000。

第 15 天:当天升级到 Pro 并购买短信包

1 月 15 日他们升级到 Pro(包含 10 个座席和 2000 条短信)。你不修改旧的授予记录,而是追加新记录:

  • 关闭旧的计划条目:把 subscription_item(Starter)结束设为 1 月 15 日
  • 创建新的计划条目:subscription_item(Pro)开始 1 月 15 日,结束 1 月 31 日
  • 新增附加项条目:subscription_item(SMS Pack 5000)开始 1 月 15 日,结束 1 月 31 日

然后在相同周期追加授予:

  • entitlement_grantagent_seats,值 10,开始 1 月 15 日,结束 1 月 31 日
  • entitlement_grantsms_messages,值 2000,开始 1 月 15 日,结束 1 月 31 日
  • entitlement_grantsms_messages,值 5000,开始 1 月 15 日,结束 1 月 31 日

1 月 15 日当天发生了什么?

  • 座席:有效座席变为 10(你可以选择对座席使用“取最大值”的规则)。他们当天可以再添加 3 位。
  • 短信:有效短信在本周期剩余时间内变为 7000(对短信包使用“累加”的规则)。

现有使用数据不需要迁移。你的使用表继续计数发送的消息;权限检查只是把本周期的使用与当前有效限制比较。

第 25 天:安排降级,保留到周期结束

1 月 25 日他们安排在 2 月 1 日降级回 Starter。你不触碰 1 月的授予。你为下个周期创建未来条目(或未来授予):

  • subscription_item(Starter)开始 2 月 1 日,结束 2 月 28 日
  • 2 月 1 日没有短信包条目

结果:他们在 1 月 31 日前保持 Pro 座席和短信包的访问。2 月 1 日他们的有效座席降到 3,短信恢复到 Starter 的新周期限制。这很容易推理,也可以在 AppMaster 的无代码工作流中良好映射:改变日期就创建新行,权限查询保持不变。

常见错误与陷阱

把模式变成表
在 AppMaster Data Designer 中几分钟内把文章里的表结构变成表。
建模数据

大多数订阅错误不是计费错误,而是访问错误,源于逻辑散落在产品各处。破坏计划与权限数据库模式的最快方式是用五个不同地方来回答“他们能用吗?”这个问题。

一个经典失败是把规则写死在 UI、API 和后台任务里。UI 隐藏了按钮,API 忘记阻止端点,夜间任务仍然运行因为它检查了别的东西。你会收到“有时能用”的模糊报告,难以复现。

另一个陷阱是用 plan_id 检查代替功能检查。起初感觉简单(Plan A 可以导出,Plan B 不行),但一旦加入附加项、保留旧制、试用或企业例外,这种做法就崩溃。如果你曾说过“if plan is Pro then allow…”,你就在建一个你要永远维护的迷宫。

时间与取消的边缘情况

当你只存一个布尔字段如 has_export = true 且不附带时间时,访问会“卡住”。取消、退款、拒付和周期中降级都需要时间界限。没有 starts_atends_at,你会错误地授予永久访问,或过早撤销访问。

避免的方法很简单:

  • 每条权限授予必须有来源(计划、附加项、人工覆盖)和时间范围。
  • 每次访问决策都应使用“现在是否在开始与结束之间”(对 null end 有明确规则)。
  • 后台任务在运行时应重新检查权限,而不是假设昨天的状态仍然有效。

不要混淆计费与访问

团队也经常把计费记录与访问规则混为一谈。计费需要发票、税项、按比例计算、第三方 ID 和重试状态。访问需要清晰的功能键与时间窗。纠缠在一起时,计费迁移可能变成产品停机。

最后,很多系统跳过审计轨迹。当用户问“我为什么能导出?”时,你需要一句话的答案:"由附加项 X 启用,生效期 2026-01-01 到 2026-02-01" 或者 "由支持人工授予,工单 1842"。没有这些,支持与工程只能猜。

如果你在 AppMaster 构建,记得在 Data Designer 模型里保留审计字段,并把“他们能吗?”的检查做成一个被 Web、移动与定时流程共用的 Business Process。

发版前的快速清单

把权限建成数据
在 PostgreSQL 中用可视化工具把计划、附加项和权限建成数据,而不是写死检查。
试用 AppMaster

在发版前用真实问题再检查一遍,目标是让访问可解释、可测试且易于变更。

现实检验问题

挑一个用户和一个功能,用你会告诉支持或财务的方式来解释结果。如果你只能回答“他们在 Pro 上”(或更糟,“代码是这么写的”),当某人中途升级或得到一次性交易时你就会痛苦。

用这份快速清单:

  • 你能否只用数据(订阅条目、附加项、覆盖项与时间窗)回答“为什么这个用户有访问?”而不去读应用代码?
  • 所有访问检查是否基于稳定的功能键(如 feature.export_csv)而非计划名(如“Starter”或“Business”)?计划名会变,功能键不该变。
  • 权限是否有清晰的开始与结束时间,包括试用、宽限期与定时取消?如果缺时间,降级会成为争议点。
  • 你能否用一条覆盖记录为某客户授予或移除访问,而不改分支逻辑?这是处理“本月给他们额外 10 个席位”这类需求的方式。
  • 你能否用几个样例行测试一次升级和一次降级并得到可预测结果?如果需要复杂脚本来模拟,你的模型就太隐含了。

一个实用测试:创建三位用户(新用户、中途升级、已取消)和一个附加项(比如“额外席位”或“高级报表”),然后对每个运行你的访问查询。如果结果清晰且可解释,你就准备好了。

在像 AppMaster 这样的工具中,保持相同原则:让一个查询或一个 Business Process 负责“他们能吗?”,这样所有 Web 与移动端屏幕都用同一个答案。

下一步:让升级易于维护

保持升级可控的最好方法是从比你想象中更小的范围开始。挑出真正驱动价值的一小组功能(5–10 个足够),并构建一条回答单一问题的权限查询或函数:"这个账户现在能做 X 吗?" 如果你不能在一个地方回答它,升级永远会感觉有风险。

当那条检查可用后,把升级路径当作产品行为,而不仅仅是计费行为。捕捉奇怪边缘情况的最快方法是写一小套基于真实客户变动的访问测试。

这里有些实用的后续步骤,通常能立即产生回报:

  • 定义最小功能目录并把每个计划映射到一组明确的权限。
  • 把附加项作为授予或扩展权限的独立“条目”加入,而不是把它们烙进计划规则里。
  • 为常见路径写 5–10 个访问测试(周期中升级、续费时降级、添加后移除附加项、试用转正、宽限期)。
  • 把定价改动做成纯数据:更新计划行、功能映射与权限授予,而不是改应用代码。
  • 养成习惯:每个新计划或附加项都必须附带至少一个新的测试以证明访问行为符合预期。

如果你使用无代码后端,也能清晰地建模这个模式。在 AppMaster 中,Data Designer 非常适合构建核心表(plans、features、subscriptions、subscription items、entitlements)。然后 Business Process Editor 可以承载访问决策流程(加载活跃权限、应用时间窗、返回允许/拒绝),避免你在端点中手工分散实现检查。

当下一次定价变动时,回报会显现:你无需重写规则,只需编辑数据:某功能从“Pro”变成了一个附加项,权限时长改变,或遗留计划保留旧授予。你的访问逻辑保持稳定,升级成为受控更新,而非一次代码冲刺。

如果你想快速验证你的模式,试着端到端建模一次升级加一次附加项,然后在添加其他东西前运行那些访问测试。

容易上手
创造一些 惊人的东西

使用免费计划试用 AppMaster。
准备就绪后,您可以选择合适的订阅。

开始吧