mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 15:06:37 +08:00
200 lines
6.7 KiB
Go
200 lines
6.7 KiB
Go
/*
|
|
Copyright 2025 linux.do
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package dispute
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/hibiken/asynq"
|
|
"github.com/linux-do/credit/internal/config"
|
|
"github.com/linux-do/credit/internal/db"
|
|
"github.com/linux-do/credit/internal/logger"
|
|
"github.com/linux-do/credit/internal/model"
|
|
"github.com/linux-do/credit/internal/task"
|
|
"github.com/linux-do/credit/internal/task/scheduler"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// HandleAutoRefundExpiredDisputes 处理所有过期争议的批量任务
|
|
func HandleAutoRefundExpiredDisputes(ctx context.Context, t *asynq.Task) error {
|
|
// 获取争议时间窗口配置(小时)
|
|
disputeTimeHours, errGet := model.GetIntByKey(ctx, model.ConfigKeyDisputeTimeWindowHours)
|
|
if errGet != nil {
|
|
logger.ErrorF(ctx, "获取争议时间窗口配置失败: %v", errGet)
|
|
return errGet
|
|
}
|
|
|
|
pageSize := 1000
|
|
lastID := uint64(0)
|
|
currentDelay := 0 * time.Second
|
|
|
|
// 计算过期时间阈值:created_at < deadline 的争议需要自动退款
|
|
deadline := time.Now().Add(-time.Duration(disputeTimeHours) * time.Hour)
|
|
|
|
for {
|
|
var disputes []model.Dispute
|
|
if err := db.DB(ctx).
|
|
Where("id > ? AND status = ? AND created_at < ?",
|
|
lastID, model.DisputeStatusDisputing, deadline).
|
|
Order("id ASC").
|
|
Limit(pageSize).
|
|
Find(&disputes).Error; err != nil {
|
|
logger.ErrorF(ctx, "查询过期争议失败: %v", err)
|
|
return err
|
|
}
|
|
|
|
// 没有更多争议,退出循环
|
|
if len(disputes) == 0 {
|
|
break
|
|
}
|
|
|
|
for _, dispute := range disputes {
|
|
currentDelay += time.Duration(config.Config.Scheduler.DisputeAutoRefundDispatchIntervalSeconds) * time.Second
|
|
|
|
payload, _ := json.Marshal(map[string]interface{}{
|
|
"dispute_id": dispute.ID,
|
|
})
|
|
|
|
if _, errTask := scheduler.AsynqClient.Enqueue(
|
|
asynq.NewTask(task.AutoRefundSingleDisputeTask, payload),
|
|
asynq.ProcessIn(currentDelay),
|
|
asynq.MaxRetry(5),
|
|
); errTask != nil {
|
|
logger.ErrorF(ctx, "下发争议[ID:%d]自动退款任务失败: %v", dispute.ID, errTask)
|
|
return errTask
|
|
} else {
|
|
logger.InfoF(ctx, "下发争议[ID:%d]自动退款任务成功", dispute.ID)
|
|
}
|
|
}
|
|
|
|
lastID = disputes[len(disputes)-1].ID
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HandleAutoRefundSingleDispute 处理单个争议的自动退款任务
|
|
func HandleAutoRefundSingleDispute(ctx context.Context, t *asynq.Task) error {
|
|
// 解析任务参数
|
|
var payload struct {
|
|
DisputeID uint64 `json:"dispute_id"`
|
|
}
|
|
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
|
|
return fmt.Errorf("解析任务参数失败: %w", err)
|
|
}
|
|
|
|
if err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var dispute model.Dispute
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}).
|
|
Where("id = ? AND status = ?", payload.DisputeID, model.DisputeStatusDisputing).
|
|
First(&dispute).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
logger.InfoF(ctx, "争议[ID:%d]已被处理或不存在,跳过", payload.DisputeID)
|
|
return nil // 已处理,不算错误
|
|
}
|
|
return err
|
|
}
|
|
|
|
var order model.Order
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}).
|
|
Where(
|
|
"id = ? AND status = ? AND type IN ?",
|
|
dispute.OrderID,
|
|
model.OrderStatusDisputing,
|
|
[]model.OrderType{model.OrderTypePayment, model.OrderTypeOnline},
|
|
).
|
|
First(&order).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
logger.ErrorF(ctx, "争议[ID:%d]关联订单[ID:%d]不存在或状态异常", payload.DisputeID, dispute.OrderID)
|
|
return nil // 订单状态异常,跳过
|
|
}
|
|
return err
|
|
}
|
|
|
|
// 获取付款方和收款方用户
|
|
var payerUser, payeeUser model.User
|
|
if err := payerUser.GetByID(tx, order.PayerUserID); err != nil {
|
|
return fmt.Errorf("查询付款方用户失败: %w", err)
|
|
}
|
|
if err := payeeUser.GetByID(tx, order.PayeeUserID); err != nil {
|
|
return fmt.Errorf("查询收款方用户失败: %w", err)
|
|
}
|
|
|
|
// 获取商家的支付配置
|
|
var merchantPayConfig model.UserPayConfig
|
|
if err := merchantPayConfig.GetByPayScore(tx, payeeUser.PayScore); err != nil {
|
|
return fmt.Errorf("查询商家支付配置失败: %w", err)
|
|
}
|
|
|
|
// 计算商家积分减少:订单金额 × 商家的 score_rate
|
|
merchantScoreDecrease := order.Amount.Mul(merchantPayConfig.ScoreRate).Round(0).IntPart()
|
|
|
|
// 商家(收款方)退款:扣除可用余额、总收款和积分
|
|
if err := tx.Model(&model.User{}).
|
|
Where("id = ?", payeeUser.ID).
|
|
UpdateColumns(map[string]interface{}{
|
|
"available_balance": gorm.Expr("available_balance - ?", order.Amount),
|
|
"total_receive": gorm.Expr("total_receive - ?", order.Amount),
|
|
"pay_score": gorm.Expr("pay_score - ?", merchantScoreDecrease),
|
|
}).Error; err != nil {
|
|
return fmt.Errorf("商家退款失败: %w", err)
|
|
}
|
|
|
|
// 付款方收到退款:增加可用余额,减少总支付和支付积分
|
|
if err := tx.Model(&model.User{}).
|
|
Where("id = ?", payerUser.ID).
|
|
UpdateColumns(map[string]interface{}{
|
|
"available_balance": gorm.Expr("available_balance + ?", order.Amount),
|
|
"total_payment": gorm.Expr("total_payment - ?", order.Amount),
|
|
"pay_score": gorm.Expr("pay_score - ?", order.Amount.Round(0).IntPart()),
|
|
}).Error; err != nil {
|
|
return fmt.Errorf("付款方退款失败: %w", err)
|
|
}
|
|
|
|
// 更新争议状态为已退款,handler_user_id 设为 0(系统自动处理)
|
|
if err := tx.Model(&model.Dispute{}).
|
|
Where("id = ?", dispute.ID).
|
|
Updates(map[string]interface{}{
|
|
"status": model.DisputeStatusRefund,
|
|
"handler_user_id": 0,
|
|
}).Error; err != nil {
|
|
return fmt.Errorf("更新争议状态失败: %w", err)
|
|
}
|
|
|
|
// 更新订单状态为已退款
|
|
if err := tx.Model(&model.Order{}).
|
|
Where("id = ?", order.ID).
|
|
Update("status", model.OrderStatusRefund).Error; err != nil {
|
|
return fmt.Errorf("更新订单状态失败: %w", err)
|
|
}
|
|
|
|
logger.InfoF(ctx, "自动退款成功: 争议[ID:%d] 订单[ID:%d] 金额[%s] 付款方[%s] 商家[%s]",
|
|
dispute.ID, order.ID, order.Amount.String(), payerUser.Username, payeeUser.Username)
|
|
|
|
return nil
|
|
}); err != nil {
|
|
logger.ErrorF(ctx, "处理争议[ID:%d]自动退款失败: %v", payload.DisputeID, err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|