news 2026/9/13 18:20:04

Activepieces Autumn 计费 Schema 演进实践:以全新增量迁移实现零破坏回滚

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Activepieces Autumn 计费 Schema 演进实践:以全新增量迁移实现零破坏回滚

Activepieces Autumn 计费 Schema 演进实践:以全新增量迁移实现零破坏回滚

【免费下载链接】activepiecesAI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces

导读

本文围绕 Activepieces 仓库中的设计决策文档 000019-autumn-platform-plan-schema-ships-additively.md 展开,剖析 Autumn 计费分支重构platform_plan表时采用的核心策略:把原本包含六条迁移(其中三条是破坏性的)的 schema 变更,压缩为一条完全增量的迁移,从而让"回滚 PR"退化为一次纯代码回滚、无需对生产库做任何外科手术。读完本文,你将掌握增量迁移的设计权衡、breaking标记的语义、旧列"退而不删"的双向兼容技巧,以及如何通过"先增量后清理"的两阶段拆分来管理回滚窗口。

背景:为什么 Autumn 计费需要动platform_plan

Activepieces 的计费体系基于platform_plan表(实体定义见 platform-plan.entity.ts),它保存每个平台(Platform)的套餐配置:功能开关(如auditLogEnabledssoEnabledscimEnabled)、各类额度(projectsLimitactiveFlowsLimit)、以及计费侧信息(autumnCustomerIdautumnApiKey等)。

当计费系统从 Stripe + OpenRouter 信贷体系切换到 Autumn(由autumn-jsSDK 与 Autumn Console 后端组成,见 autumn-utils.ts)时,schema 需要做以下调整:

  • 新增autumnCustomerIdautumnApiKey,用于标识平台在 Autumn 侧的客户身份;
  • 新增usersLimitscheduledUsersLimit,承载用户数额度及"计划内已排期的降级"对应的用户上限(scheduledUsersLimit由 Autumn 订阅状态为scheduled的项解析而来,见toScheduledUsersLimit);
  • 新增includedCredits,取代旧的includedAiCredits
  • teamProjectsLimit从 varchar 枚举(NONE/ONE/UNLIMITED)转换为整数字段billedTeamProjectsLimit

决策文档所解决的问题:Autumn 分支最初携带六条迁移,其中三条是破坏性的——删除 Stripe 相关列、删除旧的 OpenRouter AI 信贷列、重命名includedAiCredits,外加一次在位的teamProjectsLimit枚举→整数类型转换。一旦 PR 合入后被要求回滚,仅回滚代码是不够的:新代码写入的 schema 旧代码无法理解,反过来旧代码也无法正确读取新 schema 写出的数据。代码与 schema 的耦合让"回滚 PR"变成了"回滚 PR + 对生产库执行down()迁移"的高风险操作。

决策核心:一条迁移承载全部变更,breaking = false

决策的结论非常明确:整个 schema 变更以一条完全增量的迁移合入,迁移名为AddAutumnBillingColumnsToPlatformPlan(文件 1818000000000-AddAutumnBillingColumnsToPlatformPlan.ts),这样回滚 PR 就是一次纯代码回滚,数据库零操作。

迁移类头部声明了它的三个关键元数据:

export class AddAutumnBillingColumnsToPlatformPlan1818000000000 implements Migration { name = 'AddAutumnBillingColumnsToPlatformPlan1818000000000' breaking = false release = '0.86.4' transaction = true // ... }

其中breakingrelease字段定义在迁移基类 migration.ts 中,并被 postgres-connection.ts 注册进迁移列表(AddAutumnBillingColumnsToPlatformPlan1818000000000位于第 859 行附近)。breaking = false即"本次迁移不会破坏旧代码读取数据库"的官方声明,是评审与回滚策略判断的第一依据。

增量迁移的内部结构:新增、回填、补默认值

up()方法严格按照"只加不改"的原则编排,分三步走:

第一步:新增列(全部可空或带默认值)

ALTER TABLE "platform_plan" ADD COLUMN IF NOT EXISTS "autumnCustomerId" character varying ALTER TABLE "platform_plan" ADD COLUMN IF NOT EXISTS "autumnApiKey" character varying ALTER TABLE "platform_plan" ADD COLUMN IF NOT EXISTS "usersLimit" integer ALTER TABLE "platform_plan" ADD COLUMN IF NOT EXISTS "scheduledUsersLimit" integer ALTER TABLE "platform_plan" ADD COLUMN IF NOT EXISTS "includedCredits" integer NOT NULL DEFAULT 0 ALTER TABLE "platform_plan" ADD COLUMN IF NOT EXISTS "billedTeamProjectsLimit" integer

注意:includedCredits是唯一带NOT NULL DEFAULT 0的新列——因为它需要立即具备合法值;其余新列均可空,给旧代码足够宽容度。ADD COLUMN IF NOT EXISTS保证了迁移可安全重放。

第二步:回填存量数据

UPDATE "platform_plan" SET "includedCredits" = "includedAiCredits"

includedCredits从旧列includedAiCredits回填,includedAiCredits本身保留不删——旧代码继续读写它,新代码则使用新列。

团队项目数上限则用CASE把旧枚举映射为整数:

UPDATE "platform_plan" SET "billedTeamProjectsLimit" = ( CASE "teamProjectsLimit" WHEN 'NONE' THEN 0 WHEN 'ONE' THEN 1 ELSE NULL END )

映射语义为:NONE → 0(零个团队项目)、ONE → 1(一个团队项目)、UNLIMITED → NULLNULL表示无上限)。这条映射与项目创建时的校验逻辑完全对应——platform-project-controller.ts 第 150–168 行中,billedTeamProjectsLimitNULL<= 0时不限制(或按 0 处理),否则projectsCount >= billedTeamProjectsLimit即拒绝创建并提示升级套餐。

