refactor: update task refund logic and remove legacy handling

This commit is contained in:
CaIon
2026-07-26 20:47:52 +08:00
parent 2d23cdf291
commit bc14c18f60
6 changed files with 64 additions and 310 deletions
+2 -44
View File
@@ -37,11 +37,6 @@ type TaskPollingAdaptor interface {
// 打破 service -> relay -> relay/channel -> service 的循环依赖。
var GetTaskAdaptorFunc func(platform constant.TaskPlatform) TaskPollingAdaptor
const (
refundReconciliationLimit = 100
refundReconciliationGracePeriod = 30 * time.Second
)
// sweepTimedOutTasks 在主轮询之前独立清理超时任务。
// 每次最多处理 100 条,剩余的下个周期继续处理。
// 使用 per-task CAS (UpdateWithStatus) 防止覆盖被正常轮询已推进的任务。
@@ -69,7 +64,8 @@ func sweepTimedOutTasks(ctx context.Context) {
task.FinishTime = now
if isLegacy {
task.FailReason = legacyReason
// 旧系统任务明确不退款,随终态 CAS 一并清掉 quota,避免被后续对账误判。
// 旧系统任务明确不退款,随终态 CAS 一并清掉 quota,
// 避免留下可再次退款的计费状态。
task.Quota = 0
} else {
task.FailReason = reason
@@ -95,43 +91,6 @@ func sweepTimedOutTasks(ctx context.Context) {
}
}
// sweepUnrefundedFailedTasks 重试已落 FAILURE 终态但仍保留 quota 的欠退款任务。
// 先等待一个短暂宽限期,让终态 CAS 的胜出者完成主路径即时退款,避免正常
// 轮询与对账同时处理刚失败的任务。
func sweepUnrefundedFailedTasks(ctx context.Context) {
updatedBefore := time.Now().Add(-refundReconciliationGracePeriod).Unix()
tasks := model.GetUnrefundedFailedTasks(updatedBefore, refundReconciliationLimit)
for _, task := range tasks {
if ctx.Err() != nil {
return
}
quota := task.Quota
claimed, err := model.ClaimQuotaForRefund(task.ID, quota)
if err != nil {
logger.LogError(ctx, fmt.Sprintf("sweepUnrefundedFailedTasks claim error for task %s: %v", task.TaskID, err))
continue
}
if !claimed {
logger.LogDebug(ctx, "sweepUnrefundedFailedTasks: task %s claim lost, skip refund", task.TaskID)
continue
}
// 对账先清 marker 再退款,确保并发 sweep 只有一个实际退款者。若进程在
// claim 后、退款前崩溃,会偏向漏退而不是双退,需由人工账务对账兜底。
if RefundTaskQuota(ctx, task, task.FailReason) {
continue
}
restored, restoreErr := model.RestoreQuotaAfterFailedRefund(task.ID, quota)
if restoreErr != nil {
logger.LogError(ctx, fmt.Sprintf("sweepUnrefundedFailedTasks restore quota error for task %s: %v", task.TaskID, restoreErr))
} else if !restored {
logger.LogError(ctx, fmt.Sprintf("sweepUnrefundedFailedTasks could not restore quota marker for task %s", task.TaskID))
}
}
}
// TaskPollSummary is the result recorded on an async_task_poll system task row,
// summarizing one polling pass.
type TaskPollSummary struct {
@@ -156,7 +115,6 @@ func RunTaskPollingOnce(ctx context.Context, report func(processed, total int))
common.SysLog("任务进度轮询开始")
sweepTimedOutTasks(ctx)
sweepUnrefundedFailedTasks(ctx)
allTasks := model.GetAllUnFinishSyncTasks(constant.TaskQueryLimit)
summary.UnfinishedTasks = len(allTasks)
platformTask := make(map[constant.TaskPlatform][]*model.Task)