Files
iotlcm/internal/service/balance/alipay.go
T
MiFaZhan 2bec257289 chore: 初始开源发布
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。

- Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件)
- 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、
  库名 xiujie_admin → iotlcm、哨兵名占位
- 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异,
  不破坏「校验 MSISDN 格式」等测试断言)
- 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像
  digest 记录
- Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23),
  移除写死的 git 凭据
- README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0
  LICENSE,LF 行尾 + .gitattributes

已知:3 个测试包在源仓同一提交上同样失败(client/upstream、
service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
2026-10-22 16:37:56 +08:00

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