fix(trading): 每单开跑前先清理购物车,杜绝上一单失败残留被新单一起买走

上一单若在提交(submit_order)之前失败(如 enter_checkout 被 session upgrade
拦截、金额守卫拦下),残留商品会留在购物车里;下一单 add_to_cart 把新商品叠
加在旧商品上,结算时会把上一单的一起买走(已实测踩到过)。

在 execute() 加购前先 clear_cart,从空车起步,保证「一单 = 只买本单商品」——
不依赖上一次失败路径是否完整执行清理,进程中途崩溃残留也没人清也能兜住。
清理后 cart_count 仍未清空(含 count API 失败返回 -1)按闸门语义转 needs_human,
绝不带残留往下加购。清理本身是本机侧卫生操作,不走证据与上报。

补 3 个用例:clear 先于 add、清理后非空/未知 → 转 needs_human 且不加购;
并为走 execute 的既有测试补 clear_cart 桩。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 00:33:42 +08:00
co-authored by Claude Opus 5
parent f6c3976c0a
commit df93bc1e63
2 changed files with 159 additions and 3 deletions
+24 -3
View File
@@ -256,11 +256,12 @@ class WorkerRunner:
)
async def execute(self, task: LeaseTask) -> None:
"""站点交互的实际调度:加购 → 校验 → 确认页 → 金额守卫 → 提交 → 付款
"""站点交互的实际调度:清购物车 → 加购 → 校验 → 确认页 → 金额守卫 → 提交 → 付款
每一步的顺序:动作 → 落证据 → 写本地 SQLite → 回报 gateway。
站点交互当前未实现(site_interact 抛 NotImplementedError),第一步就会
_execute_with_renewal 的 except 分支上报 needs_human。
开单前的「清购物车」是本机侧卫生步骤(step 0,见下方代码注释),不走
证据与上报;站点交互失败会转 _execute_with_renewal 的 except 分支上报
needs_human / failed。
"""
await self._db.ensure_started(task.task_id, task.site, task.intent)
@@ -268,6 +269,26 @@ class WorkerRunner:
if task.site != "rakuten":
raise NotImplementedError(f"site={task.site} 暂不在交易服务范围内(仅 rakuten)")
# 步骤 0:开单前清理购物车。上一单若在提交(submit_order)之前失败——比如
# 加购成功但 enter_checkout 被 session upgrade 拦截、或金额守卫拦下——残留的
# 商品不会自己消失,会一直留在购物车里;下一次 add_to_cart 把新商品叠加在旧
# 商品上,结算时会把上一单的一起买走(已实测踩到过)。所以每单开跑前先清空
# 购物车,保证「一单 = 只买本单商品」的不变式——无论上一单是怎么失败的
# (包括进程中途崩溃,残留都没人清),这里都从空车起步。
#
# 这条不在 gateway 上报,也不写步骤证据:它是本机侧的卫生操作,不是订单
# 进度的状态迁移。清理后 count 非 0(含 count API 拿不到结果返回 -1)说明
# 清理没跑干净,**不能**带着残留往下加购——有把上一单买走的真实风险,
# 按闸门语义拦截转 needs_human,宁可卡住等人核对,也不赌「残留不会被买走」。
cleared = await self._site.clear_cart()
if cleared.get("cart_count", -1) != 0:
raise OrderGuardError(
"开单前清理购物车后仍未清空"
f"(cart_count={cleared.get('cart_count')},removed_count="
f"{cleared.get('removed_count')}):残留商品可能随本次下单一起被买走,"
"中止转人工核对"
)
# 步骤 1:加购
await self._run_step(
task, step_no=1, step_name="cart-add",