feat: 安全应用采购规格解析结果 (#257)

This commit is contained in:
chengma
2026-08-17 17:43:25 +08:00
parent 484ce4e8ec
commit afc1cdf719
19 changed files with 1669 additions and 24 deletions
+167
View File
@@ -16,6 +16,8 @@ from .admin_gateway import (
ClaimCapabilities,
ClientInfo,
RegistrationReceipt,
SpecResolutionMatch,
SpecResolutionReceipt,
SubmissionReceipt,
)
@@ -48,6 +50,10 @@ class MockAdminGateway(AdminGateway):
self._submissions: Dict[
str, Tuple[str, SubmissionReceipt]
] = {}
self._spec_resolutions: Dict[
str, Tuple[str, SpecResolutionReceipt]
] = {}
self._next_spec_resolution: Optional[SpecResolutionReceipt] = None
self._next_error: Optional[AdminGatewayError] = None
self._reject_next_submission = False
self._registrations: Dict[str, Tuple[ClientInfo, ClaimCapabilities]] = {}
@@ -170,6 +176,167 @@ class MockAdminGateway(AdminGateway):
) -> SubmissionReceipt:
return self._submit("failure", task_id, idempotency_key, failure)
def set_next_spec_resolution(
self, receipt: SpecResolutionReceipt
) -> None:
"""测试辅助:设置下一次新规格解析请求的业务响应。"""
self._next_spec_resolution = deepcopy(receipt)
@property
def spec_resolution_count(self) -> int:
return len(self._spec_resolutions)
def resolve_purchase_spec(
self,
task_id: str,
idempotency_key: str,
observation: Mapping[str, Any],
) -> SpecResolutionReceipt:
with self._lock:
self._raise_forced_error()
task = self._validate_submission_target(task_id, idempotency_key)
self._validate_spec_observation(task, idempotency_key, observation)
fingerprint = self._fingerprint(
"spec_resolution", task_id, observation
)
previous = self._spec_resolutions.get(idempotency_key)
if previous is not None:
old_fingerprint, receipt = previous
if old_fingerprint != fingerprint:
raise AdminGatewayError(
"IDEMPOTENCY_CONFLICT",
"相同规格解析幂等键携带了不同内容",
False,
)
return deepcopy(receipt)
receipt = self._next_spec_resolution or SpecResolutionReceipt(
schema_version=1,
resolution_id=str(uuid4()),
outcome="uncertain",
source=None,
candidate_snapshot_hash=str(
observation["candidate_snapshot_hash"]
),
match=None,
confidence_bps=None,
reason="Mock 未配置唯一匹配",
resolved_at=utc_now_iso(),
)
self._next_spec_resolution = None
self._spec_resolutions[idempotency_key] = (
fingerprint,
deepcopy(receipt),
)
return deepcopy(receipt)
@staticmethod
def _validate_spec_observation(
task: AdminTask,
idempotency_key: str,
observation: Mapping[str, Any],
) -> None:
if task.task_type.value != "purchase":
raise AdminGatewayError(
"TASK_NOT_PURCHASE", "当前任务不是采购任务", False
)
if observation.get("schema_version") != 1:
raise AdminGatewayError(
"INVALID_SPEC_RESOLUTION_SCHEMA", "规格解析版本无效", False
)
if observation.get("task_version") != task.version:
raise AdminGatewayError(
"TASK_VERSION_CONFLICT", "规格解析任务版本不一致", False
)
payload = task.payload
if observation.get("pdd_goods_id") != payload.get("goods_id"):
raise AdminGatewayError(
"PDD_GOODS_MISMATCH", "规格解析商品编号不一致", False
)
if dict(observation.get("original_options") or {}) != dict(
payload.get("options") or {}
):
raise AdminGatewayError(
"INVALID_SPEC_RESOLUTION_REQUEST", "原始规格不一致", False
)
candidates = observation.get("candidates")
snapshot_hash = observation.get("candidate_snapshot_hash")
selected_color = observation.get("selected_color")
if (
not isinstance(candidates, list)
or not 1 <= len(candidates) <= 100
or not isinstance(snapshot_hash, str)
or len(snapshot_hash) != 64
or not str(observation.get("attempt_id") or "").strip()
):
raise AdminGatewayError(
"INVALID_SPEC_RESOLUTION_REQUEST", "规格候选结构无效", False
)
if any(
not isinstance(candidate, Mapping)
or candidate.get("candidate_id") != f"c{index}"
or not isinstance(candidate.get("raw_text"), str)
or not candidate["raw_text"]
or dict(candidate.get("options") or {})
!= {
"color": selected_color,
"size": candidate.get("raw_text"),
}
for index, candidate in enumerate(candidates, start=1)
):
raise AdminGatewayError(
"INVALID_SPEC_RESOLUTION_REQUEST", "规格候选字段无效", False
)
candidate_material = "".join(
(
MockAdminGateway._frame("spec-resolution-v1"),
MockAdminGateway._frame(str(observation["pdd_goods_id"])),
MockAdminGateway._frame(str(selected_color)),
MockAdminGateway._frame(str(len(candidates))),
*(
"".join(
MockAdminGateway._frame(str(value))
for value in (
candidate.get("candidate_id"),
candidate.get("raw_text"),
(candidate.get("options") or {}).get("color"),
(candidate.get("options") or {}).get("size"),
)
)
for candidate in candidates
if isinstance(candidate, Mapping)
),
)
)
expected_snapshot_hash = hashlib.sha256(
candidate_material.encode("utf-8")
).hexdigest()
if snapshot_hash != expected_snapshot_hash:
raise AdminGatewayError(
"SPEC_RESOLUTION_HASH_MISMATCH", "候选快照哈希不一致", False
)
identity = "".join(
MockAdminGateway._frame(str(value))
for value in (
task.task_id,
observation["attempt_id"],
snapshot_hash,
"spec-resolution-v1",
)
)
expected_key = "spec-resolution-v1:" + hashlib.sha256(
identity.encode("utf-8")
).hexdigest()
if idempotency_key != expected_key:
raise AdminGatewayError(
"SPEC_RESOLUTION_HASH_MISMATCH", "规格解析幂等键不一致", False
)
@staticmethod
def _frame(value: str) -> str:
return f"{len(value.encode('utf-8'))}:{value}"
def _submit(
self,
submission_type: str,