Skip to content

feat(subscription): let admins set expiry, renew or replace on grant, and grant in bulk - #6324

Closed
YiKongk wants to merge 2 commits into
QuantumNous:mainfrom
YiKongk:feat/subscription-admin-grant-enhance
Closed

feat(subscription): let admins set expiry, renew or replace on grant, and grant in bulk#6324
YiKongk wants to merge 2 commits into
QuantumNous:mainfrom
YiKongk:feat/subscription-admin-grant-enhance

Conversation

@YiKongk

@YiKongk YiKongk commented Jul 19, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

依赖 #6323(本分支包含该 fix commit,为其直接后继;#6323 合并后本 PR 仅剩 feature commit)。

管理员手动绑定订阅原先只能按套餐自身时长新建一条记录。本 PR 补齐三个能力:

  1. 自定义到期时间:可传绝对 end_time 覆盖套餐默认时长(须晚于当前时间,否则拒绝)。
  2. 授予模式 mode
    • create(默认):与现状完全一致,新增记录;
    • renew:对同套餐现有活跃订阅原地 UPDATE 延长到期时间(不新增记录,因此不占用 MaxPurchasePerUser 名额;无活跃订阅时回退为新建)。已排定且仍有效的额度重置时间保持不变,避免自定义周期(custom)订阅在周期中途续期时重置计划漂移;
    • replace:先取消同套餐现有活跃记录,再插入新记录。取消+新建对活跃订阅数是净零,因此新建时跳过购买上限计数(上限查询本身未改动);同时将被取消记录的 PrevUserGroup 快照带入新记录,保证到期后用户仍能回退到原始分组。
  3. 批量授予POST /api/subscription/admin/bind/batch,逐用户独立事务,单个用户失败(上限、账号不存在)不回滚其他用户,按用户返回失败原因;failed 字段序列化为 [] 而非 null,前端结果视图可直接遍历。

前端:用户订阅弹窗与用户表格批量操作各新增授予模式与自定义到期时间选择;提示文案抽取为 getEndTimeHint 共享 helper;新增文案已按 localeCompare 排序插入全部 7 个语言文件。

说明:本 PR 代码为 AI 辅助生成(AI-assisted),已由提交者本地验证。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。(本描述由 AI 辅助撰写,如实说明,不勾选)
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

$ go build ./model/ ./controller/ ./router/ && go vet ./model/ ./controller/ ./router/
(exit 0)

$ go test ./model/ ./controller/ -count=1 -timeout 300s
ok  github.com/QuantumNous/new-api/model
ok  github.com/QuantumNous/new-api/controller

$ cd web/default && bun run typecheck && bun run build
(exit 0, 构建通过)

新增回归测试(model/subscription_admin_grant_test.go)覆盖:renew 不占用购买名额、renew 保持 custom 周期重置计划、replace 在用户已达 MaxPurchasePerUser 时成功且普通 create 仍被拒、replace 保留 PrevUserGroup 且到期后正确降组(经 ExpireDueSubscriptions 验证)、自定义到期时间生效且过去时间被拒、批量授予按用户报告失败、全部成功时 failed 序列化为 []

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added create, renew, and replace modes for administrator-granted subscriptions.
    • Added optional custom subscription expiration dates.
    • Added batch subscription assignment for multiple selected users, with success and failure reporting.
    • Added localized labels, descriptions, and status messages across supported languages.
  • Bug Fixes

    • Improved subscription limit handling and consistency during concurrent assignments.
    • Preserved subscription history and grouping information when replacing subscriptions.

YiKongk and others added 2 commits July 20, 2026 04:24
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.

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.
… and grant in bulk

Admin subscription grants could only create a new record following the
plan's own duration. Three gaps this closes:

- custom expiry: pass an absolute end time instead of the plan duration
- renew/replace: renew extends the existing active subscription in place
  (an UPDATE, so it does not consume another MaxPurchasePerUser slot) and
  falls back to creating one when there is none; replace cancels the
  existing active rows first, then inserts the new one with the purchase
  limit skipped (the swap is net-zero on active subscriptions) and the
  cancelled rows' PrevUserGroup carried over, so expiry can still revert
  the user to their origin group. Default stays "create", so existing
  behaviour is unchanged.
- batch: POST /subscription/admin/bind/batch grants one plan to several
  users, each in its own transaction, reporting per-user failures instead
  of rolling the whole set back. The failed list serializes as [] rather
  than null so the result dialog can index into it unconditionally.

