diff --git a/client/src/pdd_collect_service.py b/client/src/pdd_collect_service.py index ff51756..f3b822b 100644 --- a/client/src/pdd_collect_service.py +++ b/client/src/pdd_collect_service.py @@ -699,18 +699,6 @@ class PddCollectService: self._wait_spec_panel(device) color_dimension, color_samples = self._collect_color_prices(device) - missing_prices = [ - color.text - for color in color_dimension.values - if color.available - and color_samples[color.text].price_cent is None - ] - if missing_prices: - raise PddCollectError( - "PDD_DATA_PRICE_MISSING", - "以下可用颜色没有采集到稳定价格:" - + "、".join(missing_prices), - ) size_dimension = self._collect_size_dimension(device) dimensions = (color_dimension,) if size_dimension is not None: @@ -723,11 +711,11 @@ class PddCollectService: skus = self._build_color_price_skus( dimensions, color_samples ) - has_available_price = any( - item.price_cent is not None for item in skus if item.available - ) - if not skus or not has_available_price: - raise PddCollectError("PDD_DATA_PRICE_MISSING", "没有采集到可用 SKU 的价格") + if not skus: + raise PddCollectError( + "PDD_DATA_SPEC_INCOMPLETE", + "规格面板没有生成可提交的规格组合", + ) except PddCollectError as exc: if not exc.diagnostics and self._last_goods_xml: artifact = self._save_xml("collect-failed", self._last_goods_xml) @@ -1207,21 +1195,18 @@ class PddCollectService: options = {"color": color.text} if size is not None: options["size"] = size.text - available = ( - color.available - and (size is None or size.available) - and sample.price_cent is not None - ) + # 是否可用来自规格控件状态,不能用“是否采到价格”代替。 + available = color.available and (size is None or size.available) results.append( SkuResult( options, - sample.price_cent if available else None, + sample.price_cent, available, - sample.raw_price if available else None, + sample.raw_price, {"color": color.text} if sample.price_cent is not None else {}, - sample.list_price_cent if available else None, + sample.list_price_cent, ) ) return tuple(results) diff --git a/client/test/test_pdd_collect_service.py b/client/test/test_pdd_collect_service.py index cb0837d..a2b735f 100644 --- a/client/test/test_pdd_collect_service.py +++ b/client/test/test_pdd_collect_service.py @@ -232,6 +232,13 @@ class IgnoredColorClickDevice(SnakeColorDevice): self.clicks.append((x, y)) +class AllMissingColorPriceDevice(IgnoredColorClickDevice): + """所有颜色点击都未生效,模拟全部颜色采价为空。""" + + def _spec_xml(self): + return super()._spec_xml().replace('selected="true"', 'selected="false"') + + def keep_only_one_sku(xml_data: str) -> str: """从脱敏固件中删除蓝色和 L,只保留一个组合。""" @@ -562,6 +569,32 @@ class PddCollectParserTest(unittest.TestCase): self.assertIsNone(sample.price_cent) + def test_all_missing_color_prices_still_collects_sizes(self): + device = AllMissingColorPriceDevice(self.home_xml) + service = PddCollectService( + PddDeviceService(lambda _serial: device), + "USB-001", + "client-001", + sleeper=lambda _seconds: None, + ) + + data = service.collect( + FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123") + ).to_pdd_data() + + self.assertEqual( + [item["text"] for item in data["dimensions"][1]["values"]], + ["M", "L"], + ) + self.assertTrue(device.sizes_visible) + self.assertTrue(data["skus"]) + self.assertTrue(all(item["price_cent"] is None for item in data["skus"])) + self.assertTrue(all(item["raw_price"] is None for item in data["skus"])) + self.assertTrue( + all(item["price_observed_at"] == {} for item in data["skus"]) + ) + self.assertTrue(all(item["available"] for item in data["skus"])) + def test_missing_goods_id_is_structured_error(self): service = PddCollectService( PddDeviceService(lambda _serial: object()), diff --git a/docs/client/01-requirements.md b/docs/client/01-requirements.md index ce55179..46a8186 100644 --- a/docs/client/01-requirements.md +++ b/docs/client/01-requirements.md @@ -59,6 +59,10 @@ Client 顶级导航仅包含: `price_observed_at` 只记录实际选中的颜色,不得虚构已选尺码。以后需要按尺码 区分价格或组合库存时,必须另行确认并改用 `price_granularity=sku`。 +颜色已售罄、无法选中或未采到稳定价格时,不重试采价,也不阻断后续流程;该 +颜色的价格相关字段留空。所有颜色处理完成后仍须采集尺码、保存本地结果并提交 +Admin。规格 `available` 只反映页面控件状态,不能根据价格是否为空推断。 + 对于“1.2 万+”等近似数值,同时保存规范化数值、原始文字和近似标记。 ### 4.2 采购任务 diff --git a/docs/client/03-data-model.md b/docs/client/03-data-model.md index 98783c8..c94ac4a 100644 --- a/docs/client/03-data-model.md +++ b/docs/client/03-data-model.md @@ -525,6 +525,10 @@ CREATE TABLE app_settings ( 这里的 `available` 来自当前规格节点显示状态,不表示 Client 已逐个验证了每个 颜色与尺码组合的实时库存。 +颜色无法选中或没有采到稳定价格时,该颜色对应组合的 `price_cent`、`raw_price` +和 `list_price_cent` 使用 `null`,`price_observed_at` 使用空对象。缺价不改变 +`available`,也不阻止尺码采集、结果入库和提交。 + ### 8.2 采购结果 采购任务在同一结构中增加: diff --git a/docs/client/04-admin-api-contract.md b/docs/client/04-admin-api-contract.md index 57d32d9..6becdff 100644 --- a/docs/client/04-admin-api-contract.md +++ b/docs/client/04-admin-api-contract.md @@ -310,6 +310,7 @@ Idempotency-Key: task-id:attempt-id:result-v1 - `[必须]` Client 只有收到 `accepted: true` 后才能把本地任务标为 `succeeded`。 - `[必须]` `price_cent` 是实际支付价的整数分;划线价可放在 `list_price_cent`。 - `[必须]` `price_granularity` 只能是 `color` 或 `sku`。当前 Client 按颜色采样时填 `color`;同一颜色的尺码组合可以共享颜色价格,但 `price_observed_at` 只填写实际选中的颜色,不得虚构尺码。以后逐个选择完整组合采价时才使用 `sku`。 +- `[必须]` 采集任务中,颜色无法选中或未采到稳定价格时,相关 `price_cent`、`raw_price` 和 `list_price_cent` 允许为 `null`,`price_observed_at` 允许为空对象;Admin 不得仅因部分或全部颜色缺价拒绝结果。 - `[必须]` `shop_name` 采不到时允许省略或留空;Admin 不得因此拒绝老版本 Client,也不得用空值覆盖已保存的店铺名。 ### 6.1 Admin 必须无条件接受