第三步:给旧代码仍会写入的 NOT NULL 列补默认值

新实体(PlatformPlanEntity)不再写这些旧列,但它们都是 NOT NULL,若旧代码在回滚后继续插入/更新却拿不到值,就会失败。因此迁移为它们补上默认值,使新旧两版代码在任何方向上的插入都能成功

ALTER TABLE "platform_plan" ALTER COLUMN "includedAiCredits" SET DEFAULT 0 ALTER TABLE "platform_plan" ALTER COLUMN "aiCreditsAutoTopUpState" SET DEFAULT 'disabled' ALTER TABLE "platform_plan" ALTER COLUMN "agentsEnabled" SET DEFAULT true ALTER TABLE "platform_plan" ALTER COLUMN "teamProjectsLimit" SET DEFAULT 'NONE'

这四列正是滚动部署(rolling deploy)与回滚场景的"兼容面":不管集群中跑的是新代码还是旧代码,INSERT 都能得到合法值。

down()方法则严格逆序撤销:先 DROP 四个 DEFAULT,再按依赖顺序DROP COLUMN IF EXISTS删除六个新列——由于新列全部是"后加的",down()本身也不会触碰任何旧列,因此即便回滚迁移也是安全的。

新列与旧列如何共存:实体层的双向兼容声明

增量迁移的精髓在于:新代码的实体既声明新列,也声明旧列,但旧列只是"占位声明"。

platform-plan.entity.ts 中定义了一个专门的RetiredPlatformPlanColumns类型(第 20–34 行),其注释明确指出:

Columns the Autumn migration deliberately leaves in the database so a revert to pre-Autumn code still finds the schema it expects (decision 000019). Never read or written by this codebase — declared only so the entity matches the migrated schema.

即:这些旧列(stripeCustomerId系列、aiCreditsAutoTopUpState系列、includedAiCreditsteamProjectsLimit等)被故意保留在库里,新代码绝不读写它们,仅仅声明出来让 TypeORM 实体与实际 schema 对齐。每个旧列在实体中都以@deprecated注释标注,指向RetiredPlatformPlanColumns,等待清理迁移统一删除。

新实体正式使用的计费列包括:

列名类型默认值语义
includedCreditsNumber0套餐包含的信贷额度
billedTeamProjectsLimitNumber(可空)计费团队项目数上限,NULL表示无上限
usersLimitNumber(可空)用户数上限
scheduledUsersLimitNumber(可空)排期变更后的用户数上限
autumnCustomerIdString(可空)Autumn 侧客户 ID
autumnApiKeyString(可空)Autumn 侧 API Key

唯一的刻意例外:feature id 与列名的解耦

决策文档明确强调了一个"故意的例外":Autumn 的 feature id 仍是teamProjectsLimit,但投影到plan.billedTeamProjectsLimit

理由很实际:teamProjectsLimit作为 feature id 出现在 Autumn 控制台每一个套餐的条目里(见UnconsumableFeatureId.TEAM_PROJECTS_LIMIT),重命名它就要改动线上全部套餐数据;而列名只存在于本仓库的 schema 中,改名成本低。于是"id 等于列名"这条惯例在这里被打破一次,换取的是:feature id 稳定、列名语义准确

这种映射在 autumn-utils.ts 的mapAutumnFeaturesToPlatformPlan(第 194–208 行)中落地:

