Skip to content

feat(subscription): add admin quota reset actions - #5952

Merged
Calcium-Ion merged 3 commits into
QuantumNous:mainfrom
seefs001:feature/subscription-ops
Jul 7, 2026
Merged

feat(subscription): add admin quota reset actions#5952
Calcium-Ion merged 3 commits into
QuantumNous:mainfrom
seefs001:feature/subscription-ops

Conversation

@seefs001

@seefs001 seefs001 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

⚠️ 提交说明 / PR Notice

Important

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

📝 变更描述 / Description

(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)

针对订阅,增加对用户的订阅重置和针对套餐的全量用户重置操作。

🚀 变更类型 / Type of change

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

🔗 关联任务 / Related Issue

  • Closes # (如有)

✅ 提交前检查项 / Checklist

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

📸 运行证明 / Proof of Work

(请在此粘贴截图、关键日志或测试报告,以证明变更生效)

Summary by CodeRabbit

  • New Features
    • Added admin API routes and UI dialogs to reset subscription quota usage for a plan or for a specific user’s active subscriptions.
    • Introduced “Reset subscription quota” / “Reset quota” actions with a confirmation step and an option to advance the next reset time.
    • Returns and displays reset metrics, and refreshes subscription data after completion.
  • Documentation
    • Updated English/French/Japanese/Russian/Vietnamese/Chinese translations for the new reset flows.
  • Tests
    • Added admin reset subscription test coverage, including timestamp-advance and no-match/error scenarios.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds transactional admin subscription reset flows for plan-wide and per-user plan resets, with matching routes, audit entries, web actions/dialogs, and locale updates.

Changes

Admin Subscription Quota Reset

Layer / File(s) Summary
Model reset logic and result type
model/subscription.go
Adds SubscriptionResetResult and transactional reset routines that lock matching subscriptions, zero AmountUsed, and optionally advance reset timestamps.
Model reset tests
model/subscription_reset_test.go
Adds database tests covering successful resets, preserved timestamps, error handling, and no-match cases.
Controller handlers, audit templates, and routes
controller/subscription.go, controller/audit.go, router/api-router.go
Adds reset request handling, per-user/system logging, new audit templates, and two admin reset routes.
Web reset types and API helpers
web/default/src/features/subscriptions/types.ts, web/default/src/features/subscriptions/api.ts
Adds reset request/response types, extends the dialog union, and adds API helpers for both reset endpoints.
Reset actions and dialogs
web/default/src/features/subscriptions/components/data-table-row-actions.tsx, web/default/src/features/subscriptions/components/dialogs/reset-subscriptions-dialog.tsx, web/default/src/features/subscriptions/components/subscriptions-dialogs.tsx, web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx
Adds the reset action entry point and confirmation dialogs for plan-scoped and user-scoped resets.
Locale strings for reset UI
web/default/src/i18n/locales/{en,fr,ja,ru,vi,zh}.json
Adds and moves reset-related labels, prompts, plan-selection text, and audit-history wording across locales.

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

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant Router as api-router
  participant Controller as controller.subscription
  participant Model as model.subscription
  participant Audit as controller.audit

  Admin->>Router: POST /api/subscription/admin/plans/:id/subscriptions/reset
  Router->>Controller: AdminResetPlanSubscriptions(c)
  Controller->>Model: AdminResetPlanSubscriptions(planId, advanceResetTime)
  Model-->>Controller: SubscriptionResetResult
  Controller->>Audit: render subscription.plan_reset template
  Controller-->>Admin: JSON response
Loading

Possibly related PRs

  • QuantumNous/new-api#5462: Shares the same controller/audit.go template-rendering path for subscription audit content.
  • QuantumNous/new-api#5645: Also changes web/default/src/features/subscriptions/components/data-table-row-actions.tsx in the subscriptions action menu.
  • QuantumNous/new-api#5855: Updates the same historical audit-info warning copy in the locale files.

Suggested reviewers: Calcium-Ion

Poem

I hopped through plans with a click and a grin,
Reset the quota, let fresh usage begin.
Timestamps twinkled, then softly advanced,
With auditable trails where the bunnies all danced.
🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: new admin quota reset actions for subscriptions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
model/subscription.go (1)

1047-1063: 🚀 Performance & Scalability | 🔵 Trivial

Plan-wide reset loads all matching rows and issues one Save per subscription inside the transaction.

