Files

138 lines
4.2 KiB
Go

package balance
import (
"context"
"errors"
"net/http"
"strings"
"sync"
"time"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/smartwalle/alipay/v3"
)
type alipayConfig struct {
AppID string `yaml:"appId"`
PrivateKey string `yaml:"privateKey"`
AlipayPublicKey string `yaml:"alipayPublicKey"`
IsProduction bool `yaml:"isProduction"`
}
type alipayTradeQueryer struct {
once sync.Once
client *alipay.Client
clientErr error
}
func newAlipayTradeQueryer() *alipayTradeQueryer {
return &alipayTradeQueryer{}
}
func (q *alipayTradeQueryer) Query(ctx context.Context, outTradeNo string) (*TradeQueryResult, error) {
client, err := q.getClient(ctx)
if err != nil {
return nil, err
}
rsp, err := q.tradeQueryWithRetry(ctx, client, alipay.TradeQuery{OutTradeNo: outTradeNo})
if err != nil {
if isAlipayTradeNotFound(err.Error()) {
return &TradeQueryResult{OutTradeNo: outTradeNo, TradeStatus: string(alipay.TradeStatusWaitBuyerPay)}, nil
}
return nil, gerror.Wrap(err, "query Alipay trade failed")
}
if rsp == nil {
return nil, gerror.New("Alipay trade query returned no result")
}
if rsp.IsFailure() {
if isAlipayTradeNotFound(string(rsp.Code), rsp.SubCode, rsp.SubMsg) {
return &TradeQueryResult{OutTradeNo: outTradeNo, TradeStatus: string(alipay.TradeStatusWaitBuyerPay)}, nil
}
return nil, gerror.Newf("Alipay trade query failed: code=%s, subCode=%s, subMsg=%s", rsp.Code, rsp.SubCode, rsp.SubMsg)
}
return &TradeQueryResult{
OutTradeNo: rsp.OutTradeNo,
TradeNo: rsp.TradeNo,
TradeStatus: string(rsp.TradeStatus),
TotalAmount: rsp.TotalAmount,
BuyerLogonID: rsp.BuyerLogonId,
}, nil
}
func (q *alipayTradeQueryer) getClient(ctx context.Context) (*alipay.Client, error) {
q.once.Do(func() {
cfg := alipayConfig{}
if err := g.Cfg().MustGet(ctx, "alipay").Scan(&cfg); err != nil {
q.clientErr = gerror.Wrap(err, "read Alipay config failed")
return
}
if strings.TrimSpace(cfg.AppID) == "" || strings.TrimSpace(cfg.PrivateKey) == "" || strings.TrimSpace(cfg.AlipayPublicKey) == "" {
q.clientErr = gerror.New("Alipay credentials are not configured")
return
}
q.client, q.clientErr = alipay.New(cfg.AppID, cfg.PrivateKey, cfg.IsProduction, alipay.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}))
if q.clientErr != nil {
q.clientErr = gerror.Wrap(q.clientErr, "initialize Alipay client failed")
return
}
if err := q.client.LoadAliPayPublicKey(cfg.AlipayPublicKey); err != nil {
q.clientErr = gerror.Wrap(err, "load Alipay public key failed")
}
})
if q.clientErr != nil {
return nil, q.clientErr
}
return q.client, nil
}
func (q *alipayTradeQueryer) tradeQueryWithRetry(ctx context.Context, client *alipay.Client, query alipay.TradeQuery) (*alipay.TradeQueryRsp, error) {
var err error
for attempt := 1; attempt <= 3; attempt++ {
rsp, queryErr := client.TradeQuery(ctx, query)
if queryErr == nil {
return rsp, nil
}
err = queryErr
if !isRetryableAlipayError(err) || attempt == 3 {
return nil, err
}
if err = sleepWithContext(ctx, time.Duration(attempt)*300*time.Millisecond); err != nil {
return nil, err
}
}
return nil, err
}
func isRetryableAlipayError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, context.DeadlineExceeded) {
return true
}
msg := strings.ToLower(err.Error())
for _, marker := range []string{"invalid character '<'", "unexpected end of json", "bad response", "eof", "timeout", "connection reset", "connection refused", "tls handshake", "temporary", "too many requests", "429", "500", "502", "503", "504"} {
if strings.Contains(msg, marker) {
return true
}
}
return false
}
func isAlipayTradeNotFound(parts ...string) bool {
msg := strings.ToLower(strings.Join(parts, " "))
return strings.Contains(msg, "acq.trade_not_exist") || strings.Contains(msg, "trade_not_exist") || strings.Contains(msg, "transaction not exist") || strings.Contains(msg, "交易不存在") || strings.Contains(msg, "订单不存在")
}
func sleepWithContext(ctx context.Context, delay time.Duration) error {
timer := time.NewTimer(delay)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}