Dev Log: 2026-07-07
Dev Log: 2026-07-07
TL;DR: Collapsed a billing model from à-la-carte features to plans-only, in four safe phases. Unified authorization across web, API, and MCP so all three obey one permission layer. Fixed a legacy Oracle password-sync writing to the wrong column. Four repos moved today. Here’s the thread that ties most of them together: one source of truth beats two.
简述: 将计费模式从“单项功能点选”简化为“套餐制”,并分四个安全阶段完成。统一了 Web、API 和 MCP 的授权逻辑,确保三者遵循同一权限层。修复了一个旧版 Oracle 密码同步写入错误列的 Bug。今天迁移了四个代码库。将这些工作串联起来的核心逻辑是:单一事实来源胜过多个来源。
Billing: plans-only
Spent most of the day migrating a SaaS off per-feature à-la-carte subscriptions and onto plans-only entitlements. The interesting part isn’t the model — it’s doing it without a billing outage: seed plans, switch reads to plans, backfill every org, then delete the old machinery. Expand/contract, four deployable phases. Full write-up in the focused post.
计费:仅限套餐制
今天大部分时间都在将一个 SaaS 产品从“按功能点选”的订阅模式迁移到“套餐制”授权模式。有趣的部分不在于模式本身,而在于如何在不中断计费的情况下完成迁移:先植入套餐数据,将读取逻辑切换到套餐,为所有组织回填数据,最后删除旧的机制。通过“扩展/收缩”的四个可部署阶段完成。详细内容请见专题文章。
One permission layer for three surfaces
An ops tool exposed the same actions three ways — web UI, API, and an MCP server for agent access. The bug: each surface checked authorization slightly differently, so an MCP tool could allow something the web UI blocked. The fix was to make the MCP tools gate on the same permission layer as everything else, so: web ─┐ API ─┼─► one permission check ─► allow / deny MCP ─┘ TL;DR: web ≡ API ≡ MCP — three doors, one lock. Also added a dedicated support-engineer role scoped for debugging without handing over the keys to everything, plus identity/diagnostics/SLA read tools so an agent can answer “why didn’t this notification send?” without shell access.
三个入口,统一权限层
一个运维工具通过三种方式暴露了相同的操作——Web UI、API 和用于 Agent 访问的 MCP 服务器。Bug 在于:每个入口的授权检查方式略有不同,导致 MCP 工具可能会允许 Web UI 禁止的操作。修复方案是让 MCP 工具与其他入口使用相同的权限层,即:
Web ─┐
API ─┼─► 统一权限检查 ─► 允许 / 拒绝
MCP ─┘
简而言之:Web ≡ API ≡ MCP —— 三扇门,一把锁。此外,还增加了一个专门的 support_engineer 角色,用于在不授予全部权限的情况下进行调试,并添加了身份/诊断/SLA 读取工具,使客服人员无需 Shell 访问权限即可回答“为什么这条通知没发送?”之类的问题。
Legacy Oracle password sync
Smaller but sharp: a password reset was writing to the wrong Oracle column and also touching a date_modified field it had no business updating. Routed the student reset to the correct password column and dropped the stray write. Lesson with legacy schemas — the column that looks right and the column the app actually reads from are not always the same. Confirm against the read path, not the name.
旧版 Oracle 密码同步
一个虽小但棘手的问题:密码重置功能在写入时指向了错误的 Oracle 列,并错误地触碰了本不该更新的 date_modified 字段。已将学生重置逻辑路由到正确的密码列,并删除了多余的写入操作。关于旧版数据库模式的教训是:看起来正确的列与应用程序实际读取的列并不总是同一个。请务必根据读取路径而非列名进行确认。
Pricing config as one source of truth
A one-line fix, but a good reminder: a plan-tier flag (which tier unlocks a given capability) had drifted between the billing logic and the public pricing page. When entitlement rules live in two places, they will disagree. Keep the tier matrix in one config and read it everywhere.
定价配置作为单一事实来源
这是一个仅需一行代码的修复,但提醒了我们:套餐等级标志(即哪个等级解锁特定功能)在计费逻辑和公开定价页面之间出现了偏差。当授权规则存在于两个地方时,它们终将产生冲突。请将等级矩阵保存在一个配置中,并在各处统一读取。
Takeaway
Three of today’s four changes are the same idea wearing different clothes: remove the second source of truth. Two entitlement models → one plan. Three authorization paths → one permission layer. Two pricing configs → one. Duplication in rules is worse than duplication in code, because the copies drift silently until someone gets billed wrong or let in where they shouldn’t be.
总结
今天四项变更中的三项,本质上都是同一个想法的不同表现:消除第二个事实来源。两个授权模型 → 一个套餐;三个授权路径 → 一个权限层;两个定价配置 → 一个。规则的重复比代码的重复更糟糕,因为副本会悄无声息地产生偏差,直到有人被错误计费,或者被允许进入本不该进入的地方。