Renew keeps an already-scheduled future quota reset so mid-cycle renewals
do not shift base-relative (custom-period) schedules; it only recomputes
the schedule when it was switched off or the new end time invalidates it
(calcNextResetTime zeroes the schedule when the next reset would fall past
the end time).

GetDBTimestamp read through the global DB handle, which checks out a
second pooled connection when called inside a transaction. The subscription
paths now read it through their tx, which also makes them testable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a8ae3ef4-ed75-462f-bbd7-417be2715568

📥 Commits

Reviewing files that changed from the base of the PR and between 5a6c53d and bd2e6eb.

📒 Files selected for processing (27)
  • controller/audit.go
  • controller/subscription.go
  • model/db_time.go
  • model/subscription.go
  • model/subscription_admin_grant_test.go
  • router/api-router.go
  • web/classic/src/components/table/users/modals/UserSubscriptionsModal.jsx
  • web/classic/src/i18n/locales/en.json
  • web/classic/src/i18n/locales/fr.json
  • web/classic/src/i18n/locales/ja.json
  • web/classic/src/i18n/locales/ru.json
  • web/classic/src/i18n/locales/vi.json
  • web/classic/src/i18n/locales/zh-CN.json
  • web/classic/src/i18n/locales/zh-TW.json
  • web/default/src/features/subscriptions/api.ts
  • web/default/src/features/subscriptions/components/dialogs/batch-assign-subscription-dialog.tsx
  • web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx
  • web/default/src/features/subscriptions/constants.ts
  • web/default/src/features/subscriptions/types.ts
  • web/default/src/features/users/components/data-table-bulk-actions.tsx
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh-TW.json
  • web/default/src/i18n/locales/zh.json

Walkthrough

Admin subscription grants now support create, renew, replace, custom expiration, and batch assignment. The changes update transactional model logic, admin APIs and auditing, classic and default management interfaces, routing, tests, and localization.

Changes

Subscription grant behavior

Layer / File(s) Summary
Grant modes and transaction handling
model/subscription.go, model/db_time.go, model/subscription_admin_grant_test.go
Adds create, renew, and replace semantics, custom expiration validation, row locking, group snapshot preservation, batch results, and coverage for these behaviors.
Admin API and auditing
controller/subscription.go, controller/audit.go, router/api-router.go
Passes grant options through admin requests, exposes batch binding, records grant audit/manage logs, and registers the batch endpoint.
Classic management UI
web/classic/src/components/table/users/modals/UserSubscriptionsModal.jsx, web/classic/src/i18n/locales/*
Adds grant-mode controls, custom expiration selection, contextual hints, and translations.

Default subscription management

Layer / File(s) Summary
Typed API and grant controls
web/default/src/features/subscriptions/types.ts, constants.ts, api.ts
Defines grant-mode and batch result contracts, shared translated options, expiration hints, and the batch API client.
Single-user and batch assignment flows
web/default/src/features/subscriptions/components/dialogs/*, web/default/src/features/users/components/data-table-bulk-actions.tsx
Adds grant-mode and expiration controls for individual grants, a batch assignment dialog with per-user results, and a users-table bulk action.
Default UI localization
web/default/src/i18n/locales/*
Adds translations for grant modes, expiration behavior, batch assignment, subscription labels, and success/failure summaries.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant BatchAssignSubscriptionDialog
  participant SubscriptionAPI
  participant AdminBindSubscriptionBatch
  participant AdminBindSubscription
  participant Database

  Admin->>BatchAssignSubscriptionDialog: Select users, plan, mode, and end time
  BatchAssignSubscriptionDialog->>SubscriptionAPI: Submit batch grant request
  SubscriptionAPI->>AdminBindSubscriptionBatch: POST batch grant
  AdminBindSubscriptionBatch->>AdminBindSubscription: Grant each user independently
  AdminBindSubscription->>Database: Create, renew, or replace subscription
  AdminBindSubscriptionBatch-->>SubscriptionAPI: Success and failure results
  SubscriptionAPI-->>BatchAssignSubscriptionDialog: Render batch outcome
Loading

Possibly related PRs

Suggested reviewers: calcium-ion

Poem

A rabbit grants plans with a hop and a cheer,
Renew, replace, or create them here.
Batch by batch, the records align,
Custom end times make schedules shine.
Logs softly thump like a burrowed drum—
Subscription magic: admin-approved fun!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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