项目概述
本项目实现幂等的支付回调处理器:在解析前捕获原始请求体,对原始字节验签,以提供商事件 id 作为幂等键,并只在新事件时于事务中更新状态。回调是一个声称钱已到账的不可信请求,且必然会被重复投递。
架构与关键决策
端点先以字节读取原始体并对其验签,验签通过后才解析,避免信任未认证的任何字段。事件 id 作为唯一幂等键:已存在即返回成功而不改状态,使提供商至少一次的投递成为无害。状态变更与幂等键插入在同一事务完成,崩溃不会留下已标记而无效果或有效果而未标记。排序守卫比较事件序列与已应用的最后一个,忽略更旧的事件,因为乱序投递不能让状态倒退。
实施与测试
同一签名事件发送两次断言只发生一次状态变更;篡改请求体断言 400;发送更旧事件断言状态不变;并发投递同一事件断言唯一约束使恰好一次生效;断言有效签名但未知事件类型被确认并忽略。
限制与后续
- 仅覆盖单一提供商的事件格式。
- 无与提供商 API 的对账任务,漏发依赖提供商重试。
- 排序仅以防序列比较,未处理真正并发事件。
代码示例
pnpm init
pnpm add express pg
pnpm add -D typescript tsx vitest
payment-webhooks/
├── src/verify.ts
├── src/handle.ts
├── src/idempotency.ts
├── src/order-state.ts
├── tests/webhook.test.ts
└── package.json
app.post('/webhook', express.raw({ type: 'application/json' }), handler);
const expected = hmac(secret, ts + '.' + body);
if (!timingSafeEqual(expected, provided)) return res.status(400).end();
const fresh = await db.insertIgnore('processed_events', { id: event.id });
if (!fresh) return res.status(200).end();
await db.tx(async (t) => { await t.markProcessed(event.id); await t.updateOrder(orderId, nextState); });
if (event.version <= order.version) return;
pnpm vitest run