feat: 实现受控真实下单安全边界 (#99)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""一条本地采购任务的安全演练流程。
|
||||
"""一条本地采购任务的安全执行流程。
|
||||
|
||||
本模块固定为 ``dry_run``。它会核对商品、动态规格、数量和价格,并停在最终
|
||||
提交订单之前;代码中没有提交订单或付款入口。
|
||||
``dry_run`` 始终停在提交前;``live`` 只在独立 Adapter 就绪时允许一次提交,
|
||||
并且必须先持久化不可逆标记。任何路径都不包含付款。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -12,6 +12,7 @@ from typing import Callable, Mapping
|
||||
|
||||
from .admin_gateway import AdminGateway, AdminGatewayError, ClientInfo
|
||||
from .pdd_purchase_adapter import (
|
||||
PddLivePurchaseAdapter,
|
||||
PddPurchaseAdapter,
|
||||
PddPurchaseError,
|
||||
PurchasePageState,
|
||||
@@ -51,6 +52,9 @@ class PurchaseTaskOutcome:
|
||||
PurchaseAdapterFactory = Callable[
|
||||
[str, Callable[[], bool]], PddPurchaseAdapter
|
||||
]
|
||||
LivePurchaseAdapterFactory = Callable[
|
||||
[str, Callable[[], bool]], PddLivePurchaseAdapter
|
||||
]
|
||||
|
||||
|
||||
class PurchaseTaskService:
|
||||
@@ -87,7 +91,7 @@ class PurchaseTaskService:
|
||||
return self.execute_selected(task.remote_task_id)
|
||||
|
||||
def execute_selected(self, remote_task_id: str) -> PurchaseTaskOutcome:
|
||||
"""对指定的本地采购任务执行一次演练。"""
|
||||
"""按任务不可变执行模式安全执行一次采购。"""
|
||||
|
||||
if not self._device_address:
|
||||
raise ValueError("请先在设置页选择并保存 Android 设备")
|
||||
@@ -104,6 +108,15 @@ class PurchaseTaskService:
|
||||
try:
|
||||
target = self._target_from_task(started.task)
|
||||
adapter = self._factory(self._device_address, self._cancelled)
|
||||
execution_mode = started.task.execution_mode
|
||||
if execution_mode == "live" and not isinstance(
|
||||
adapter, PddLivePurchaseAdapter
|
||||
):
|
||||
raise PddPurchaseError(
|
||||
"PURCHASE_LIVE_ADAPTER_UNAVAILABLE",
|
||||
"真实采购执行器未就绪,未进入商品页面",
|
||||
step="purchase_prepare",
|
||||
)
|
||||
|
||||
step = "purchase_open_goods"
|
||||
self._enter_step(remote_task_id, started.attempt_id, step)
|
||||
@@ -150,6 +163,15 @@ class PurchaseTaskService:
|
||||
)
|
||||
self._validate_checkout_values(state, target)
|
||||
|
||||
if execution_mode == "live":
|
||||
assert isinstance(adapter, PddLivePurchaseAdapter)
|
||||
return self._submit_live_once(
|
||||
remote_task_id,
|
||||
started.attempt_id,
|
||||
target,
|
||||
adapter,
|
||||
)
|
||||
|
||||
step = "purchase_dry_run_stopped"
|
||||
self._enter_step(remote_task_id, started.attempt_id, step)
|
||||
adapter.stop_before_submit()
|
||||
@@ -182,6 +204,64 @@ class PurchaseTaskService:
|
||||
adapter.close()
|
||||
return self._submit(event)
|
||||
|
||||
def _submit_live_once(
|
||||
self,
|
||||
remote_task_id: str,
|
||||
attempt_id: str,
|
||||
target: PurchaseTarget,
|
||||
adapter: PddLivePurchaseAdapter,
|
||||
) -> PurchaseTaskOutcome:
|
||||
"""最终复核、先落不可逆标记、单击一次,然后只转核单。"""
|
||||
|
||||
step = "purchase_live_final_check"
|
||||
self._enter_step(remote_task_id, attempt_id, step)
|
||||
state = adapter.read_state()
|
||||
self._validate_live_confirmation(state, target)
|
||||
if self._cancelled():
|
||||
raise PddPurchaseError(
|
||||
"PURCHASE_CANCELLED",
|
||||
"用户已在不可逆标记写入前停止真实采购",
|
||||
step=step,
|
||||
)
|
||||
|
||||
irreversible_at = self._repository.mark_purchase_irreversible(
|
||||
remote_task_id, attempt_id
|
||||
)
|
||||
submitted_at = None
|
||||
message = "订单提交点击已执行,结果待只读核对;绝不重新下单"
|
||||
try:
|
||||
adapter.submit_order_once()
|
||||
submitted_at = utc_now_iso()
|
||||
except Exception as exc:
|
||||
message = (
|
||||
"订单提交点击结果不确定,已转只读核对;"
|
||||
f"绝不重新下单:{str(exc) or type(exc).__name__}"
|
||||
)
|
||||
|
||||
try:
|
||||
self._repository.move_purchase_to_reconcile(
|
||||
remote_task_id,
|
||||
attempt_id,
|
||||
order_submitted_at=submitted_at,
|
||||
message=message,
|
||||
)
|
||||
except Exception as exc:
|
||||
# 不可逆标记已经提交成功。这里绝不能保存成普通失败或再次执行;
|
||||
# 重启恢复会根据 irreversible_action_at 强制进入只读核单。
|
||||
return PurchaseTaskOutcome(
|
||||
"manual_review",
|
||||
(
|
||||
f"任务 {remote_task_id} 已在 {irreversible_at} 进入不可逆阶段,"
|
||||
f"但核单状态保存失败:{exc};请关闭自动获取并重启,绝不重下"
|
||||
),
|
||||
remote_task_id,
|
||||
)
|
||||
return PurchaseTaskOutcome(
|
||||
"manual_review",
|
||||
f"任务 {remote_task_id} {message}",
|
||||
remote_task_id,
|
||||
)
|
||||
|
||||
def _enter_step(
|
||||
self, remote_task_id: str, attempt_id: str, step: str
|
||||
) -> None:
|
||||
@@ -328,6 +408,35 @@ class PurchaseTaskService:
|
||||
step="purchase_verify_quantity_price",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _validate_live_confirmation(
|
||||
state: PurchasePageState, target: PurchaseTarget
|
||||
) -> None:
|
||||
"""在不可逆标记前核对最新确认页和唯一提交目标。"""
|
||||
|
||||
if state.page_kind != "order_confirmation":
|
||||
raise PddPurchaseError(
|
||||
"PURCHASE_CONFIRMATION_LOST",
|
||||
"最终提交前确认页已变化,禁止真实下单",
|
||||
step="purchase_live_final_check",
|
||||
)
|
||||
PurchaseTaskService._validate_checkout_values(state, target)
|
||||
if not state.in_stock:
|
||||
raise PddPurchaseError(
|
||||
"PURCHASE_OUT_OF_STOCK",
|
||||
"当前规格库存不足或已售罄,禁止真实下单",
|
||||
step="purchase_live_final_check",
|
||||
)
|
||||
if state.submit_candidate_count != 1:
|
||||
raise PddPurchaseError(
|
||||
"PURCHASE_SUBMIT_TARGET_AMBIGUOUS",
|
||||
"最终提交按钮不是唯一可靠目标,禁止真实下单",
|
||||
step="purchase_live_final_check",
|
||||
diagnostics={
|
||||
"candidate_count": state.submit_candidate_count
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _validate_common_state(state: PurchasePageState) -> None:
|
||||
if state.page_kind == "captcha":
|
||||
|
||||
Reference in New Issue
Block a user