预付款退款退还已使用额度 - #4355
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdded configuration storage and APIs for model display names and modalities, wired through option updates, cached exposure, and pricing/controller surfaces; minor commented-out quota counter calls were introduced in task billing functions. Changes
Sequence Diagram(s)sequenceDiagram
participant Admin
participant Controller
participant RatioSetting as "ratio_setting\n(config store)"
participant Cache
participant PricingModel as "model/pricing & dto"
Admin->>Controller: POST UpdateOption (ModelDisplayName/Modalities)
Controller->>RatioSetting: UpdateModelDisplayName/ModalitiesByJSONString
RatioSetting-->>Controller: OK / error
Controller-->>Admin: HTTP 200 (success/failure)
Note over RatioSetting,Cache: configuration updated
RatioSetting->>Cache: GetExposedData (includes new maps)
Cache-->>Controller: exposed data includes model_display_name/modalities
Controller->>PricingModel: ListModels / RetrieveModel
PricingModel->>RatioSetting: GetModelDisplayName / GetModelModalities
RatioSetting-->>PricingModel: display name / modalities
PricingModel-->>Controller: enriched model DTOs
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@service/task_billing.go`:
- Around line 167-169: In RefundTaskQuota and RecalculateTaskQuota replace calls
that pass a negative quota into UpdateUserUsedQuotaAndRequestCount (e.g.,
UpdateUserUsedQuotaAndRequestCount(task.UserId, -quota) and
UpdateUserUsedQuotaAndRequestCount(task.UserId, -quotaDelta)) with the
quota-only helper updateUserUsedQuota (or the public wrapper
updateUserUsedQuota() from model/user.go) so only used_quota is decremented and
request_count remains unchanged; also audit UpdateChannelUsedQuota for the same
request_count side-effect and, if it similarly mutates request_count, switch to
a channel-only quota update helper for channel updates (e.g., a dedicated
UpdateChannelUsedQuotaOnly) to ensure refunds do not decrement request_count.
🪄 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: 3b6a5be5-78a0-4503-8ebb-706049e13122
📒 Files selected for processing (1)
service/task_billing.go
| //3. 更新用户和渠道的使用额度 | ||
| model.UpdateUserUsedQuotaAndRequestCount(task.UserId, -quota) | ||
| model.UpdateChannelUsedQuota(task.ChannelId, -quota) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1. Show the implementation of UpdateUserUsedQuotaAndRequestCount and UpdateChannelUsedQuota.
ast-grep --pattern $'func UpdateUserUsedQuotaAndRequestCount($$$) {
$$$
}'
ast-grep --pattern $'func UpdateChannelUsedQuota($$$) {
$$$
}'
# 2. Look for any sibling helpers that only touch used_quota (no request count).
rg -nP --type=go '\bfunc\s+(Update|Decrease|Increase)\w*(UsedQuota|RequestCount)\w*\s*\('
# 3. See how existing refund flows in the repo adjust these counters (if at all).
rg -nP --type=go -C3 'LogTypeRefund' -g '!service/task_billing.go'Repository: QuantumNous/new-api
Length of output: 4392
🏁 Script executed:
# Inspect the underlying updateUserUsedQuotaAndRequestCount function implementation
cat -n model/user.go | sed -n '960,1050p'Repository: QuantumNous/new-api
Length of output: 3164
🏁 Script executed:
# Check for alternative helpers that update quota without request count
rg -nP --type=go '\bfunc\s+(Update|Decrease)\w*User\w*(Quota|UsedQuota)\w*\s*\(' model/Repository: QuantumNous/new-api
Length of output: 224
🏁 Script executed:
# Look at how RefundTaskQuota and RecalculateTaskQuota are called/tested
cat -n service/task_billing.go | sed -n '160,180p'
cat -n service/task_billing.go | sed -n '225,245p'Repository: QuantumNous/new-api
Length of output: 1700
🏁 Script executed:
# Check if there are test cases that verify request_count behavior during refund
grep -n "request_count\|RequestCount" service/task_billing_test.go | head -20Repository: QuantumNous/new-api
Length of output: 45
Refund paths incorrectly decrement user request_count.
model.UpdateUserUsedQuotaAndRequestCount updates both used_quota and request_count by 1, regardless of whether the quota parameter is positive or negative. Passing -quota on the refund path (lines 168 and 233) will decrement request_count by 1.
Semantically, a failed/refunded task should roll back quota usage but NOT decrement request_count — the user made a request regardless of success. Over time, this causes request_count to drift and violates the expectation that it is monotonically increasing.
The codebase already has updateUserUsedQuota() (model/user.go:981) which updates only used_quota and leaves request_count unchanged. Use this helper (or a public wrapper) for refunds instead:
- Line 168 in
RefundTaskQuota: ReplaceUpdateUserUsedQuotaAndRequestCount(task.UserId, -quota)with a quota-only update. - Line 233 in
RecalculateTaskQuota: ReplaceUpdateUserUsedQuotaAndRequestCount(task.UserId, -quotaDelta)with a quota-only update.
Apply the same fix to channel updates if UpdateChannelUsedQuota exhibits the same pattern.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@service/task_billing.go` around lines 167 - 169, In RefundTaskQuota and
RecalculateTaskQuota replace calls that pass a negative quota into
UpdateUserUsedQuotaAndRequestCount (e.g.,
UpdateUserUsedQuotaAndRequestCount(task.UserId, -quota) and
UpdateUserUsedQuotaAndRequestCount(task.UserId, -quotaDelta)) with the
quota-only helper updateUserUsedQuota (or the public wrapper
updateUserUsedQuota() from model/user.go) so only used_quota is decremented and
request_count remains unchanged; also audit UpdateChannelUsedQuota for the same
request_count side-effect and, if it similarly mutates request_count, switch to
a channel-only quota update helper for channel updates (e.g., a dedicated
UpdateChannelUsedQuotaOnly) to ensure refunds do not decrement request_count.
预付款退款退还已使用额度
暂时用回监听机制
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
controller/usedata.go (1)
66-101:⚠️ Potential issue | 🟠 MajorSame breaking transform issue + the 1-month guard defeats large
daysvalues.Two concerns:
- Lines 79-85: when the caller passes
days > 30, the handler has just setstartTimestamp = endTimestamp - days*24*3600, then immediately fails the 2_592_000-second check and returns"时间跨度不能超过 1 个月". Either capdays(e.g.if days > 30 { days = 30 }) or raise/remove the limit for thedayspath; otherwise the new parameter is unusable beyond 30.- Lines 91-101:
model.GetQuotaDataByUserIdreturns raw, ungrouped rows, so multiple records for the samecreated_atwill each produce a separate{date, quota}entry with duplicatedatevalues. If the frontend expects one value per day, aggregate by date before buildingresult. AlsoamountduplicatingQuotahas the same units concern flagged above.Suggested fix for the day cap
- days, err := strconv.Atoi(c.Query("days")) - if err == nil && days > 0 { - endTimestamp = time.Now().Unix() - startTimestamp = endTimestamp - int64(days*24*3600) - } + if days, derr := strconv.Atoi(c.Query("days")); derr == nil && days > 0 { + if days > 30 { + days = 30 + } + endTimestamp = time.Now().Unix() + startTimestamp = endTimestamp - int64(days)*24*3600 + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controller/usedata.go` around lines 66 - 101, GetUserQuotaDates: cap the incoming days to 30 (e.g., if days>30 set days=30) so the days path doesn't always trigger the 1-month guard, then aggregate the rows returned by model.GetQuotaDataByUserId by date (use time.Unix(d.CreatedAt,0).Format("2006-01-02") as key) to coalesce multiple records per day into a single map entry summing Quota (and set amount accordingly) before building the final result slice; update logic in GetUserQuotaDates where days is parsed and where result is constructed to perform the grouping and summation.
🧹 Nitpick comments (1)
controller/usedata.go (1)
20-23: Shadowederrand minor overflow hygiene.
errdeclared here on Line 20 is later reused/redeclared on Line 26 (dates, err := ...) — functionally fine due to:=with a new LHS (dates), but it makes the control flow harder to follow and previously caused subtle bugs in similar handlers. Prefer a scoped block as in the suggested patch forGetUserQuotaDatesabove. Also computeint64(days)*24*3600rather thanint64(days*24*3600)to avoid truncation if a very largedaysis ever passed on a 32-bit build.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controller/usedata.go` around lines 20 - 23, Wrap the days parsing logic in its own scoped block to avoid shadowing err (so the initial err from strconv.Atoi(c.Query("days")) doesn’t get reused later when you do dates, err := ...) and assign to startTimestamp/endTimestamp inside that block; also change the duration calc to use int64(days) * 24 * 3600 (i.e. multiply after converting days to int64) to avoid 32-bit overflow/truncation. Locate the strconv.Atoi(c.Query("days")) call and the subsequent use of startTimestamp/endTimestamp and adjust accordingly to a local block, leaving later usages of dates, err (e.g., dates, err := ...) unaffected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@controller/usedata.go`:
- Around line 31-41: The current transformation over "dates" (building result :=
make([]map[string]interface{}, len(dates)) and populating map with only "date",
"quota", and "amount") drops model_name, count, and token_used returned by the
query GetAllQuotaDates; update this mapping to preserve those fields (e.g.,
include "model_name": d.ModelName, "count": d.Count, "token_used": d.TokenUsed)
and correct the "amount" value to match frontend expectations (do not set
"amount": d.Quota blindly—use the proper conversion or unit like
d.Quota/QuotaPerUnit if required), or alternatively remove this transformation
and return the original slice if per-model breakdown is intended; modify the
code that constructs result from "dates" accordingly.
---
Outside diff comments:
In `@controller/usedata.go`:
- Around line 66-101: GetUserQuotaDates: cap the incoming days to 30 (e.g., if
days>30 set days=30) so the days path doesn't always trigger the 1-month guard,
then aggregate the rows returned by model.GetQuotaDataByUserId by date (use
time.Unix(d.CreatedAt,0).Format("2006-01-02") as key) to coalesce multiple
records per day into a single map entry summing Quota (and set amount
accordingly) before building the final result slice; update logic in
GetUserQuotaDates where days is parsed and where result is constructed to
perform the grouping and summation.
---
Nitpick comments:
In `@controller/usedata.go`:
- Around line 20-23: Wrap the days parsing logic in its own scoped block to
avoid shadowing err (so the initial err from strconv.Atoi(c.Query("days"))
doesn’t get reused later when you do dates, err := ...) and assign to
startTimestamp/endTimestamp inside that block; also change the duration calc to
use int64(days) * 24 * 3600 (i.e. multiply after converting days to int64) to
avoid 32-bit overflow/truncation. Locate the strconv.Atoi(c.Query("days")) call
and the subsequent use of startTimestamp/endTimestamp and adjust accordingly to
a local block, leaving later usages of dates, err (e.g., dates, err := ...)
unaffected.
🪄 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: 2f936870-5f92-4952-935b-056af2844771
📒 Files selected for processing (1)
controller/usedata.go
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@setting/ratio_setting/model_ratio.go`:
- Around line 707-728: GetModelDisplayName and GetModelModalities currently
normalize the input via FormatMatchingModelName before any lookup, which
prevents exact metadata entries (e.g., concrete Gemini thinking-budget or gizmo
names) from being found; modify both functions (GetModelDisplayName and
GetModelModalities) to first attempt an exact lookup on modelDisplayNameMap /
modelModalitiesMap with the original name, and only if that fails perform the
FormatMatchingModelName normalization and retry the lookup (falling back to
empty string if still not found), ensuring existing normalized behavior remains
as a fallback.
🪄 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: db27c633-8760-42bb-a8d3-e68df87f958f
📒 Files selected for processing (3)
controller/option.gosetting/ratio_setting/exposed_cache.gosetting/ratio_setting/model_ratio.go
| func GetModelDisplayName(name string) string { | ||
| name = FormatMatchingModelName(name) | ||
| if displayName, ok := modelDisplayNameMap.Get(name); ok { | ||
| return displayName | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| func ModelModalities2JSONString() string { | ||
| return modelModalitiesMap.MarshalJSONString() | ||
| } | ||
|
|
||
| func UpdateModelModalitiesByJSONString(jsonStr string) error { | ||
| return types.LoadFromJsonStringWithCallback(modelModalitiesMap, jsonStr, InvalidateExposedDataCache) | ||
| } | ||
|
|
||
| func GetModelModalities(name string) string { | ||
| name = FormatMatchingModelName(name) | ||
| if modalities, ok := modelModalitiesMap.Get(name); ok { | ||
| return modalities | ||
| } | ||
| return "" |
There was a problem hiding this comment.
Try exact metadata lookup before pricing-style normalization.
FormatMatchingModelName collapses concrete Gemini thinking-budget and gizmo model names to wildcard keys. That is useful for pricing, but it makes exact model_display_name / model_modalities entries unreachable for those models. Prefer exact lookup first, then fall back to normalized lookup for wildcard defaults.
🐛 Proposed fix
func GetModelDisplayName(name string) string {
- name = FormatMatchingModelName(name)
if displayName, ok := modelDisplayNameMap.Get(name); ok {
return displayName
}
+ formattedName := FormatMatchingModelName(name)
+ if formattedName != name {
+ if displayName, ok := modelDisplayNameMap.Get(formattedName); ok {
+ return displayName
+ }
+ }
return ""
}
func ModelModalities2JSONString() string {
return modelModalitiesMap.MarshalJSONString()
@@
func GetModelModalities(name string) string {
- name = FormatMatchingModelName(name)
if modalities, ok := modelModalitiesMap.Get(name); ok {
return modalities
}
+ formattedName := FormatMatchingModelName(name)
+ if formattedName != name {
+ if modalities, ok := modelModalitiesMap.Get(formattedName); ok {
+ return modalities
+ }
+ }
return ""
}📝 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.
| func GetModelDisplayName(name string) string { | |
| name = FormatMatchingModelName(name) | |
| if displayName, ok := modelDisplayNameMap.Get(name); ok { | |
| return displayName | |
| } | |
| return "" | |
| } | |
| func ModelModalities2JSONString() string { | |
| return modelModalitiesMap.MarshalJSONString() | |
| } | |
| func UpdateModelModalitiesByJSONString(jsonStr string) error { | |
| return types.LoadFromJsonStringWithCallback(modelModalitiesMap, jsonStr, InvalidateExposedDataCache) | |
| } | |
| func GetModelModalities(name string) string { | |
| name = FormatMatchingModelName(name) | |
| if modalities, ok := modelModalitiesMap.Get(name); ok { | |
| return modalities | |
| } | |
| return "" | |
| func GetModelDisplayName(name string) string { | |
| if displayName, ok := modelDisplayNameMap.Get(name); ok { | |
| return displayName | |
| } | |
| formattedName := FormatMatchingModelName(name) | |
| if formattedName != name { | |
| if displayName, ok := modelDisplayNameMap.Get(formattedName); ok { | |
| return displayName | |
| } | |
| } | |
| return "" | |
| } | |
| func ModelModalities2JSONString() string { | |
| return modelModalitiesMap.MarshalJSONString() | |
| } | |
| func UpdateModelModalitiesByJSONString(jsonStr string) error { | |
| return types.LoadFromJsonStringWithCallback(modelModalitiesMap, jsonStr, InvalidateExposedDataCache) | |
| } | |
| func GetModelModalities(name string) string { | |
| if modalities, ok := modelModalitiesMap.Get(name); ok { | |
| return modalities | |
| } | |
| formattedName := FormatMatchingModelName(name) | |
| if formattedName != name { | |
| if modalities, ok := modelModalitiesMap.Get(formattedName); ok { | |
| return modalities | |
| } | |
| } | |
| return "" | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@setting/ratio_setting/model_ratio.go` around lines 707 - 728,
GetModelDisplayName and GetModelModalities currently normalize the input via
FormatMatchingModelName before any lookup, which prevents exact metadata entries
(e.g., concrete Gemini thinking-budget or gizmo names) from being found; modify
both functions (GetModelDisplayName and GetModelModalities) to first attempt an
exact lookup on modelDisplayNameMap / modelModalitiesMap with the original name,
and only if that fails perform the FormatMatchingModelName normalization and
retry the lookup (falling back to empty string if still not found), ensuring
existing normalized behavior remains as a fallback.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
controller/model.go (1)
142-154: Consider extracting the repeatedoaiModel/custom-model construction.Lines 142‑154 and 192‑204 are now near‑identical blocks; both branches set
SupportedEndpointTypes+Modalitieson the map hit and construct an equivalentdto.OpenAIModelson miss. A small helper likebuildUserModel(name string) dto.OpenAIModelswould remove the duplication and guarantee future fields (e.g.,DisplayName) are added in one place rather than two.♻️ Proposed helper
func buildUserModel(name string) dto.OpenAIModels { if m, ok := openAIModelsMap[name]; ok { m.SupportedEndpointTypes = model.GetModelSupportEndpointTypes(name) m.Modalities = ratio_setting.GetModelModalities(name) return m } return dto.OpenAIModels{ Id: name, Object: "model", Created: 1626777600, OwnedBy: "custom", SupportedEndpointTypes: model.GetModelSupportEndpointTypes(name), Modalities: ratio_setting.GetModelModalities(name), } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@controller/model.go` around lines 142 - 154, Extract the duplicated construction of user model entries into a single helper like buildUserModel(name string) dto.OpenAIModels: inside it, check openAIModelsMap[name], if present set m.SupportedEndpointTypes = model.GetModelSupportEndpointTypes(name) and m.Modalities = ratio_setting.GetModelModalities(name) and return m; otherwise construct and return the dto.OpenAIModels literal with Id=name, Object="model", Created=1626777600, OwnedBy="custom", and the same SupportedEndpointTypes/Modalities calls. Replace both duplicated blocks that append into userOpenAiModels with calls to userOpenAiModels = append(userOpenAiModels, buildUserModel(allowModel)) (or the appropriate variable name) so all future fields (e.g., DisplayName) are maintained in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@controller/model.go`:
- Around line 142-154: Extract the duplicated construction of user model entries
into a single helper like buildUserModel(name string) dto.OpenAIModels: inside
it, check openAIModelsMap[name], if present set m.SupportedEndpointTypes =
model.GetModelSupportEndpointTypes(name) and m.Modalities =
ratio_setting.GetModelModalities(name) and return m; otherwise construct and
return the dto.OpenAIModels literal with Id=name, Object="model",
Created=1626777600, OwnedBy="custom", and the same
SupportedEndpointTypes/Modalities calls. Replace both duplicated blocks that
append into userOpenAiModels with calls to userOpenAiModels =
append(userOpenAiModels, buildUserModel(allowModel)) (or the appropriate
variable name) so all future fields (e.g., DisplayName) are maintained in one
place.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 35a4af5f-95b3-4dfa-bb0a-13822ec7b238
📒 Files selected for processing (3)
controller/model.godto/pricing.gomodel/pricing.go
# Conflicts: # service/task_billing.go
stc-5/7-21.12修复空返回仍然算任务成功
stc-5/7-22.01解决文本等模型二次应用企业折扣
stc-5/8-15:53为用户使用量聚合表添加end_at字段
新增 PublishBillingSnapshotForOpsLog 与 ops 相关 context key;在文本/WSS/音频消费、违规费、MJ、LogTaskConsumption 等 RecordConsumeLog 前写入 token 与额度快照;增加 EmitAsyncBillingOpsLog 钩子及异步任务结算/退款的结构化事件字段;RecalculateTaskQuota 增加 billingTotalTokens 参数并调整调用与测试
stc-5/13-18:07 fix(channel): 修复渠道多模型时除首个外报模型ID错误
…uting Co-authored-by: Cursor <cursoragent@cursor.com>
51fdfc5 to
2b6f1df
Compare
Important
📝 变更描述 / Description
(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
(请在此粘贴截图、关键日志或测试报告,以证明变更生效)
Summary by CodeRabbit
New Features
Bug Fixes