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:
wans10
2026-08-13 22:06:40 +08:00
committed by GitHub
co-authored by CaIon
parent ccd535ef8e
commit 58d4e9bd3b
9 changed files with 667 additions and 81 deletions
+62 -50
View File
@@ -232,30 +232,6 @@ func RelaySwapFace(c *gin.Context, info *relaycommon.RelayInfo) *dto.MidjourneyR
if err != nil {
return &mjResp.Response
}
defer func() {
if mjResp.StatusCode == 200 && mjResp.Response.Code == 1 {
err := service.PostConsumeQuota(info, priceData.Quota, 0, true)
if err != nil {
common.SysLog("error consuming token remain quota: " + err.Error())
}
tokenName := c.GetString("token_name")
logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", priceData.ModelPrice, priceData.GroupRatioInfo.GroupRatio, constant.MjActionSwapFace)
other := service.GenerateMjOtherInfo(info, priceData)
model.RecordConsumeLog(c, info.UserId, model.RecordConsumeLogParams{
ChannelId: info.ChannelId,
ModelName: modelName,
TokenName: tokenName,
Quota: priceData.Quota,
Content: logContent,
TokenId: info.TokenId,
Group: info.UsingGroup,
Other: other,
})
model.UpdateUserUsedQuotaAndRequestCount(info.UserId, priceData.Quota)
model.UpdateChannelUsedQuota(info.ChannelId, priceData.Quota)
}
}()
midjResponse := &mjResp.Response
midjourneyTask := &model.Midjourney{
UserId: info.UserId,
@@ -274,12 +250,42 @@ func RelaySwapFace(c *gin.Context, info *relaycommon.RelayInfo) *dto.MidjourneyR
Progress: "0%",
FailReason: "",
ChannelId: c.GetInt("channel_id"),
Quota: priceData.Quota,
}
billingPrepared, billingErr := service.PrepareMidjourneyTaskBilling(
info,
midjourneyTask,
priceData.Quota,
mjResp.StatusCode == http.StatusOK && midjResponse.Code == 1,
)
if billingErr != nil {
common.SysLog("error consuming Midjourney quota: " + billingErr.Error())
}
err = midjourneyTask.Insert()
if err != nil {
return service.MidjourneyErrorWrapper(constant.MjRequestError, "insert_midjourney_task_failed")
}
billingApplied, billingErr := service.SettleMidjourneyTaskBilling(info, midjourneyTask, billingPrepared)
if billingErr != nil {
common.SysLog("error settling Midjourney quota: " + billingErr.Error())
}
if billingApplied {
billingChannelId := midjourneyTask.GetBillingChannelId()
tokenName := c.GetString("token_name")
logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", priceData.ModelPrice, priceData.GroupRatioInfo.GroupRatio, constant.MjActionSwapFace)
other := service.GenerateMjOtherInfo(info, priceData)
model.RecordConsumeLog(c, info.UserId, model.RecordConsumeLogParams{
ChannelId: billingChannelId,
ModelName: modelName,
TokenName: tokenName,
Quota: midjourneyTask.Quota,
Content: logContent,
TokenId: midjourneyTask.TokenId,
Group: info.UsingGroup,
Other: other,
})
model.UpdateUserUsedQuotaAndRequestCount(info.UserId, midjourneyTask.Quota)
model.UpdateChannelUsedQuota(billingChannelId, midjourneyTask.Quota)
}
c.Writer.WriteHeader(mjResp.StatusCode)
respBody, err := json.Marshal(midjResponse)
if err != nil {
@@ -539,30 +545,6 @@ func RelayMidjourneySubmit(c *gin.Context, relayInfo *relaycommon.RelayInfo) *dt
}
midjResponse := &midjResponseWithStatus.Response
defer func() {
if consumeQuota && midjResponseWithStatus.StatusCode == 200 {
err := service.PostConsumeQuota(relayInfo, priceData.Quota, 0, true)
if err != nil {
common.SysLog("error consuming token remain quota: " + err.Error())
}
tokenName := c.GetString("token_name")
logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %sID %s", priceData.ModelPrice, priceData.GroupRatioInfo.GroupRatio, midjRequest.Action, midjResponse.Result)
other := service.GenerateMjOtherInfo(relayInfo, priceData)
model.RecordConsumeLog(c, relayInfo.UserId, model.RecordConsumeLogParams{
ChannelId: relayInfo.ChannelId,
ModelName: modelName,
TokenName: tokenName,
Quota: priceData.Quota,
Content: logContent,
TokenId: relayInfo.TokenId,
Group: relayInfo.UsingGroup,
Other: other,
})
model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, priceData.Quota)
model.UpdateChannelUsedQuota(relayInfo.ChannelId, priceData.Quota)
}
}()
// 文档:https://github.com/novicezk/midjourney-proxy/blob/main/docs/api.md
//1-提交成功
// 21-任务已存在(处理中或者有结果了) {"code":21,"description":"任务已存在","result":"0741798445574458","properties":{"status":"SUCCESS","imageUrl":"https://xxxx"}}
@@ -587,7 +569,6 @@ func RelayMidjourneySubmit(c *gin.Context, relayInfo *relaycommon.RelayInfo) *dt
Progress: "0%",
FailReason: "",
ChannelId: c.GetInt("channel_id"),
Quota: priceData.Quota,
}
if midjResponse.Code == 3 {
//无实例账号自动禁用渠道(No available account instance
@@ -632,6 +613,15 @@ func RelayMidjourneySubmit(c *gin.Context, relayInfo *relaycommon.RelayInfo) *dt
midjourneyTask.Progress = "100%"
midjourneyTask.Status = "SUCCESS"
}
billingPrepared, billingErr := service.PrepareMidjourneyTaskBilling(
relayInfo,
midjourneyTask,
priceData.Quota,
consumeQuota && midjResponseWithStatus.StatusCode == http.StatusOK,
)
if billingErr != nil {
common.SysLog("error consuming Midjourney quota: " + billingErr.Error())
}
err = midjourneyTask.Insert()
if err != nil {
return &dto.MidjourneyResponse{
@@ -639,6 +629,28 @@ func RelayMidjourneySubmit(c *gin.Context, relayInfo *relaycommon.RelayInfo) *dt
Description: "insert_midjourney_task_failed",
}
}
billingApplied, billingErr := service.SettleMidjourneyTaskBilling(relayInfo, midjourneyTask, billingPrepared)
if billingErr != nil {
common.SysLog("error settling Midjourney quota: " + billingErr.Error())
}
if billingApplied {
billingChannelId := midjourneyTask.GetBillingChannelId()
tokenName := c.GetString("token_name")
logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %sID %s", priceData.ModelPrice, priceData.GroupRatioInfo.GroupRatio, midjRequest.Action, midjResponse.Result)
other := service.GenerateMjOtherInfo(relayInfo, priceData)
model.RecordConsumeLog(c, relayInfo.UserId, model.RecordConsumeLogParams{
ChannelId: billingChannelId,
ModelName: modelName,
TokenName: tokenName,
Quota: midjourneyTask.Quota,
Content: logContent,
TokenId: midjourneyTask.TokenId,
Group: relayInfo.UsingGroup,
Other: other,
})
model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, midjourneyTask.Quota)
model.UpdateChannelUsedQuota(billingChannelId, midjourneyTask.Quota)
}
if midjResponse.Code == 22 { //22-排队中,说明任务已存在
//修改返回值