creem webhook incident 2026 01 07
本地来源:Knowledge/World/项目/Practice/Saas功能文档/creem-webhook-incident-2026-01-07.md
Creem Webhook 事件记录 - 2026-01-07
事件概述
时间:2026-01-07 影响用户:[email protected] (Mihai Moldoveanu) 问题:用户支付 $30.13 购买 2000 积分,但 Creem webhook 处理失败,积分未到账
问题原因
技术根源
文件:src/app/api/webhooks/creem/route.ts
错误:PostgreSQL UUID 类型错误
Error: invalid input syntax for type uuid: "evt_77rdEketDnJbpdUn9OW7Eh"
原因分析:
- Creem 的 event ID 格式是 evt_xxx(字符串),不是 UUID 格式
- 代码错误地将 Creem event ID 直接赋值给数据库的 UUID 字段
- PostgreSQL 拒绝了非 UUID 格式的值,导致 webhook 处理失败
代码问题
修复前:
// ❌ 错误:将 Creem event ID 赋值给 UUID 字段
const eventId = typeof parsed?.id === 'string' ? parsed.id : randomUUID();
await db.insert(webhookEvent).values({
id: eventId, // 这里会将 "evt_xxx" 赋值给 UUID 字段
eventId,
source: 'creem',
eventType,
processedAt: null,
})
修复后:
// ✅ 正确:分开处理 UUID 和 Creem event ID
const creemEventId = typeof parsed?.id === 'string'
? parsed.id
: `unknown-${randomUUID()}`;
await db.insert(webhookEvent).values({
id: randomUUID(), // 生成标准 UUID
eventId: creemEventId, // 存储 Creem 的 event ID
source: 'creem',
eventType,
processedAt: null,
})
处理流程
1. 紧急响应(手动补发积分)
执行时间:2026-01-07 16:11:34 操作:手动补发 2000 积分给用户
NODE_OPTIONS='--import tsx' node scripts/grant-credits-manually.ts
交易记录:
- Transaction ID: 169a3798-06b7-4fc2-b79a-5c70dbae90d1
- Amount: +2000
- Type: PURCHASE_PACKAGE
- Description: "Manual credit grant: 2000 credits for Creem payment $30.13 (手动补发 - 支付成功但 webhook 未触发)"
2. 修复代码
Commit: fa923e4
修改文件:src/app/api/webhooks/creem/route.ts
修改内容:分离 UUID 生成和 Creem event ID 存储逻辑
3. 部署修复
平台:Vercel 部署时间:2026-01-07 ~16:30 状态:✅ 部署成功
4. Webhook 重试
操作:在 Creem 后台点击 "Resend" 按钮 结果:✅ HTTP 200 Success 时间:2026-01-07 16:50:58
新交易记录:
- Transaction ID: 309f3759-e631-4cbc-a724-9eef0059a847
- Amount: +2000
- Type: PURCHASE_PACKAGE
- Description: "+2000 credits via Creem purchase"
- Payment ID: 84ed8621-231e-44d6-ab45-fa6a7e160459
5. 处理重复积分
问题:用户收到了两次 2000 积分(手动补发 + webhook 正常发放)
处理方式:直接删除 webhook 发放的记录
NODE_OPTIONS='--import tsx' node scripts/remove-creem-duplicate-credits.ts
结果:
- 删除了 Transaction ID: 309f3759-e631-4cbc-a724-9eef0059a847
- 从余额中扣除 2000 积分
- 保留了手动补发的记录
- 最终余额:约 1,395 积分
问题反思
处理不当的地方
- 直接删除记录:违反了审计原则,应该使用撤销记录而不是删除原记录
- 缺少审计轨迹:删除后无法追溯为什么积分变化
- 应对措施仓促:没有时间使用更优雅的撤销机制
更好的处理方式
应该使用撤销记录:
// 创建负数的撤销记录,而不是删除原记录
await tx.insert(creditTransaction).values({
id: randomUUID(),
userId: userRecord.id,
amount: -2000, // 负数表示扣除
type: CREDIT_TRANSACTION_TYPE.MANUAL_ADJUST,
description: `Revoked transaction ${targetTransaction.id}: -2000 credits. Reason: Duplicate webhook - user already received manual credits`,
paymentId: targetTransaction.paymentId,
createdAt: new Date(),
updatedAt: new Date(),
});
优点: - ✅ 完整的审计轨迹 - ✅ 可以追溯所有变更 - ✅ 符合会计原则(只增不删) - ✅ 方便客服查询问题
技术改进
已实施
- 修复 UUID 类型错误:正确分离 UUID 和 event ID
- 创建诊断脚本:
-
scripts/check-user-credits.ts- 检查用户积分状态 -scripts/quick-check-credits.sh- 快速检查入口
建议改进
-
改进幂等性检查:
typescript // 在手动补发时也记录 paymentId,避免与 webhook 重复 await addCredits({ userId: userRecord.id, amount: 2000, type: CREDIT_TRANSACTION_TYPE.PURCHASE_PACKAGE, description: 'Manual credit grant', paymentId: 'manual-补发-原因', // 添加这个字段 expireDays: 30, }); -
添加 webhook 失败告警: - 监控 webhook 失败率 - 自动通知管理员 - 记录详细错误日志
-
实现撤销机制: - 创建
scripts/revoke-transaction.ts通用撤销脚本 - 添加撤销原因枚举 - 在管理后台显示撤销记录 -
改进错误处理:
typescript // 在 webhook handler 中添加类型验证 if (typeof parsed?.id !== 'string' || !parsed.id.startsWith('evt_')) { logger.error('Invalid Creem event ID format', { eventId: parsed?.id }); return NextResponse.json({ error: 'Invalid event ID' }, { status: 400 }); }
相关文件
核心文件
src/app/api/webhooks/creem/route.ts- Webhook 处理器(已修复)src/payment/provider/creem-utils.ts- Creem 支付逻辑src/credits/credits.ts- 积分管理核心逻辑src/db/schema.ts- 数据库表结构定义
脚本文件
scripts/check-user-credits.ts- 检查用户积分详情scripts/quick-check-credits.sh- 快速检查入口scripts/grant-credits-manually.ts- 手动补发积分scripts/revoke-creem-duplicate-credits.ts- 撤销重复积分(推荐使用)- ~~
scripts/remove-manual-credits.ts~~ - 已删除(不推荐删除记录) - ~~
scripts/remove-creem-duplicate-credits.ts~~ - 已删除(不推荐删除记录)
未来预防措施
-
测试 webhook:
bash # 使用测试脚本验证 webhook NODE_OPTIONS='--import tsx' node scripts/test-creem-webhook.ts -
监控积分异常: - 定期检查重复发放 - 监控异常余额变化 - 审计大额积分变动
-
文档化流程: - 记录所有手动操作 - 创建标准处理流程 - 培训客服团队
总结
问题:UUID 类型错误导致 webhook 失败 影响:1 个用户短暂未收到积分 处理:手动补发 → 修复代码 → 重试 webhook → 删除重复积分 教训:应该使用撤销记录而不是删除记录,保持完整审计轨迹 状态:✅ 已解决,系统恢复正常
记录人:Claude Code 记录时间:2026-01-08 最后更新:2026-01-08
本文档为站内渲染。原始文件本地路径:saas/source/knowledge-world/Knowledge-World-项目-Practice-Saas功能文档-creem-webhook-incident--482782.md(仅本地保留,不入库不部署)