Files
hm-server/app/main/api/internal/logic/pay/wechatpayrefundcallbacklogic.go
2026-01-07 16:50:42 +08:00

337 lines
11 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"
"tydata-server/app/main/api/internal/svc"
"tydata-server/app/main/model"
"tydata-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 处理退款后的佣金状态更新和钱包金额扣除
// 这是一个公共函数,可以被支付宝和微信退款逻辑共享使用
func HandleCommissionAndWalletDeduction(ctx context.Context, svcCtx *svc.ServiceContext, session sqlx.Session, order *model.Order) error {
// 查询非已退款的佣金
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因为佣金更新失败不应影响退款流程
}
for _, commission := range commissions {
// 记录更新佣金状态前的状态值,用于后续判断
oldCommissionStatus := commission.Status
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 // 如果佣金状态更新失败,就不继续处理钱包
}
// 处理用户钱包的金额扣除
wallet, err := svcCtx.AgentWalletModel.FindOneByAgentId(ctx, commission.AgentId)
if err != nil {
logx.Errorf("查询代理钱包失败代理ID: %d, 错误: %v", commission.AgentId, err)
continue
}
// 记录变动前的余额
balanceBefore := wallet.Balance
frozenBalanceBefore := wallet.FrozenBalance
// 如果是已结算状态的佣金status = 1直接扣减钱包余额
if oldCommissionStatus == 1 {
// 冻结状态的佣金,根据订单金额优先减少冻结金额,如果冻结金额不足则减少钱包余额
if wallet.FrozenBalance >= order.Amount {
// 冻结余额足够,优先减少冻结金额
wallet.FrozenBalance -= order.Amount
} else {
// 冻结余额不足,先扣减所有冻结金额,再扣减余额
remaining := order.Amount - wallet.FrozenBalance
wallet.FrozenBalance = 0
wallet.Balance -= remaining
}
// 其他状态的佣金比如已结算状态status = 1直接扣减钱包余额
} else {
wallet.Balance -= order.Amount
}
// 变动后余额和冻结余额
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", commission.AgentId, updateWalletErr)
continue
}
// 创建钱包交易流水记录(退款)
transErr := svcCtx.AgentService.CreateWalletTransaction(
ctx,
session,
commission.AgentId,
model.WalletTransactionTypeRefund,
-order.Amount,
balanceBefore,
balanceAfter,
frozenBalanceBefore,
frozenBalanceAfter,
order.OrderNo,
commission.Id,
"订单退款,佣金已扣除",
)
if transErr != nil {
logx.Errorf("创建代理钱包流水记录失败代理ID: %d, 错误: %v", commission.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 处理查询订单退款
func (l *WechatPayRefundCallbackLogic) handleQueryOrderRefund(orderNo string, status refunddomestic.Status) 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)
}
// 查找最新的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
// 4. 根据订单号前缀处理不同类型的订单
switch {
case strings.HasPrefix(orderNo, "Q_"):
processErr = l.handleQueryOrderRefund(orderNo, status)
case strings.HasPrefix(orderNo, "A_"):
processErr = l.handleAgentOrderRefund(orderNo, status)
default:
// 兼容旧订单,假设没有前缀的是查询订单
processErr = l.handleQueryOrderRefund(orderNo, status)
}
// 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
}