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
+94 -3
View File
@@ -1,7 +1,7 @@
"""uiautomator2 采购演练 Adapter。
"""uiautomator2 采购 Adapter。
本模块只到 PDD 最终提交订单按钮前。代码中没有点击提交订单
或付款的方法。
演练 factory 返回没有提交方法的窄接口;live factory 单独返回只允许一次提交的
接口。两条路径都不提供付款、取消订单或绕过安全校验的方法。
"""
from __future__ import annotations
@@ -19,6 +19,7 @@ from .pdd_device_service import (
PddDeviceService,
)
from .pdd_purchase_adapter import (
PddLivePurchaseAdapter,
PddPurchaseAdapter,
PddPurchaseError,
PurchasePageState,
@@ -36,6 +37,7 @@ _CAPTCHA_MARKERS = ("请完成验证", "拖动滑块", "安全验证", "点击
_RISK_MARKERS = ("操作频繁", "异常请求", "风险提示", "账号异常")
_PAYMENT_MARKERS = ("输入支付密码", "立即支付", "支付成功", "支付失败")
_FINAL_SUBMIT_MARKERS = ("提交订单", "现在买,仅", "确认购买")
_OUT_OF_STOCK_MARKERS = ("已售罄", "暂时缺货", "库存不足", "该商品已售罄")
_SUPPORTED_OPTION_KEYS = frozenset({"color", "size"})
@@ -200,6 +202,33 @@ def _price_cent(root: ET.Element) -> int:
return max(parsed_nodes)[2]
def _final_submit_targets(root: ET.Element) -> list[Bounds]:
"""返回底部可见、启用且文字明确的唯一提交按钮坐标。"""
screen_bottom = 0
for node in root.iter("node"):
bounds = _parse_bounds(node.get("bounds", ""))
if bounds is not None:
screen_bottom = max(screen_bottom, bounds[3])
if screen_bottom <= 0:
return []
targets = set()
for node in root.iter("node"):
label = _label(node)
bounds = _parse_bounds(node.get("bounds", ""))
if not label or bounds is None:
continue
if not any(marker in label for marker in _FINAL_SUBMIT_MARKERS):
continue
if node.get("visible-to-user") != "true" or node.get("enabled") != "true":
continue
if (bounds[1] + bounds[3]) // 2 < screen_bottom * 0.6:
continue
targets.add(bounds)
return sorted(targets)
class U2PddPurchaseAdapter(PddPurchaseAdapter):
"""PDD 真机采购演练会话,不提供真实下单能力。"""
@@ -229,6 +258,7 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
self._device = None
self._goods_id = ""
self._requested_options: dict[str, str] = {}
self._submit_attempted = False
def open_goods(self, goods_url: str) -> None:
self._goods_id = _goods_id_from_url(goods_url)
@@ -268,6 +298,8 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
xml_data = self._dump_hierarchy()
root = _parse_xml(xml_data)
kind = _page_kind(root, str(current.get("package") or ""))
labels = _labels(root)
submit_targets = _final_submit_targets(root)
selected = {
key: value
for key, value in self._requested_options.items()
@@ -280,6 +312,11 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
quantity=_quantity(root),
price_cent=_price_cent(root),
candidate_count=1 if kind != "unknown" else 0,
in_stock=not any(
marker in " ".join(labels)
for marker in _OUT_OF_STOCK_MARKERS
),
submit_candidate_count=len(submit_targets),
)
except PddPurchaseError:
raise
@@ -512,3 +549,57 @@ def create_u2_purchase_adapter(
device_service=_PURCHASE_DEVICE_SERVICE,
cancelled=cancelled,
)
class U2PddLivePurchaseAdapter(U2PddPurchaseAdapter, PddLivePurchaseAdapter):
"""只允许一次最终提交点击的真机 Adapter,不包含付款路径。"""
def submit_order_once(self) -> None:
if self._submit_attempted:
raise PddPurchaseError(
"PURCHASE_SUBMIT_ALREADY_ATTEMPTED",
"本次采购已经尝试提交,禁止再次点击",
step="purchase_submit_once",
)
self._check_cancelled("purchase_submit_once")
device = self._require_device()
try:
current = device.app_current()
root = _parse_xml(self._dump_hierarchy())
kind = _page_kind(root, str(current.get("package") or ""))
if kind in {"captcha", "login_required", "risk_control", "payment"}:
self._raise_special_page(kind)
if kind != "order_confirmation":
raise PddPurchaseError(
"PURCHASE_CONFIRMATION_LOST",
"最终提交前页面已经变化,禁止提交订单",
step="purchase_submit_once",
)
targets = _final_submit_targets(root)
if len(targets) != 1:
raise PddPurchaseError(
"PURCHASE_SUBMIT_TARGET_AMBIGUOUS",
"最终提交按钮不是唯一可靠目标,禁止提交订单",
step="purchase_submit_once",
diagnostics={"candidate_count": len(targets)},
)
left, top, right, bottom = targets[0]
self._submit_attempted = True
device.click((left + right) // 2, (top + bottom) // 2)
except PddPurchaseError:
raise
except Exception as exc:
self._submit_attempted = True
self._raise_device_or_page_error(exc, "purchase_submit_once")
def create_u2_live_purchase_adapter(
device_address: str, cancelled: Callable[[], bool]
) -> PddLivePurchaseAdapter:
"""为已通过本地绑定授权的任务创建一次 live 会话。"""
return U2PddLivePurchaseAdapter(
device_address,
device_service=_PURCHASE_DEVICE_SERVICE,
cancelled=cancelled,
)