知识库首页 知识库-世界 creem-webhook-incident-2026-01-07.md

creem webhook incident 2026 01 07

本地来源:Knowledge/World/项目/文档/BUG_SOLUTIONS/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 积分

问题反思

处理不当的地方

  1. 直接删除记录:违反了审计原则,应该使用撤销记录而不是删除原记录
  2. 缺少审计轨迹:删除后无法追溯为什么积分变化
  3. 应对措施仓促:没有时间使用更优雅的撤销机制

更好的处理方式

应该使用撤销记录

// 创建负数的撤销记录,而不是删除原记录
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(),
});

优点: - ✅ 完整的审计轨迹 - ✅ 可以追溯所有变更 - ✅ 符合会计原则(只增不删) - ✅ 方便客服查询问题

技术改进

已实施

  1. 修复 UUID 类型错误:正确分离 UUID 和 event ID
  2. 创建诊断脚本: - scripts/check-user-credits.ts - 检查用户积分状态 - scripts/quick-check-credits.sh - 快速检查入口

建议改进

  1. 改进幂等性检查typescript // 在手动补发时也记录 paymentId,避免与 webhook 重复 await addCredits({ userId: userRecord.id, amount: 2000, type: CREDIT_TRANSACTION_TYPE.PURCHASE_PACKAGE, description: 'Manual credit grant', paymentId: 'manual-补发-原因', // 添加这个字段 expireDays: 30, });

  2. 添加 webhook 失败告警: - 监控 webhook 失败率 - 自动通知管理员 - 记录详细错误日志

  3. 实现撤销机制: - 创建 scripts/revoke-transaction.ts 通用撤销脚本 - 添加撤销原因枚举 - 在管理后台显示撤销记录

  4. 改进错误处理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~~ - 已删除(不推荐删除记录)

未来预防措施

  1. 测试 webhookbash # 使用测试脚本验证 webhook NODE_OPTIONS='--import tsx' node scripts/test-creem-webhook.ts

  2. 监控积分异常: - 定期检查重复发放 - 监控异常余额变化 - 审计大额积分变动

  3. 文档化流程: - 记录所有手动操作 - 创建标准处理流程 - 培训客服团队

总结

问题:UUID 类型错误导致 webhook 失败 影响:1 个用户短暂未收到积分 处理:手动补发 → 修复代码 → 重试 webhook → 删除重复积分 教训:应该使用撤销记录而不是删除记录,保持完整审计轨迹 状态:✅ 已解决,系统恢复正常


记录人:Claude Code 记录时间:2026-01-08 最后更新:2026-01-08

本文档为站内渲染。原始文件本地路径:saas/source/knowledge-world/Knowledge-World-项目-文档-BUG_SOLUTIONS-creem-webhook-incident-2-b06312.md(仅本地保留,不入库不部署)