package alipay import ( "context" "fmt" "github.com/smartwalle/alipay/v3" "github.com/break/junhong_cmp_fiber/internal/model" apperrors "github.com/break/junhong_cmp_fiber/pkg/errors" ) // RefundStatusSuccess 支付宝退款查询接口(alipay.trade.fastpay.refund.query) // 响应字段 refund_status 中表示「退款处理成功」的取值。 const RefundStatusSuccess = "REFUND_SUCCESS" // RefundRequest 支付宝退款请求参数。 // OutTradeNo 与 TradeNo 二选一;OutRequestNo 标识一次退款请求,同一笔交易多次退款必须保证唯一。 type RefundRequest struct { OutTradeNo string // 商户订单号,与 TradeNo 二选一 TradeNo string // 支付宝交易号,与 OutTradeNo 二选一 OutRequestNo string // 商户退款请求号,必填 RefundAmount int64 // 退款金额,单位:分 RefundReason string // 退款原因说明,可选 TotalAmount int64 // 订单总金额,单位:分,可选(alipay.trade.refund 不接收该字段,仅供调用方上下文使用) } // RefundResult 支付宝退款、退款查询的统一结果。 // RefundFee、字段金额单位均为分;Success 表示渠道业务处理成功。 type RefundResult struct { Success bool // 渠道业务是否成功 TradeNo string // 支付宝交易号 OutTradeNo string // 商户订单号 OutRequestNo string // 商户退款请求号 RefundFee int64 // 退款金额,单位:分 RefundStatus string // 渠道返回的退款状态原文,可能为空 Message string // 渠道返回的失败原因,成功时为空 } // Refund 发起支付宝退款(alipay.trade.refund)。 // RefundAmount(分)按金额精度要求转换为元字符串(两位小数)后提交。 // 渠道业务失败时返回 Success=false 与渠道 Message,不返回 error(渠道拒绝不是传输错误); // 传输失败、响应不可解析等 SDK 层错误才返回 error,此时 result 为 nil, // 调用方因无法确认退款结果,应通过 QueryRefund 查询退款状态。 // TradeRefundRsp 不返回 out_request_no,故 OutRequestNo 以入参为准; // 该响应亦无 refund_status 字段,需要退款状态原文时使用 QueryRefund。 func Refund(ctx context.Context, cfg *model.WechatConfig, req RefundRequest) (*RefundResult, error) { client, err := NewClientFromConfig(cfg) if err != nil { return nil, err } param := alipay.TradeRefund{ OutTradeNo: req.OutTradeNo, TradeNo: req.TradeNo, OutRequestNo: req.OutRequestNo, RefundAmount: FenToYuan(req.RefundAmount), RefundReason: req.RefundReason, } rsp, err := client.TradeRefund(ctx, param) if err != nil { return nil, apperrors.Wrap(apperrors.CodeServiceUnavailable, err, "支付宝退款请求失败") } result := &RefundResult{ TradeNo: preferChannel(rsp.TradeNo, req.TradeNo), OutTradeNo: preferChannel(rsp.OutTradeNo, req.OutTradeNo), OutRequestNo: req.OutRequestNo, } if rsp.IsFailure() { result.Message = channelMessage(rsp.Error) return result, nil } refundFee, err := parseChannelAmount(rsp.RefundFee) if err != nil { return nil, apperrors.Wrap(apperrors.CodeServiceUnavailable, err, "支付宝退款响应不可解析") } result.Success = true result.RefundFee = refundFee return result, nil } // QueryRefund 查询支付宝退款(alipay.trade.fastpay.refund.query)。 // 以 OutRequestNo 定位一次退款请求,映射渠道返回的退款金额与退款状态。 // 仅当渠道业务码成功且 refund_status 为 REFUND_SUCCESS 时 Success=true; // 渠道拒绝、refund_status 非成功(未返回该字段表示退款请求未收到或退款失败)时 // 返回 Success=false 与渠道 Message/RefundStatus 原文,不返回 error; // 传输失败、响应不可解析等 SDK 层错误才返回 error,此时 result 为 nil。 func QueryRefund(ctx context.Context, cfg *model.WechatConfig, req RefundRequest) (*RefundResult, error) { client, err := NewClientFromConfig(cfg) if err != nil { return nil, err } param := alipay.TradeFastPayRefundQuery{ OutTradeNo: req.OutTradeNo, TradeNo: req.TradeNo, OutRequestNo: req.OutRequestNo, } rsp, err := client.TradeFastPayRefundQuery(ctx, param) if err != nil { return nil, apperrors.Wrap(apperrors.CodeServiceUnavailable, err, "支付宝退款查询请求失败") } result := &RefundResult{ TradeNo: preferChannel(rsp.TradeNo, req.TradeNo), OutTradeNo: preferChannel(rsp.OutTradeNo, req.OutTradeNo), OutRequestNo: preferChannel(rsp.OutRequestNo, req.OutRequestNo), RefundStatus: rsp.RefundStatus, } if rsp.IsFailure() { result.Message = channelMessage(rsp.Error) return result, nil } refundFee, err := parseChannelAmount(rsp.RefundAmount) if err != nil { return nil, apperrors.Wrap(apperrors.CodeServiceUnavailable, err, "支付宝退款查询响应不可解析") } result.RefundFee = refundFee if rsp.RefundStatus != RefundStatusSuccess { result.Message = "支付宝退款状态非成功" if rsp.RefundStatus != "" { result.Message += ": " + rsp.RefundStatus } return result, nil } result.Success = true return result, nil } // preferChannel 渠道返回值优先,渠道未返回时回退到请求值。 func preferChannel(channelValue, requestValue string) string { if channelValue != "" { return channelValue } return requestValue } // channelMessage 拼接支付宝返回的错误信息,格式为「错误码/子错误码: 错误描述」,忽略空字段。 func channelMessage(e alipay.Error) string { code := string(e.Code) if e.SubCode != "" { if code == "" { code = e.SubCode } else { code += "/" + e.SubCode } } detail := e.SubMsg if detail == "" { detail = e.Msg } switch { case code == "": return detail case detail == "": return code default: return code + ": " + detail } } // parseChannelAmount 将渠道返回的元字符串(如 "1.00")精确转换为分。 // 渠道未返回该字段(空字符串)时按 0 处理,不丢精度。 func parseChannelAmount(yuan string) (int64, error) { if yuan == "" { return 0, nil } fen, err := YuanToFen(yuan) if err != nil { return 0, fmt.Errorf("渠道返回金额 %q 无法解析: %w", yuan, err) } return fen, nil }