fix(subscription): record audit logs and lock user row on admin grant - #6325
Open
YiKongk wants to merge 1 commit into
Open
fix(subscription): record audit logs and lock user row on admin grant#6325YiKongk wants to merge 1 commit into
YiKongk wants to merge 1 commit into
Conversation
Admin subscription grants left no trace: neither a manage log for the target user nor an operator audit entry was written, even though the grant can also upgrade the user's group. The reset handlers in the same file already do both. Both new logs store a language-neutral op descriptor (action + params) with an English fallback content, like the other audit logs, so the frontend localizes them per viewer instead of freezing one language into the database. The MaxPurchasePerUser count in CreateUserSubscriptionFromPlanTx also ran without a row lock, so two concurrent grants could both read count-1 and both insert. Locking the user row serializes them; the limit check itself is unchanged. This covers the order, balance and admin paths at once. Locking the row also rejects grants for a non-existent user up front instead of creating an orphan subscription. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
11 tasks
Contributor
WalkthroughSubscription grants now lock target users during purchase-limit checks, record operation and management audit entries, and render localized grant messages with plan and user metadata. ChangesSubscription Grant Flow
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Admin
participant SubscriptionController
participant SubscriptionModel
participant AuditLog
participant UsageLogs
Admin->>SubscriptionController: grant subscription plan
SubscriptionController->>SubscriptionModel: bind subscription with user-row lock
SubscriptionModel-->>SubscriptionController: successful binding
SubscriptionController->>AuditLog: record subscription.granted and subscription.admin_grant
AuditLog->>UsageLogs: render localized audit content
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
YiKongk
marked this pull request as ready for review
July 19, 2026 23:20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important
📝 变更描述 / Description
问题一:管理员分配订阅无任何日志记录。
管理员通过
/api/subscription/admin/bind或/api/subscription/admin/users/:id/subscriptions为用户直接开通订阅时,系统不写入任何日志:被操作用户的 manage 日志与操作者的审计日志均缺失。同文件中的订阅重置操作(AdminResetUserSubscriptionsByPlan)已实现两类日志的写入,此处属于遗漏。由于套餐配置upgrade_group时,开通订阅会同步变更用户分组(即权限等级变更),缺失日志意味着该类变更无法追溯操作者、时间与依据。问题二:
MaxPurchasePerUser限购校验存在并发绕过。CreateUserSubscriptionFromPlanTx的限购校验为先COUNT后INSERT,中间无锁。两个并发请求可同时读到count == max-1并各自插入,最终超出限购上限。余额购买路径(PurchaseSubscriptionWithBalance)因事先锁定 user 行而不受影响;管理员分配路径无锁,可被绕过。具体改动
model/subscription.go:在限购COUNT之前通过lockForUpdate锁定 user 行。订单回调、余额购买、管理员分配三条开通路径均收敛于CreateUserSubscriptionFromPlanTx,一处加锁即可全覆盖;限购判断逻辑本身未做任何修改。余额购买路径会对同一行重复加锁,行锁可重入,无副作用。controller/subscription.go:新增recordSubscriptionGrantLogs,参照AdminResetUserSubscriptionsByPlan的模式写入两类日志。两条日志均采用项目现有的 op 描述符模式(action + 结构化 params,Content仅存英文兜底文本):用户日志 action 为subscription.granted,操作者审计为subscription.admin_grant。前端按查看者语言在渲染期本地化,不将写库时的语言固化到数据库。controller/audit.go:注册subscription.admin_grant与subscription.granted两个英文兜底模板。web/default:在AUDIT_TEMPLATES注册上述两个 action 的渲染模板,并在 7 个语言文件中按localeCompare顺序插入对应文案。经典前端(web/classic)展示英文兜底文本,与其他审计日志行为一致。行为变化说明
加锁需要先
SELECT到 user 行,因此为不存在的用户 ID 分配订阅将直接返回错误,而非创建一条无归属的订阅记录。该行为已通过测试固定。🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
新增
model/subscription_admin_grant_test.go:go build ./...、go vet、go test ./model/ ./controller/均通过;web/default的bun run typecheck与bun run build通过,7 个 locale JSON 校验合法。行锁在测试中通过 dry-run session 断言生成的 SQL 包含
FOR UPDATE(沿用model/locking_test.go的做法)。测试环境为内存 SQLite,lockForUpdate在 SQLite 下按设计跳过,故并发效果无法在单元测试中直接验证。(部署验证截图:管理员分配订阅后,被操作用户日志与操作者审计日志均可查询,见下方评论区补充。)