diff --git a/sundynix-gateway/internal/payment/wechat.go b/sundynix-gateway/internal/payment/wechat.go index fe4b24f..26d0b65 100644 --- a/sundynix-gateway/internal/payment/wechat.go +++ b/sundynix-gateway/internal/payment/wechat.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "net/http" + "time" "github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" @@ -33,6 +34,12 @@ type Wechat struct { 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 接口。 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) } 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 { return nil, fmt.Errorf("客户端初始化失败: %w", err) } @@ -65,6 +73,8 @@ func New(ctx context.Context, c Config) (*Wechat, error) { // CreatePay Native 下单:返回 code_url(前端渲染成二维码)。金额取订单锁定值。 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{ Appid: core.String(w.appID), Mchid: core.String(w.mchID), @@ -108,6 +118,8 @@ func fromTransaction(t *payments.Transaction) PayResult { // QueryOrder 主动查单(本地开发确认到账、生产掉单补偿共用)。 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{ OutTradeNo: core.String(orderID), Mchid: core.String(w.mchID),