For a popular plan this materializes every active subscription into memory and performs N individual UPDATE statements while holding FOR UPDATE locks, producing a long-lived transaction and elevated lock contention. When advanceResetTime is false the update is uniform (amount_used = 0) and could be a single bulk UPDATE; when true, consider batching. Not blocking, but worth planning before this runs against large plans.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/subscription.go` around lines 1047 - 1063, The plan-wide reset in
adminResetPlanSubscriptionsTx currently loads every matching UserSubscription
and then calls resetUserSubscriptionTx per row, which causes N updates and long
lock holding. Refactor the reset path to avoid per-subscription Save in the
uniform case: when advanceResetTime is false, use a single bulk update for all
matching rows; when advanceResetTime is true, consider batching or a set-based
approach instead of materializing all rows. Keep the existing result-building
behavior in buildSubscriptionResetResult and preserve the locking/query criteria
around plan_id, status, and end_time.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@model/subscription.go`:
- Around line 1030-1033: Gate the locking behavior in the subscription reset
query so `FOR UPDATE` is only applied on dialects that support it. Update the
query built in `subscription.go` around the `tx.Set("gorm:query_option", "FOR
UPDATE")` usage to conditionally add the lock based on the current DB dialect,
and apply the same dialect-based fallback to the second reset query in the same
flow so SQLite/local `model.InitDB()` setups do not execute `SELECT ... FOR
UPDATE`.

In
`@web/default/src/features/subscriptions/components/dialogs/reset-subscriptions-dialog.tsx`:
- Around line 42-63: The reset flow in handleConfirm only shows an error in the
catch block, so a resolved response from resetPlanSubscriptions with success
false is ignored and the dialog appears to do nothing. Update handleConfirm in
reset-subscriptions-dialog.tsx to explicitly handle the non-throwing failure
path by checking res.success and showing a toast.error when it is false, while
keeping the existing success path (toast.success, triggerRefresh, setOpen(null))
unchanged.

In
`@web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx`:
- Around line 131-147: The `activePlanOptions` memo in
`user-subscriptions-dialog` is filtering out never-expiring subscriptions
because `(sub.end_time || 0) <= now` treats `end_time === 0` as expired. Update
the filtering logic in `activePlanOptions` to match the existing
`isExpired`/`isActive` convention used elsewhere in this component, so `end_time
=== 0` is treated as active. Keep the dedupe-by-`plan_id` behavior intact and
ensure unlimited/lifetime plans appear in the reset dropdown.

---

Nitpick comments:
In `@model/subscription.go`:
- Around line 1047-1063: The plan-wide reset in adminResetPlanSubscriptionsTx
currently loads every matching UserSubscription and then calls
resetUserSubscriptionTx per row, which causes N updates and long lock holding.
Refactor the reset path to avoid per-subscription Save in the uniform case: when
advanceResetTime is false, use a single bulk update for all matching rows; when
advanceResetTime is true, consider batching or a set-based approach instead of
materializing all rows. Keep the existing result-building behavior in
buildSubscriptionResetResult and preserve the locking/query criteria around
plan_id, status, and end_time.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1cf954e9-5317-4191-b492-259f56f5665c

📥 Commits

Reviewing files that changed from the base of the PR and between d0bd8aa and 560f60d.

📒 Files selected for processing (17)
  • controller/audit.go
  • controller/subscription.go
  • model/subscription.go
  • model/subscription_reset_test.go
  • router/api-router.go
  • web/default/src/features/subscriptions/api.ts
  • web/default/src/features/subscriptions/components/data-table-row-actions.tsx
  • web/default/src/features/subscriptions/components/dialogs/reset-subscriptions-dialog.tsx
  • web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx
  • web/default/src/features/subscriptions/components/subscriptions-dialogs.tsx
  • web/default/src/features/subscriptions/types.ts
  • 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.json

Comment thread model/subscription.go
Comment on lines +1030 to +1033
if err := tx.Set("gorm:query_option", "FOR UPDATE").
Where("user_id = ? AND plan_id = ? AND status = ? AND end_time > ?", userId, plan.Id, "active", now).
Order("end_time asc, id asc").
Find(&subs).Error; err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# How is FOR UPDATE / locking used elsewhere, and what DB do model tests run on?
rg -nP 'gorm:query_option|FOR UPDATE|clause\.Locking' --type=go -C2 -g '!**/vendor/**'
echo '--- test DB backend setup ---'
rg -nP 'sqlite|SQLITE|InitDB|TestMain|truncateTables' --type=go -g '!**/vendor/**' -C2 model/ | head -100

Repository: QuantumNous/new-api

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- model/subscription.go outline ---'
ast-grep outline model/subscription.go --view expanded | sed -n '1,220p'

