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 } }