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 处理退款后的佣金状态更新和钱包金额扣除 // 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 }