fix(server): pay webhook 兜底补行不再漏停用用户(修静默丢钱)+ 未知事件日志 + PAY_BIZ_SECRET 启动守卫

- webhook 兜底路径(台账缺行按 biz_ref 定位用户)去掉 status='active' 过滤,
  与正常路径(row.UserID 直接开通不看状态)对齐:钱已实收,不因用户被停用
  (banned)而拒绝补行开通,避免 500→pay 12 次重投后死信→静默丢钱。
- 未知 event_type 分支加 slog.Warn,便于将来 pay 侧误注册无 handler 的事件
  类型时能被观测到。
- PAY_BASE_URL 已设但 PAY_BIZ_SECRET 为空时 log.Fatal 拒绝启动,避免出站
  签名失败+入站验签全 401 的静默瘫痪。
- InsertFromWebhookTx 不再把 channel 冒充 method 写入台账(payload 无 method
  字段可复原,留空并加注释,消除台账观感误导)。

新增 TestWebhook_MissingLedgerFallsBackEvenWhenUserSuspended 覆盖 #1:
台账缺行 + 目标用户 banned 时,兜底补行仍成功开通并回 SUCCESS。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
wangjia
2026-07-11 00:48:57 +08:00
parent 390b8b8f84
commit bc9aa38392
5 changed files with 52 additions and 5 deletions
+4 -1
View File
@@ -84,6 +84,7 @@ func (h *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if ev.EventType != "payment.succeeded" {
// 事件白名单外(pay 侧只应配 payment.succeeded):确认不处理,免重投。
slog.Warn("pay webhook: 未知事件已 ack 未处理", "event_type", ev.EventType, "out_trade_no", ev.OutTradeNo)
writeSuccess(w)
return
}
@@ -141,8 +142,10 @@ func (h *WebhookHandler) settle(ctx context.Context, ev *webhookEvent) error {
switch {
case err == sql.ErrNoRows:
// 台账缺行(下单后本地写失败)→ 按 biz_ref=用户 uuid 兜底定位补建。
// 与正常路径(row.UserID 直接开通,不看 user 状态)对齐:钱已实收,
// 兜底定位不应因用户被停用(suspended)而拒绝开通——否则静默丢钱。
if err := h.db.QueryRowContext(ctx,
`SELECT id FROM users WHERE uuid = ? AND status = 'active'`, ev.BizRef).Scan(&userID); err != nil {
`SELECT id FROM users WHERE uuid = ?`, ev.BizRef).Scan(&userID); err != nil {
return fmt.Errorf("biz_ref %q 定位用户失败: %w", ev.BizRef, err)
}
purchaseID, err = h.store.InsertFromWebhookTx(ctx, tx, userID, ev.BizRef, ev.ProductBizCode, ev.OutTradeNo, ev.Channel)