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 |
getConfigs → getPublicConfigs |
src/app/[locale]/(auth)/sign-up/page.tsx |
getConfigs → getPublicConfigs |
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 登录按钮
现象
- 页面只显示邮箱登录方式
- Google 登录选项完全不可见
- 用户无法通过 Google 账号登录
- 管理员无法通过 Google 登录访问后台
本质
配置读取函数选择错误:
- getConfigs() 只从数据库读取配置
- Google OAuth 配置 (NEXT_PUBLIC_AUTH_GOOGLE_ENABLED) 存储在环境变量中
- 数据库中没有 Google 认证相关配置,导致返回 undefined
罪魁祸首
- 错误的函数调用 - 使用了
getConfigs()而非getPublicConfigs() - 配置来源混合 - 部分配置在数据库,部分在环境变量,两个函数行为不同
解决办法
将配置读取函数从 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 支付成功,但网站未发放积分
现象
- Creem 后台显示支付成功($5.98,paid 状态)
- 数据库
orders表订单状态为created(应为paid) - 数据库
subscriptions表无对应记录 - 用户积分余额为 30(注册赠送),未增加购买的 300 积分
本质
Webhook 回调处理存在竞态条件,导致某些事件处理失败。虽然 checkout.completed 理论上应该成功,但数据库状态显示未正确更新。
罪魁祸首
- Webhook 事件时序问题 - Creem 快速发送多个事件,部分事件在订阅创建前到达
- 错误处理策略 - 找不到订阅就抛出错误,导致 Creem 认为整体失败
解决办法
手动修复步骤:
- 更新订单状态为
paid - 创建订阅记录(使用 Creem 的 subscription_id:
sub_4HwtQHM3FrvZqcUAJBTZGD) - 发放 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.update 和 subscription.active 事件失败
现象
checkout.completed✅ 成功subscription.paid✅ 成功subscription.update❌ 失败 - "subscription not found"subscription.active❌ 失败 - "subscription not found"
本质
事件处理顺序问题:
1. 用户完成支付后,Creem 快速发送多个 webhook 事件
2. subscription.update/subscription.active 可能在 checkout.completed 之前或同时到达
3. checkout.completed 负责创建订阅记录
4. 其他事件到达时订阅记录尚未创建,导致查询失败
罪魁祸首
- 严格的订阅存在性检查 - 找不到订阅就抛出错误
- 无容错机制 - 没有考虑事件可能乱序到达的情况
// 问题代码
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 配置 |
潜在改进点
- 事件队列 - 考虑实现 webhook 事件队列,保证处理顺序
- 重试机制 - 对于找不到订阅的情况,可以延迟重试而非直接跳过
- 配置统一 - 考虑将所有配置迁移到数据库或环境变量,避免混用
- 监控告警 - 增加 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 |
订阅不存在时返回成功而非报错 |
测试建议
- Google 登录测试 - 访问
/sign-in,确认 Google 登录按钮显示 - Google 注册测试 - 访问
/sign-up,确认 Google 注册按钮显示 - 支付流程测试 - 完成一次订阅支付,确认积分正确发放
- Webhook 测试 - 检查 Creem webhook logs,确认无新的失败事件
总结
本次修复解决了两类问题:
- 配置读取问题 - 使用了错误的配置读取方法导致 Google 登录不显示,修改为正确的函数调用
- 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(仅本地保留,不入库不部署)