feat: 实现受控真实下单安全边界 (#99)

This commit is contained in:
chengma
2026-08-10 16:35:38 +08:00
parent 7106f6b024
commit e960cab1fb
30 changed files with 1562 additions and 66 deletions
+117 -2
View File
@@ -70,14 +70,16 @@ class TaskRepository:
with connection:
cursor = connection.execute(
"INSERT INTO pdd_tasks ("
" remote_task_id, task_type, goods_id, goods_url, title,"
" remote_task_id, task_type, execution_mode, goods_id,"
" goods_url, title,"
" target_color, target_size, price_cent, quantity, status,"
" priority, version, admin_payload, received_at, created_at,"
" updated_at"
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
task.remote_task_id.strip(),
task.task_type.value,
task.execution_mode,
task.goods_id,
task.goods_url.strip(),
task.title,
@@ -298,6 +300,22 @@ class TaskRepository:
connection.close()
return self._to_detail(row) if row is not None else None
def unresolved_irreversible_purchase(self) -> Optional[TaskDetail]:
"""查找仍在运行且已有不可逆标记的采购,防止继续控制设备。"""
connection = open_database(self._db_path)
try:
row = connection.execute(
"SELECT t.* FROM pdd_tasks t JOIN task_runs r ON r.task_id = t.id"
" WHERE t.task_type = 'purchase' AND t.status = 'running'"
" AND r.run_status = 'running'"
" AND r.irreversible_action_at IS NOT NULL"
" ORDER BY r.attempt_no DESC LIMIT 1"
).fetchone()
finally:
connection.close()
return self._to_detail(row) if row is not None else None
def latest_task_run(
self, remote_task_id: str
) -> Optional[TaskRunRecord]:
@@ -583,6 +601,102 @@ class TaskRepository:
finally:
connection.close()
def mark_purchase_irreversible(
self, remote_task_id: str, attempt_id: str
) -> str:
"""事务写入不可逆时间;成功返回后才允许点击提交订单。"""
now = utc_now_iso()
step = "purchase_irreversible_step_entered"
connection = open_database(self._db_path)
try:
with connection:
task = connection.execute(
"SELECT id, task_type, status, execution_mode FROM pdd_tasks"
" WHERE remote_task_id = ?",
(remote_task_id,),
).fetchone()
if task is None:
raise ValueError(f"任务 {remote_task_id} 不存在")
if task["task_type"] != TaskType.PURCHASE.value:
raise ValueError("当前任务不是采购任务")
if task["execution_mode"] != "live":
raise ValueError("演练任务不能进入不可逆阶段")
if task["status"] != TaskStatus.RUNNING.value:
raise ValueError("采购任务当前不在执行中")
cursor = connection.execute(
"UPDATE task_runs SET irreversible_action_at = ?,"
" current_step = ?, updated_at = ?"
" WHERE task_id = ? AND attempt_id = ?"
" AND run_status = 'running'"
" AND irreversible_action_at IS NULL",
(now, step, now, task["id"], attempt_id),
)
if cursor.rowcount != 1:
raise ValueError("不可逆标记写入失败或已经存在")
connection.execute(
"UPDATE pdd_tasks SET current_step = ?, updated_at = ?"
" WHERE id = ?",
(step, now, task["id"]),
)
return now
finally:
connection.close()
def move_purchase_to_reconcile(
self,
remote_task_id: str,
attempt_id: str,
*,
order_submitted_at: Optional[str] = None,
message: str = "订单提交结果待核对,绝不重新下单",
) -> None:
"""不可逆点击后转入只读核单状态,不创建可重试下单路径。"""
now = utc_now_iso()
connection = open_database(self._db_path)
try:
with connection:
task = connection.execute(
"SELECT id, task_type, status FROM pdd_tasks"
" WHERE remote_task_id = ?",
(remote_task_id,),
).fetchone()
if task is None:
raise ValueError(f"任务 {remote_task_id} 不存在")
if task["task_type"] != TaskType.PURCHASE.value:
raise ValueError("当前任务不是采购任务")
cursor = connection.execute(
"UPDATE task_runs SET run_status = 'manual_review',"
" current_step = 'reconcile_purchase',"
" order_submitted_at = COALESCE(order_submitted_at, ?),"
" error_code = 'PURCHASE_OUTCOME_UNKNOWN',"
" error_message = ?, finished_at = ?, updated_at = ?"
" WHERE task_id = ? AND attempt_id = ?"
" AND run_status = 'running'"
" AND irreversible_action_at IS NOT NULL",
(
order_submitted_at,
message,
now,
now,
task["id"],
attempt_id,
),
)
if cursor.rowcount != 1:
raise ValueError("不可逆采购执行记录不存在或已经结束")
connection.execute(
"UPDATE pdd_tasks SET status = 'manual_review',"
" current_step = 'reconcile_purchase',"
" last_error_code = 'PURCHASE_OUTCOME_UNKNOWN',"
" last_error_message = ?, finished_at = ?, updated_at = ?"
" WHERE id = ?",
(message, now, now, task["id"]),
)
finally:
connection.close()
def save_purchase_reconciliation(
self,
remote_task_id: str,
@@ -1369,6 +1483,7 @@ class TaskRepository:
id=row["id"],
remote_task_id=row["remote_task_id"],
task_type=TaskType(row["task_type"]),
execution_mode=row["execution_mode"],
goods_id=row["goods_id"],
goods_url=row["goods_url"],
title=row["title"],