package wechat import ( "context" refundRequest "github.com/ArtisanCloud/PowerWeChat/v3/src/payment/refund/request" refundResponse "github.com/ArtisanCloud/PowerWeChat/v3/src/payment/refund/response" "github.com/break/junhong_cmp_fiber/pkg/errors" "go.uber.org/zap" ) // 微信支付 v3 退款单状态(渠道 status 字段原文) const ( // refundStatusSuccess 退款成功 refundStatusSuccess = "SUCCESS" // refundStatusClosed 退款关闭 refundStatusClosed = "CLOSED" // refundStatusProcessing 退款处理中 refundStatusProcessing = "PROCESSING" // refundStatusAbnormal 退款异常 refundStatusAbnormal = "ABNORMAL" ) // RefundOrderRequest 微信支付 v3 原路退款请求 // // 金额单位均为分,且均为整型直传渠道;本结构不包含任何通知地址字段, // 退款进度由调用方通过 QueryRefund 主动查询获得。 type RefundOrderRequest struct { // OutTradeNo 商户订单号(原支付单号) OutTradeNo string `json:"out_trade_no"` // OutRefundNo 商户退款单号 OutRefundNo string `json:"out_refund_no"` // Amount 原订单金额,单位分 Amount int64 `json:"amount"` // Refund 本次退款金额,单位分 Refund int64 `json:"refund"` // Reason 退款原因,可选 Reason string `json:"reason"` // Currency 退款币种,可选,为空时使用默认值 CNY Currency string `json:"currency"` } // RefundOrderResult 微信支付 v3 退款结果 type RefundOrderResult struct { // Success 是否退款成功,仅当渠道状态为 SUCCESS 时为 true Success bool `json:"success"` // RefundID 微信支付退款单号 RefundID string `json:"refund_id"` // OutRefundNo 商户退款单号 OutRefundNo string `json:"out_refund_no"` // OutTradeNo 商户订单号 OutTradeNo string `json:"out_trade_no"` // TransactionID 微信支付订单号 TransactionID string `json:"transaction_id"` // Status 渠道退款状态原文:SUCCESS/CLOSED/PROCESSING/ABNORMAL Status string `json:"status"` // RefundFee 退款金额,单位分 RefundFee int64 `json:"refund_fee"` // ChannelCode 渠道业务错误码,仅在渠道返回错误响应体时非空 ChannelCode string `json:"channel_code"` // Message 状态说明 Message string `json:"message"` } // RefundOrder 发起微信支付 v3 原路退款 // // 不向渠道传递任何通知地址(notify_url 留空,接口调用时该字段不会出现在请求体中), // 退款结果以本次返回的 Status 为准,异步进度由调用方通过 QueryRefund 查询。 func (s *PaymentService) RefundOrder(ctx context.Context, req RefundOrderRequest) (*RefundOrderResult, error) { if req.OutTradeNo == "" || req.OutRefundNo == "" { return nil, errors.New(errors.CodeInvalidParam, "商户订单号和商户退款单号不能为空") } if req.Amount <= 0 || req.Refund <= 0 { return nil, errors.New(errors.CodeInvalidParam, "原订单金额和退款金额必须大于 0") } if req.Refund > req.Amount { return nil, errors.New(errors.CodeInvalidParam, "退款金额不能大于原订单金额") } currency := req.Currency if currency == "" { currency = "CNY" } resp, err := s.app.Refund.Refund(ctx, &refundRequest.RequestRefund{ OutTradeNo: req.OutTradeNo, OutRefundNo: req.OutRefundNo, Reason: req.Reason, Amount: &refundRequest.RefundAmount{ Refund: int(req.Refund), Total: int(req.Amount), Currency: currency, }, }) if err != nil { s.logger.Error("发起微信退款失败", zap.String("out_trade_no", req.OutTradeNo), zap.String("out_refund_no", req.OutRefundNo), zap.Int64("refund", req.Refund), zap.Error(err), ) return nil, errors.Wrap(errors.CodeWechatPayFailed, err) } if resp == nil { s.logger.Error("发起微信退款失败:空响应", zap.String("out_trade_no", req.OutTradeNo), zap.String("out_refund_no", req.OutRefundNo), ) return nil, errors.New(errors.CodeWechatPayFailed, "微信退款失败:渠道返回空响应") } result := mapRefundResult(resp) if result.Success { s.logger.Info("发起微信退款成功", zap.String("out_trade_no", req.OutTradeNo), zap.String("out_refund_no", req.OutRefundNo), zap.String("refund_id", result.RefundID), zap.String("status", result.Status), ) } else { s.logger.Warn("发起微信退款未成功", zap.String("out_trade_no", req.OutTradeNo), zap.String("out_refund_no", req.OutRefundNo), zap.String("status", result.Status), zap.String("channel_code", result.ChannelCode), zap.String("message", result.Message), ) } return result, nil } // QueryRefund 查询微信支付 v3 退款单 // // 状态与金额映射规则与 RefundOrder 一致。 func (s *PaymentService) QueryRefund(ctx context.Context, outRefundNo string) (*RefundOrderResult, error) { if outRefundNo == "" { return nil, errors.New(errors.CodeInvalidParam, "商户退款单号不能为空") } resp, err := s.app.Refund.Query(ctx, outRefundNo) if err != nil { s.logger.Error("查询微信退款失败", zap.String("out_refund_no", outRefundNo), zap.Error(err), ) return nil, errors.Wrap(errors.CodeWechatPayFailed, err) } if resp == nil { s.logger.Error("查询微信退款失败:空响应", zap.String("out_refund_no", outRefundNo)) return nil, errors.New(errors.CodeWechatPayFailed, "查询微信退款失败:渠道返回空响应") } result := mapRefundResult(resp) s.logger.Debug("查询微信退款完成", zap.String("out_refund_no", outRefundNo), zap.String("refund_id", result.RefundID), zap.String("status", result.Status), zap.String("channel_code", result.ChannelCode), ) return result, nil } // mapRefundResult 将微信退款接口响应映射为统一结果 // // 渠道 status 为 SUCCESS 时 Success 为 true;CLOSED/ABNORMAL/PROCESSING 等明确状态 // 返回结果且不报错,由调用方按 Status 与 Message 处理。 // 渠道返回业务错误码(响应体含 code,通常为 4xx,退款单未被受理)同样返回结果, // 由调用方按 ChannelCode 判定;只有传输或解析失败才在调用处作为 error 返回—— // 这样调用方才能把「渠道明确表态」与「结果未知」区分开。 func mapRefundResult(resp *refundResponse.ResponseRefund) *RefundOrderResult { if resp.Code != "" { return &RefundOrderResult{ OutRefundNo: resp.OutRefundNO, OutTradeNo: resp.OutTradeNO, ChannelCode: resp.Code, Message: "微信退款失败:" + resp.Code + " " + resp.Message, } } result := &RefundOrderResult{ RefundID: resp.RefundID, OutRefundNo: resp.OutRefundNO, OutTradeNo: resp.OutTradeNO, TransactionID: resp.TransactionID, Status: resp.Status, } if resp.Amount != nil { result.RefundFee = int64(resp.Amount.Refund) } result.Success = resp.Status == refundStatusSuccess result.Message = refundStatusMessage(resp.Status) return result } // refundStatusMessage 返回微信退款状态的说明文案 func refundStatusMessage(status string) string { switch status { case refundStatusSuccess: return "退款成功" case refundStatusClosed: return "退款已关闭" case refundStatusAbnormal: return "退款异常" case refundStatusProcessing: return "退款处理中" case "": return "退款状态未知,请稍后查询" default: return "退款状态:" + status } }