From 76a522f13f4fb53f65d0684be16fe738bb9c30f1 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Tue, 4 Aug 2026 15:23:02 +0800 Subject: [PATCH] fix(client): bind T-103 observed price prefix --- .../device/sku_evidence_sanitizer.py | 12 ++-- .../device/test_sku_evidence_sanitizer.py | 57 ++++++++++++------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/client/src/cmbuyer_client/device/sku_evidence_sanitizer.py b/client/src/cmbuyer_client/device/sku_evidence_sanitizer.py index d6b4d3b..dd45a6c 100644 --- a/client/src/cmbuyer_client/device/sku_evidence_sanitizer.py +++ b/client/src/cmbuyer_client/device/sku_evidence_sanitizer.py @@ -22,7 +22,7 @@ from ..pdd.product_url import ProductUrl, ProductUrlError, parse_product_url from ..pdd.sku_panel_state import HUMAN_DECLARED_STATES -SANITIZER_VERSION = "t103-privacy-v4" +SANITIZER_VERSION = "t103-privacy-v5" EXPECTED_GOODS_ID = "937122477375" EXPECTED_PDD_VERSION = "8.17.0" EXPECTED_DEVICE_MODEL = "PKG110" @@ -56,9 +56,11 @@ _PRICE_PROJECTION_ATTRIBUTES = ( ) # 仅接受普通 ASCII 空格,且每个可分隔位置最多一个;禁止换行、折扣、支付/提交文案和 # 任何其它字符。前缀捕获组用于区分当前价与至多一个划线/原价候选。 -_CROSSING_PRICE_TEXT_RE = re.compile(r" {0,1}(?:(快卖光) {0,1})?[¥¥] {0,1}[1-9]\d*\.\d{2} {0,1}\Z") -_CROSSING_PRICE_PREFIX_RE = re.compile(r" {0,1}(?:快卖光 {0,1})?[¥¥] {0,1}[1-9]\d*\.\d{2} {0,1}") -_CROSSING_PRICE_ALLOWED_CHARACTERS = frozenset(" 快卖光¥¥0123456789.") +# T-103 人工在 live 规格面板确认当前价槽的完整非敏感前缀仅为“快卖完”;不得兼容 +# 未取证的“快卖光”或其它相近文案。 +_CROSSING_PRICE_TEXT_RE = re.compile(r" {0,1}(?:(快卖完) {0,1})?[¥¥] {0,1}[1-9]\d*\.\d{2} {0,1}\Z") +_CROSSING_PRICE_PREFIX_RE = re.compile(r" {0,1}(?:快卖完 {0,1})?[¥¥] {0,1}[1-9]\d*\.\d{2} {0,1}") +_CROSSING_PRICE_ALLOWED_CHARACTERS = frozenset(" 快卖完¥¥0123456789.") class SkuEvidenceSanitizationError(RuntimeError): @@ -426,7 +428,7 @@ def _crossing_price_text_mismatch_reason(text: str) -> str: without_one_leading_space = text[1:] if text.startswith(" ") else text if without_one_leading_space.startswith("快要抢光"): return "observed_prefix_kuaiyaoqiangguang" - if without_one_leading_space.startswith("快") and not without_one_leading_space.startswith("快卖光"): + if without_one_leading_space.startswith("快") and not without_one_leading_space.startswith("快卖完"): return "known_prefix_missing" if "¥" not in text and "¥" not in text: return "currency_missing" diff --git a/client/tests/device/test_sku_evidence_sanitizer.py b/client/tests/device/test_sku_evidence_sanitizer.py index d6193d7..e3ea48a 100644 --- a/client/tests/device/test_sku_evidence_sanitizer.py +++ b/client/tests/device/test_sku_evidence_sanitizer.py @@ -34,7 +34,7 @@ TEST_ADDRESS = "SYNTHETIC_ADDRESS_NEVER_PUBLISH" FULL_PHONE = "13800138000" MASKED_PHONE = "138****0000" SAFE_TEXT = "synthetic-safe-lower-content" -CURRENT_PRICE = "快卖光 ¥12.88" +CURRENT_PRICE = "快卖完 ¥12.88" ORIGINAL_PRICE = "¥29.00" PRICE_CURRENT_BOUNDS = "[396,503][712,570]" PRICE_ORIGINAL_BOUNDS = "[730,503][895,570]" @@ -166,7 +166,7 @@ class SkuEvidenceSanitizerTests(unittest.TestCase): self.assertNotIn(MASKED_PHONE, derived_xml) self.assertIn(f'"human_declared_state": "{state}"', manifest) self.assertIn('"privacy_tier": "SANITIZED"', manifest) - self.assertIn('"sanitizer_version": "t103-privacy-v4"', manifest) + self.assertIn('"sanitizer_version": "t103-privacy-v5"', manifest) self.assertIn('"screenshot_space": {', manifest) self.assertIn('"xml_coordinate_space": {', manifest) self.assertIn('"height": 2376', manifest) @@ -256,16 +256,16 @@ class SkuEvidenceSanitizerTests(unittest.TestCase): def test_crossing_price_window_rejects_text_and_structure_drift(self) -> None: bad_texts = ( - f"快卖光 ¥12.88 {TEST_ADDRESS}", - f"快卖光 ¥12.88 {FULL_PHONE}", - "快卖光 ¥12.88 使用微信支付", - "快卖光 ¥12.88 提交订单", - "快卖光 ¥12.88 优惠-11元", + f"快卖完 ¥12.88 {TEST_ADDRESS}", + f"快卖完 ¥12.88 {FULL_PHONE}", + "快卖完 ¥12.88 使用微信支付", + "快卖完 ¥12.88 提交订单", + "快卖完 ¥12.88 优惠-11元", "快要抢光 ¥12.88", - "快卖光 ¥0.00", - "快卖光 ¥12.8", - "快卖光 ¥12.880", - "快卖光 ¥12.88", + "快卖完 ¥0.00", + "快卖完 ¥12.8", + "快卖完 ¥12.880", + "快卖完 ¥12.88", ) for text in bad_texts: with self.subTest(text=text), TemporaryDirectory() as temporary: @@ -276,13 +276,13 @@ class SkuEvidenceSanitizerTests(unittest.TestCase): def test_crossing_price_text_mismatch_reports_only_fixed_slot_and_reason(self) -> None: newline_node = _price_node(CURRENT_PRICE, PRICE_CURRENT_BOUNDS).replace( - "快卖光 ¥12.88", "快卖光 ¥12.88" + "快卖完 ¥12.88", "快卖完 ¥12.88" ) cases = ( ("newline", _xml_with_prices(current_node=newline_node), "newline", PRICE_CURRENT_BOUNDS), ( "non-ascii-whitespace", - _xml_with_prices(current="快卖光 ¥12.88"), + _xml_with_prices(current="快卖完 ¥12.88"), "non_ascii_whitespace", PRICE_CURRENT_BOUNDS, ), @@ -300,13 +300,13 @@ class SkuEvidenceSanitizerTests(unittest.TestCase): ), ( "currency-missing", - _xml_with_prices(current="快卖光 12.88"), + _xml_with_prices(current="快卖完 12.88"), "currency_missing", PRICE_CURRENT_BOUNDS, ), ( "amount-shape", - _xml_with_prices(current="快卖光 ¥12.8"), + _xml_with_prices(current="快卖完 ¥12.8"), "amount_shape", PRICE_CURRENT_BOUNDS, ), @@ -343,10 +343,10 @@ class SkuEvidenceSanitizerTests(unittest.TestCase): def test_crossing_price_text_mismatch_never_echoes_sensitive_or_order_text(self) -> None: cases = ( - f"快卖光 ¥12.88 {TEST_ADDRESS}", - f"快卖光 ¥12.88 {FULL_PHONE}", - "快卖光 ¥12.88 使用微信支付", - "快卖光 ¥12.88 提交订单", + f"快卖完 ¥12.88 {TEST_ADDRESS}", + f"快卖完 ¥12.88 {FULL_PHONE}", + "快卖完 ¥12.88 使用微信支付", + "快卖完 ¥12.88 提交订单", ) for text in cases: with self.subTest(text=text), TemporaryDirectory() as temporary: @@ -378,13 +378,26 @@ class SkuEvidenceSanitizerTests(unittest.TestCase): self.assertNotIn("¥12.88", message) def test_crossing_price_projection_allows_only_limited_ascii_spaces_and_yen_variants(self) -> None: - for current in ("快卖光 ¥12.88", " 快卖光 ¥ 12.88 ", "快卖光 ¥12.88"): + for current in ("快卖完 ¥12.88", " 快卖完 ¥ 12.88 ", "快卖完 ¥12.88"): with self.subTest(current=current), TemporaryDirectory() as temporary: raw = _write_raw(Path(temporary), xml=_xml_with_prices(current=current)) result = sanitize_sku_panel_evidence(raw, raw.parent / "derived") hierarchy = result.hierarchy_path.read_text(encoding="utf-8") self.assertIn(current, hierarchy) + def test_unverified_or_missing_current_price_prefixes_fail_closed(self) -> None: + cases = ( + ("old-prefix", "快卖光 ¥12.88"), + ("bottom-button-prefix", "快要抢光 ¥12.88"), + ("missing-prefix", "¥12.88"), + ) + for name, current in cases: + with self.subTest(name=name), TemporaryDirectory() as temporary: + raw = _write_raw(Path(temporary), xml=_xml_with_prices(current=current)) + with self.assertRaises(SkuEvidenceSanitizationError): + sanitize_sku_panel_evidence(raw, raw.parent / "derived") + self.assertFalse((raw.parent / "derived").exists()) + def test_unique_current_price_without_original_price_is_published(self) -> None: with TemporaryDirectory() as temporary: raw = _write_raw(Path(temporary), xml=_xml_with_prices(include_original=False)) @@ -398,7 +411,7 @@ class SkuEvidenceSanitizerTests(unittest.TestCase): def test_crossing_price_projection_rejects_newline_and_structure_drift(self) -> None: encoded_newline = _price_node(CURRENT_PRICE, PRICE_CURRENT_BOUNDS).replace( - "快卖光 ¥12.88", "快卖光 ¥12.88" + "快卖完 ¥12.88", "快卖完 ¥12.88" ) with TemporaryDirectory() as temporary: raw = _write_raw(Path(temporary), xml=_xml_with_prices(current_node=encoded_newline)) @@ -432,7 +445,7 @@ class SkuEvidenceSanitizerTests(unittest.TestCase): ), ( "duplicate-current", - _xml_with_prices(current=CURRENT_PRICE, original=f"快卖光 {ORIGINAL_PRICE}"), + _xml_with_prices(current=CURRENT_PRICE, original=f"快卖完 {ORIGINAL_PRICE}"), ), ( "multiple-original",