Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,568 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/payment"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/h5"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/utils"
|
||||
)
|
||||
|
||||
// WeChat Pay constants.
|
||||
const (
|
||||
wxpayCurrency = "CNY"
|
||||
wxpayH5Type = "Wap"
|
||||
wxpayResultPath = "/payment/result"
|
||||
)
|
||||
|
||||
const (
|
||||
wxpayMetadataAppID = "appid"
|
||||
wxpayMetadataMerchantID = "mchid"
|
||||
wxpayMetadataCurrency = "currency"
|
||||
wxpayMetadataTradeState = "trade_state"
|
||||
)
|
||||
|
||||
// WeChat Pay create-payment modes.
|
||||
const (
|
||||
wxpayModeNative = "native"
|
||||
wxpayModeH5 = "h5"
|
||||
wxpayModeJSAPI = "jsapi"
|
||||
)
|
||||
|
||||
// WeChat Pay trade states.
|
||||
const (
|
||||
wxpayTradeStateSuccess = "SUCCESS"
|
||||
wxpayTradeStateRefund = "REFUND"
|
||||
wxpayTradeStateClosed = "CLOSED"
|
||||
wxpayTradeStatePayError = "PAYERROR"
|
||||
)
|
||||
|
||||
// WeChat Pay notification event types.
|
||||
const (
|
||||
wxpayEventTransactionSuccess = "TRANSACTION.SUCCESS"
|
||||
)
|
||||
|
||||
var (
|
||||
wxpayNativePrepay = func(ctx context.Context, svc native.NativeApiService, req native.PrepayRequest) (*native.PrepayResponse, *core.APIResult, error) {
|
||||
return svc.Prepay(ctx, req)
|
||||
}
|
||||
wxpayH5Prepay = func(ctx context.Context, svc h5.H5ApiService, req h5.PrepayRequest) (*h5.PrepayResponse, *core.APIResult, error) {
|
||||
return svc.Prepay(ctx, req)
|
||||
}
|
||||
wxpayJSAPIPrepayWithRequestPayment = func(ctx context.Context, svc jsapi.JsapiApiService, req jsapi.PrepayRequest) (*jsapi.PrepayWithRequestPaymentResponse, *core.APIResult, error) {
|
||||
return svc.PrepayWithRequestPayment(ctx, req)
|
||||
}
|
||||
)
|
||||
|
||||
type Wxpay struct {
|
||||
instanceID string
|
||||
config map[string]string
|
||||
mu sync.Mutex
|
||||
coreClient *core.Client
|
||||
notifyHandler *notify.Handler
|
||||
}
|
||||
|
||||
const wxpayAPIv3KeyLength = 32
|
||||
|
||||
func NewWxpay(instanceID string, config map[string]string) (*Wxpay, error) {
|
||||
// All fields are required. Platform-certificate mode is intentionally unsupported —
|
||||
// WeChat has been migrating all merchants to the pubkey verifier since 2024-10,
|
||||
// and newly-provisioned merchants cannot download platform certificates at all.
|
||||
required := []string{"appId", "mchId", "privateKey", "apiV3Key", "certSerial", "publicKey", "publicKeyId"}
|
||||
for _, k := range required {
|
||||
if config[k] == "" {
|
||||
return nil, infraerrors.BadRequest("WXPAY_CONFIG_MISSING_KEY", "missing_required_key").
|
||||
WithMetadata(map[string]string{"key": k})
|
||||
}
|
||||
}
|
||||
if len(config["apiV3Key"]) != wxpayAPIv3KeyLength {
|
||||
return nil, infraerrors.BadRequest("WXPAY_CONFIG_INVALID_KEY_LENGTH", "invalid_key_length").
|
||||
WithMetadata(map[string]string{
|
||||
"key": "apiV3Key",
|
||||
"expected": strconv.Itoa(wxpayAPIv3KeyLength),
|
||||
"actual": strconv.Itoa(len(config["apiV3Key"])),
|
||||
})
|
||||
}
|
||||
// Parse PEMs eagerly so malformed keys surface at save time, not at order creation.
|
||||
if _, err := utils.LoadPrivateKey(formatPEM(config["privateKey"], "PRIVATE KEY")); err != nil {
|
||||
return nil, infraerrors.BadRequest("WXPAY_CONFIG_INVALID_KEY", "invalid_key").
|
||||
WithMetadata(map[string]string{"key": "privateKey"})
|
||||
}
|
||||
if _, err := utils.LoadPublicKey(formatPEM(config["publicKey"], "PUBLIC KEY")); err != nil {
|
||||
return nil, infraerrors.BadRequest("WXPAY_CONFIG_INVALID_KEY", "invalid_key").
|
||||
WithMetadata(map[string]string{"key": "publicKey"})
|
||||
}
|
||||
return &Wxpay{instanceID: instanceID, config: config}, nil
|
||||
}
|
||||
|
||||
func (w *Wxpay) Name() string { return "Wxpay" }
|
||||
func (w *Wxpay) ProviderKey() string { return payment.TypeWxpay }
|
||||
func (w *Wxpay) SupportedTypes() []payment.PaymentType {
|
||||
return []payment.PaymentType{payment.TypeWxpay}
|
||||
}
|
||||
|
||||
// ResolveWxpayJSAPIAppID returns the AppID that JSAPI prepay will use for a
|
||||
// given provider config. A dedicated MP AppID takes precedence over the base
|
||||
// merchant AppID.
|
||||
func ResolveWxpayJSAPIAppID(config map[string]string) string {
|
||||
if appID := strings.TrimSpace(config["mpAppId"]); appID != "" {
|
||||
return appID
|
||||
}
|
||||
return strings.TrimSpace(config["appId"])
|
||||
}
|
||||
|
||||
func formatPEM(key, keyType string) string {
|
||||
key = strings.TrimSpace(key)
|
||||
if strings.HasPrefix(key, "-----BEGIN") {
|
||||
return key
|
||||
}
|
||||
return fmt.Sprintf("-----BEGIN %s-----\n%s\n-----END %s-----", keyType, key, keyType)
|
||||
}
|
||||
|
||||
func (w *Wxpay) ensureClient() (*core.Client, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.coreClient != nil {
|
||||
return w.coreClient, nil
|
||||
}
|
||||
privateKey, err := utils.LoadPrivateKey(formatPEM(w.config["privateKey"], "PRIVATE KEY"))
|
||||
if err != nil {
|
||||
return nil, infraerrors.BadRequest("WXPAY_CONFIG_INVALID_KEY", "invalid_key").
|
||||
WithMetadata(map[string]string{"key": "privateKey"})
|
||||
}
|
||||
publicKey, err := utils.LoadPublicKey(formatPEM(w.config["publicKey"], "PUBLIC KEY"))
|
||||
if err != nil {
|
||||
return nil, infraerrors.BadRequest("WXPAY_CONFIG_INVALID_KEY", "invalid_key").
|
||||
WithMetadata(map[string]string{"key": "publicKey"})
|
||||
}
|
||||
verifier := verifiers.NewSHA256WithRSAPubkeyVerifier(w.config["publicKeyId"], *publicKey)
|
||||
client, err := core.NewClient(context.Background(),
|
||||
option.WithMerchantCredential(w.config["mchId"], w.config["certSerial"], privateKey),
|
||||
option.WithVerifier(verifier))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay init client: %w", err)
|
||||
}
|
||||
handler, err := notify.NewRSANotifyHandler(w.config["apiV3Key"], verifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay init notify handler: %w", err)
|
||||
}
|
||||
w.notifyHandler = handler
|
||||
w.coreClient = client
|
||||
return w.coreClient, nil
|
||||
}
|
||||
|
||||
func (w *Wxpay) CreatePayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
|
||||
client, err := w.ensureClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Request-first, config-fallback (consistent with EasyPay/Alipay)
|
||||
notifyURL := req.NotifyURL
|
||||
if notifyURL == "" {
|
||||
notifyURL = w.config["notifyUrl"]
|
||||
}
|
||||
if notifyURL == "" {
|
||||
return nil, fmt.Errorf("wxpay notifyUrl is required")
|
||||
}
|
||||
totalFen, err := payment.YuanToFen(req.Amount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay create payment: %w", err)
|
||||
}
|
||||
|
||||
mode, err := resolveWxpayCreateMode(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch mode {
|
||||
case wxpayModeJSAPI:
|
||||
return w.prepayJSAPI(ctx, client, req, notifyURL, totalFen)
|
||||
case wxpayModeH5:
|
||||
return w.prepayH5(ctx, client, req, notifyURL, totalFen)
|
||||
case wxpayModeNative:
|
||||
return w.prepayNative(ctx, client, req, notifyURL, totalFen)
|
||||
default:
|
||||
return nil, fmt.Errorf("wxpay create payment: unsupported mode %q", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Wxpay) prepayJSAPI(ctx context.Context, c *core.Client, req payment.CreatePaymentRequest, notifyURL string, totalFen int64) (*payment.CreatePaymentResponse, error) {
|
||||
svc := jsapi.JsapiApiService{Client: c}
|
||||
cur := wxpayCurrency
|
||||
appID := ResolveWxpayJSAPIAppID(w.config)
|
||||
prepayReq := jsapi.PrepayRequest{
|
||||
Appid: core.String(appID),
|
||||
Mchid: core.String(w.config["mchId"]),
|
||||
Description: core.String(req.Subject),
|
||||
OutTradeNo: core.String(req.OrderID),
|
||||
NotifyUrl: core.String(notifyURL),
|
||||
Amount: &jsapi.Amount{Total: core.Int64(totalFen), Currency: &cur},
|
||||
Payer: &jsapi.Payer{Openid: core.String(strings.TrimSpace(req.OpenID))},
|
||||
}
|
||||
if clientIP := strings.TrimSpace(req.ClientIP); clientIP != "" {
|
||||
prepayReq.SceneInfo = &jsapi.SceneInfo{PayerClientIp: core.String(clientIP)}
|
||||
}
|
||||
resp, _, err := wxpayJSAPIPrepayWithRequestPayment(ctx, svc, prepayReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay jsapi prepay: %w", err)
|
||||
}
|
||||
return &payment.CreatePaymentResponse{
|
||||
TradeNo: req.OrderID,
|
||||
ResultType: payment.CreatePaymentResultJSAPIReady,
|
||||
JSAPI: &payment.WechatJSAPIPayload{
|
||||
AppID: wxSV(resp.Appid),
|
||||
TimeStamp: wxSV(resp.TimeStamp),
|
||||
NonceStr: wxSV(resp.NonceStr),
|
||||
Package: wxSV(resp.Package),
|
||||
SignType: wxSV(resp.SignType),
|
||||
PaySign: wxSV(resp.PaySign),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Wxpay) prepayNative(ctx context.Context, c *core.Client, req payment.CreatePaymentRequest, notifyURL string, totalFen int64) (*payment.CreatePaymentResponse, error) {
|
||||
svc := native.NativeApiService{Client: c}
|
||||
cur := wxpayCurrency
|
||||
resp, _, err := wxpayNativePrepay(ctx, svc, native.PrepayRequest{
|
||||
Appid: core.String(w.config["appId"]), Mchid: core.String(w.config["mchId"]),
|
||||
Description: core.String(req.Subject), OutTradeNo: core.String(req.OrderID),
|
||||
NotifyUrl: core.String(notifyURL),
|
||||
Amount: &native.Amount{Total: core.Int64(totalFen), Currency: &cur},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay native prepay: %w", err)
|
||||
}
|
||||
codeURL := ""
|
||||
if resp.CodeUrl != nil {
|
||||
codeURL = *resp.CodeUrl
|
||||
}
|
||||
return &payment.CreatePaymentResponse{TradeNo: req.OrderID, QRCode: codeURL}, nil
|
||||
}
|
||||
|
||||
func (w *Wxpay) prepayH5(ctx context.Context, c *core.Client, req payment.CreatePaymentRequest, notifyURL string, totalFen int64) (*payment.CreatePaymentResponse, error) {
|
||||
svc := h5.H5ApiService{Client: c}
|
||||
cur := wxpayCurrency
|
||||
resp, _, err := wxpayH5Prepay(ctx, svc, h5.PrepayRequest{
|
||||
Appid: core.String(w.config["appId"]), Mchid: core.String(w.config["mchId"]),
|
||||
Description: core.String(req.Subject), OutTradeNo: core.String(req.OrderID),
|
||||
NotifyUrl: core.String(notifyURL),
|
||||
Amount: &h5.Amount{Total: core.Int64(totalFen), Currency: &cur},
|
||||
SceneInfo: &h5.SceneInfo{PayerClientIp: core.String(req.ClientIP), H5Info: buildWxpayH5Info(w.config)},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay h5 prepay: %w", err)
|
||||
}
|
||||
h5URL := ""
|
||||
if resp.H5Url != nil {
|
||||
h5URL = *resp.H5Url
|
||||
}
|
||||
h5URL, err = appendWxpayRedirectURL(h5URL, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &payment.CreatePaymentResponse{TradeNo: req.OrderID, PayURL: h5URL}, nil
|
||||
}
|
||||
|
||||
func buildWxpayH5Info(config map[string]string) *h5.H5Info {
|
||||
tp := wxpayH5Type
|
||||
info := &h5.H5Info{Type: &tp}
|
||||
if appName := strings.TrimSpace(config["h5AppName"]); appName != "" {
|
||||
info.AppName = core.String(appName)
|
||||
}
|
||||
if appURL := strings.TrimSpace(config["h5AppUrl"]); appURL != "" {
|
||||
info.AppUrl = core.String(appURL)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func resolveWxpayCreateMode(req payment.CreatePaymentRequest) (string, error) {
|
||||
if strings.TrimSpace(req.OpenID) != "" {
|
||||
return wxpayModeJSAPI, nil
|
||||
}
|
||||
if req.IsMobile {
|
||||
if strings.TrimSpace(req.ClientIP) == "" {
|
||||
return "", fmt.Errorf("wxpay H5 payment requires client IP")
|
||||
}
|
||||
return wxpayModeH5, nil
|
||||
}
|
||||
return wxpayModeNative, nil
|
||||
}
|
||||
|
||||
func appendWxpayRedirectURL(h5URL string, req payment.CreatePaymentRequest) (string, error) {
|
||||
h5URL = strings.TrimSpace(h5URL)
|
||||
returnURL := strings.TrimSpace(req.ReturnURL)
|
||||
if h5URL == "" || returnURL == "" {
|
||||
return h5URL, nil
|
||||
}
|
||||
|
||||
redirectURL, err := buildWxpayResultURL(returnURL, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sep := "&"
|
||||
if !strings.Contains(h5URL, "?") {
|
||||
sep = "?"
|
||||
}
|
||||
return h5URL + sep + "redirect_url=" + url.QueryEscape(redirectURL), nil
|
||||
}
|
||||
|
||||
func buildWxpayResultURL(returnURL string, req payment.CreatePaymentRequest) (string, error) {
|
||||
u, err := url.Parse(returnURL)
|
||||
if err != nil || !u.IsAbs() || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
|
||||
return "", fmt.Errorf("return URL must be an absolute http(s) URL")
|
||||
}
|
||||
|
||||
values := u.Query()
|
||||
values.Set("out_trade_no", strings.TrimSpace(req.OrderID))
|
||||
if paymentType := strings.TrimSpace(req.PaymentType); paymentType != "" {
|
||||
values.Set("payment_type", paymentType)
|
||||
}
|
||||
if strings.TrimSpace(u.Path) == "" {
|
||||
u.Path = wxpayResultPath
|
||||
}
|
||||
u.RawPath = ""
|
||||
u.RawQuery = values.Encode()
|
||||
u.Fragment = ""
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func wxSV(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func mapWxState(s string) string {
|
||||
switch s {
|
||||
case wxpayTradeStateSuccess:
|
||||
return payment.ProviderStatusPaid
|
||||
case wxpayTradeStateRefund:
|
||||
return payment.ProviderStatusRefunded
|
||||
case wxpayTradeStateClosed, wxpayTradeStatePayError:
|
||||
return payment.ProviderStatusFailed
|
||||
default:
|
||||
return payment.ProviderStatusPending
|
||||
}
|
||||
}
|
||||
|
||||
func buildWxpayTransactionMetadata(tx *payments.Transaction) map[string]string {
|
||||
if tx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
metadata := map[string]string{}
|
||||
if appID := wxSV(tx.Appid); appID != "" {
|
||||
metadata[wxpayMetadataAppID] = appID
|
||||
}
|
||||
if merchantID := wxSV(tx.Mchid); merchantID != "" {
|
||||
metadata[wxpayMetadataMerchantID] = merchantID
|
||||
}
|
||||
if tradeState := wxSV(tx.TradeState); tradeState != "" {
|
||||
metadata[wxpayMetadataTradeState] = tradeState
|
||||
}
|
||||
if tx.Amount != nil {
|
||||
if currency := wxSV(tx.Amount.Currency); currency != "" {
|
||||
metadata[wxpayMetadataCurrency] = currency
|
||||
}
|
||||
}
|
||||
if len(metadata) == 0 {
|
||||
return nil
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
func (w *Wxpay) QueryOrder(ctx context.Context, tradeNo string) (*payment.QueryOrderResponse, error) {
|
||||
c, err := w.ensureClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svc := native.NativeApiService{Client: c}
|
||||
tx, _, err := svc.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{
|
||||
OutTradeNo: core.String(tradeNo), Mchid: core.String(w.config["mchId"]),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay query order: %w", err)
|
||||
}
|
||||
var amt float64
|
||||
if tx.Amount != nil && tx.Amount.Total != nil {
|
||||
amt = payment.FenToYuan(*tx.Amount.Total)
|
||||
}
|
||||
id := tradeNo
|
||||
if tx.TransactionId != nil {
|
||||
id = *tx.TransactionId
|
||||
}
|
||||
pa := ""
|
||||
if tx.SuccessTime != nil {
|
||||
pa = *tx.SuccessTime
|
||||
}
|
||||
return &payment.QueryOrderResponse{
|
||||
TradeNo: id,
|
||||
Status: mapWxState(wxSV(tx.TradeState)),
|
||||
Amount: amt,
|
||||
PaidAt: pa,
|
||||
Metadata: buildWxpayTransactionMetadata(tx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Wxpay) VerifyNotification(ctx context.Context, rawBody string, headers map[string]string) (*payment.PaymentNotification, error) {
|
||||
if _, err := w.ensureClient(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, err := http.NewRequestWithContext(ctx, http.MethodPost, "/", io.NopCloser(bytes.NewBufferString(rawBody)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay construct request: %w", err)
|
||||
}
|
||||
for k, v := range headers {
|
||||
r.Header.Set(k, v)
|
||||
}
|
||||
var tx payments.Transaction
|
||||
nr, err := w.notifyHandler.ParseNotifyRequest(ctx, r, &tx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay verify notification: %w", err)
|
||||
}
|
||||
if nr.EventType != wxpayEventTransactionSuccess {
|
||||
return nil, nil
|
||||
}
|
||||
var amt float64
|
||||
if tx.Amount != nil && tx.Amount.Total != nil {
|
||||
amt = payment.FenToYuan(*tx.Amount.Total)
|
||||
}
|
||||
st := payment.ProviderStatusFailed
|
||||
if wxSV(tx.TradeState) == wxpayTradeStateSuccess {
|
||||
st = payment.ProviderStatusSuccess
|
||||
}
|
||||
return &payment.PaymentNotification{
|
||||
TradeNo: wxSV(tx.TransactionId), OrderID: wxSV(tx.OutTradeNo),
|
||||
Amount: amt, Status: st, RawData: rawBody, Metadata: buildWxpayTransactionMetadata(&tx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Wxpay) Refund(ctx context.Context, req payment.RefundRequest) (*payment.RefundResponse, error) {
|
||||
c, err := w.ensureClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rf, err := payment.YuanToFen(req.Amount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay refund amount: %w", err)
|
||||
}
|
||||
tf, err := w.queryOrderTotalFen(ctx, c, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rs := refunddomestic.RefundsApiService{Client: c}
|
||||
cur := wxpayCurrency
|
||||
outRefundNo := wxpayRefundID(req.OrderID, req.Amount)
|
||||
res, _, err := rs.Create(ctx, refunddomestic.CreateRequest{
|
||||
OutTradeNo: core.String(req.OrderID),
|
||||
OutRefundNo: core.String(outRefundNo),
|
||||
Reason: core.String(req.Reason),
|
||||
Amount: &refunddomestic.AmountReq{Refund: core.Int64(rf), Total: core.Int64(tf), Currency: &cur},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay refund: %w", err)
|
||||
}
|
||||
st := payment.ProviderStatusPending
|
||||
if res.Status != nil && *res.Status == refunddomestic.STATUS_SUCCESS {
|
||||
st = payment.ProviderStatusSuccess
|
||||
}
|
||||
return &payment.RefundResponse{RefundID: outRefundNo, Status: st}, nil
|
||||
}
|
||||
|
||||
func (w *Wxpay) QueryRefund(ctx context.Context, req payment.RefundQueryRequest) (*payment.RefundResponse, error) {
|
||||
c, err := w.ensureClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outRefundNo := strings.TrimSpace(req.RefundID)
|
||||
if outRefundNo == "" {
|
||||
outRefundNo = wxpayRefundID(req.OrderID, req.Amount)
|
||||
}
|
||||
if outRefundNo == "" {
|
||||
return nil, fmt.Errorf("wxpay query refund: missing refund id")
|
||||
}
|
||||
rs := refunddomestic.RefundsApiService{Client: c}
|
||||
res, _, err := rs.QueryByOutRefundNo(ctx, refunddomestic.QueryByOutRefundNoRequest{
|
||||
OutRefundNo: core.String(outRefundNo),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay query refund: %w", err)
|
||||
}
|
||||
status := payment.ProviderStatusPending
|
||||
if res != nil && res.Status != nil {
|
||||
switch *res.Status {
|
||||
case refunddomestic.STATUS_SUCCESS:
|
||||
status = payment.ProviderStatusSuccess
|
||||
case refunddomestic.STATUS_CLOSED, refunddomestic.STATUS_ABNORMAL:
|
||||
status = payment.ProviderStatusFailed
|
||||
default:
|
||||
status = payment.ProviderStatusPending
|
||||
}
|
||||
}
|
||||
return &payment.RefundResponse{RefundID: outRefundNo, Status: status}, nil
|
||||
}
|
||||
|
||||
func wxpayRefundID(orderID, amount string) string {
|
||||
orderID = strings.TrimSpace(orderID)
|
||||
if orderID == "" {
|
||||
return ""
|
||||
}
|
||||
amount = strings.NewReplacer(".", "", "-", "").Replace(strings.TrimSpace(amount))
|
||||
if amount == "" {
|
||||
return orderID + "-refund"
|
||||
}
|
||||
return orderID + "-refund-" + amount
|
||||
}
|
||||
|
||||
func (w *Wxpay) queryOrderTotalFen(ctx context.Context, c *core.Client, orderID string) (int64, error) {
|
||||
svc := native.NativeApiService{Client: c}
|
||||
tx, _, err := svc.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{
|
||||
OutTradeNo: core.String(orderID), Mchid: core.String(w.config["mchId"]),
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("wxpay refund query order: %w", err)
|
||||
}
|
||||
var tf int64
|
||||
if tx.Amount != nil && tx.Amount.Total != nil {
|
||||
tf = *tx.Amount.Total
|
||||
}
|
||||
return tf, nil
|
||||
}
|
||||
|
||||
func (w *Wxpay) CancelPayment(ctx context.Context, tradeNo string) error {
|
||||
c, err := w.ensureClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
svc := native.NativeApiService{Client: c}
|
||||
_, err = svc.CloseOrder(ctx, native.CloseOrderRequest{
|
||||
OutTradeNo: core.String(tradeNo), Mchid: core.String(w.config["mchId"]),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("wxpay cancel payment: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ payment.Provider = (*Wxpay)(nil)
|
||||
_ payment.CancelableProvider = (*Wxpay)(nil)
|
||||
)
|
||||
Reference in New Issue
Block a user