fix: 微信 H5 下单被明确拒绝时报真实原因并关单,不再显示请求超时
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
微信 v2 统一下单返回 result_code=FAIL(如 NOAUTH 商户未开通 H5 产品)时订单未创建, 原来一律按结果未知返回"请求超时"、订单挂到支付截止才关。改为验签后按明确失败返回微信原因, 调用方立即关单;用微信配置不可用错误码,不触发商户自动停用。ORDERPAID、OUT_TRADE_NO_USED、SYSTEMERROR 仍按未知处理。
This commit is contained in:
@@ -47,6 +47,18 @@ func (a *WechatWebAdapter) CreatePaymentURL(ctx context.Context, request agentre
|
||||
PayerClientIP: request.PayerClientIP,
|
||||
H5Type: "Wap",
|
||||
})
|
||||
// 微信明确拒绝(订单未创建):记失败并返回真实原因,调用方立即关单;
|
||||
// 用"微信配置不可用"错误码,不触发支付失败自动停用商户(如 H5 产品未开通不影响该商户的公众号支付)。
|
||||
var bizErr *wechatpay.V2BusinessError
|
||||
if stderrors.As(callErr, &bizErr) {
|
||||
if _, err = a.integration.Complete(ctx, attempt.IntegrationID, integrationlog.Completion{
|
||||
Result: constants.IntegrationResultFailed, ProviderCode: bizErr.Code, ProviderMessage: bizErr.Message,
|
||||
ResponseSummary: map[string]any{"success": false}, DurationMS: time.Since(startedAt).Milliseconds(),
|
||||
}); err != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, err
|
||||
}
|
||||
return agentrecharge.OnlinePaymentResult{}, apperrors.New(apperrors.CodeWechatConfigUnavailable, bizErr.Error())
|
||||
}
|
||||
if callErr != nil {
|
||||
return agentrecharge.OnlinePaymentResult{}, a.completeUnknown(ctx, attempt.IntegrationID, startedAt, callErr)
|
||||
}
|
||||
|
||||
@@ -85,6 +85,35 @@ type v2UnifiedOrderResponse struct {
|
||||
MwebURL string `xml:"mweb_url"`
|
||||
}
|
||||
|
||||
// V2BusinessError 是微信 v2 统一下单明确返回业务失败(result_code=FAIL),订单未在微信侧创建。
|
||||
// 调用方据此直接报真实原因并关闭本地订单,而不是按"结果未知"等待查单。
|
||||
type V2BusinessError struct {
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *V2BusinessError) Error() string {
|
||||
return fmt.Sprintf("微信支付下单被拒绝:%s(%s)", e.Message, e.Code)
|
||||
}
|
||||
|
||||
// v2UnifiedOrderUncertainCodes 是业务失败但订单可能已存在或结果不确定的错误码,仍按结果未知处理。
|
||||
var v2UnifiedOrderUncertainCodes = map[string]bool{"ORDERPAID": true, "OUT_TRADE_NO_USED": true, "SYSTEMERROR": true}
|
||||
|
||||
// v2BusinessFailure 判断统一下单响应是否为明确的业务失败;只有签名校验通过的响应才认定,避免伪造响应误关单。
|
||||
func v2BusinessFailure(resp v2UnifiedOrderResponse, respBytes []byte, apiKey string) *V2BusinessError {
|
||||
if resp.ReturnCode != "SUCCESS" || resp.ResultCode != "FAIL" || resp.ErrCode == "" || v2UnifiedOrderUncertainCodes[resp.ErrCode] {
|
||||
return nil
|
||||
}
|
||||
if verifyV2ResponseSign(respBytes, apiKey) != nil {
|
||||
return nil
|
||||
}
|
||||
message := resp.ErrCodeDes
|
||||
if message == "" {
|
||||
message = resp.ErrCode
|
||||
}
|
||||
return &V2BusinessError{Code: resp.ErrCode, Message: message}
|
||||
}
|
||||
|
||||
// v2OrderQueryRequest 商户订单号查单请求(v2 XML 格式)
|
||||
type v2OrderQueryRequest struct {
|
||||
XMLName xml.Name `xml:"xml"`
|
||||
@@ -266,6 +295,9 @@ func (s *PaymentV2Service) CreateH5Order(ctx context.Context, orderNo, descripti
|
||||
if resp.ReturnCode != "SUCCESS" || resp.ResultCode != "SUCCESS" || resp.MwebURL == "" {
|
||||
s.logger.Error("微信 v2 MWEB 统一下单失败", zap.String("order_no", orderNo), zap.String("return_code", resp.ReturnCode),
|
||||
zap.String("return_msg", resp.ReturnMsg), zap.String("result_code", resp.ResultCode), zap.String("err_code", resp.ErrCode), zap.String("err_code_des", resp.ErrCodeDes))
|
||||
if bizErr := v2BusinessFailure(resp, respBytes, s.apiKey); bizErr != nil {
|
||||
return nil, bizErr
|
||||
}
|
||||
return nil, errors.New(errors.CodeWechatPayFailed, "创建 H5 支付订单失败")
|
||||
}
|
||||
if err = verifyV2ResponseSign(respBytes, s.apiKey); err != nil {
|
||||
|
||||
51
pkg/wechat/payment_v2_business_error_test.go
Normal file
51
pkg/wechat/payment_v2_business_error_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func signedUnifiedOrderResponse(apiKey string, fields map[string]string) ([]byte, v2UnifiedOrderResponse) {
|
||||
fields["sign"] = v2SignMD5(fields, apiKey)
|
||||
body := "<xml>"
|
||||
for key, value := range fields {
|
||||
body += fmt.Sprintf("<%s><![CDATA[%s]]></%s>", key, value, key)
|
||||
}
|
||||
body += "</xml>"
|
||||
var resp v2UnifiedOrderResponse
|
||||
_ = xml.Unmarshal([]byte(body), &resp)
|
||||
return []byte(body), resp
|
||||
}
|
||||
|
||||
func TestV2BusinessFailure(t *testing.T) {
|
||||
const key = "test-api-key-0123456789abcdef01234"
|
||||
noauth := map[string]string{"return_code": "SUCCESS", "return_msg": "OK", "result_code": "FAIL",
|
||||
"err_code": "NOAUTH", "err_code_des": "商户号该产品权限未开通", "nonce_str": "n1"}
|
||||
body, resp := signedUnifiedOrderResponse(key, noauth)
|
||||
got := v2BusinessFailure(resp, body, key)
|
||||
if got == nil || got.Code != "NOAUTH" || got.Message != "商户号该产品权限未开通" {
|
||||
t.Fatalf("NOAUTH should be explicit failure, got %+v", got)
|
||||
}
|
||||
if got.Error() != "微信支付下单被拒绝:商户号该产品权限未开通(NOAUTH)" {
|
||||
t.Fatalf("unexpected message %q", got.Error())
|
||||
}
|
||||
|
||||
if v2BusinessFailure(resp, body, "wrong-key-0123456789abcdef0123456") != nil {
|
||||
t.Fatal("unsigned / wrongly signed response must not be treated as explicit failure")
|
||||
}
|
||||
|
||||
for _, code := range []string{"ORDERPAID", "OUT_TRADE_NO_USED", "SYSTEMERROR"} {
|
||||
fields := map[string]string{"return_code": "SUCCESS", "result_code": "FAIL", "err_code": code, "nonce_str": "n2"}
|
||||
b, r := signedUnifiedOrderResponse(key, fields)
|
||||
if v2BusinessFailure(r, b, key) != nil {
|
||||
t.Fatalf("%s must stay uncertain", code)
|
||||
}
|
||||
}
|
||||
|
||||
commFail := map[string]string{"return_code": "FAIL", "return_msg": "签名错误", "nonce_str": "n3"}
|
||||
b, r := signedUnifiedOrderResponse(key, commFail)
|
||||
if v2BusinessFailure(r, b, key) != nil {
|
||||
t.Fatal("return_code FAIL is a communication failure, not an explicit business failure")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user