起因:义父确认「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 收费,篡改值被忽略。
且不报错(避免泄露内部价)—— 这个取舍是对的。
create_order L962:async with conn.transaction():_create_goods_order L646:同样有L809 productId 非法 → 422
L836 产品不存在或已下架 → 404
L897/904/907 拆售价格未配/异常/与套餐售价不一致 → 422
L915 抵扣金额超过上限 → 422
L972 60s 同用户+同产品+同日期 → 409(防弱网双击)
L995 福钥余额不足 → 409 整单回滚
orders_pkey
orders_order_no_key ← 订单号唯一
uq_orders_idempotency_key ← 🔴 幂等键唯一(并发下真正的防线)
这是关键:代码是「先查后插」,并发时有竞态,
但 DB 唯一约束兜住了 —— 数据不会重复。
实测三层都是空的:
| 层 | 应有 | 实测 |
|---|---|---|
| C 端 HTTP 头 | Idempotency-Key | 🔴 request.ts 里零处提到 |
| C 端 body | idempotencyKey | 🔴 三个调用点的 payload 全都没有(product-detail.ts:178/229、item-detail.ts:383) |
| 后端中间件 | 拦 POST + SCOPE_MAP + 带该头 | 因头不存在 ⇒ 中间件直接放行 |
| 后端 body 判据 | body.idempotencyKey | 因字段不存在 ⇒ idem_key = None,那段检查整个跳过 |
⚠️ 实际发生的事:
idempotencyKey 也不会兜(因为压根没传)# miniapp_orders.py 里 grep UniqueViolationError → 🔴 零处命中
# 只有 services/idempotency.py:109 捕获了
⚠️ 即便将来 C 端开始传 idempotencyKey,两个并发请求同时通过
「先查后插」后,第二个会被 DB 唯一约束挡下⇒ 抛 500 而不是返回"幂等命中"。
_create_goods_order 与 create_order 都没有捕获 UniqueViolationError。
if client_claim_fen != server_locked_price_fen:
# 篡改:记录但以服务端为准(不报错,避免泄露内部价)
pass # 🔴 注释说"记录",实际什么都没做
🔴 注释与代码不符:没有任何日志/告警。
⇒ 有人在试图改价,我们完全不知道。
| 缺陷 | 触发条件 | 后果 | 严重度 |
|---|---|---|---|
| 1 无幂等键 | 弱网/手抖/重复点击 | 重复下单(用户可能被迫付两次) | 🔴 高 |
| 2 并发 500 | 将来传了幂等键 + 并发 | 用户看到 500 而非"已下单" | 🟡 中(当前不触发) |
| 3 篡改无记录 | 有人改前端价格 | 无法发现改价攻击 | 🟠 中 |
① 补「并发撞约束」的兜底(改后端,独立生效)
在两个 INSERT 处捕获 asyncpg.UniqueViolationError,
捕获后重新 SELECT 那条已有订单,按幂等命中的格式返回。
⇒ 即使 C 端还没传幂等键,将来也不会 500。
② 补篡改日志(改后端,独立生效)
把 pass 换成一行日志(打 event 或 logger.warning),带 user_id / product_id / 客户端价 / 服务端价。
⚠️ 只记不发(不改变响应,避免泄露内部价)。
③ C 端生成并传 idempotencyKey
在 createOrderAndPay 里生成(推荐:进入结算页时生成一次,重试复用同一个,
而不是每次点击都新建 —— 否则防不住重复点击)。
⚠️ 关键设计点:幂等键必须在「用户意图产生时」生成,
不能在「发请求时」生成 —— 否则两次点击会得到两个不同的键,幂等等于没用。
| 未查 | 为什么 |
|---|---|
支付回调 wxpay_notify.py 的幂等 | 只查了下单端点的幂等 |
| 退款链路的幂等 | 同上 |
create_booking(酒店)的完整校验 | 只确认了事务边界,校验点未逐条审 |
| 库存扣减的并发(酒店已修,风物/套餐未查) | 超卖问题只在酒店域修过 |
| 券码生成的并发唯一性 | redemption_codes 插入在事务内,但唯一约束未查 |
⇒ 这份报告可用于「下单链路幂等」维度的决策,不能当作「下单链路已审查」。