下单链路审查 · 2026-10-09 23:25

起因:义父确认「12 笔订单都是测试的」⇒ 顺势审整条下单链路。
方法:读全部 3 个写入口 + 查 DB 约束 + 查 C 端实际传的字段。不假设,逐个验证。

一、链路全貌

环节位置
写 orders 的入口3 处:create_order(L1023) / _create_goods_order(L672) / create_booking(hotel.py:1869)
C 端调用utils/pay.ts 的 createOrderAndPay ⇒ 3 个调用点
幂等两条独立路径(见下)

二、✅ 做对的地方(实测确认)

服务端锁价 ✅

server_locked_price_fen = int(package["price_fen"] or 0)
if body.totalAmount is not None:
    client_claim_fen = int(round(float(body.totalAmount) * 100))
    if client_claim_fen != server_locked_price_fen:
        pass  # 服务端锁价,client_claim 被忽略

🔴 前端传 amountFen=1 篡改 → 实际仍按 packages.price_fen 收费,篡改值被忽略。

且不报错(避免泄露内部价)—— 这个取舍是对的。

事务边界 ✅

商品与价格校验密集 ✅

L809  productId 非法 → 422
L836  产品不存在或已下架 → 404
L897/904/907  拆售价格未配/异常/与套餐售价不一致 → 422
L915  抵扣金额超过上限 → 422
L972  60s 同用户+同产品+同日期 → 409(防弱网双击)
L995  福钥余额不足 → 409 整单回滚

DB 层有唯一约束 ✅

orders_pkey
orders_order_no_key       ← 订单号唯一
uq_orders_idempotency_key ← 🔴 幂等键唯一(并发下真正的防线)

这是关键:代码是「先查后插」,并发时有竞态,

但 DB 唯一约束兜住了 —— 数据不会重复。


三、🔴 查实的缺陷

缺陷 1:C 端从不传幂等键⇒ 幂等能力完全没启用

实测三层都是空的:

层应有实测
C 端 HTTP 头Idempotency-Key🔴 request.ts 里零处提到
C 端 bodyidempotencyKey🔴 三个调用点的 payload 全都没有(product-detail.ts:178/229、item-detail.ts:383)
后端中间件拦 POST + SCOPE_MAP + 带该头因头不存在 ⇒ 中间件直接放行
后端 body 判据body.idempotencyKey因字段不存在 ⇒ idem_key = None,那段检查整个跳过

⚠️ 实际发生的事:

缺陷 2:并发撞唯一约束时会 500,不是幂等命中

# miniapp_orders.py 里 grep UniqueViolationError → 🔴 零处命中
# 只有 services/idempotency.py:109 捕获了

⚠️ 即便将来 C 端开始传 idempotencyKey,两个并发请求同时通过

「先查后插」后,第二个会被 DB 唯一约束挡下⇒ 抛 500 而不是返回"幂等命中"。

_create_goods_order 与 create_order 都没有捕获 UniqueViolationError。

缺陷 3:价格篡改尝试没有被记录

if client_claim_fen != server_locked_price_fen:
    # 篡改:记录但以服务端为准(不报错,避免泄露内部价)
    pass  # 🔴 注释说"记录",实际什么都没做

🔴 注释与代码不符:没有任何日志/告警。

⇒ 有人在试图改价,我们完全不知道。


四、影响评估

缺陷触发条件后果严重度
1 无幂等键弱网/手抖/重复点击重复下单(用户可能被迫付两次)🔴 高
2 并发 500将来传了幂等键 + 并发用户看到 500 而非"已下单"🟡 中(当前不触发)
3 篡改无记录有人改前端价格无法发现改价攻击🟠 中

五、修复建议(按性价比)

立刻可做(不依赖 C 端发版)

① 补「并发撞约束」的兜底(改后端,独立生效)

在两个 INSERT 处捕获 asyncpg.UniqueViolationError,

捕获后重新 SELECT 那条已有订单,按幂等命中的格式返回。

⇒ 即使 C 端还没传幂等键,将来也不会 500。

② 补篡改日志(改后端,独立生效)

把 pass 换成一行日志(打 event 或 logger.warning),带 user_id / product_id / 客户端价 / 服务端价。

⚠️ 只记不发(不改变响应,避免泄露内部价)。

需要 C 端配合(要发版)

③ C 端生成并传 idempotencyKey

在 createOrderAndPay 里生成(推荐:进入结算页时生成一次,重试复用同一个,

而不是每次点击都新建 —— 否则防不住重复点击)。

⚠️ 关键设计点:幂等键必须在「用户意图产生时」生成,

不能在「发请求时」生成 —— 否则两次点击会得到两个不同的键,幂等等于没用。


六、🔴 本轮未覆盖(诚实标注)

未查为什么
支付回调 wxpay_notify.py 的幂等只查了下单端点的幂等
退款链路的幂等同上
create_booking(酒店)的完整校验只确认了事务边界,校验点未逐条审
库存扣减的并发(酒店已修,风物/套餐未查)超卖问题只在酒店域修过
券码生成的并发唯一性redemption_codes 插入在事务内,但唯一约束未查

⇒ 这份报告可用于「下单链路幂等」维度的决策,不能当作「下单链路已审查」。