echo '--- relevant lines around 980-1070 ---'
sed -n '980,1070p' model/subscription.go | cat -n

echo '--- search for SQLite / test DB setup ---'
rg -n --hidden -g '!**/vendor/**' -e 'sqlite|SQLITE|InitDB|TestMain|truncateTables|gorm.*sqlite|dsn' model . | sed -n '1,220p'

echo '--- search for locking APIs elsewhere ---'
rg -n --hidden -g '!**/vendor/**' -e 'gorm:query_option|FOR UPDATE|clause\.Locking|Locking\{' . | sed -n '1,220p'

Repository: QuantumNous/new-api

Length of output: 29058


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file list for model ---'
git ls-files model | sed -n '1,200p'

echo '--- size of model/subscription.go ---'
wc -l model/subscription.go

echo '--- snippets around the queried functions ---'
sed -n '1020,1068p' model/subscription.go | cat -n

Repository: QuantumNous/new-api

Length of output: 3473


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- locate subscription.go ---'
fd -a 'subscription.go$' . | sed -n '1,40p'

echo '--- locate test main / sqlite config ---'
rg -n --hidden -g '!**/vendor/**' -e 'sqlite|gorm.*sqlite|TestMain|InitDB|dsn|database/sql' . | sed -n '1,240p'

Repository: QuantumNous/new-api

Length of output: 6980


🌐 Web query:

SQLite FOR UPDATE support syntax error row-level locking documentation

💡 Result:

SQLite does not support the SELECT FOR UPDATE syntax [1][2]. Attempting to use this syntax in an SQLite database will result in a syntax error [1]. SQLite does not support row-level locking [3]. Instead, SQLite uses database-level locking [4][3], or in WAL (Write-Ahead Logging) mode, it allows for concurrent readers and a single writer, but still lacks the fine-grained row-level locking mechanisms found in client-server database systems [3][5]. To manage transaction behavior and concurrency in SQLite, consider the following approaches: 1. Transaction Modes: Use explicit transaction types to control locking behavior. - BEGIN IMMEDIATE: Starts a write transaction immediately, preventing other connections from starting a write transaction [6][7]. This can help avoid SQLITE_BUSY errors that might occur if a transaction is upgraded from a read to a write later [6][7]. - BEGIN EXCLUSIVE: Similar to IMMEDIATE but also prevents other connections from reading the database [7]. 2. WAL Mode: Enable Write-Ahead Logging to improve concurrency, allowing multiple readers to operate simultaneously with a writer, without blocking each other [5]. 3. Application-Level Logic: Since SQLite handles locking at the database level [3], race conditions must be managed by the application, often by ensuring write transactions are started early or by handling busy/retry logic via SQLite's busy_handler [3][8]. For authoritative details on SQLite's concurrency and locking, see the official documentation on File Locking and Concurrency [4] and Transaction behavior [6][7].

Citations:


Gate the FOR UPDATE lock by dialect. SQLite rejects SELECT ... FOR UPDATE, and model.InitDB() defaults to SQLite in local setups, so this path will fail there. Apply the same fallback to the second reset query below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/subscription.go` around lines 1030 - 1033, Gate the locking behavior in
the subscription reset query so `FOR UPDATE` is only applied on dialects that
support it. Update the query built in `subscription.go` around the
`tx.Set("gorm:query_option", "FOR UPDATE")` usage to conditionally add the lock
based on the current DB dialect, and apply the same dialect-based fallback to
the second reset query in the same flow so SQLite/local `model.InitDB()` setups
do not execute `SELECT ... FOR UPDATE`.

Source: Coding guidelines

Comment on lines +42 to +63
const handleConfirm = async () => {
if (!plan?.id) return
setResetting(true)
try {
const res = await resetPlanSubscriptions(plan.id, {
advance_reset_time: advanceResetTime,
})
if (res.success) {
toast.success(
t('Reset {{count}} active subscriptions', {
count: res.data?.reset_count || 0,
})
)
triggerRefresh()
setOpen(null)
}
} catch {
toast.error(t('Operation failed'))
} finally {
setResetting(false)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle res.success === false case.

If resetPlanSubscriptions resolves with success: false (rather than throwing), no error is surfaced to the user — the dialog just does nothing since only the catch block calls toast.error.

🐛 Proposed fix
       if (res.success) {
         toast.success(
           t('Reset {{count}} active subscriptions', {
             count: res.data?.reset_count || 0,
           })
         )
         triggerRefresh()
         setOpen(null)
+      } else {
+        toast.error(res.message || t('Operation failed'))
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleConfirm = async () => {
if (!plan?.id) return
setResetting(true)
try {
const res = await resetPlanSubscriptions(plan.id, {
advance_reset_time: advanceResetTime,
})
if (res.success) {
toast.success(
t('Reset {{count}} active subscriptions', {
count: res.data?.reset_count || 0,
})
)
triggerRefresh()
setOpen(null)
}
} catch {
toast.error(t('Operation failed'))
} finally {
setResetting(false)
}
}
const handleConfirm = async () => {
if (!plan?.id) return
setResetting(true)
try {
const res = await resetPlanSubscriptions(plan.id, {
advance_reset_time: advanceResetTime,
})
if (res.success) {
toast.success(
t('Reset {{count}} active subscriptions', {
count: res.data?.reset_count || 0,
})
)
triggerRefresh()
setOpen(null)
} else {
toast.error(res.message || t('Operation failed'))
}
} catch {
toast.error(t('Operation failed'))
} finally {
setResetting(false)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/subscriptions/components/dialogs/reset-subscriptions-dialog.tsx`
around lines 42 - 63, The reset flow in handleConfirm only shows an error in the
catch block, so a resolved response from resetPlanSubscriptions with success
false is ignored and the dialog appears to do nothing. Update handleConfirm in
reset-subscriptions-dialog.tsx to explicitly handle the non-throwing failure
path by checking res.success and showing a toast.error when it is false, while
keeping the existing success path (toast.success, triggerRefresh, setOpen(null))
unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx (2)

