知识库首页 知识库-世界 2026-01-17-payment-webhook-auth-issues.md

2026 01 17 payment webhook auth issues

本地来源:Knowledge/World/项目/Practice/BUG-SOLUTIONS/2026-01-17-payment-webhook-auth-issues.md

2026-01-17 支付 Webhook 与认证配置问题报告


TL;DR 速查表

问题速览

# 问题 原因 解决方案
1 Google 登录按钮不显示 getConfigs() 不读取环境变量 改用 getPublicConfigs()
2 用户付款未收到积分 Webhook 竞态条件 手动补发积分
3 Webhook "subscription not found" 事件到达顺序问题 找不到订阅时返回成功

关键函数速查

// 配置读取(正确方式)
getPublicConfigs()  // 合并环境变量 + 数据库配置

// 配置读取(错误方式)
getConfigs()  // 仅读取数据库配置

// 按 provider subscription ID 查找订阅
findSubscriptionByProviderSubscriptionId({ provider, subscriptionId })

决策流程图

Webhook 事件到达
  ↓
是 checkout.completed? ─是→ 创建订单、订阅、发放积分
  ↓否
是 subscription.update/canceled?
  ↓是
订阅记录存在? ─否→ 返回成功(跳过,避免重试)
  ↓是
更新订阅状态

修改文件清单

文件 改动
src/app/[locale]/(auth)/sign-in/page.tsx getConfigsgetPublicConfigs
src/app/[locale]/(auth)/sign-up/page.tsx getConfigsgetPublicConfigs
src/app/api/payment/notify/[provider]/route.ts 订阅不存在时返回成功

项目信息

  • 项目名称: longcat-video.org
  • 涉及模块: 认证模块、支付模块
  • 相关服务: Creem 支付、Google OAuth
  • 报告日期: 2026-01-17

问题一:Sign-in/Sign-up 页面 Google 登录按钮缺失

具体问题

/sign-in/sign-up 页面没有显示 Google OAuth 登录按钮

现象

  1. 页面只显示邮箱登录方式
  2. Google 登录选项完全不可见
  3. 用户无法通过 Google 账号登录
  4. 管理员无法通过 Google 登录访问后台

本质

配置读取函数选择错误: - getConfigs() 只从数据库读取配置 - Google OAuth 配置 (NEXT_PUBLIC_AUTH_GOOGLE_ENABLED) 存储在环境变量中 - 数据库中没有 Google 认证相关配置,导致返回 undefined

罪魁祸首

  1. 错误的函数调用 - 使用了 getConfigs() 而非 getPublicConfigs()
  2. 配置来源混合 - 部分配置在数据库,部分在环境变量,两个函数行为不同

解决办法

将配置读取函数从 getConfigs() 改为 getPublicConfigs()

// 错误写法
import { getConfigs } from '@/shared/models/config';
const configs = await getConfigs();

// 正确写法
import { getPublicConfigs } from '@/shared/models/config';
const configs = await getPublicConfigs();

关键代码修改

文件: src/app/[locale]/(auth)/sign-in/page.tsx

  • 修改 import 语句
  • 修改函数调用

文件: src/app/[locale]/(auth)/sign-up/page.tsx

  • 同上修改

问题二:用户付款但未收到积分 ([email protected])

具体问题

用户在 Creem 支付成功,但网站未发放积分

