fix(client): bind T-103 observed price prefix

This commit is contained in:
QiuSW
2026-08-04 15:23:02 +08:00
parent 6e001b2729
commit 76a522f13f
2 changed files with 42 additions and 27 deletions
@@ -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"
@@ -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",