fix: 修复微信/富友支付回调配置加载错误
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m5s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m5s
- WechatPayCallback 移除对永远为 nil 的单例 wechatPayment 的依赖, 改为动态按 payment_config_id 加载创建订单时所用的精确配置 - PaymentV2Service 新增 VerifyCallback()(v2 XML 验签)和 PeekOrderNo()(不验签预解析) - FuiouPayCallback 由 GetActiveConfig 改为 GetConfigForCallback, 通过 ParseNotify 预解析订单号后精确加载配置,防止切换配置后旧订单验签失败 - wechat_config.Service 注入 assetRechargeStore/agentRechargeStore, 新增 GetConfigForCallback():按订单号前缀查 payment_config_id, GetByIDUnscoped 加载配置(含已停用/软删除记录),找不到则回退激活配置 - 修复微信 v2 回调响应格式:从 JSON 改为 XML,避免微信持续重试
This commit is contained in:
@@ -7,6 +7,29 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseNotify 解析回调通知(不验签)
|
||||
// 用于在验签前提取订单号以确定使用哪套支付配置
|
||||
func ParseNotify(rawBody []byte) (*NotifyRequest, error) {
|
||||
content := string(rawBody)
|
||||
if strings.HasPrefix(content, "req=") {
|
||||
content = content[4:]
|
||||
}
|
||||
decoded, err := url.QueryUnescape(content)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("URL 解码回调数据失败: %w", err)
|
||||
}
|
||||
utf8Bytes, err := GBKToUTF8([]byte(decoded))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GBK 转 UTF-8 失败: %w", err)
|
||||
}
|
||||
utf8Str := strings.Replace(string(utf8Bytes), `encoding="GBK"`, `encoding="UTF-8"`, 1)
|
||||
var notify NotifyRequest
|
||||
if err := xml.Unmarshal([]byte(utf8Str), ¬ify); err != nil {
|
||||
return nil, fmt.Errorf("XML 解析回调数据失败: %w", err)
|
||||
}
|
||||
return ¬ify, nil
|
||||
}
|
||||
|
||||
// VerifyNotify 验证回调通知并解析数据
|
||||
// rawBody: 原始请求 body(可能包含 req= 前缀的 URL 编码 GBK XML)
|
||||
func (c *Client) VerifyNotify(rawBody []byte) (*NotifyRequest, error) {
|
||||
|
||||
@@ -201,3 +201,96 @@ func v2SignMD5(params map[string]string, apiKey string) string {
|
||||
func v2GenerateNonceStr() string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d", time.Now().UnixNano()))))
|
||||
}
|
||||
|
||||
// v2CallbackNotify v2 支付回调通知 XML 结构
|
||||
// 所有字段均为 string,避免 CDATA 整数字段的类型转换问题
|
||||
type v2CallbackNotify struct {
|
||||
XMLName xml.Name `xml:"xml"`
|
||||
AppID string `xml:"appid"`
|
||||
BankType string `xml:"bank_type"`
|
||||
CashFee string `xml:"cash_fee"`
|
||||
FeeType string `xml:"fee_type"`
|
||||
IsSubscribe string `xml:"is_subscribe"`
|
||||
MchID string `xml:"mch_id"`
|
||||
NonceStr string `xml:"nonce_str"`
|
||||
OpenID string `xml:"openid"`
|
||||
OutTradeNo string `xml:"out_trade_no"`
|
||||
ResultCode string `xml:"result_code"`
|
||||
ReturnCode string `xml:"return_code"`
|
||||
Sign string `xml:"sign"`
|
||||
TimeEnd string `xml:"time_end"`
|
||||
TotalFee string `xml:"total_fee"`
|
||||
TradeType string `xml:"trade_type"`
|
||||
TransactionID string `xml:"transaction_id"`
|
||||
}
|
||||
|
||||
// PeekOrderNo 从 v2 回调 XML 中解析订单号(不验签)
|
||||
// 用于在验签前确定使用哪套支付配置
|
||||
func PeekOrderNo(body []byte) (string, error) {
|
||||
var notify v2CallbackNotify
|
||||
if err := xml.Unmarshal(body, ¬ify); err != nil {
|
||||
return "", fmt.Errorf("解析回调 XML 失败: %w", err)
|
||||
}
|
||||
return notify.OutTradeNo, nil
|
||||
}
|
||||
|
||||
// V2CallbackResult v2 回调解析结果
|
||||
type V2CallbackResult struct {
|
||||
OutTradeNo string
|
||||
TransactionID string
|
||||
// TradeState 为 "SUCCESS" 表示支付成功(return_code 和 result_code 均为 SUCCESS)
|
||||
TradeState string
|
||||
TotalFee string
|
||||
OpenID string
|
||||
}
|
||||
|
||||
// VerifyCallback 解析并验证 v2 回调 XML,返回解析结果
|
||||
func (s *PaymentV2Service) VerifyCallback(body []byte) (*V2CallbackResult, error) {
|
||||
var notify v2CallbackNotify
|
||||
if err := xml.Unmarshal(body, ¬ify); err != nil {
|
||||
return nil, fmt.Errorf("解析回调 XML 失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建待签名参数(排除 sign 字段,v2SignMD5 内部自动跳过空值)
|
||||
params := map[string]string{
|
||||
"appid": notify.AppID,
|
||||
"bank_type": notify.BankType,
|
||||
"cash_fee": notify.CashFee,
|
||||
"fee_type": notify.FeeType,
|
||||
"is_subscribe": notify.IsSubscribe,
|
||||
"mch_id": notify.MchID,
|
||||
"nonce_str": notify.NonceStr,
|
||||
"openid": notify.OpenID,
|
||||
"out_trade_no": notify.OutTradeNo,
|
||||
"result_code": notify.ResultCode,
|
||||
"return_code": notify.ReturnCode,
|
||||
"time_end": notify.TimeEnd,
|
||||
"total_fee": notify.TotalFee,
|
||||
"trade_type": notify.TradeType,
|
||||
"transaction_id": notify.TransactionID,
|
||||
}
|
||||
|
||||
expected := v2SignMD5(params, s.apiKey)
|
||||
if !strings.EqualFold(notify.Sign, expected) {
|
||||
s.logger.Error("v2 回调签名验证失败",
|
||||
zap.String("received", notify.Sign),
|
||||
zap.String("expected", expected),
|
||||
zap.String("mch_id", notify.MchID),
|
||||
)
|
||||
return nil, fmt.Errorf("签名验证失败")
|
||||
}
|
||||
|
||||
// return_code 和 result_code 均为 SUCCESS 才算支付成功
|
||||
tradeState := ""
|
||||
if notify.ReturnCode == "SUCCESS" && notify.ResultCode == "SUCCESS" {
|
||||
tradeState = "SUCCESS"
|
||||
}
|
||||
|
||||
return &V2CallbackResult{
|
||||
OutTradeNo: notify.OutTradeNo,
|
||||
TransactionID: notify.TransactionID,
|
||||
TradeState: tradeState,
|
||||
TotalFee: notify.TotalFee,
|
||||
OpenID: notify.OpenID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user