fix(billing): 异步任务退款时同步减少 used_quota (#6795)
* fix(billing): 异步任务退款时同步减少 used_quota 退款时仅恢复了 quota(剩余额度),但未同步减少 used_quota(已用额度), 导致"总额度"(quota + used_quota)随退款次数持续虚增,超出用户实际充值金额。 修复三处退款路径: - RefundTaskQuota:任务失败完整退款 - RecalculateTaskQuota:差额结算退款分支 - controller/midjourney.go:Midjourney 任务失败退款 新增 model.UpdateUserUsedQuota 公开函数,仅调整 used_quota 不影响 request_count。 * fix(billing): 任务退款时同步扣减渠道 used_quota * fix(billing): complete async task refund accounting * style(model): group internal Midjourney fields --------- Co-authored-by: CaIon <i@caion.me>
This commit is contained in:
@@ -3,6 +3,8 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -13,6 +15,8 @@ import (
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
relayconstant "github.com/QuantumNous/new-api/relay/constant"
|
||||
"github.com/QuantumNous/new-api/setting"
|
||||
|
||||
@@ -27,6 +31,113 @@ func CovertMjpActionToModelName(mjAction string) string {
|
||||
return modelName
|
||||
}
|
||||
|
||||
// PrepareMidjourneyTaskBilling sets the durable refund marker before the task is inserted.
|
||||
func PrepareMidjourneyTaskBilling(relayInfo *relaycommon.RelayInfo, task *model.Midjourney, quota int, shouldBill bool) (bool, error) {
|
||||
if task == nil {
|
||||
return false, errors.New("Midjourney task is nil")
|
||||
}
|
||||
task.Quota = 0
|
||||
task.TokenId = 0
|
||||
task.BillingChannelId = 0
|
||||
if !shouldBill {
|
||||
return false, nil
|
||||
}
|
||||
if relayInfo == nil {
|
||||
return false, errors.New("relay info is nil")
|
||||
}
|
||||
if quota < 0 {
|
||||
return false, errors.New("quota cannot be negative")
|
||||
}
|
||||
if relayInfo.BillingSource == BillingSourceSubscription {
|
||||
return false, errors.New("legacy Midjourney billing does not support subscriptions")
|
||||
}
|
||||
|
||||
task.Quota = quota
|
||||
task.BillingChannelId = task.ChannelId
|
||||
if relayInfo.ChannelMeta != nil && relayInfo.ChannelId > 0 {
|
||||
task.BillingChannelId = relayInfo.ChannelId
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SettleMidjourneyTaskBilling charges a persisted legacy task and records the applied stages.
|
||||
func SettleMidjourneyTaskBilling(relayInfo *relaycommon.RelayInfo, task *model.Midjourney, prepared bool) (bool, error) {
|
||||
if !prepared {
|
||||
return false, nil
|
||||
}
|
||||
if relayInfo == nil {
|
||||
return false, errors.New("relay info is nil")
|
||||
}
|
||||
if task == nil || task.Id == 0 {
|
||||
return false, errors.New("Midjourney task must be persisted before billing")
|
||||
}
|
||||
|
||||
result, billingErr := postConsumeQuotaWithResult(relayInfo, task.Quota, 0, true)
|
||||
if !result.FundingApplied {
|
||||
task.Quota = 0
|
||||
task.TokenId = 0
|
||||
task.BillingChannelId = 0
|
||||
if updateErr := task.UpdateBillingState(); updateErr != nil {
|
||||
return false, errors.Join(billingErr, fmt.Errorf("clear Midjourney billing state: %w", updateErr))
|
||||
}
|
||||
return false, billingErr
|
||||
}
|
||||
|
||||
task.TokenId = 0
|
||||
if result.TokenApplied {
|
||||
task.TokenId = relayInfo.TokenId
|
||||
}
|
||||
if updateErr := task.UpdateBillingState(); updateErr != nil {
|
||||
return true, errors.Join(billingErr, fmt.Errorf("update Midjourney billing state: %w", updateErr))
|
||||
}
|
||||
return true, billingErr
|
||||
}
|
||||
|
||||
// RefundMidjourneyQuota reverses every accounting element recorded for a billed legacy task.
|
||||
func RefundMidjourneyQuota(ctx context.Context, task *model.Midjourney, reason string) bool {
|
||||
quota := task.Quota
|
||||
if quota == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if err := model.IncreaseUserQuota(task.UserId, quota, false); err != nil {
|
||||
logger.LogWarn(ctx, fmt.Sprintf("退还 Midjourney 用户额度失败 task %s: %s", task.MjId, err.Error()))
|
||||
return false
|
||||
}
|
||||
|
||||
if task.TokenId > 0 {
|
||||
tokenKey := resolveTokenKey(ctx, task.TokenId, task.MjId)
|
||||
if tokenKey != "" {
|
||||
if err := model.IncreaseTokenQuota(task.TokenId, tokenKey, quota); err != nil {
|
||||
logger.LogWarn(ctx, fmt.Sprintf("退还 Midjourney 令牌额度失败 task %s: %s", task.MjId, err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
billingChannelId := task.GetBillingChannelId()
|
||||
model.UpdateUserUsedQuota(task.UserId, -quota)
|
||||
model.UpdateChannelUsedQuota(billingChannelId, -quota)
|
||||
model.RecordTaskBillingLog(model.RecordTaskBillingLogParams{
|
||||
UserId: task.UserId,
|
||||
LogType: model.LogTypeRefund,
|
||||
Content: "",
|
||||
ChannelId: billingChannelId,
|
||||
ModelName: CovertMjpActionToModelName(task.Action),
|
||||
Quota: quota,
|
||||
TokenId: task.TokenId,
|
||||
Other: map[string]interface{}{
|
||||
"task_id": task.MjId,
|
||||
"reason": reason,
|
||||
},
|
||||
})
|
||||
|
||||
task.Quota = 0
|
||||
if err := task.UpdateBillingState(); err != nil {
|
||||
logger.LogError(ctx, fmt.Sprintf("Midjourney 退款成功但清除 quota 失败 task %s: %s", task.MjId, err.Error()))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func GetMjRequestModel(relayMode int, midjRequest *dto.MidjourneyRequest) (string, *dto.MidjourneyResponse, bool) {
|
||||
action := ""
|
||||
if relayMode == relayconstant.RelayModeMidjourneyAction {
|
||||
|
||||
Reference in New Issue
Block a user