现象

  1. Creem 后台显示支付成功($5.98,paid 状态)
  2. 数据库 orders 表订单状态为 created(应为 paid
  3. 数据库 subscriptions 表无对应记录
  4. 用户积分余额为 30(注册赠送),未增加购买的 300 积分

本质

Webhook 回调处理存在竞态条件,导致某些事件处理失败。虽然 checkout.completed 理论上应该成功,但数据库状态显示未正确更新。

罪魁祸首

  1. Webhook 事件时序问题 - Creem 快速发送多个事件,部分事件在订阅创建前到达
  2. 错误处理策略 - 找不到订阅就抛出错误,导致 Creem 认为整体失败

解决办法

手动修复步骤

  1. 更新订单状态为 paid
  2. 创建订阅记录(使用 Creem 的 subscription_id: sub_4HwtQHM3FrvZqcUAJBTZGD
  3. 发放 300 积分
-- 1. 更新订单状态
UPDATE orders SET status = 'paid' WHERE order_no = 'xxx';

-- 2. 创建订阅记录
INSERT INTO subscriptions (
  subscription_no, user_id, user_email,
  provider, subscription_id, status, ...
) VALUES (...);

-- 3. 发放积分
INSERT INTO credits (user_id, amount, type, ...) VALUES (...);
UPDATE users SET credits = credits + 300 WHERE id = 'xxx';

关键代码修改

此问题为手动修复,无代码修改。


问题三:Webhook 竞态条件导致 "subscription not found"

具体问题

Creem webhook logs 显示大量 subscription.updatesubscription.active 事件失败

现象

  1. checkout.completed ✅ 成功
  2. subscription.paid ✅ 成功
  3. subscription.update ❌ 失败 - "subscription not found"
  4. subscription.active ❌ 失败 - "subscription not found"

本质

事件处理顺序问题: 1. 用户完成支付后,Creem 快速发送多个 webhook 事件 2. subscription.update/subscription.active 可能在 checkout.completed 之前或同时到达 3. checkout.completed 负责创建订阅记录 4. 其他事件到达时订阅记录尚未创建,导致查询失败

罪魁祸首

  1. 严格的订阅存在性检查 - 找不到订阅就抛出错误
  2. 无容错机制 - 没有考虑事件可能乱序到达的情况
// 问题代码
if (!existingSubscription) {
  throw new Error('subscription not found');  // 直接报错
}

解决办法

当找不到订阅时,返回成功响应而非抛出错误,避免 Creem 无谓重试:

// SUBSCRIBE_UPDATED 事件处理
if (!existingSubscription) {
  // 订阅可能还未创建(checkout.completed 正在处理中),返回成功避免重试
  console.log(
    `[SUBSCRIBE_UPDATED] subscription not found yet, skipping: ${session.subscriptionId}`
  );
  return Response.json({
    message: 'subscription not ready, skipped',
  });
}

// SUBSCRIBE_CANCELED 事件处理(同样逻辑)
if (!existingSubscription) {
  console.log(
    `[SUBSCRIBE_CANCELED] subscription not found yet, skipping: ${session.subscriptionId}`
  );
  return Response.json({
    message: 'subscription not ready, skipped',
  });
}

关键代码修改

文件: src/app/api/payment/notify/[provider]/route.ts

  • 第 262-269 行:SUBSCRIBE_UPDATED 容错处理
  • 第 287-294 行:SUBSCRIBE_CANCELED 容错处理

查漏补缺

已确认不受影响的场景

场景 是否受影响 原因
一次性支付 ❌ 不受影响 不涉及订阅事件
订阅首次支付 ✅ 正常工作 checkout.completed 处理
订阅续费 ✅ 正常工作 subscription.paid 处理
积分发放 ✅ 正常工作 checkout.completed 中处理
邮箱登录 ❌ 不受影响 不依赖 Google OAuth 配置

潜在改进点

  1. 事件队列 - 考虑实现 webhook 事件队列,保证处理顺序
  2. 重试机制 - 对于找不到订阅的情况,可以延迟重试而非直接跳过
  3. 配置统一 - 考虑将所有配置迁移到数据库或环境变量,避免混用
  4. 监控告警 - 增加 webhook 处理失败的监控告警

修改文件清单

文件路径 修改内容
src/app/[locale]/(auth)/sign-in/page.tsx 配置读取改用 getPublicConfigs()
src/app/[locale]/(auth)/sign-up/page.tsx 配置读取改用 getPublicConfigs()
src/app/api/payment/notify/[provider]/route.ts 订阅不存在时返回成功而非报错

测试建议

  1. Google 登录测试 - 访问 /sign-in,确认 Google 登录按钮显示
  2. Google 注册测试 - 访问 /sign-up,确认 Google 注册按钮显示
  3. 支付流程测试 - 完成一次订阅支付,确认积分正确发放
  4. Webhook 测试 - 检查 Creem webhook logs,确认无新的失败事件

总结

本次修复解决了两类问题:

  1. 配置读取问题 - 使用了错误的配置读取方法导致 Google 登录不显示,修改为正确的函数调用
  2. Webhook 竞态条件 - Creem 快速发送事件导致部分事件处理失败,通过容错机制解决

修复后: - Google 登录按钮正常显示 - Webhook 竞态条件得到容错处理 - domenico 用户积分已手动补发(330 积分)


SOP 检查清单

部署前检查

  • [x] 代码编译通过 - npm run build 无错误
  • [x] TypeScript 类型检查 - 无 type error
  • [x] 环境变量配置 - NEXT_PUBLIC_AUTH_GOOGLE_ENABLED 已配置

功能验证清单

1. 正常流程测试

  • [ ] 访问 /sign-in 页面,Google 登录按钮显示
  • [ ] 访问 /sign-up 页面,Google 注册按钮显示
  • [ ] 使用 Google 账号登录成功
  • [ ] 完成订阅支付,积分正确发放

2. 异常流程测试

  • [ ] Webhook subscription.update 在订阅创建前到达,返回成功
  • [ ] Webhook subscription.canceled 在订阅创建前到达,返回成功
  • [ ] 网络中断后重新发送 webhook,正常处理

3. 边界条件测试

  • [ ] 数据库配置为空时,仍能读取环境变量配置
  • [ ] 订阅记录被手动删除后,webhook 不报错

监控与告警

  • [ ] 日志关键词监控
  • [SUBSCRIBE_UPDATED] subscription not found yet - 订阅更新事件被跳过(正常)
  • [SUBSCRIBE_CANCELED] subscription not found yet - 订阅取消事件被跳过(正常)
  • handle payment notify failed - Webhook 处理失败(需关注)

  • [ ] 异常情况检查

  • 正常情况:Webhook logs 显示 200 状态码
  • 异常情况:大量 500 状态码,需检查日志

回滚方案

如需回滚,直接 revert 以下 commit:

git revert 8b47009  # fix(payment): gracefully handle webhook race condition
git revert d6f8637  # fix(auth): use getPublicConfigs for sign-in/sign-up pages

回滚后影响: - Google 登录按钮可能不显示 - Webhook subscription.update/canceled 可能报错(不影响积分发放)


变更记录

日期 版本 变更内容 作者
2026-01-17 v1.0 初始版本 Claude

本文档为站内渲染。原始文件本地路径:saas/source/knowledge-world/Knowledge-World-项目-Practice-BUG-SOLUTIONS-2026-01-17-payment-fc434f.md(仅本地保留,不入库不部署)