feat: 安全应用采购规格解析结果 (#257)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user