208-232: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use handleServerError for server error handling instead of a generic catch.

handleResetConfirm swallows all failures into a single t('Operation failed') toast, losing any server-provided validation detail (e.g. why the reset was rejected).

♻️ Proposed fix
     } catch {
-      toast.error(t('Operation failed'))
+      handleServerError(err)
     } finally {

As per coding guidelines, "Handle server errors with handleServerError, surface user-facing errors through toast.error... and map form/server validation errors to fields."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx`
around lines 208 - 232, The handleResetConfirm flow is catching every failure as
a generic toast, which hides server-provided validation details. Update
handleResetConfirm to use handleServerError for the resetUserSubscriptionsByPlan
request failure path, and surface the returned user-facing message through
toast.error instead of a fixed Operation failed string. Keep the success path
and state cleanup intact, and reference the existing
resetUserSubscriptionsByPlan and loadData flow when wiring the error handling.

Source: Coding guidelines


110-467: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Component file exceeds the 200-line guideline.

The component now spans 400+ lines with three inline dialogs, a large StaticDataTable column config, and multiple handlers. Consider extracting the reset-confirmation dialog (443-464) and/or the row-action menu (363-410) into dedicated subcomponents, mirroring how the plan-wide reset flow already lives in its own reset-subscriptions-dialog.tsx.

As per coding guidelines, "Keep files reasonably small; when a single file grows beyond about 200 lines, consider extracting subcomponents or custom hooks."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx`
around lines 110 - 467, The UserSubscriptionsDialog component is too large and
should be split into smaller pieces. Extract the reset-confirmation dialog
and/or the per-row action menu from UserSubscriptionsDialog into dedicated
subcomponents, and consider moving the table column/action logic into a helper
or hook so the main component stays under the file size guideline. Use the
existing UserSubscriptionsDialog, handleResetConfirm, handleConfirmAction, and
the row actions rendered inside StaticDataTable as the main extraction points.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx`:
- Around line 208-232: The handleResetConfirm flow is catching every failure as
a generic toast, which hides server-provided validation details. Update
handleResetConfirm to use handleServerError for the resetUserSubscriptionsByPlan
request failure path, and surface the returned user-facing message through
toast.error instead of a fixed Operation failed string. Keep the success path
and state cleanup intact, and reference the existing
resetUserSubscriptionsByPlan and loadData flow when wiring the error handling.
- Around line 110-467: The UserSubscriptionsDialog component is too large and
should be split into smaller pieces. Extract the reset-confirmation dialog
and/or the per-row action menu from UserSubscriptionsDialog into dedicated
subcomponents, and consider moving the table column/action logic into a helper
or hook so the main component stays under the file size guideline. Use the
existing UserSubscriptionsDialog, handleResetConfirm, handleConfirmAction, and
the row actions rendered inside StaticDataTable as the main extraction points.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cf316e5c-42e8-4e35-92b7-2f0d9edfc60e

📥 Commits

Reviewing files that changed from the base of the PR and between f602775 and 92bf914.

📒 Files selected for processing (1)
  • web/default/src/features/subscriptions/components/dialogs/user-subscriptions-dialog.tsx

@Calcium-Ion
Calcium-Ion merged commit 9b93d61 into QuantumNous:main Jul 7, 2026
1 check passed
liulixin-lex pushed a commit to liulixin-lex/xy-api that referenced this pull request Jul 7, 2026
* feat(subscription): add admin quota reset actions

* fix(subscription): keep quota reset in plan row actions

* refactor(subscription): move user subscription actions into menu
nsuanningmeng added a commit to nsuanningmeng/LemonHub that referenced this pull request Jul 7, 2026
SECURITY: pulls the critical quota-overflow billing fix (rc.18/rc.19).
Root cause: user-controlled billing multipliers (image `n`, video/task
`seconds`, NaN/Inf ratios) were unbounded and quota was cast float64->int
with a raw conversion. When baseQuota * ratios * count/duration exceeded
int32, the conversion wrapped to a NEGATIVE value, so consumption CREDITED
the account instead of debiting it -> abnormal balance growth for any user
who could call the relay API (unrelated to the payment/top-up gateway).

Fix set adopted: common.QuotaFromFloat saturating conversion (clamp int32,
NaN->0), MaxImageN=128, MaxTaskDurationSeconds=3600, AddOtherRatio NaN/Inf
guard, and saturating conversions across all billing paths (relay_task,
text_quota, task_billing, token_counter, tool_billing, kling, price,
billingexpr). Plus admin quota-reset disposal tooling (QuantumNous#5952) and quota
saturation audit logging.

Conflict resolutions (fork features preserved per abandon-or-adapt policy):
- Multi-site user isolation kept; upstream's global email-normalization +
  advisory-lock refactor (NormalizeEmail/EnsureEmailAvailable/BindEmailToUser/
  prepareForInsert/GetUniqueUserByEmail) dropped for model/user.go,
  controller/user.go, oauth.go, misc.go (architecturally incompatible with
  per-site email uniqueness; not the security fix).
- Redemption: combined upstream status filter + hardened atomic Redeem
  (row-lock + conditional update) with fork's site-scoped SearchRedemptions
  (keyword, status, startIdx, num, siteScope) and RedeemForSite.
- SSRF: kept fork ValidateRelayTargetURL + upstream GetSSRFProtectedHTTPClient.
- Grafted upstream password hardening (errUserPasswordUnset /
  errOriginalPasswordFail in checkUpdatePassword) onto fork user.go; passwordless
  users still set passwords via the site-scoped email reset flow.
- audit templates merged; web deps took upstream bumps (ai 6->7 etc.) + kept
  fork altcha; bun.lock regenerated.

Verified: go build ./... clean; common/relay/service/model/controller tests
pass; web/default build passes; SQLite boot + migrations clean.
LibraScorpion pushed a commit to LibraScorpion/new-api-nexusroute that referenced this pull request Jul 8, 2026
* feat(subscription): add admin quota reset actions

* fix(subscription): keep quota reset in plan row actions

* refactor(subscription): move user subscription actions into menu
xiaomingchen pushed a commit to xiaomingchen/new-api that referenced this pull request Jul 10, 2026
* feat(subscription): add admin quota reset actions

* fix(subscription): keep quota reset in plan row actions

* refactor(subscription): move user subscription actions into menu
Jacobinwwey pushed a commit to Jacobinwwey/new-api that referenced this pull request Jul 11, 2026
* feat(subscription): add admin quota reset actions

* fix(subscription): keep quota reset in plan row actions

* refactor(subscription): move user subscription actions into menu
ruanhangjian pushed a commit to ruanhangjian/new-api that referenced this pull request Jul 11, 2026
* feat(subscription): add admin quota reset actions

* fix(subscription): keep quota reset in plan row actions

* refactor(subscription): move user subscription actions into menu
zhaodechao2008 pushed a commit to zhaodechao2008/new-api that referenced this pull request Jul 27, 2026
* feat(subscription): add admin quota reset actions

* fix(subscription): keep quota reset in plan row actions

* refactor(subscription): move user subscription actions into menu
lizhongyi1209 pushed a commit to lizhongyi1209/new-api that referenced this pull request Jul 28, 2026
* feat(subscription): add admin quota reset actions

* fix(subscription): keep quota reset in plan row actions

* refactor(subscription): move user subscription actions into menu
330079598 pushed a commit to 330079598/new-api that referenced this pull request Aug 19, 2026
* feat(subscription): add admin quota reset actions

* fix(subscription): keep quota reset in plan row actions

* refactor(subscription): move user subscription actions into menu
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.

2 participants