fix: 兼容多微信选择器核单返回 (#261)
This commit is contained in:
@@ -53,6 +53,12 @@ _NON_UNPAID_MARKERS = ("已付款", "交易成功", "交易完成", "已取消",
|
|||||||
_SAFE_NAVIGATION_LABELS = ("个人中心", "我的订单", "待付款")
|
_SAFE_NAVIGATION_LABELS = ("个人中心", "我的订单", "待付款")
|
||||||
_ORDER_DETAIL_PAY_LABELS = ("去支付",)
|
_ORDER_DETAIL_PAY_LABELS = ("去支付",)
|
||||||
_ORDER_DETAIL_CANCEL_LABELS = ("取消订单",)
|
_ORDER_DETAIL_CANCEL_LABELS = ("取消订单",)
|
||||||
|
_ANDROID_CHOOSER_PACKAGE = "android"
|
||||||
|
_ANDROID_CHOOSER_TITLES = ("选择要使用的应用",)
|
||||||
|
_ANDROID_CHOOSER_ACTIVITIES = (
|
||||||
|
"com.android.internal.app.ChooserActivity",
|
||||||
|
"com.android.internal.app.ResolverActivity",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PddPurchaseReconcileError(RuntimeError):
|
class PddPurchaseReconcileError(RuntimeError):
|
||||||
@@ -138,6 +144,26 @@ def _is_single_order_detail(root: ET.Element) -> bool:
|
|||||||
return pay_count == 1 and cancel_count <= 1 and len(order_numbers) <= 1
|
return pay_count == 1 and cancel_count <= 1 and len(order_numbers) <= 1
|
||||||
|
|
||||||
|
|
||||||
|
def _is_android_wechat_chooser(root: ET.Element) -> bool:
|
||||||
|
"""严格识别覆盖在 PDD 上方的 Android 微信应用选择器。"""
|
||||||
|
|
||||||
|
if _foreground_package_from_tree(root) != _ANDROID_CHOOSER_PACKAGE:
|
||||||
|
return False
|
||||||
|
labels = tuple(_label(node) for node in root.iter("node"))
|
||||||
|
has_title = any(label in _ANDROID_CHOOSER_TITLES for label in labels)
|
||||||
|
has_wechat_candidate = any(
|
||||||
|
label == "微信" or label.startswith("微信分身") for label in labels
|
||||||
|
)
|
||||||
|
return has_title and has_wechat_candidate
|
||||||
|
|
||||||
|
|
||||||
|
def _is_known_android_chooser_activity(activity: str) -> bool:
|
||||||
|
"""前台 Activity 只接受 Android 标准选择器,避免误退未知系统页。"""
|
||||||
|
|
||||||
|
normalized = str(activity or "").strip()
|
||||||
|
return not normalized or normalized in _ANDROID_CHOOSER_ACTIVITIES
|
||||||
|
|
||||||
|
|
||||||
def _parse_bounds(value: str) -> Optional[Bounds]:
|
def _parse_bounds(value: str) -> Optional[Bounds]:
|
||||||
match = _BOUNDS_PATTERN.fullmatch(str(value or "").strip())
|
match = _BOUNDS_PATTERN.fullmatch(str(value or "").strip())
|
||||||
if match is None:
|
if match is None:
|
||||||
@@ -465,6 +491,7 @@ class U2PddPurchaseReconcileAdapter(PddPurchaseReconcileAdapter):
|
|||||||
checked_foreground = False
|
checked_foreground = False
|
||||||
started_pdd = False
|
started_pdd = False
|
||||||
pressed_back = False
|
pressed_back = False
|
||||||
|
pressed_chooser_back = False
|
||||||
clicked_navigation: set[str] = set()
|
clicked_navigation: set[str] = set()
|
||||||
last_reason = "等待 PDD 待付款页稳定"
|
last_reason = "等待 PDD 待付款页稳定"
|
||||||
while self._monotonic() <= deadline:
|
while self._monotonic() <= deadline:
|
||||||
@@ -474,6 +501,29 @@ class U2PddPurchaseReconcileAdapter(PddPurchaseReconcileAdapter):
|
|||||||
last_reason = "读取手机控件树超时"
|
last_reason = "读取手机控件树超时"
|
||||||
break
|
break
|
||||||
if not _package_hint_from_tree(root):
|
if not _package_hint_from_tree(root):
|
||||||
|
if _is_android_wechat_chooser(root):
|
||||||
|
if pressed_chooser_back:
|
||||||
|
raise PddPurchaseReconcileError(
|
||||||
|
"RECONCILE_ANDROID_CHOOSER_STUCK",
|
||||||
|
"Android 微信应用选择器在一次安全返回后仍未关闭,请人工返回 PDD 核单",
|
||||||
|
)
|
||||||
|
current = device.app_current()
|
||||||
|
chooser_activity = str(current.get("activity") or "")
|
||||||
|
if not _is_known_android_chooser_activity(
|
||||||
|
chooser_activity
|
||||||
|
):
|
||||||
|
raise PddPurchaseReconcileError(
|
||||||
|
"RECONCILE_ANDROID_CHOOSER_UNRECOGNIZED",
|
||||||
|
"检测到疑似微信应用选择页,但前台 Activity 不在安全白名单,请人工处理",
|
||||||
|
)
|
||||||
|
if self._monotonic() > deadline:
|
||||||
|
last_reason = "确认 Android 微信应用选择器超时"
|
||||||
|
break
|
||||||
|
device.press("back")
|
||||||
|
pressed_chooser_back = True
|
||||||
|
last_reason = "已关闭 Android 微信应用选择器,等待 PDD 待付款详情加载"
|
||||||
|
self._settle()
|
||||||
|
continue
|
||||||
if not checked_foreground:
|
if not checked_foreground:
|
||||||
current_package = _foreground_package_from_tree(root)
|
current_package = _foreground_package_from_tree(root)
|
||||||
if not current_package:
|
if not current_package:
|
||||||
|
|||||||
@@ -119,10 +119,26 @@ def xml_package_page(package: str, text: str = "") -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def xml_android_wechat_chooser() -> str:
|
||||||
|
return (
|
||||||
|
'<hierarchy>'
|
||||||
|
'<node package="android" text="选择要使用的应用" />'
|
||||||
|
'<node package="android" text="微信" />'
|
||||||
|
'<node package="android" text="微信分身" />'
|
||||||
|
'</hierarchy>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TransitionDevice(DetailDevice):
|
class TransitionDevice(DetailDevice):
|
||||||
def __init__(self, pages, current_package="com.tencent.mm"):
|
def __init__(
|
||||||
|
self,
|
||||||
|
pages,
|
||||||
|
current_package="com.tencent.mm",
|
||||||
|
current_activity="",
|
||||||
|
):
|
||||||
super().__init__(pages)
|
super().__init__(pages)
|
||||||
self.current_package = current_package
|
self.current_package = current_package
|
||||||
|
self.current_activity = current_activity
|
||||||
self.dump_calls = 0
|
self.dump_calls = 0
|
||||||
self.back_calls = 0
|
self.back_calls = 0
|
||||||
|
|
||||||
@@ -133,7 +149,10 @@ class TransitionDevice(DetailDevice):
|
|||||||
|
|
||||||
def app_current(self):
|
def app_current(self):
|
||||||
self.app_current_calls += 1
|
self.app_current_calls += 1
|
||||||
return {"package": self.current_package}
|
return {
|
||||||
|
"package": self.current_package,
|
||||||
|
"activity": self.current_activity,
|
||||||
|
}
|
||||||
|
|
||||||
def press(self, key):
|
def press(self, key):
|
||||||
if key == "back":
|
if key == "back":
|
||||||
@@ -304,6 +323,105 @@ class PddU2PurchaseReconcileAdapterTest(unittest.TestCase):
|
|||||||
self.assertEqual(device.app_current_calls, 0)
|
self.assertEqual(device.app_current_calls, 0)
|
||||||
self.assertLess(clock.now, 30)
|
self.assertLess(clock.now, 30)
|
||||||
|
|
||||||
|
def test_android_wechat_chooser_is_closed_once_before_reading_detail(self):
|
||||||
|
device = TransitionDevice(
|
||||||
|
[
|
||||||
|
xml_android_wechat_chooser(),
|
||||||
|
xml_package_page("com.xunmeng.pinduoduo", "页面加载中"),
|
||||||
|
xml_detail("待付款"),
|
||||||
|
],
|
||||||
|
current_package="android",
|
||||||
|
current_activity="com.android.internal.app.ChooserActivity",
|
||||||
|
)
|
||||||
|
clock = FakeClock()
|
||||||
|
adapter = U2PddPurchaseReconcileAdapter(
|
||||||
|
"serial",
|
||||||
|
device_service=FakeDeviceService(device),
|
||||||
|
settle_seconds=1,
|
||||||
|
transition_timeout_seconds=30,
|
||||||
|
monotonic=clock.monotonic,
|
||||||
|
sleep=clock.sleep,
|
||||||
|
)
|
||||||
|
|
||||||
|
adapter._open_unpaid_orders(device)
|
||||||
|
|
||||||
|
self.assertEqual(device.back_calls, 1)
|
||||||
|
self.assertEqual(device.app_start_calls, 0)
|
||||||
|
self.assertEqual(device.app_current_calls, 1)
|
||||||
|
|
||||||
|
def test_android_wechat_chooser_stops_when_one_back_does_not_close_it(self):
|
||||||
|
device = TransitionDevice(
|
||||||
|
[xml_android_wechat_chooser(), xml_android_wechat_chooser()],
|
||||||
|
current_package="android",
|
||||||
|
current_activity="com.android.internal.app.ResolverActivity",
|
||||||
|
)
|
||||||
|
clock = FakeClock()
|
||||||
|
adapter = U2PddPurchaseReconcileAdapter(
|
||||||
|
"serial",
|
||||||
|
device_service=FakeDeviceService(device),
|
||||||
|
settle_seconds=1,
|
||||||
|
transition_timeout_seconds=30,
|
||||||
|
monotonic=clock.monotonic,
|
||||||
|
sleep=clock.sleep,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(PddPurchaseReconcileError) as captured:
|
||||||
|
adapter._open_unpaid_orders(device)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
captured.exception.code, "RECONCILE_ANDROID_CHOOSER_STUCK"
|
||||||
|
)
|
||||||
|
self.assertEqual(device.back_calls, 1)
|
||||||
|
self.assertEqual(device.app_start_calls, 0)
|
||||||
|
|
||||||
|
def test_android_wechat_chooser_with_unknown_activity_stops_safely(self):
|
||||||
|
device = TransitionDevice(
|
||||||
|
[xml_android_wechat_chooser()],
|
||||||
|
current_package="android",
|
||||||
|
current_activity="com.example.UnknownSystemActivity",
|
||||||
|
)
|
||||||
|
clock = FakeClock()
|
||||||
|
adapter = U2PddPurchaseReconcileAdapter(
|
||||||
|
"serial",
|
||||||
|
device_service=FakeDeviceService(device),
|
||||||
|
settle_seconds=1,
|
||||||
|
transition_timeout_seconds=30,
|
||||||
|
monotonic=clock.monotonic,
|
||||||
|
sleep=clock.sleep,
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(PddPurchaseReconcileError) as captured:
|
||||||
|
adapter._open_unpaid_orders(device)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
captured.exception.code,
|
||||||
|
"RECONCILE_ANDROID_CHOOSER_UNRECOGNIZED",
|
||||||
|
)
|
||||||
|
self.assertEqual(device.back_calls, 0)
|
||||||
|
self.assertEqual(device.app_start_calls, 0)
|
||||||
|
|
||||||
|
def test_unknown_android_page_is_not_treated_as_wechat_chooser(self):
|
||||||
|
unknown_system_page = xml_package_page("android", "系统设置")
|
||||||
|
device = TransitionDevice(
|
||||||
|
[unknown_system_page, xml_detail("待付款")],
|
||||||
|
current_package="android",
|
||||||
|
current_activity="com.android.settings.Settings",
|
||||||
|
)
|
||||||
|
clock = FakeClock()
|
||||||
|
adapter = U2PddPurchaseReconcileAdapter(
|
||||||
|
"serial",
|
||||||
|
device_service=FakeDeviceService(device),
|
||||||
|
settle_seconds=1,
|
||||||
|
transition_timeout_seconds=30,
|
||||||
|
monotonic=clock.monotonic,
|
||||||
|
sleep=clock.sleep,
|
||||||
|
)
|
||||||
|
|
||||||
|
adapter._open_unpaid_orders(device)
|
||||||
|
|
||||||
|
self.assertEqual(device.back_calls, 0)
|
||||||
|
self.assertEqual(device.app_start_calls, 1)
|
||||||
|
|
||||||
def test_payment_back_is_pressed_at_most_once_while_page_changes(self):
|
def test_payment_back_is_pressed_at_most_once_while_page_changes(self):
|
||||||
payment_page = xml_package_page(
|
payment_page = xml_package_page(
|
||||||
"com.xunmeng.pinduoduo", "立即支付"
|
"com.xunmeng.pinduoduo", "立即支付"
|
||||||
|
|||||||
Reference in New Issue
Block a user