fix: 校验采购尺码候选边界 (#256)
This commit is contained in:
@@ -1168,6 +1168,8 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
|
|||||||
diagnostics={"selection_failure": "target_not_visible"},
|
diagnostics={"selection_failure": "target_not_visible"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._validate_size_candidate_dimension(dimension)
|
||||||
|
|
||||||
available_values = tuple(
|
available_values = tuple(
|
||||||
item for item in dimension.values if item.available
|
item for item in dimension.values if item.available
|
||||||
)
|
)
|
||||||
@@ -1213,6 +1215,39 @@ class U2PddPurchaseAdapter(PddPurchaseAdapter):
|
|||||||
)
|
)
|
||||||
raise PddPurchaseSpecResolutionRequired(snapshot)
|
raise PddPurchaseSpecResolutionRequired(snapshot)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_size_candidate_dimension(dimension: SpecDimension) -> None:
|
||||||
|
"""候选进入快照前满足 #254 的数量、文字和维度边界。"""
|
||||||
|
|
||||||
|
def valid_text(value: str) -> bool:
|
||||||
|
return (
|
||||||
|
1 <= len(value) <= 191
|
||||||
|
and not any(ord(char) < 32 or 127 <= ord(char) <= 159 for char in value)
|
||||||
|
)
|
||||||
|
|
||||||
|
texts = [item.text for item in dimension.values]
|
||||||
|
available_count = sum(item.available for item in dimension.values)
|
||||||
|
invalid_reason = ""
|
||||||
|
if dimension.key != "size" or not valid_text(dimension.name):
|
||||||
|
invalid_reason = "dimension_invalid"
|
||||||
|
elif len(set(texts)) != len(texts):
|
||||||
|
invalid_reason = "candidate_duplicated"
|
||||||
|
elif any(not valid_text(text) for text in texts):
|
||||||
|
invalid_reason = "candidate_text_invalid"
|
||||||
|
elif available_count > 100:
|
||||||
|
invalid_reason = "candidate_count_exceeded"
|
||||||
|
if invalid_reason:
|
||||||
|
raise PddPurchaseError(
|
||||||
|
"PURCHASE_SIZE_CANDIDATE_SCAN_INCOMPLETE",
|
||||||
|
"完整尺码候选不符合规格解析边界,已停止采购",
|
||||||
|
step="purchase_select_options",
|
||||||
|
diagnostics={
|
||||||
|
"selection_failure": "target_not_visible",
|
||||||
|
"candidate_validation": invalid_reason,
|
||||||
|
"candidate_count": available_count,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def _collect_size_candidates(self, device: Any) -> Optional[SpecDimension]:
|
def _collect_size_candidates(self, device: Any) -> Optional[SpecDimension]:
|
||||||
"""使用采集模块同一套第二规格遍历,不建立新连接也不保存 XML。"""
|
"""使用采集模块同一套第二规格遍历,不建立新连接也不保存 XML。"""
|
||||||
|
|
||||||
|
|||||||
@@ -1537,7 +1537,7 @@ class PddCollectParserTest(unittest.TestCase):
|
|||||||
sleeper=lambda _seconds: None,
|
sleeper=lambda _seconds: None,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = service._collect_size_dimension(device)
|
result = service.collect_second_dimension_candidates(device)
|
||||||
|
|
||||||
self.assertIsNotNone(result)
|
self.assertIsNotNone(result)
|
||||||
self.assertEqual(result.name, "套餐(16)")
|
self.assertEqual(result.name, "套餐(16)")
|
||||||
|
|||||||
@@ -1052,6 +1052,54 @@ class U2PddPurchaseAdapterTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
adapter.close()
|
adapter.close()
|
||||||
|
|
||||||
|
def test_invalid_candidate_list_never_requests_remote_resolution(self):
|
||||||
|
for values, expected_reason in (
|
||||||
|
(
|
||||||
|
(DimensionValue("M", True), DimensionValue("M", False)),
|
||||||
|
"candidate_duplicated",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
tuple(
|
||||||
|
DimensionValue(f"尺码{index}", True)
|
||||||
|
for index in range(101)
|
||||||
|
),
|
||||||
|
"candidate_count_exceeded",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
with self.subTest(expected_reason=expected_reason):
|
||||||
|
device = FakeDevice()
|
||||||
|
adapter = U2PddPurchaseAdapter(
|
||||||
|
"USB-001",
|
||||||
|
device_service=PddDeviceService(
|
||||||
|
connector=lambda _serial: device
|
||||||
|
),
|
||||||
|
sleeper=lambda _seconds: None,
|
||||||
|
select_color_fn=lambda *_args, **_kwargs: True,
|
||||||
|
select_size_fn=lambda *_args, **_kwargs: False,
|
||||||
|
size_candidate_collector=lambda _device, items=values: (
|
||||||
|
SpecDimension("size", "尺码", items)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
adapter.open_goods(GOODS_URL)
|
||||||
|
|
||||||
|
with self.assertRaises(PddPurchaseError) as raised:
|
||||||
|
adapter.select_options(
|
||||||
|
{"color": "黑色", "size": "不存在"}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotIsInstance(
|
||||||
|
raised.exception, PddPurchaseSpecResolutionRequired
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
raised.exception.code,
|
||||||
|
"PURCHASE_SIZE_CANDIDATE_SCAN_INCOMPLETE",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
raised.exception.diagnostics["candidate_validation"],
|
||||||
|
expected_reason,
|
||||||
|
)
|
||||||
|
adapter.close()
|
||||||
|
|
||||||
def test_size_without_selected_color_never_requests_remote_resolution(self):
|
def test_size_without_selected_color_never_requests_remote_resolution(self):
|
||||||
device = FakeDevice()
|
device = FakeDevice()
|
||||||
scans = []
|
scans = []
|
||||||
|
|||||||
@@ -432,7 +432,8 @@ PDD URL,包名和页面类型仍以最新控件树确认。
|
|||||||
第二规格只读遍历。遍历完成后,Adapter 在内存中保留全部观察项及可用状态,并仅为
|
第二规格只读遍历。遍历完成后,Adapter 在内存中保留全部观察项及可用状态,并仅为
|
||||||
可购买项按页面顺序生成 `c1`、`c2` 等候选和 `spec-resolution-v1` 快照哈希。只有完整
|
可购买项按页面顺序生成 `c1`、`c2` 等候选和 `spec-resolution-v1` 快照哈希。只有完整
|
||||||
遍历才能抛出 `PddPurchaseSpecResolutionRequired`;遍历不完整、页面丢失、没有可购买项,
|
遍历才能抛出 `PddPurchaseSpecResolutionRequired`;遍历不完整、页面丢失、没有可购买项,
|
||||||
或完整列表中仍存在目标的精确/繁简等价文字,都返回普通采购错误。该 Adapter 不调用
|
候选重复、超过 100 条、文字或维度不合法,或完整列表中仍存在目标的精确/繁简等价文字,
|
||||||
|
都返回普通采购错误。该 Adapter 不调用
|
||||||
Admin,也不点击任何远端解析结果;网络请求和解析后复核属于后续工单。
|
Admin,也不点击任何远端解析结果;网络请求和解析后复核属于后续工单。
|
||||||
|
|
||||||
部分 PDD 页面在点击商品页购买入口后直接进入包含规格和数量的
|
部分 PDD 页面在点击商品页购买入口后直接进入包含规格和数量的
|
||||||
|
|||||||
Reference in New Issue
Block a user