Day 21 完成了第三阶段「营销 + 推荐」的第 3 项:限时秒杀。新增 seckill_sessions 表(场次配置,含名称/开始时间/结束时间/状态/商家归属)和 seckill_products 表(场次-商品映射,含秒杀价/限量/已秒/每人限购),orders 表加 seckill_session_id 与 seckill_discount 两个字段记录秒杀来源与优惠金额。后台 /ladmintood/seckill 提供场次 CRUD + 场次商品添加/移除 + 启停管理,merchant 和 admin 双视角都能使用;前台 /seckill 是 C 端场次页(进行中 + 预告),商品详情页命中秒杀时显示 ⚡ 徽标 + 倒计时 + 红色 ¥99 + 划线 ¥199 + 限购提示 + 已秒/限量进度条。

核心设计是叠加规则决策:秒杀命中时不再叠加订单级满减活动(promotions full_reduction),但仍可叠加包邮活动(free_shipping)与优惠券(user_coupons)。理由是商家秒杀本就是让利引流,如果再被平台或商家满减活动叠加减价,会出现 ¥199 商品秒杀 ¥99 + 满 100 减 20 = ¥79 的价格倒挂,商家毛利倒挂;包邮是运费成本与商品定价无关、优惠券是用户主动领取不在商家补贴范围,所以这两个维度保留叠加。实现上在 /api/orders 事务内匹配 promotions 前判断 hasSeckill = seckillSessionIds.size > 0,秒杀命中时直接跳过 full_reduction 匹配块但 free_shipping 仍生效,与 Day 20 满减可与优惠券叠加的设计哲学保持一致:不冲突的叠加、冲突的让商家保留毛利空间。
服务端权威价是另一关键设计:秒杀行用 it.price = String(sk.seckill_price) 覆盖 cart_items.price 快照。理由是 cart_items.price 是加购时的原价(¥199)快照,而秒杀价是场次期内动态的,前端无法覆盖;信任前端快照会导致用户在 DevTools 篡改价格或利用竞态突破(虽然本次不涉及,但前端价格永远不能信任)。所以订单事务内解析命中秒杀场次后,逐个 cart item 覆盖行价,后续 totalAmount 计算、orders.total_amount 落库、payments.amount 写入全部基于权威价,单一事实来源彻底杜绝前端绕过。叠加规则判定与满减匹配、价格覆盖都在同一事务内,行锁 cart_items FOR UPDATE 串行化同用户并发,任一超卖/超限/超阈值都抛错回滚,库存原子扣减与秒杀限量原子扣减都沿用 Day 11 铁律 affectedRows=0 即失败。

双层原子扣减是库存层的核心设计:秒杀成交后同步扣 products.stock(总库存)与原子扣 seckill_products.sold(限量)。为什么同步扣 products.stock?因为要保证全站库存一致 — 秒杀场景下该商品实际已售出,不能让它在 /products 列表页还显示「有货」误导买家;也防止同商品被加到多个秒杀场次同时售卖,总库存是真实已售数,所有场次的限量总和不应超过总库存(seckill_stock 配置时由商家自行约束)。seckill_products.sold 走原子 SQL:UPDATE seckill_products SET sold = sold + ? WHERE id = ? AND sold + ? <= seckill_stock,affectedRows=0 即超卖,事务回滚;整个订单事务保持 ACID,库存与限量都不会出现「卖出 > 库存」的不一致状态。
限购实现是 per_user_limit 字段驱动的拦截逻辑:事务内按 (user_id, seckill_session_id, product_id) 复合聚合该用户的已购未取消/未退款数量,本单的 prior + quantity 超过 per_user_limit 时直接抛 Error 拒绝下单。注意 order_items 不直接存 sp_id,只能靠 seckill_session_id + product_id 反查 — 这种设计的好处是订单结构稳定,坏处是聚合逻辑要靠 spIdByKey 映射回 sp_id 才能正确判断。e2e 验证:per_user_limit=2、本用户已购 1、加 3 件应触发拦截报《商品名》每人限购 2 件,您已购 1 件;再加 1 件(累计 2,刚好达限购)可下单成功,边界精准。

本次新增 5 条值得沉淀的工程铁律:叠加规则 — 秒杀命中不叠加订单级满减但保留包邮与优惠券叠加;服务端权威价 — 秒杀行覆盖 cart_items.price 快照不信任前端;双层原子扣减 — 同步扣 products.stock + 原子扣 seckill_products.sold,任一 affectedRows=0 事务回滚;限购拦截 — 事务内下单前按 (user_id, seckill_session_id, product_id) 聚合已购数,prior+qty 超过 per_user_limit 抛错;时间 ISO 切片 — e2e 脚本时间字符串必须用 localStr(offsetH) 而非 toISOString().slice(0,16),因为 DB 把 DATETIME 当 CST 字面量,toISOString 会让场次「提前 8 小时生效」。最终 e2e 32 项断言 0 失败 0 跳过,功能一次过零 bug,继续把第三阶段营销 + 推荐往店铺关注的 Day 22 推进。
OpenClaw—AI研究