621 lines
20 KiB
Go
621 lines
20 KiB
Go
package payment
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"kra/app/system/internal/biz"
|
|
|
|
"github.com/go-pay/gopay"
|
|
gopayAlipay "github.com/go-pay/gopay/alipay"
|
|
"github.com/go-pay/gopay/pkg/xhttp"
|
|
)
|
|
|
|
type alipayAdapter struct{}
|
|
|
|
type alipayClient struct {
|
|
client *gopayAlipay.Client
|
|
gateway *url.URL
|
|
response bytes.Buffer
|
|
}
|
|
|
|
type alipayTransport struct {
|
|
base http.RoundTripper
|
|
gateway *url.URL
|
|
response *bytes.Buffer
|
|
}
|
|
|
|
type alipayResponseBody struct {
|
|
io.Reader
|
|
io.Closer
|
|
}
|
|
|
|
func (a *alipayAdapter) Create(ctx context.Context, req *biz.PaymentRequest, c map[string]any) (*biz.PaymentResult, error) {
|
|
if req == nil {
|
|
return nil, errors.New("支付宝下单参数为空")
|
|
}
|
|
amount, err := formatDecimalAmount(req.Amount, 100)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bm := gopay.BodyMap{"out_trade_no": req.TradeNo, "total_amount": amount, "subject": req.Subject}
|
|
mergeMap(bm, req.Extra,
|
|
"out_trade_no", "total_amount", "subject", "notify_url", "return_url",
|
|
"method", "pay_method", "trade_type", "channel",
|
|
)
|
|
sdk, err := newAlipayClient(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if req.NotifyURL != "" {
|
|
sdk.client.SetNotifyUrl(req.NotifyURL)
|
|
}
|
|
if req.ReturnURL != "" {
|
|
sdk.client.SetReturnUrl(req.ReturnURL)
|
|
}
|
|
|
|
method, err := alipayCreateMethod(req.Extra, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch method {
|
|
case "alipay.trade.pay":
|
|
rsp, callErr := sdk.client.TradePay(ctx, bm)
|
|
if err = sdk.finish(method, rsp != nil && rsp.Response != nil, callErr, c); err != nil {
|
|
return nil, err
|
|
}
|
|
response := rsp.Response
|
|
if response.OutTradeNo != "" && strings.TrimSpace(response.OutTradeNo) != req.TradeNo {
|
|
return nil, errors.New("支付宝付款码支付响应的 out_trade_no 不匹配")
|
|
}
|
|
status := ""
|
|
switch response.Code {
|
|
case "10000":
|
|
status = "success"
|
|
case "10003":
|
|
status = "pending"
|
|
default:
|
|
return nil, fmt.Errorf("支付宝付款码支付失败: %s", firstNonEmptyAlipay(response.SubMsg, response.Msg, response.SubCode, response.Code))
|
|
}
|
|
if strings.TrimSpace(response.OutTradeNo) == "" {
|
|
return nil, errors.New("支付宝付款码支付响应缺少 out_trade_no")
|
|
}
|
|
if status == "success" && strings.TrimSpace(response.TradeNo) == "" {
|
|
return nil, errors.New("支付宝付款码支付成功响应缺少 trade_no")
|
|
}
|
|
return &biz.PaymentResult{
|
|
Provider: biz.PaymentAlipay, Status: status, TradeNo: req.TradeNo,
|
|
ProviderTradeNo: strings.TrimSpace(response.TradeNo), Payload: sdk.payload(rsp),
|
|
}, nil
|
|
case "alipay.trade.precreate":
|
|
rsp, callErr := sdk.client.TradePrecreate(ctx, bm)
|
|
if err = sdk.finish(method, rsp != nil && rsp.Response != nil, callErr, c); err != nil {
|
|
return nil, err
|
|
}
|
|
response := rsp.Response
|
|
queryID := strings.TrimSpace(response.OutTradeNo)
|
|
if queryID == "" {
|
|
queryID = req.TradeNo
|
|
} else if queryID != req.TradeNo {
|
|
return nil, errors.New("支付宝预创建响应的 out_trade_no 不匹配")
|
|
}
|
|
if strings.TrimSpace(response.QrCode) == "" {
|
|
return nil, errors.New("支付宝预创建响应缺少 qr_code")
|
|
}
|
|
// Precreate has no Alipay trade number yet. Keep the QR code in Payload
|
|
// and query later by the merchant out_trade_no.
|
|
return &biz.PaymentResult{
|
|
Provider: biz.PaymentAlipay, Status: "created", TradeNo: req.TradeNo,
|
|
QueryID: queryID, Payload: sdk.payload(rsp),
|
|
}, nil
|
|
case "alipay.trade.app.pay":
|
|
orderString, callErr := sdk.client.TradeAppPay(ctx, bm)
|
|
if callErr != nil {
|
|
return nil, callErr
|
|
}
|
|
if strings.TrimSpace(orderString) == "" {
|
|
return nil, errors.New("支付宝 APP 支付参数为空")
|
|
}
|
|
return &biz.PaymentResult{
|
|
Provider: biz.PaymentAlipay, Status: "created", TradeNo: req.TradeNo,
|
|
Payload: mustMarshalAlipayPayload(map[string]string{"order_string": orderString}),
|
|
}, nil
|
|
case "alipay.trade.page.pay":
|
|
payURL, callErr := sdk.client.TradePagePay(ctx, bm)
|
|
if callErr != nil {
|
|
return nil, callErr
|
|
}
|
|
if strings.TrimSpace(payURL) == "" {
|
|
return nil, errors.New("支付宝电脑网站支付 URL 为空")
|
|
}
|
|
payURL, err = sdk.rewriteGeneratedURL(payURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &biz.PaymentResult{
|
|
Provider: biz.PaymentAlipay, Status: "created", TradeNo: req.TradeNo,
|
|
Payload: mustMarshalAlipayPayload(map[string]string{"pay_url": payURL}),
|
|
}, nil
|
|
case "alipay.trade.wap.pay":
|
|
payURL, callErr := sdk.client.TradeWapPay(ctx, bm)
|
|
if callErr != nil {
|
|
return nil, callErr
|
|
}
|
|
if strings.TrimSpace(payURL) == "" {
|
|
return nil, errors.New("支付宝手机网站支付 URL 为空")
|
|
}
|
|
payURL, err = sdk.rewriteGeneratedURL(payURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &biz.PaymentResult{
|
|
Provider: biz.PaymentAlipay, Status: "created", TradeNo: req.TradeNo,
|
|
Payload: mustMarshalAlipayPayload(map[string]string{"pay_url": payURL}),
|
|
}, nil
|
|
default:
|
|
rsp, callErr := sdk.client.TradeCreate(ctx, bm)
|
|
if err = sdk.finish(method, rsp != nil && rsp.Response != nil, callErr, c); err != nil {
|
|
return nil, err
|
|
}
|
|
response := rsp.Response
|
|
if response.OutTradeNo != "" && strings.TrimSpace(response.OutTradeNo) != req.TradeNo {
|
|
return nil, errors.New("支付宝创建响应的 out_trade_no 不匹配")
|
|
}
|
|
providerTradeNo := strings.TrimSpace(response.TradeNo)
|
|
if providerTradeNo == "" {
|
|
return nil, errors.New("支付宝创建响应缺少 trade_no")
|
|
}
|
|
return &biz.PaymentResult{
|
|
Provider: biz.PaymentAlipay, Status: "created", TradeNo: req.TradeNo,
|
|
ProviderTradeNo: providerTradeNo, Payload: sdk.payload(rsp),
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (a *alipayAdapter) Query(ctx context.Context, tradeNo string, c map[string]any) (*biz.PaymentResult, error) {
|
|
tradeNo = strings.TrimSpace(tradeNo)
|
|
if tradeNo == "" {
|
|
return nil, errors.New("支付宝查单缺少 out_trade_no")
|
|
}
|
|
sdk, err := newAlipayClient(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
const method = "alipay.trade.query"
|
|
rsp, callErr := sdk.client.TradeQuery(ctx, gopay.BodyMap{"out_trade_no": tradeNo})
|
|
if err = sdk.finish(method, rsp != nil && rsp.Response != nil, callErr, c); err != nil {
|
|
return nil, err
|
|
}
|
|
response := rsp.Response
|
|
result := &biz.PaymentResult{
|
|
Provider: biz.PaymentAlipay, Status: normalizePaymentStatus(response.TradeStatus, "pending"),
|
|
TradeNo: strings.TrimSpace(response.OutTradeNo), ProviderTradeNo: strings.TrimSpace(response.TradeNo),
|
|
Payload: sdk.payload(rsp),
|
|
}
|
|
if result.TradeNo == "" {
|
|
result.TradeNo = tradeNo
|
|
} else if result.TradeNo != tradeNo {
|
|
return nil, errors.New("支付宝查询响应的 out_trade_no 不匹配")
|
|
}
|
|
if result.Status != "success" {
|
|
return result, nil
|
|
}
|
|
if result.ProviderTradeNo == "" {
|
|
return nil, errors.New("支付宝查询响应缺少 trade_no")
|
|
}
|
|
result.Amount, err = parseDecimalAmount(response.TotalAmount, 100)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("解析支付宝订单金额: %w", err)
|
|
}
|
|
result.Currency = strings.ToUpper(firstNonEmptyAlipay(response.TransCurrency, response.PayCurrency))
|
|
if result.Currency == "" {
|
|
result.Currency = "CNY"
|
|
}
|
|
values, err := alipayResponseValues(response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = populateAlipayBreakdown(result, values); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (a *alipayAdapter) Refund(ctx context.Context, req *biz.PaymentRefundRequest, c map[string]any) (*biz.PaymentResult, error) {
|
|
if req == nil {
|
|
return nil, errors.New("支付宝退款请求为空")
|
|
}
|
|
tradeNo := strings.TrimSpace(req.TradeNo)
|
|
refundNo := strings.TrimSpace(req.RefundNo)
|
|
if tradeNo == "" || refundNo == "" {
|
|
return nil, errors.New("支付宝退款缺少 out_trade_no 或 out_request_no")
|
|
}
|
|
formatted, err := formatDecimalAmount(req.Amount, 100)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sdk, err := newAlipayClient(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bm := gopay.BodyMap{"out_trade_no": tradeNo, "out_request_no": refundNo, "refund_amount": formatted}
|
|
mergeGoPayConfigExtras(bm, c, "refund_extra", "out_trade_no", "out_request_no", "refund_amount")
|
|
const method = "alipay.trade.refund"
|
|
rsp, callErr := sdk.client.TradeRefund(ctx, bm)
|
|
if err = sdk.finish(method, rsp != nil && rsp.Response != nil, callErr, c); err != nil {
|
|
return nil, err
|
|
}
|
|
response := rsp.Response
|
|
return alipayRefundResult(req, response, sdk.payload(rsp))
|
|
}
|
|
|
|
func alipayRefundResult(req *biz.PaymentRefundRequest, response *gopayAlipay.TradeRefund, payload []byte) (*biz.PaymentResult, error) {
|
|
if req == nil || response == nil {
|
|
return nil, errors.New("支付宝退款响应为空")
|
|
}
|
|
tradeNo := strings.TrimSpace(req.TradeNo)
|
|
returnedTradeNo := strings.TrimSpace(response.OutTradeNo)
|
|
if returnedTradeNo == "" {
|
|
return nil, errors.New("支付宝退款响应缺少 out_trade_no")
|
|
}
|
|
if returnedTradeNo != tradeNo {
|
|
return nil, errors.New("支付宝退款响应的 out_trade_no 不匹配")
|
|
}
|
|
providerTradeNo := strings.TrimSpace(response.TradeNo)
|
|
if providerTradeNo == "" {
|
|
return nil, errors.New("支付宝退款响应缺少 trade_no")
|
|
}
|
|
if expected := strings.TrimSpace(req.ProviderTradeNo); expected != "" && providerTradeNo != expected {
|
|
return nil, errors.New("支付宝退款响应的 trade_no 不匹配")
|
|
}
|
|
refundFee := strings.TrimSpace(response.RefundFee)
|
|
if refundFee == "" {
|
|
return nil, errors.New("支付宝退款响应缺少 refund_fee")
|
|
}
|
|
amount, err := parseDecimalAmount(refundFee, 100)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("解析支付宝退款金额: %w", err)
|
|
}
|
|
if amount != req.Amount {
|
|
return nil, errors.New("支付宝退款响应金额不匹配")
|
|
}
|
|
currency := strings.ToUpper(strings.TrimSpace(response.RefundCurrency))
|
|
requestCurrency := strings.ToUpper(strings.TrimSpace(req.Currency))
|
|
if currency == "" {
|
|
currency = requestCurrency
|
|
} else if requestCurrency != "" && currency != requestCurrency {
|
|
return nil, errors.New("支付宝退款响应币种不匹配")
|
|
}
|
|
if currency == "" {
|
|
currency = "CNY"
|
|
}
|
|
return &biz.PaymentResult{
|
|
Provider: biz.PaymentAlipay, Status: "created", TradeNo: tradeNo,
|
|
ProviderTradeNo: providerTradeNo, Amount: amount, Currency: currency, Payload: payload,
|
|
}, nil
|
|
}
|
|
|
|
func (a *alipayAdapter) Callback(_ context.Context, callback *biz.PaymentCallback, c map[string]any) (*biz.PaymentResult, error) {
|
|
if callback == nil || len(callback.Body) == 0 {
|
|
return nil, errors.New("支付宝回调为空")
|
|
}
|
|
fields := callbackFields(callback)
|
|
if err := verifyAlipay(fields, c); err != nil {
|
|
return nil, err
|
|
}
|
|
if fields["app_id"] != text(c, "app_id") {
|
|
return nil, errors.New("支付宝回调 app_id 不匹配")
|
|
}
|
|
status := "pending"
|
|
if fields["trade_status"] == "TRADE_SUCCESS" || fields["trade_status"] == "TRADE_FINISHED" {
|
|
status = "success"
|
|
}
|
|
payload, _ := json.Marshal(fields)
|
|
return &biz.PaymentResult{
|
|
Provider: biz.PaymentAlipay, Status: status,
|
|
TradeNo: first(fields, "out_trade_no"), ProviderTradeNo: first(fields, "trade_no"),
|
|
Payload: payload,
|
|
}, nil
|
|
}
|
|
func newAlipayClient(c map[string]any) (*alipayClient, error) {
|
|
appID := strings.TrimSpace(text(c, "app_id"))
|
|
privateKey := strings.TrimSpace(text(c, "private_key"))
|
|
if appID == "" || privateKey == "" {
|
|
return nil, errors.New("支付宝缺少 app_id 或 private_key")
|
|
}
|
|
production := !strings.EqualFold(text(c, "environment"), "sandbox")
|
|
client, err := gopayAlipay.NewClient(appID, goPayAlipayKey(privateKey, false), production)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if charset := strings.TrimSpace(text(c, "charset")); charset != "" {
|
|
client.SetCharset(charset)
|
|
}
|
|
if signType := strings.TrimSpace(text(c, "sign_type")); signType != "" {
|
|
client.SetSignType(signType)
|
|
}
|
|
if token := strings.TrimSpace(text(c, "app_auth_token")); token != "" {
|
|
client.SetAppAuthToken(token)
|
|
}
|
|
if location := strings.TrimSpace(text(c, "location")); location != "" {
|
|
client.SetLocation(location)
|
|
}
|
|
|
|
sdk := &alipayClient{client: client}
|
|
if endpoint := strings.TrimSpace(text(c, "gateway_url")); endpoint != "" {
|
|
sdk.gateway, err = url.Parse(endpoint)
|
|
if err != nil || sdk.gateway.Scheme == "" || sdk.gateway.Host == "" {
|
|
return nil, fmt.Errorf("支付宝 gateway_url 无效: %s", endpoint)
|
|
}
|
|
}
|
|
httpClient := xhttp.NewClient().SetTimeout(20 * time.Second).SetBodySize(8)
|
|
httpClient.SetTransport(&alipayTransport{
|
|
base: httpClient.HttpClient.Transport, gateway: sdk.gateway, response: &sdk.response,
|
|
})
|
|
client.SetHttpClient(httpClient)
|
|
return sdk, nil
|
|
}
|
|
|
|
func (c *alipayClient) finish(method string, hasResponse bool, callErr error, config map[string]any) error {
|
|
if !hasResponse {
|
|
if callErr != nil {
|
|
return callErr
|
|
}
|
|
return fmt.Errorf("支付宝 %s 响应为空", method)
|
|
}
|
|
if c.response.Len() == 0 {
|
|
return fmt.Errorf("支付宝 %s 响应原文为空", method)
|
|
}
|
|
if err := verifyAlipayResponse(c.response.Bytes(), method, config); err != nil {
|
|
return err
|
|
}
|
|
return callErr
|
|
}
|
|
|
|
func (c *alipayClient) payload(fallback any) []byte {
|
|
if c.response.Len() > 0 {
|
|
return append([]byte(nil), c.response.Bytes()...)
|
|
}
|
|
return mustMarshalAlipayPayload(fallback)
|
|
}
|
|
|
|
func (c *alipayClient) rewriteGeneratedURL(value string) (string, error) {
|
|
if c.gateway == nil {
|
|
return value, nil
|
|
}
|
|
source, err := url.Parse(value)
|
|
if err != nil {
|
|
return "", fmt.Errorf("解析支付宝支付 URL: %w", err)
|
|
}
|
|
return alipayTargetURL(source, c.gateway).String(), nil
|
|
}
|
|
|
|
func (t *alipayTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
if t.base == nil {
|
|
t.base = http.DefaultTransport
|
|
}
|
|
target := req
|
|
if t.gateway != nil {
|
|
clone := req.Clone(req.Context())
|
|
clone.URL = alipayTargetURL(req.URL, t.gateway)
|
|
clone.Host = ""
|
|
target = clone
|
|
}
|
|
if t.response != nil {
|
|
t.response.Reset()
|
|
}
|
|
resp, err := t.base.RoundTrip(target)
|
|
if err != nil || resp == nil || resp.Body == nil || t.response == nil {
|
|
return resp, err
|
|
}
|
|
resp.Body = &alipayResponseBody{Reader: io.TeeReader(resp.Body, t.response), Closer: resp.Body}
|
|
return resp, nil
|
|
}
|
|
|
|
func alipayTargetURL(source, gateway *url.URL) *url.URL {
|
|
target := *gateway
|
|
switch {
|
|
case target.RawQuery == "":
|
|
target.RawQuery = source.RawQuery
|
|
case source.RawQuery != "":
|
|
target.RawQuery += "&" + source.RawQuery
|
|
}
|
|
return &target
|
|
}
|
|
|
|
func alipayCreateMethod(extra, config map[string]any) (string, error) {
|
|
value := firstAny(extra, "method", "pay_method", "trade_type", "channel")
|
|
if value == "" {
|
|
value = firstAny(config, "method", "pay_method", "trade_type", "channel")
|
|
}
|
|
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
|
|
switch normalized {
|
|
case "", "create", "trade_create", "alipay_trade_create", "jsapi", "miniapp", "mini_program":
|
|
return "alipay.trade.create", nil
|
|
case "pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face":
|
|
return "alipay.trade.pay", nil
|
|
case "precreate", "trade_precreate", "alipay_trade_precreate", "native", "qr", "qrcode", "scan":
|
|
return "alipay.trade.precreate", nil
|
|
case "app", "app_pay", "trade_app_pay", "alipay_trade_app_pay":
|
|
return "alipay.trade.app.pay", nil
|
|
case "page", "page_pay", "trade_page_pay", "alipay_trade_page_pay", "web", "pc", "desktop":
|
|
return "alipay.trade.page.pay", nil
|
|
case "wap", "wap_pay", "trade_wap_pay", "alipay_trade_wap_pay", "h5", "mobile":
|
|
return "alipay.trade.wap.pay", nil
|
|
default:
|
|
return "", fmt.Errorf("支付宝不支持的下单方式: %s", value)
|
|
}
|
|
}
|
|
|
|
func alipayResponseValues(value any) (map[string]any, error) {
|
|
raw, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := map[string]any{}
|
|
if err = json.Unmarshal(raw, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func mustMarshalAlipayPayload(value any) []byte {
|
|
raw, _ := json.Marshal(value)
|
|
return raw
|
|
}
|
|
|
|
func firstNonEmptyAlipay(values ...string) string {
|
|
for _, value := range values {
|
|
if value = strings.TrimSpace(value); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func populateAlipayBreakdown(result *biz.PaymentResult, response map[string]any) error {
|
|
if result == nil || result.Status != "success" {
|
|
return nil
|
|
}
|
|
// Alipay returns these values in major currency units. Keep all arithmetic
|
|
// integer-only and only mark the breakdown complete when the core fields are
|
|
// present and internally consistent.
|
|
total, err := parseDecimalAmount(strings.TrimSpace(fmt.Sprint(response["total_amount"])), 100)
|
|
if err != nil {
|
|
return fmt.Errorf("解析支付宝 total_amount: %w", err)
|
|
}
|
|
result.Amount = total
|
|
payer, payerOK, err := parseAlipayDecimal(response, "buyer_pay_amount")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !payerOK {
|
|
payer, payerOK, err = parseAlipayDecimal(response, "receipt_amount")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
point, pointOK, err := parseAlipayDecimal(response, "point_amount")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !payerOK {
|
|
return nil
|
|
}
|
|
if pointOK && point > payer {
|
|
return errors.New("支付宝 point_amount 大于用户实付金额")
|
|
}
|
|
result.PayerPaidAmount = payer
|
|
result.PointPaidAmount = point
|
|
result.CashPaidAmount = payer - point
|
|
result.DiscountAmount = total - payer
|
|
if result.DiscountAmount < 0 {
|
|
return errors.New("支付宝用户实付金额超过订单总额")
|
|
}
|
|
// receipt_amount is the merchant receipt amount. It is not always present;
|
|
// when it is, use it as settlement amount. Alipay's detailed voucher list
|
|
// is retained in Payload for audit and is intentionally not guessed here.
|
|
if receipt, ok, receiptErr := parseAlipayDecimal(response, "receipt_amount"); receiptErr != nil {
|
|
return receiptErr
|
|
} else if ok {
|
|
result.SettlementAmount = receipt
|
|
}
|
|
result.AmountBreakdownKnown = true
|
|
return nil
|
|
}
|
|
|
|
func parseAlipayDecimal(values map[string]any, key string) (int64, bool, error) {
|
|
value, exists := values[key]
|
|
if !exists || strings.TrimSpace(fmt.Sprint(value)) == "" || fmt.Sprint(value) == "<nil>" {
|
|
return 0, false, nil
|
|
}
|
|
amount, err := parseDecimalAmount(strings.TrimSpace(fmt.Sprint(value)), 100)
|
|
return amount, true, err
|
|
}
|
|
|
|
func verifyAlipayResponse(raw []byte, method string, c map[string]any) error {
|
|
publicKeyValue := text(c, "public_key")
|
|
if publicKeyValue == "" {
|
|
return errors.New("支付宝缺少 public_key")
|
|
}
|
|
var envelope map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &envelope); err != nil {
|
|
return err
|
|
}
|
|
responseKey := strings.ReplaceAll(method, ".", "_") + "_response"
|
|
responseRaw := envelope[responseKey]
|
|
if len(responseRaw) == 0 {
|
|
return errors.New("支付宝响应缺少 " + responseKey)
|
|
}
|
|
var sign string
|
|
if err := json.Unmarshal(envelope["sign"], &sign); err != nil || sign == "" {
|
|
return errors.New("支付宝响应缺少 sign")
|
|
}
|
|
ok, err := gopayAlipay.VerifySyncSign(goPayAlipayKey(publicKeyValue, true), string(responseRaw), sign)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
return errors.New("支付宝响应签名校验失败")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func alipaySignContent(v url.Values) string {
|
|
keys := make([]string, 0, len(v))
|
|
for key := range v {
|
|
if key != "sign" && v.Get(key) != "" {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
parts := make([]string, 0, len(keys))
|
|
for _, key := range keys {
|
|
parts = append(parts, key+"="+v.Get(key))
|
|
}
|
|
return strings.Join(parts, "&")
|
|
}
|
|
|
|
func verifyAlipay(fields map[string]string, c map[string]any) error {
|
|
if text(c, "app_id") == "" || text(c, "public_key") == "" || fields["sign"] == "" {
|
|
return errors.New("支付宝回调验签配置或 sign 缺失")
|
|
}
|
|
values := url.Values{}
|
|
for key, value := range fields {
|
|
values.Set(key, value)
|
|
}
|
|
body, err := gopayAlipay.ParseNotifyByURLValues(values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ok, err := gopayAlipay.VerifySign(goPayAlipayKey(text(c, "public_key"), true), cloneGoPayBodyMap(body))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
return errors.New("支付宝回调签名校验失败")
|
|
}
|
|
return nil
|
|
}
|
|
func mergeMap(dst map[string]any, src map[string]any, protected ...string) {
|
|
protectedKeys := make(map[string]struct{}, len(protected))
|
|
for _, key := range protected {
|
|
protectedKeys[key] = struct{}{}
|
|
}
|
|
for key, value := range src {
|
|
if _, exists := protectedKeys[key]; exists {
|
|
continue
|
|
}
|
|
dst[key] = value
|
|
}
|
|
}
|