Skip to content

fix(billing): keep deleted groups out of subscription plan coverage - #1

Closed
SHLE1 wants to merge 4 commits into
mainfrom
fix/plan-group-references
Closed

SHLE1 wants to merge 4 commits into
mainfrom
fix/plan-group-references

Conversation

@SHLE1

@SHLE1 SHLE1 commented Oct 24, 2026

Copy link
Copy Markdown
Owner

概要

套餐记录的分组可以在分组被删除之后继续保留,订阅页于是把这个分组显示成 #15 这样的裸编号。实际触发场景是一个 Max 订阅,套餐里仍然列着已经删除的分组 15。

原因

分组删除走的是软删除(mixins.SoftDeleteMixin),行还留在表里,subscription_plan_groups.group_id 上的外键级联永远不会触发。其余环节都没有清掉这条引用:

  • NormalizePlanGroupIDs 只过滤非正数和重复值,套餐写入接受任何正整数。
  • GroupStore.DeleteCascade 清理分组授权和提供商关联,套餐侧不动。
  • 管理端编辑页保留套餐原有的分组选择,只为现存分组渲染复选框,于是这个编号在每次保存时被原样提交,界面上也没有可以取消它的地方。
  • EnrichSubscriptionPlanGroups 对任何无法利用的查询结果都保留只有 ID 的条目,SubscriptionsView.vue 把它渲染成 #{id}。

这个编号从来没有带来权限:额度判定读的是同一份列表,而没有任何分组能与它匹配。它占着套餐里的一个位置,还让订阅者看到一个没有信息量的标签。

改动

  • 管理端编辑页(PlanEditDialog.vue):分组列表加载成功后,不在列表中的编号连同它们的倍率覆盖一起从草稿里移除,并提示移除了几个。停用的分组仍然可以勾选。加载失败时草稿保持不变;请求代次守卫保证迟到的旧响应不会用过期列表去裁剪新草稿。
  • 订阅展示(EnrichSubscriptionPlanGroups):只有存储明确报告分组不存在时才移除条目。返回空行、读取器缺失或读取失败都保留只有 ID 的条目,因为这些结果说明分组是否存在仍未确定。
  • 套餐写入(Plans.CreatePlan、Plans.UpdatePlan):提交的分组选择在落库前核对现存分组,指向已删除或不存在的分组时返回 GROUP_NOT_FOUND(404)。billing 定义 PlanGroupValidator,由 app 适配到 routing.ValidateGroupIDs;billing 不能直接 import routing,否则会经 provider 形成 import cycle。
  • 文档:docs/domains/payments_and_entitlements.md 记录这条规则和展示行为,docs/interfaces/http_api.md 补充两个套餐接口的 GROUP_NOT_FOUND 响应。

边界决定

  • 更新请求不带 group_ids 时保留原有分组选择,也不做分组查询。已经记录旧编号的套餐仍能改价格和额度,编辑页在下一次保存时顺带修复分组。
  • 不写数据迁移。把悬空编号清成空数组,会让受限套餐变成不限制分组,等于扩大现有订阅者的可用范围;这个决定应由运维人员做。
  • 删除分组时仍然不反写套餐。校验放在写入引用的位置,因为 routing 反向写套餐存储会把两个模块按错误的方向耦合起来。

验证

  • make test-go:后端全量测试,含 tests/integration
  • pnpm run test:run:400 个文件、3101 个测试通过
  • make lint-go:架构规则、文件布局检查、golangci-lint(0 issues)
  • make fmt-check、前端 typecheck、注释与 UI token 检查

新增覆盖:删除的分组连同倍率覆盖被移除并给出提示;停用分组及其覆盖保留;加载失败保留原有选择;迟到的旧响应不能裁剪新草稿;展示层移除确认不存在的分组,保留不可读或空行的分组;创建和更新拒绝指向已删除分组的列表,包含 legacy group_id 的合并,省略分组时不做查询。

上线处理

已部署实例的套餐里可能仍有悬空编号。订阅页不再显示它们,管理员下一次在编辑页保存时写入会得到修正。不经过编辑页的修复方式是把去掉该编号的 group_ids 提交到 PUT /api/v1/admin/payment/plans/{id}。

SHLE1 added 4 commits October 24, 2026 12:12
The plan editor loaded the stored group selection and submitted it
unchanged, while only rendering checkboxes for groups that still exist.
A group deleted after the plan was saved therefore stayed in the request
without any control that could remove it, so the plan kept naming a group
that no longer resolves.

Loading the group choices now prunes selected identifiers missing from
the loaded list together with their rate overrides, and reports how many
were dropped. A disabled group remains selectable, and a failed load
leaves the draft untouched so a temporary error cannot erase the
selection.
A plan keeps the group identifiers recorded when it was saved, so a group
deleted afterwards still appeared in a subscriber's plan card as a bare
identifier with no name. The lookup result was discarded, leaving an
identifier-only entry for every unresolved group.

Enrichment now removes an entry when storage reports that the group does
not exist, and keeps the identifier-only entry for a nil row or another
read failure, where the group's existence is undetermined.
Group deletion removes a group's permissions and provider memberships,
but plans keep the identifiers they recorded. A later plan save then
resubmitted a selection pointing at a group that no longer resolves, and
nothing rejected it because plan writes only deduplicate and drop
nonpositive identifiers.

Plan writes now confirm a declared group selection against stored groups
before persistence. CreatePlan checks the selection it normalizes, and
UpdatePlan checks the selection it is about to store, including a legacy
leading identifier. An update that leaves group_ids out keeps the
recorded selection and runs no lookup, so an unrelated edit still works
on a plan that already names a deleted group.
The plan and subscription documents described what a nonempty group
selection means without saying what happens when a selected group is
deleted. Add the write-time check, the display behaviour for a confirmed
missing group, and the editor repair, and list the plan endpoints with
their GROUP_NOT_FOUND response.
@SHLE1
SHLE1 force-pushed the fix/plan-group-references branch from fd7a6a0 to 819d2a9 Compare October 24, 2026 04:59
@SHLE1

SHLE1 commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Superseded by the upstream pull request: TokenFlux#1990. The branch stays on the fork until upstream merges, then the fork syncs from upstream.

@SHLE1 SHLE1 closed this Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant