fix(client): bind T-103 observed price prefix
This commit is contained in:
@@ -22,7 +22,7 @@ from ..pdd.product_url import ProductUrl, ProductUrlError, parse_product_url
|
|||||||
from ..pdd.sku_panel_state import HUMAN_DECLARED_STATES
|
from ..pdd.sku_panel_state import HUMAN_DECLARED_STATES
|
||||||
|
|
||||||
|
|
||||||
SANITIZER_VERSION = "t103-privacy-v4"
|
SANITIZER_VERSION = "t103-privacy-v5"
|
||||||
EXPECTED_GOODS_ID = "937122477375"
|
EXPECTED_GOODS_ID = "937122477375"
|
||||||
EXPECTED_PDD_VERSION = "8.17.0"
|
EXPECTED_PDD_VERSION = "8.17.0"
|
||||||
EXPECTED_DEVICE_MODEL = "PKG110"
|
EXPECTED_DEVICE_MODEL = "PKG110"
|
||||||
@@ -56,9 +56,11 @@ _PRICE_PROJECTION_ATTRIBUTES = (
|
|||||||
)
|
)
|
||||||
# 仅接受普通 ASCII 空格,且每个可分隔位置最多一个;禁止换行、折扣、支付/提交文案和
|
# 仅接受普通 ASCII 空格,且每个可分隔位置最多一个;禁止换行、折扣、支付/提交文案和
|
||||||
# 任何其它字符。前缀捕获组用于区分当前价与至多一个划线/原价候选。
|
# 任何其它字符。前缀捕获组用于区分当前价与至多一个划线/原价候选。
|
||||||
_CROSSING_PRICE_TEXT_RE = re.compile(r" {0,1}(?:(快卖光) {0,1})?[¥¥] {0,1}[1-9]\d*\.\d{2} {0,1}\Z")
|
# T-103 人工在 live 规格面板确认当前价槽的完整非敏感前缀仅为“快卖完”;不得兼容
|
||||||
_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.")
|
_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):
|
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
|
without_one_leading_space = text[1:] if text.startswith(" ") else text
|
||||||
if without_one_leading_space.startswith("快要抢光"):
|
if without_one_leading_space.startswith("快要抢光"):
|
||||||
return "observed_prefix_kuaiyaoqiangguang"
|
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"
|
return "known_prefix_missing"
|
||||||
if "¥" not in text and "¥" not in text:
|
if "¥" not in text and "¥" not in text:
|
||||||
return "currency_missing"
|
return "currency_missing"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ TEST_ADDRESS = "SYNTHETIC_ADDRESS_NEVER_PUBLISH"
|
|||||||
FULL_PHONE = "13800138000"
|
FULL_PHONE = "13800138000"
|
||||||
MASKED_PHONE = "138****0000"
|
MASKED_PHONE = "138****0000"
|
||||||
SAFE_TEXT = "synthetic-safe-lower-content"
|
SAFE_TEXT = "synthetic-safe-lower-content"
|
||||||
CURRENT_PRICE = "快卖光 ¥12.88"
|
CURRENT_PRICE = "快卖完 ¥12.88"
|
||||||
ORIGINAL_PRICE = "¥29.00"
|
ORIGINAL_PRICE = "¥29.00"
|
||||||
PRICE_CURRENT_BOUNDS = "[396,503][712,570]"
|
PRICE_CURRENT_BOUNDS = "[396,503][712,570]"
|
||||||
PRICE_ORIGINAL_BOUNDS = "[730,503][895,570]"
|
PRICE_ORIGINAL_BOUNDS = "[730,503][895,570]"
|
||||||
@@ -166,7 +166,7 @@ class SkuEvidenceSanitizerTests(unittest.TestCase):
|
|||||||
self.assertNotIn(MASKED_PHONE, derived_xml)
|
self.assertNotIn(MASKED_PHONE, derived_xml)
|
||||||
self.assertIn(f'"human_declared_state": "{state}"', manifest)
|
self.assertIn(f'"human_declared_state": "{state}"', manifest)
|
||||||
self.assertIn('"privacy_tier": "SANITIZED"', 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('"screenshot_space": {', manifest)
|
||||||
self.assertIn('"xml_coordinate_space": {', manifest)
|
self.assertIn('"xml_coordinate_space": {', manifest)
|
||||||
self.assertIn('"height": 2376', 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:
|
def test_crossing_price_window_rejects_text_and_structure_drift(self) -> None:
|
||||||
bad_texts = (
|
bad_texts = (
|
||||||
f"快卖光 ¥12.88 {TEST_ADDRESS}",
|
f"快卖完 ¥12.88 {TEST_ADDRESS}",
|
||||||
f"快卖光 ¥12.88 {FULL_PHONE}",
|
f"快卖完 ¥12.88 {FULL_PHONE}",
|
||||||
"快卖光 ¥12.88 使用微信支付",
|
"快卖完 ¥12.88 使用微信支付",
|
||||||
"快卖光 ¥12.88 提交订单",
|
"快卖完 ¥12.88 提交订单",
|
||||||
"快卖光 ¥12.88 优惠-11元",
|
"快卖完 ¥12.88 优惠-11元",
|
||||||
"快要抢光 ¥12.88",
|
"快要抢光 ¥12.88",
|
||||||
"快卖光 ¥0.00",
|
"快卖完 ¥0.00",
|
||||||
"快卖光 ¥12.8",
|
"快卖完 ¥12.8",
|
||||||
"快卖光 ¥12.880",
|
"快卖完 ¥12.880",
|
||||||
"快卖光 ¥12.88",
|
"快卖完 ¥12.88",
|
||||||
)
|
)
|
||||||
for text in bad_texts:
|
for text in bad_texts:
|
||||||
with self.subTest(text=text), TemporaryDirectory() as temporary:
|
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:
|
def test_crossing_price_text_mismatch_reports_only_fixed_slot_and_reason(self) -> None:
|
||||||
newline_node = _price_node(CURRENT_PRICE, PRICE_CURRENT_BOUNDS).replace(
|
newline_node = _price_node(CURRENT_PRICE, PRICE_CURRENT_BOUNDS).replace(
|
||||||
"快卖光 ¥12.88", "快卖光 ¥12.88"
|
"快卖完 ¥12.88", "快卖完 ¥12.88"
|
||||||
)
|
)
|
||||||
cases = (
|
cases = (
|
||||||
("newline", _xml_with_prices(current_node=newline_node), "newline", PRICE_CURRENT_BOUNDS),
|
("newline", _xml_with_prices(current_node=newline_node), "newline", PRICE_CURRENT_BOUNDS),
|
||||||
(
|
(
|
||||||
"non-ascii-whitespace",
|
"non-ascii-whitespace",
|
||||||
_xml_with_prices(current="快卖光 ¥12.88"),
|
_xml_with_prices(current="快卖完 ¥12.88"),
|
||||||
"non_ascii_whitespace",
|
"non_ascii_whitespace",
|
||||||
PRICE_CURRENT_BOUNDS,
|
PRICE_CURRENT_BOUNDS,
|
||||||
),
|
),
|
||||||
@@ -300,13 +300,13 @@ class SkuEvidenceSanitizerTests(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"currency-missing",
|
"currency-missing",
|
||||||
_xml_with_prices(current="快卖光 12.88"),
|
_xml_with_prices(current="快卖完 12.88"),
|
||||||
"currency_missing",
|
"currency_missing",
|
||||||
PRICE_CURRENT_BOUNDS,
|
PRICE_CURRENT_BOUNDS,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"amount-shape",
|
"amount-shape",
|
||||||
_xml_with_prices(current="快卖光 ¥12.8"),
|
_xml_with_prices(current="快卖完 ¥12.8"),
|
||||||
"amount_shape",
|
"amount_shape",
|
||||||
PRICE_CURRENT_BOUNDS,
|
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:
|
def test_crossing_price_text_mismatch_never_echoes_sensitive_or_order_text(self) -> None:
|
||||||
cases = (
|
cases = (
|
||||||
f"快卖光 ¥12.88 {TEST_ADDRESS}",
|
f"快卖完 ¥12.88 {TEST_ADDRESS}",
|
||||||
f"快卖光 ¥12.88 {FULL_PHONE}",
|
f"快卖完 ¥12.88 {FULL_PHONE}",
|
||||||
"快卖光 ¥12.88 使用微信支付",
|
"快卖完 ¥12.88 使用微信支付",
|
||||||
"快卖光 ¥12.88 提交订单",
|
"快卖完 ¥12.88 提交订单",
|
||||||
)
|
)
|
||||||
for text in cases:
|
for text in cases:
|
||||||
with self.subTest(text=text), TemporaryDirectory() as temporary:
|
with self.subTest(text=text), TemporaryDirectory() as temporary:
|
||||||
@@ -378,13 +378,26 @@ class SkuEvidenceSanitizerTests(unittest.TestCase):
|
|||||||
self.assertNotIn("¥12.88", message)
|
self.assertNotIn("¥12.88", message)
|
||||||
|
|
||||||
def test_crossing_price_projection_allows_only_limited_ascii_spaces_and_yen_variants(self) -> None:
|
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:
|
with self.subTest(current=current), TemporaryDirectory() as temporary:
|
||||||
raw = _write_raw(Path(temporary), xml=_xml_with_prices(current=current))
|
raw = _write_raw(Path(temporary), xml=_xml_with_prices(current=current))
|
||||||
result = sanitize_sku_panel_evidence(raw, raw.parent / "derived")
|
result = sanitize_sku_panel_evidence(raw, raw.parent / "derived")
|
||||||
hierarchy = result.hierarchy_path.read_text(encoding="utf-8")
|
hierarchy = result.hierarchy_path.read_text(encoding="utf-8")
|
||||||
self.assertIn(current, hierarchy)
|
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:
|
def test_unique_current_price_without_original_price_is_published(self) -> None:
|
||||||
with TemporaryDirectory() as temporary:
|
with TemporaryDirectory() as temporary:
|
||||||
raw = _write_raw(Path(temporary), xml=_xml_with_prices(include_original=False))
|
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:
|
def test_crossing_price_projection_rejects_newline_and_structure_drift(self) -> None:
|
||||||
encoded_newline = _price_node(CURRENT_PRICE, PRICE_CURRENT_BOUNDS).replace(
|
encoded_newline = _price_node(CURRENT_PRICE, PRICE_CURRENT_BOUNDS).replace(
|
||||||
"快卖光 ¥12.88", "快卖光 ¥12.88"
|
"快卖完 ¥12.88", "快卖完 ¥12.88"
|
||||||
)
|
)
|
||||||
with TemporaryDirectory() as temporary:
|
with TemporaryDirectory() as temporary:
|
||||||
raw = _write_raw(Path(temporary), xml=_xml_with_prices(current_node=encoded_newline))
|
raw = _write_raw(Path(temporary), xml=_xml_with_prices(current_node=encoded_newline))
|
||||||
@@ -432,7 +445,7 @@ class SkuEvidenceSanitizerTests(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"duplicate-current",
|
"duplicate-current",
|
||||||
_xml_with_prices(current=CURRENT_PRICE, original=f"快卖光 {ORIGINAL_PRICE}"),
|
_xml_with_prices(current=CURRENT_PRICE, original=f"快卖完 {ORIGINAL_PRICE}"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"multiple-original",
|
"multiple-original",
|
||||||
|
|||||||
Reference in New Issue
Block a user