fix(payment): 微信 API 调用加超时兜底(B3 出网韧性)
微信是外部第三方、最可能慢/挂,而 wechatpay-go SDK 默认 http.Client 无 Timeout: 一次卡住的 Prepay/QueryOrder 会拖住请求 goroutine;尤其掉单补偿定时器用的是 context.Background()(无超时)→ 微信一挂那轮 tick 无限期卡死。 双保险:客户端级 HTTP 超时(WithHTTPClient Timeout=15s,belt) + CreatePay/QueryOrder 每次调用 ctx 超时(suspenders,兜住 SDK 忽略或背景 ctx 的情况)。 范围克制:只硬化微信这个真外部依赖。PG/Redis(连接池+降级)、NATS(无限重连)、mcp-go RAG 客户端(embed 30s/chat 60s/rerank 20s 已有超时)本就有韧性,不额外套熔断(那对本规模是过度设计)。 build+vet+test 绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
||||||
@@ -33,6 +34,12 @@ type Wechat struct {
|
|||||||
pubKeyID string
|
pubKeyID string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wechatAPITimeout 限制单次微信 API 往返。微信是外部第三方依赖、最可能慢/挂,
|
||||||
|
// SDK 默认 http.Client 无 Timeout:一次卡住的 Prepay/QueryOrder 会拖住请求 goroutine,
|
||||||
|
// 尤其掉单补偿定时器用的是 context.Background()(无超时)→ 微信一挂那轮 tick 无限期卡死。
|
||||||
|
// 双保险:客户端级 HTTP 超时(belt)+ 每次调用的 ctx 超时(suspenders,兜住 SDK 忽略/背景 ctx)。
|
||||||
|
const wechatAPITimeout = 15 * time.Second
|
||||||
|
|
||||||
// 编译期断言:Wechat 实现 Channel 接口。
|
// 编译期断言:Wechat 实现 Channel 接口。
|
||||||
var _ Channel = (*Wechat)(nil)
|
var _ Channel = (*Wechat)(nil)
|
||||||
|
|
||||||
@@ -52,7 +59,8 @@ func New(ctx context.Context, c Config) (*Wechat, error) {
|
|||||||
return nil, fmt.Errorf("微信支付公钥加载失败(%s): %w", c.PublicKeyPath, err)
|
return nil, fmt.Errorf("微信支付公钥加载失败(%s): %w", c.PublicKeyPath, err)
|
||||||
}
|
}
|
||||||
client, err := core.NewClient(ctx,
|
client, err := core.NewClient(ctx,
|
||||||
option.WithWechatPayPublicKeyAuthCipher(c.MchID, c.CertSerial, priv, c.PublicKeyID, pub))
|
option.WithWechatPayPublicKeyAuthCipher(c.MchID, c.CertSerial, priv, c.PublicKeyID, pub),
|
||||||
|
option.WithHTTPClient(&http.Client{Timeout: wechatAPITimeout})) // 客户端级超时兜底
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("客户端初始化失败: %w", err)
|
return nil, fmt.Errorf("客户端初始化失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -65,6 +73,8 @@ func New(ctx context.Context, c Config) (*Wechat, error) {
|
|||||||
|
|
||||||
// CreatePay Native 下单:返回 code_url(前端渲染成二维码)。金额取订单锁定值。
|
// CreatePay Native 下单:返回 code_url(前端渲染成二维码)。金额取订单锁定值。
|
||||||
func (w *Wechat) CreatePay(ctx context.Context, orderID, description string, amountFen int64) (PayIntent, error) {
|
func (w *Wechat) CreatePay(ctx context.Context, orderID, description string, amountFen int64) (PayIntent, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, wechatAPITimeout)
|
||||||
|
defer cancel()
|
||||||
resp, _, err := w.svc.Prepay(ctx, native.PrepayRequest{
|
resp, _, err := w.svc.Prepay(ctx, native.PrepayRequest{
|
||||||
Appid: core.String(w.appID),
|
Appid: core.String(w.appID),
|
||||||
Mchid: core.String(w.mchID),
|
Mchid: core.String(w.mchID),
|
||||||
@@ -108,6 +118,8 @@ func fromTransaction(t *payments.Transaction) PayResult {
|
|||||||
|
|
||||||
// QueryOrder 主动查单(本地开发确认到账、生产掉单补偿共用)。
|
// QueryOrder 主动查单(本地开发确认到账、生产掉单补偿共用)。
|
||||||
func (w *Wechat) QueryOrder(ctx context.Context, orderID string) (PayResult, error) {
|
func (w *Wechat) QueryOrder(ctx context.Context, orderID string) (PayResult, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, wechatAPITimeout)
|
||||||
|
defer cancel()
|
||||||
t, _, err := w.svc.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{
|
t, _, err := w.svc.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{
|
||||||
OutTradeNo: core.String(orderID),
|
OutTradeNo: core.String(orderID),
|
||||||
Mchid: core.String(w.mchID),
|
Mchid: core.String(w.mchID),
|
||||||
|
|||||||
Reference in New Issue
Block a user