const teamProjects = entitlements.balances[UnconsumableFeatureId.TEAM_PROJECTS_LIMIT] // ... billedTeamProjectsLimit: toPlatformPlanLimit(teamProjects, 1), usersLimit: toPlatformPlanLimit(users, null), scheduledUsersLimit: entitlements.scheduledUsersLimit, activeFlowsLimit: toPlatformPlanLimit(activeFlows, null), includedCredits: credits?.granted ?? 0,

toPlatformPlanLimit的语义(第 528–536 行):余额不存在时返回兜底值(团队项目兜底1,用户数兜底null);unlimited时返回null;否则返回granted。这解释了为什么回填映射里UNLIMITED → NULL——NULL在代码里就是"无上限"的统一表示。

为何不做另外两个方案:被否决的设计

决策文档记录了被否决的两个备选方案,理解它们能更清楚增量迁移的取舍:

否决一:在位类型转换(原设计)

直接把teamProjectsLimit从 varchar 原地转成 integer。缺点正如文档所说:纯代码回滚后,旧代码会读到整数却期待NONE/ONE/UNLIMITED字符串——旧代码对新 schema 的"误读"是静默的、危险的。增量方案让新列以全新名字出现,旧代码永远不会去读它。

否决二:过渡列名 + 实体name:覆盖

例如先建一个teamProjectsLimitNumeric列,再用实体的name:覆盖映射成想要的属性名。文档评价它"可行,但留下了清理 PR 里的重命名步骤,以及期间隐藏的属性↔列不一致";相比之下,直接采用明确的领域新名字billedTeamProjectsLimit作为永久名字,一步到位,不留改名尾巴。

后续清理 PR:两阶段拆分如何关闭回滚窗口

增量迁移的代价是库里同时存在新旧两套列。决策文档明确要求:在发布版本 soak(充分观察)之后、尽快提交一个清理 PR,把不再使用的列删掉。这就是"两阶段拆分":

第一阶段(本决策):只加不删,breaking = false,回滚窗口完全开放;第二阶段(清理 PR):删除旧列,breaking = true,回滚窗口正式关闭。

清理 PR 的删除清单(与原文档一致,可作为实现检查表):

  • 删除 Stripe 列:stripeCustomerIdstripeSubscriptionIdstripeSubscriptionStatusstripeSubscriptionStartDatestripeSubscriptionEndDatestripeSubscriptionCancelDate
  • 删除 OpenRouter AI 信贷自动充值列:aiCreditsAutoTopUpStateaiCreditsAutoTopUpThresholdaiCreditsAutoTopUpCreditsToAddmaxAutoTopUpCreditsMonthlylastFreeAiCreditsRenewalDate
  • 删除includedAiCreditsagentsEnabled
  • 删除旧的 varcharteamProjectsLimit(连同其过渡默认值'NONE')。

billedTeamProjectsLimit保留原名,不做 rename-back——因为第一阶段就已经用了永久名,清理阶段无需任何重命名操作。实体中的RetiredPlatformPlanColumns类型也会随清理迁移一并移除。

这套"先增量、后清理"的节奏,把破坏性变更的风险窗口压缩在两次发布之间:发布后若发现 Autumn 计费有问题,直接回滚代码即可,数据库无需任何操作;确认稳定后,再通过breaking = true的清理迁移彻底移除旧列。

源码验证路径小结

  • 决策原文:000019-autumn-platform-plan-schema-ships-additively.md;
  • 增量迁移实现:1818000000000-AddAutumnBillingColumnsToPlatformPlan.ts(breaking = false,注册于 postgres-connection.ts 第 859 行附近);
  • 实体声明与旧列占位:platform-plan.entity.ts(RetiredPlatformPlanColumns第 20–34 行);
  • Autumn feature→plan 投影:autumn-utils.ts(mapAutumnFeaturesToPlatformPlan第 194–208 行、toPlatformPlanLimit第 528–536 行);
  • 额度落地校验:platform-project-controller.ts 第 150–168 行。

适用前提与限制

本文描述的 schema 演进策略是 Activepieces 在 Autumn 计费迁移(release0.86.4)中的内部实践。增量迁移的收益(零 DB 手术回滚)成立的前提是:迁移只加列、回填、补默认值,且新旧两版代码对共存列都有兼容的读写路径。如果你的场景需要删除列、改主键或做 NOT NULL 收紧,则不能照搬此模式;此时应把破坏性步骤推迟到独立的、breaking = true的后续迁移中,并以充分的发布观察期换取回滚窗口——这正是本决策文档给出的方法论。

【免费下载链接】activepiecesAI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!