Files
tyass-server/app/main/api/internal/logic/pay/wechatpayrefundcallbacklogic.go
2026-01-30 16:51:19 +08:00

394 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pay
import (
"context"
"database/sql"
"net/http"
"strings"
"time"
"tyass-server/app/main/api/internal/svc"
"tyass-server/app/main/model"
"tyass-server/common/globalkey"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
// HandleCommissionAndWalletDeduction 处理退款后的佣金状态更新和钱包金额扣除
// refundAmount 为本次实际退款金额(单位:元),从代理侧总共需要承担的金额
// 该函数会优先冲减当前订单相关的佣金(基于 RefundedAmount不足部分再从钱包余额/冻结余额中扣除
func HandleCommissionAndWalletDeduction(ctx context.Context, svcCtx *svc.ServiceContext, session sqlx.Session, order *model.Order, refundAmount float64) error {
if refundAmount <= 0 {
return nil
}
// 查询当前订单关联的所有佣金记录(包括已结算和冻结),剔除已经完全退款的
commissionBuilder := svcCtx.AgentCommissionModel.SelectBuilder()
commissions, commissionsErr := svcCtx.AgentCommissionModel.FindAll(ctx, commissionBuilder.Where(squirrel.And{
squirrel.Eq{"order_id": order.Id},
squirrel.NotEq{"status": 2}, // 排除已全部退款的佣金
}), "")
if commissionsErr != nil {
logx.Errorf("查询代理佣金失败订单ID: %d, 错误: %v", order.Id, commissionsErr)
return nil // 返回 nil因为佣金更新失败不应影响退款流程
}
if len(commissions) == 0 {
return nil
}
// 剩余需要由佣金 + 钱包共同承担的退款金额
remainRefundAmount := refundAmount
// 记录每个代理本次需要从钱包扣除的金额,避免同一代理多条佣金时重复查钱包并产生多条流水
type walletAdjust struct {
agentId int64
amount float64 // 需要从该代理钱包扣除的金额(正数)
}
walletAdjustMap := make(map[int64]*walletAdjust)
// 1. 先在佣金记录上做冲减:增加 RefundedAmount必要时将状态置为已退款
for _, commission := range commissions {
available := commission.Amount - commission.RefundedAmount
if available <= 0 {
continue
}
if remainRefundAmount <= 0 {
break
}
// 当前这条佣金最多可冲减 available本次实际冲减 currentRefund
currentRefund := available
if currentRefund > remainRefundAmount {
currentRefund = remainRefundAmount
}
// 更新佣金的已退款金额
commission.RefundedAmount += currentRefund
// 如果这条佣金已经被完全冲减,则标记为已退款
if commission.RefundedAmount >= commission.Amount {
commission.Status = 2
}
// 更新佣金状态到数据库
var updateCommissionErr error
if session != nil {
updateCommissionErr = svcCtx.AgentCommissionModel.UpdateWithVersion(ctx, session, commission)
} else {
updateCommissionErr = svcCtx.AgentCommissionModel.UpdateWithVersion(ctx, nil, commission)
}
if updateCommissionErr != nil {
logx.Errorf("更新代理佣金状态失败佣金ID: %d, 订单ID: %d, 错误: %v", commission.Id, order.Id, updateCommissionErr)
continue // 如果佣金状态更新失败,就不继续计入本次冲减
}
// 记录该代理需要从钱包扣除的金额(可能后续还有其他佣金叠加)
wa, ok := walletAdjustMap[commission.AgentId]
if !ok {
wa = &walletAdjust{agentId: commission.AgentId}
walletAdjustMap[commission.AgentId] = wa
}
wa.amount += currentRefund
remainRefundAmount -= currentRefund
}
// 2. 再按代理维度,从钱包(冻结余额/可用余额)中扣除对应金额
for _, wa := range walletAdjustMap {
if wa.amount <= 0 {
continue
}
// 处理用户钱包的金额扣除
wallet, err := svcCtx.AgentWalletModel.FindOneByAgentId(ctx, wa.agentId)
if err != nil {
logx.Errorf("查询代理钱包失败代理ID: %d, 错误: %v", wa.agentId, err)
continue
}
// 记录变动前的余额
balanceBefore := wallet.Balance
frozenBalanceBefore := wallet.FrozenBalance
// 优先从冻结余额中扣除(与原先“冻结佣金优先使用冻结余额”的设计一致)
deduct := wa.amount
if wallet.FrozenBalance >= deduct {
wallet.FrozenBalance -= deduct
} else {
remaining := deduct - wallet.FrozenBalance
wallet.FrozenBalance = 0
// 可用余额可以为负数,由业务承担风险
wallet.Balance -= remaining
}
// 变动后余额和冻结余额
balanceAfter := wallet.Balance
frozenBalanceAfter := wallet.FrozenBalance
// 更新钱包
var updateWalletErr error
if session != nil {
updateWalletErr = svcCtx.AgentWalletModel.UpdateWithVersion(ctx, session, wallet)
} else {
updateWalletErr = svcCtx.AgentWalletModel.UpdateWithVersion(ctx, nil, wallet)
}
if updateWalletErr != nil {
logx.Errorf("更新代理钱包失败代理ID: %d, 错误: %v", wa.agentId, updateWalletErr)
continue
}
// 创建钱包交易流水记录(退款)
transErr := svcCtx.AgentService.CreateWalletTransaction(
ctx,
session,
wa.agentId,
model.WalletTransactionTypeRefund,
-wa.amount*-1, // 钱包流水金额为负数
balanceBefore,
balanceAfter,
frozenBalanceBefore,
frozenBalanceAfter,
order.OrderNo,
0, // 这里不强绑到某一条具体佣金记录,按订单维度记录
"订单退款,佣金已扣除",
)
if transErr != nil {
logx.Errorf("创建代理钱包流水记录失败代理ID: %d, 错误: %v", wa.agentId, transErr)
continue
}
}
return nil
}
type WechatPayRefundCallbackLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewWechatPayRefundCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *WechatPayRefundCallbackLogic {
return &WechatPayRefundCallbackLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// handleQueryOrderRefund 处理查询订单退款
// refundAmountYuan 表示微信本次实际退款金额(单位:元)
func (l *WechatPayRefundCallbackLogic) handleQueryOrderRefund(orderNo string, status refunddomestic.Status, refundAmountYuan float64) error {
order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, orderNo)
if err != nil {
return errors.Wrapf(err, "查找查询订单信息失败: %s", orderNo)
}
// 检查订单是否已经处理过退款
if order.Status == model.OrderStatusRefunded {
logx.Infof("订单已经是退款状态,无需重复处理: orderNo=%s", orderNo)
return nil
}
// 只处理成功和失败状态
var orderStatus, refundStatus string
switch status {
case refunddomestic.STATUS_SUCCESS:
orderStatus = model.OrderStatusRefunded
refundStatus = model.OrderRefundStatusSuccess
case refunddomestic.STATUS_CLOSED:
// 退款关闭,保持订单原状态,更新退款记录为失败
refundStatus = model.OrderRefundStatusFailed
case refunddomestic.STATUS_ABNORMAL:
// 退款异常,保持订单原状态,更新退款记录为失败
refundStatus = model.OrderRefundStatusFailed
default:
// 其他状态暂不处理
return nil
}
// 使用事务同时更新订单和退款记录
err = l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 更新订单状态(仅在退款成功时更新)
if status == refunddomestic.STATUS_SUCCESS {
order.Status = orderStatus
order.RefundTime = sql.NullTime{
Time: time.Now(),
Valid: true,
}
if err := l.svcCtx.OrderModel.UpdateWithVersion(ctx, session, order); err != nil {
return errors.Wrapf(err, "更新查询订单状态失败: %s", orderNo)
}
// 退款成功时,按本次实际退款金额更新代理佣金状态并扣除钱包金额
_ = HandleCommissionAndWalletDeduction(ctx, l.svcCtx, session, order, refundAmountYuan)
}
// 查找最新的pending状态的退款记录
refund, err := l.findLatestPendingRefund(ctx, order.Id)
if err != nil {
if err == model.ErrNotFound {
logx.Errorf("未找到订单对应的待处理退款记录: orderNo=%s, orderId=%d", orderNo, order.Id)
return nil // 没有退款记录时不报错,只记录警告
}
return errors.Wrapf(err, "查找退款记录失败: orderNo=%s", orderNo)
}
// 检查退款记录是否已经处理过
if refund.Status == model.OrderRefundStatusSuccess {
logx.Infof("退款记录已经是成功状态,无需重复处理: orderNo=%s, refundId=%d", orderNo, refund.Id)
return nil
}
refund.Status = refundStatus
if status == refunddomestic.STATUS_SUCCESS {
refund.RefundTime = sql.NullTime{
Time: time.Now(),
Valid: true,
}
} else if status == refunddomestic.STATUS_CLOSED {
refund.CloseTime = sql.NullTime{
Time: time.Now(),
Valid: true,
}
}
if _, err := l.svcCtx.OrderRefundModel.Update(ctx, session, refund); err != nil {
return errors.Wrapf(err, "更新退款记录状态失败: orderNo=%s", orderNo)
}
return nil
})
if err != nil {
return errors.Wrapf(err, "更新订单和退款记录失败: %s", orderNo)
}
return nil
}
// handleAgentOrderRefund 处理代理会员订单退款
func (l *WechatPayRefundCallbackLogic) handleAgentOrderRefund(orderNo string, status refunddomestic.Status) error {
order, err := l.svcCtx.AgentMembershipRechargeOrderModel.FindOneByOrderNo(l.ctx, orderNo)
if err != nil {
return errors.Wrapf(err, "查找代理会员订单信息失败: %s", orderNo)
}
// 检查订单是否已经处理过退款
if order.Status == "refunded" {
logx.Infof("代理会员订单已经是退款状态,无需重复处理: orderNo=%s", orderNo)
return nil
}
if status == refunddomestic.STATUS_SUCCESS {
order.Status = "refunded"
} else if status == refunddomestic.STATUS_ABNORMAL {
return nil // 异常状态直接返回
} else {
return nil // 其他状态直接返回
}
if err := l.svcCtx.AgentMembershipRechargeOrderModel.UpdateWithVersion(l.ctx, nil, order); err != nil {
return errors.Wrapf(err, "更新代理会员订单状态失败: %s", orderNo)
}
return nil
}
// sendSuccessResponse 发送成功响应
func (l *WechatPayRefundCallbackLogic) sendSuccessResponse(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("success"))
}
func (l *WechatPayRefundCallbackLogic) WechatPayRefundCallback(w http.ResponseWriter, r *http.Request) error {
// 1. 处理微信退款通知
notification, err := l.svcCtx.WechatPayService.HandleRefundNotification(l.ctx, r)
if err != nil {
logx.Errorf("微信退款回调处理失败: %v", err)
l.sendSuccessResponse(w)
return nil
}
// 2. 检查关键字段是否为空
if notification.OutTradeNo == nil {
logx.Errorf("微信退款回调OutTradeNo字段为空")
l.sendSuccessResponse(w)
return nil
}
orderNo := *notification.OutTradeNo
// 3. 判断退款状态优先使用Status如果Status为nil则使用SuccessTime判断
var status refunddomestic.Status
var statusDetermined bool = false
if notification.Status != nil {
status = *notification.Status
statusDetermined = true
} else if notification.SuccessTime != nil && !notification.SuccessTime.IsZero() {
// 如果Status为空但SuccessTime有值说明退款成功
status = refunddomestic.STATUS_SUCCESS
statusDetermined = true
} else {
logx.Errorf("微信退款回调Status和SuccessTime都为空无法确定退款状态: orderNo=%s", orderNo)
l.sendSuccessResponse(w)
return nil
}
if !statusDetermined {
logx.Errorf("微信退款回调无法确定退款状态: orderNo=%s", orderNo)
l.sendSuccessResponse(w)
return nil
}
var processErr error
// 计算本次实际退款金额(单位:元),用于后续佣金和钱包扣减
var refundAmountYuan float64
if notification.Amount != nil && notification.Amount.Refund != nil {
// 微信退款金额单位为分,这里转换为元
refundAmountYuan = float64(*notification.Amount.Refund) / 100.0
}
// 4. 根据订单号前缀处理不同类型的订单
switch {
case strings.HasPrefix(orderNo, "Q_"):
processErr = l.handleQueryOrderRefund(orderNo, status, refundAmountYuan)
case strings.HasPrefix(orderNo, "A_"):
processErr = l.handleAgentOrderRefund(orderNo, status)
default:
// 兼容旧订单,假设没有前缀的是查询订单
processErr = l.handleQueryOrderRefund(orderNo, status, refundAmountYuan)
}
// 5. 处理错误并响应
if processErr != nil {
logx.Errorf("处理退款订单失败: orderNo=%s, err=%v", orderNo, processErr)
}
// 无论处理是否成功,都返回成功响应给微信
l.sendSuccessResponse(w)
return nil
}
// findLatestPendingRefund 查找订单最新的pending状态退款记录
func (l *WechatPayRefundCallbackLogic) findLatestPendingRefund(ctx context.Context, orderId int64) (*model.OrderRefund, error) {
// 使用SelectBuilder查询最新的pending状态退款记录
builder := l.svcCtx.OrderRefundModel.SelectBuilder().
Where("order_id = ? AND status = ? AND del_state = ?", orderId, model.OrderRefundStatusPending, globalkey.DelStateNo).
OrderBy("id DESC").
Limit(1)
refunds, err := l.svcCtx.OrderRefundModel.FindAll(ctx, builder, "")
if err != nil {
return nil, err
}
if len(refunds) == 0 {
return nil, model.ErrNotFound
}
return refunds[0], nil
}