package payment import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/url" "os" "strings" "time" "kra/internal/biz" "github.com/go-pay/gopay" gopayAlipay "github.com/go-pay/gopay/alipay" gopayAlipayV3 "github.com/go-pay/gopay/alipay/v3" "github.com/go-pay/gopay/pkg/xhttp" ) type alipayV3Adapter struct{} type alipayV3Client struct { client *gopayAlipayV3.ClientV3 gateway *url.URL } func (a *alipayV3Adapter) Create(ctx context.Context, req *biz.PaymentRequest, c map[string]any) (*biz.PaymentResult, error) { if req == nil { return nil, errors.New("支付宝 V3 下单参数为空") } amount, err := formatDecimalAmount(req.Amount, 100) if err != nil { return nil, err } sdk, err := newAlipayV3Client(c) if err != nil { return nil, err } method, err := alipayCreateMethod(req.Extra, c) if err != nil { return nil, err } bm := gopay.BodyMap{ "out_trade_no": req.TradeNo, "total_amount": amount, "subject": req.Subject, } protected := []string{ "out_trade_no", "total_amount", "subject", "notify_url", "return_url", "method", "pay_method", "trade_type", "channel", } mergeGoPayConfigExtras(bm, c, "create_extra", protected...) mergeMap(bm, req.Extra, protected...) if req.NotifyURL != "" { bm.Set("notify_url", req.NotifyURL) } if req.ReturnURL != "" { bm.Set("return_url", req.ReturnURL) } switch method { case "alipay.trade.pay": if bm.GetString("scene") == "" { bm.Set("scene", "bar_code") } rsp, callErr := sdk.client.TradePay(ctx, bm) if callErr != nil { return nil, callErr } if rsp == nil { return nil, errors.New("支付宝 V3 付款码支付响应为空") } if err = alipayV3CheckResponse("付款码支付", rsp.StatusCode, rsp.ErrResponse); err != nil { return nil, err } return alipayV3TradePayResult(req, rsp) case "alipay.trade.precreate": rsp, callErr := sdk.client.TradePrecreate(ctx, bm) if callErr != nil { return nil, callErr } if rsp == nil { return nil, errors.New("支付宝 V3 预创建响应为空") } if err = alipayV3CheckResponse("预创建", rsp.StatusCode, rsp.ErrResponse); err != nil { return nil, err } outTradeNo := strings.TrimSpace(rsp.OutTradeNo) if outTradeNo == "" || outTradeNo != req.TradeNo { return nil, errors.New("支付宝 V3 预创建响应的 out_trade_no 缺失或不匹配") } if strings.TrimSpace(rsp.QrCode) == "" { return nil, errors.New("支付宝 V3 预创建响应缺少 qr_code") } return &biz.PaymentResult{ Provider: biz.PaymentAlipayV3, Status: "created", TradeNo: req.TradeNo, QueryID: outTradeNo, Payload: mustJSON(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("支付宝 V3 APP 支付参数为空") } return &biz.PaymentResult{ Provider: biz.PaymentAlipayV3, Status: "created", TradeNo: req.TradeNo, Payload: mustJSON(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 } payURL, err = sdk.rewriteGeneratedURL(payURL) if err != nil { return nil, err } if strings.TrimSpace(payURL) == "" { return nil, errors.New("支付宝 V3 电脑网站支付 URL 为空") } return &biz.PaymentResult{ Provider: biz.PaymentAlipayV3, Status: "created", TradeNo: req.TradeNo, Payload: mustJSON(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 } payURL, err = sdk.rewriteGeneratedURL(payURL) if err != nil { return nil, err } if strings.TrimSpace(payURL) == "" { return nil, errors.New("支付宝 V3 手机网站支付 URL 为空") } return &biz.PaymentResult{ Provider: biz.PaymentAlipayV3, Status: "created", TradeNo: req.TradeNo, Payload: mustJSON(map[string]string{"pay_url": payURL}), }, nil default: if bm.GetString("product_code") == "" { bm.Set("product_code", firstAnyOr(c, "JSAPI_PAY", "product_code")) } if bm.GetString("op_app_id") == "" { bm.Set("op_app_id", firstAnyOr(c, text(c, "app_id"), "op_app_id")) } rsp, callErr := sdk.client.TradeCreate(ctx, bm) if callErr != nil { return nil, callErr } if rsp == nil { return nil, errors.New("支付宝 V3 创建响应为空") } if err = alipayV3CheckResponse("创建", rsp.StatusCode, rsp.ErrResponse); err != nil { return nil, err } outTradeNo := strings.TrimSpace(rsp.OutTradeNo) if outTradeNo == "" || outTradeNo != req.TradeNo { return nil, errors.New("支付宝 V3 创建响应的 out_trade_no 缺失或不匹配") } providerTradeNo := strings.TrimSpace(rsp.TradeNo) if providerTradeNo == "" { return nil, errors.New("支付宝 V3 创建响应缺少 trade_no") } return &biz.PaymentResult{ Provider: biz.PaymentAlipayV3, Status: "created", TradeNo: req.TradeNo, ProviderTradeNo: providerTradeNo, QueryID: outTradeNo, Payload: mustJSON(rsp), }, nil } } func (a *alipayV3Adapter) Query(ctx context.Context, tradeNo string, c map[string]any) (*biz.PaymentResult, error) { tradeNo = strings.TrimSpace(tradeNo) if tradeNo == "" { return nil, errors.New("支付宝 V3 查单缺少 out_trade_no") } sdk, err := newAlipayV3Client(c) if err != nil { return nil, err } rsp, callErr := sdk.client.TradeQuery(ctx, gopay.BodyMap{"out_trade_no": tradeNo}) if callErr != nil { return nil, callErr } if rsp == nil { return nil, errors.New("支付宝 V3 查询响应为空") } if err = alipayV3CheckResponse("查单", rsp.StatusCode, rsp.ErrResponse); err != nil { return nil, err } outTradeNo := strings.TrimSpace(rsp.OutTradeNo) if outTradeNo == "" || outTradeNo != tradeNo { return nil, errors.New("支付宝 V3 查询响应的 out_trade_no 缺失或不匹配") } result := &biz.PaymentResult{ Provider: biz.PaymentAlipayV3, Status: normalizePaymentStatus(rsp.TradeStatus, "pending"), TradeNo: outTradeNo, ProviderTradeNo: strings.TrimSpace(rsp.TradeNo), Currency: alipayV3Currency(c, ""), Payload: mustJSON(rsp), } if result.Status != "success" { return result, nil } if result.ProviderTradeNo == "" { return nil, errors.New("支付宝 V3 查询响应缺少 trade_no") } result.Amount, err = parseDecimalAmount(rsp.TotalAmount, 100) if err != nil { return nil, fmt.Errorf("解析支付宝 V3 订单金额: %w", err) } values, err := alipayResponseValues(rsp) if err != nil { return nil, err } if err = populateAlipayBreakdown(result, values); err != nil { return nil, err } return result, nil } func (a *alipayV3Adapter) Refund(ctx context.Context, req *biz.PaymentRefundRequest, c map[string]any) (*biz.PaymentResult, error) { if req == nil { return nil, errors.New("支付宝 V3 退款请求为空") } tradeNo := strings.TrimSpace(req.TradeNo) refundNo := strings.TrimSpace(req.RefundNo) if tradeNo == "" || refundNo == "" { return nil, errors.New("支付宝 V3 退款缺少 out_trade_no 或 out_request_no") } amount, err := formatDecimalAmount(req.Amount, 100) if err != nil { return nil, err } sdk, err := newAlipayV3Client(c) if err != nil { return nil, err } bm := gopay.BodyMap{ "out_trade_no": tradeNo, "out_request_no": refundNo, "refund_amount": amount, } mergeGoPayConfigExtras(bm, c, "refund_extra", "out_trade_no", "out_request_no", "refund_amount") rsp, callErr := sdk.client.TradeRefund(ctx, bm) if callErr != nil { return nil, callErr } if rsp == nil { return nil, errors.New("支付宝 V3 退款响应为空") } if err = alipayV3CheckResponse("退款", rsp.StatusCode, rsp.ErrResponse); err != nil { return nil, err } outTradeNo := strings.TrimSpace(rsp.OutTradeNo) if outTradeNo == "" || outTradeNo != tradeNo { return nil, errors.New("支付宝 V3 退款响应的 out_trade_no 缺失或不匹配") } providerTradeNo := strings.TrimSpace(rsp.TradeNo) if providerTradeNo == "" { return nil, errors.New("支付宝 V3 退款响应缺少 trade_no") } if expected := strings.TrimSpace(req.ProviderTradeNo); expected != "" && providerTradeNo != expected { return nil, errors.New("支付宝 V3 退款响应的 trade_no 不匹配") } refundedAmount, err := parseDecimalAmount(rsp.RefundFee, 100) if err != nil { return nil, fmt.Errorf("解析支付宝 V3 退款金额: %w", err) } if refundedAmount != req.Amount { return nil, errors.New("支付宝 V3 退款响应金额不匹配") } return &biz.PaymentResult{ Provider: biz.PaymentAlipayV3, Status: "created", TradeNo: tradeNo, ProviderTradeNo: providerTradeNo, Amount: refundedAmount, Currency: alipayV3Currency(c, req.Currency), Payload: mustJSON(rsp), }, nil } func (a *alipayV3Adapter) Callback(_ context.Context, callback *biz.PaymentCallback, c map[string]any) (*biz.PaymentResult, error) { if callback == nil || len(callback.Body) == 0 { return nil, errors.New("支付宝 V3 回调为空") } fields := callbackFields(callback) if fields["sign"] == "" { if values, parseErr := url.ParseQuery(string(callback.Body)); parseErr == nil { for key, value := range values { if len(value) > 0 { fields[key] = value[0] } } } } if err := verifyAlipayV3Callback(fields, c); err != nil { return nil, err } if fields["app_id"] != text(c, "app_id") { return nil, errors.New("支付宝 V3 回调 app_id 不匹配") } tradeNo := strings.TrimSpace(fields["out_trade_no"]) providerTradeNo := strings.TrimSpace(fields["trade_no"]) if tradeNo == "" || providerTradeNo == "" { return nil, errors.New("支付宝 V3 回调缺少 out_trade_no 或 trade_no") } payload, _ := json.Marshal(fields) return &biz.PaymentResult{ Provider: biz.PaymentAlipayV3, Status: normalizePaymentStatus(fields["trade_status"], "pending"), TradeNo: tradeNo, ProviderTradeNo: providerTradeNo, Payload: payload, }, nil } func alipayV3TradePayResult(req *biz.PaymentRequest, rsp *gopayAlipayV3.TradePayRsp) (*biz.PaymentResult, error) { if req == nil || rsp == nil { return nil, errors.New("支付宝 V3 付款码支付响应为空") } outTradeNo := strings.TrimSpace(rsp.OutTradeNo) if outTradeNo == "" || outTradeNo != req.TradeNo { return nil, errors.New("支付宝 V3 付款码支付响应的 out_trade_no 缺失或不匹配") } providerTradeNo := strings.TrimSpace(rsp.TradeNo) if providerTradeNo == "" { return nil, errors.New("支付宝 V3 付款码支付响应缺少 trade_no") } amount, err := parseDecimalAmount(rsp.TotalAmount, 100) if err != nil { return nil, fmt.Errorf("解析支付宝 V3 付款码支付金额: %w", err) } if amount != req.Amount { return nil, errors.New("支付宝 V3 付款码支付响应金额不匹配") } result := &biz.PaymentResult{ Provider: biz.PaymentAlipayV3, Status: "success", TradeNo: outTradeNo, ProviderTradeNo: providerTradeNo, Amount: amount, Currency: strings.ToUpper(strings.TrimSpace(req.Currency)), Payload: mustJSON(rsp), } if result.Currency == "" { result.Currency = "CNY" } values, err := alipayResponseValues(rsp) if err != nil { return nil, err } if err = populateAlipayBreakdown(result, values); err != nil { return nil, err } return result, nil } func newAlipayV3Client(c map[string]any) (*alipayV3Client, error) { appID := strings.TrimSpace(text(c, "app_id")) privateKey, err := alipayV3PrivateKey(c) if err != nil { return nil, err } if appID == "" { return nil, errors.New("支付宝 V3 缺少 app_id") } production := !strings.EqualFold(strings.TrimSpace(text(c, "environment")), "sandbox") client, err := gopayAlipayV3.NewClientV3(appID, goPayAlipayKey(privateKey, false), production) if err != nil { return nil, err } appCert, err := alipayV3AppCertificate(c) if err != nil { return nil, err } rootCert, err := alipayV3RootCertificate(c) if err != nil { return nil, err } publicCert, err := alipayV3PublicCertificate(c) if err != nil { return nil, err } if err = client.SetCert(appCert, rootCert, publicCert); err != nil { return nil, fmt.Errorf("配置支付宝 V3 证书: %w", err) } if token := strings.TrimSpace(text(c, "app_auth_token")); token != "" { client.SetAppAuthToken(token) } client.SetHttpClient(xhttp.NewClient().SetTimeout(20 * time.Second).SetBodySize(8)) sdk := &alipayV3Client{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("支付宝 V3 gateway_url 无效: %s", endpoint) } } apiEndpoint := strings.TrimSpace(firstAny(c, "api_base_url", "proxy_host", "base_url")) if apiEndpoint == "" && sdk.gateway != nil && (sdk.gateway.Path == "" || sdk.gateway.Path == "/") { apiEndpoint = sdk.gateway.String() } if apiEndpoint != "" { parsed, parseErr := url.Parse(apiEndpoint) if parseErr != nil || parsed.Scheme == "" || parsed.Host == "" { return nil, fmt.Errorf("支付宝 V3 api_base_url 无效: %s", apiEndpoint) } client.SetProxyHost(strings.TrimRight(apiEndpoint, "/")) } return sdk, nil } func (c *alipayV3Client) rewriteGeneratedURL(value string) (string, error) { if strings.TrimSpace(value) == "" || c.gateway == nil { return value, nil } source, err := url.Parse(value) if err != nil { return "", fmt.Errorf("解析支付宝 V3 支付 URL: %w", err) } return alipayTargetURL(source, c.gateway).String(), nil } func verifyAlipayV3Callback(fields map[string]string, c map[string]any) error { if text(c, "app_id") == "" || fields["sign"] == "" { return errors.New("支付宝 V3 回调验签配置或 sign 缺失") } values := url.Values{} for key, value := range fields { values.Set(key, value) } body, err := gopayAlipay.ParseNotifyByURLValues(values) if err != nil { return err } publicCert, err := alipayV3PublicCertificate(c) if err != nil { return err } ok, err := gopayAlipay.VerifySignWithCert(publicCert, cloneGoPayBodyMap(body)) if err != nil { return err } if !ok { return errors.New("支付宝 V3 回调签名校验失败") } return nil } func alipayV3CheckResponse(operation string, status int, response gopayAlipayV3.ErrResponse) error { if status == http.StatusOK { return nil } message := strings.TrimSpace(response.Message) if code := strings.TrimSpace(response.Code); code != "" { if message == "" { message = code } else { message = code + ": " + message } } if message == "" { return fmt.Errorf("支付宝 V3 %s HTTP %d", operation, status) } return fmt.Errorf("支付宝 V3 %s HTTP %d: %s", operation, status, message) } func alipayV3Currency(c map[string]any, fallback string) string { currency := strings.ToUpper(strings.TrimSpace(firstAny(c, "currency", "default_currency"))) if currency == "" { currency = strings.ToUpper(strings.TrimSpace(fallback)) } if currency == "" { currency = "CNY" } return currency } func alipayV3PrivateKey(c map[string]any) (string, error) { if value := strings.TrimSpace(firstAny(c, "private_key_content", "private_key")); value != "" { return value, nil } path := strings.TrimSpace(text(c, "private_key_path")) if path == "" { return "", errors.New("支付宝 V3 缺少 private_key") } content, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("读取支付宝 V3 private_key: %w", err) } return string(content), nil } func alipayV3AppCertificate(c map[string]any) ([]byte, error) { return alipayV3Certificate(c, "app_cert", []string{"app_cert_content", "app_public_cert_content"}, []string{"app_cert_path", "app_public_cert_path"}, []string{"app_cert", "app_public_cert"}, ) } func alipayV3RootCertificate(c map[string]any) ([]byte, error) { return alipayV3Certificate(c, "root_cert", []string{"alipay_root_cert_content", "root_cert_content"}, []string{"alipay_root_cert_path", "root_cert_path"}, []string{"alipay_root_cert", "root_cert"}, ) } func alipayV3PublicCertificate(c map[string]any) ([]byte, error) { return alipayV3Certificate(c, "public_cert", []string{"alipay_public_cert_content", "public_cert_content"}, []string{"alipay_public_cert_path", "public_cert_path"}, []string{"alipay_public_cert", "public_cert"}, ) } func alipayV3Certificate(c map[string]any, label string, contentKeys, pathKeys, flexibleKeys []string) ([]byte, error) { if value := strings.TrimSpace(firstAny(c, contentKeys...)); value != "" { return []byte(value), nil } if path := strings.TrimSpace(firstAny(c, pathKeys...)); path != "" { content, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("读取支付宝 V3 %s: %w", label, err) } return content, nil } if value := strings.TrimSpace(firstAny(c, flexibleKeys...)); value != "" { if strings.Contains(value, "-----BEGIN CERTIFICATE-----") { return []byte(value), nil } content, err := os.ReadFile(value) if err != nil { return nil, fmt.Errorf("读取支付宝 V3 %s: %w", label, err) } return content, nil } return nil, fmt.Errorf("支付宝 V3 缺少 %s", label) }