fix(payment): 修正扫呗签名并自动查询支付结果

This commit is contained in:
yml
2026-11-11 16:32:22 +08:00
parent d403ffc734
commit 0963e58fa3
6 changed files with 125 additions and 14 deletions
@@ -160,7 +160,7 @@ func (c *Client) CreatePayment(ctx context.Context, req CreatePaymentRequest) (*
"terminal_trace": req.ThirdOrderID,
"terminal_time": now.Format("20060102150405"),
"total_fee": strconv.FormatInt(req.AmountCent, 10),
"order_body": sanitizeText(req.Body, 128),
"order_body": sanitizeOrderBody(req.Body),
"notify_url": firstNonEmpty(req.NotifyURL, c.cfg.NotifyURL),
"attach": sanitizeText(req.Attach, 128),
"timeout_express": strconv.Itoa(expireSeconds),
@@ -175,8 +175,12 @@ func (c *Client) CreatePayment(ctx context.Context, req CreatePaymentRequest) (*
if err != nil {
return nil, err
}
if err := c.verifyResponse(raw); err != nil {
return nil, err
if err := c.verifyResponseOrChannelError(raw); err != nil {
return &CreatePaymentResponse{
ErrorMessage: responseMessage(raw),
Raw: raw,
RawRequest: copyMap(params),
}, err
}
return &CreatePaymentResponse{
OK: responseOK(raw),
@@ -219,7 +223,7 @@ func (c *Client) QueryPayment(ctx context.Context, req QueryPaymentRequest) (*Qu
if err != nil {
return nil, err
}
if err := c.verifyResponse(raw); err != nil {
if err := c.verifyResponseOrChannelError(raw); err != nil {
return nil, err
}
return &QueryPaymentResponse{
@@ -269,7 +273,7 @@ func (c *Client) CreateRefund(ctx context.Context, req CreateRefundRequest) (*Cr
if err != nil {
return nil, err
}
if err := c.verifyResponse(raw); err != nil {
if err := c.verifyResponseOrChannelError(raw); err != nil {
return nil, err
}
return &CreateRefundResponse{
@@ -313,7 +317,7 @@ func (c *Client) QueryRefund(ctx context.Context, req QueryRefundRequest) (*Quer
if err != nil {
return nil, err
}
if err := c.verifyResponse(raw); err != nil {
if err := c.verifyResponseOrChannelError(raw); err != nil {
return nil, err
}
return &QueryRefundResponse{
@@ -468,6 +472,18 @@ func (c *Client) verifyResponse(raw map[string]string) error {
return nil
}
func (c *Client) verifyResponseOrChannelError(raw map[string]string) error {
if err := c.verifyResponse(raw); err == nil {
return nil
}
// 扫呗的失败响应通常不带 key_sign。失败响应只会阻断支付或触发退款查询,
// 可以安全地保留为业务错误;成功响应仍必须严格验签。
if responseCode(raw) != "01" {
return nil
}
return ErrVerifyFailed
}
func responseCode(raw map[string]string) string {
return strings.TrimSpace(raw["return_code"])
}
@@ -573,6 +589,22 @@ func sanitizeText(value string, max int) string {
return value
}
func sanitizeOrderBody(value string) string {
value = strings.Join(strings.Fields(value), "-")
value = strings.NewReplacer(
"+", "-",
"/", "-",
"?", "",
"%", "",
"#", "",
"&", "",
"=", "",
"【", "[",
"】", "]",
).Replace(value)
return sanitizeText(value, 128)
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
@@ -67,7 +67,7 @@ func TestCreatePaymentPostsQRPayAndVerifiesResponse(t *testing.T) {
resp, err := client.CreatePayment(context.Background(), CreatePaymentRequest{
ThirdOrderID: "PAY1",
AmountCent: 100,
Body: "租号订单",
Body: "租号订单 ORD1",
Attach: "ORD1",
})
if err != nil {
@@ -82,6 +82,33 @@ func TestCreatePaymentPostsQRPayAndVerifiesResponse(t *testing.T) {
if captured["repeated_trace"] != "3" || captured["auto_pay"] != "1" {
t.Fatalf("qrpay safety fields = %#v", captured)
}
if captured["order_body"] != "租号订单-ORD1" {
t.Fatalf("order_body = %q, want safe characters only", captured["order_body"])
}
}
func TestCreatePaymentTreatsUnsignedFailureAsBusinessError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"return_code":"02","return_msg":"签名未通过校验!","result_code":"02"}`))
}))
defer server.Close()
client := NewClient(Config{
GatewayURL: server.URL,
MerchantID: "M1",
TerminalID: "T1",
AccessToken: "token-1",
})
resp, err := client.CreatePayment(context.Background(), CreatePaymentRequest{
ThirdOrderID: "PAY1",
AmountCent: 100,
})
if err != nil {
t.Fatalf("CreatePayment() error = %v, want nil business error", err)
}
if resp.OK || resp.ErrorMessage == "" || resp.Raw["return_code"] != "02" {
t.Fatalf("CreatePayment() response = %+v", resp)
}
}
func TestQueryPaymentUsesTraceTimeAndMapsSuccess(t *testing.T) {
@@ -497,6 +497,13 @@ func (c saobeiChannel) CreatePayment(ctx context.Context, req channelCreatePayme
Attach: req.Attach,
})
if err != nil {
if resp != nil {
return &channelCreatePaymentResponse{
ErrorMessage: resp.ErrorMessage,
RawRequest: resp.RawRequest,
Raw: resp.Raw,
}, err
}
return nil, err
}
return &channelCreatePaymentResponse{
@@ -68,11 +68,15 @@ func (r *Repository) StartMohong(ctx context.Context, userID uint64, orderID uin
NotifyURL: runtimeConfig.NotifyURL,
JumpURL: runtimeConfig.JumpURL,
ClientIP: clientIP,
Body: "撞车订单 " + orderRow.OrderNo,
Body: "撞车订单-" + orderRow.OrderNo,
Attach: orderRow.OrderNo,
})
if err != nil {
_ = r.markPaymentFailed(ctx, payment.ID, nil, err.Error())
var raw map[string]string
if resp != nil {
raw = resp.Raw
}
_ = r.markPaymentFailed(ctx, payment.ID, raw, err.Error())
r.log().Warn("魔哄支付下单失败", paymentLogFields(ctx, appendFields(
paymentOrderFields(payment),
runtimeConfigFields(runtimeConfig),
@@ -64,11 +64,15 @@ func (r *Repository) Start(ctx context.Context, userID uint64, orderID uint64, r
NotifyURL: runtimeConfig.NotifyURL,
JumpURL: runtimeConfig.JumpURL,
ClientIP: clientIP,
Body: "租号订单 " + orderRow.OrderNo,
Body: "租号订单-" + orderRow.OrderNo,
Attach: orderRow.OrderNo,
})
if err != nil {
_ = r.markPaymentFailed(ctx, payment.ID, nil, err.Error())
var raw map[string]string
if resp != nil {
raw = resp.Raw
}
_ = r.markPaymentFailed(ctx, payment.ID, raw, err.Error())
r.log().Warn("支付下单请求失败", paymentLogFields(ctx, appendFields(
paymentOrderFields(payment),
runtimeConfigFields(runtimeConfig),