From 9de73152ec66dc42e418f1606579f04f32eec234 Mon Sep 17 00:00:00 2001 From: guke Date: Tue, 4 Aug 2026 15:35:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=AF=94=E4=BB=B7=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E5=B1=95=E7=A4=BA=E5=8F=A3=E5=BE=84=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E6=A8=A1=E5=9D=97(=E5=A4=96=E9=83=A8=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E5=88=A4=E4=B8=BA=E6=88=90=E5=8A=9F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- app/admin/repositories/comparison_outcome.py | 60 ++++++++++++++++++++ tests/test_admin_comparison_outcome.py | 33 +++++++++++ 2 files changed, 93 insertions(+) create mode 100644 app/admin/repositories/comparison_outcome.py create mode 100644 tests/test_admin_comparison_outcome.py diff --git a/app/admin/repositories/comparison_outcome.py b/app/admin/repositories/comparison_outcome.py new file mode 100644 index 0000000..1196a73 --- /dev/null +++ b/app/admin/repositories/comparison_outcome.py @@ -0,0 +1,60 @@ +"""admin 比价记录展示口径:把「流程跑完、外部原因致结果缺失」的记录判为成功。 + +#209 / C 端落库把 store_not_found / items_not_found / store_closed / no_delivery / +unsupported 归一化成 status='failed';admin 排查视角改按记录级原始业务结局 +(raw_payload.record_status)重判——这些「外部缺失」算成功(附缺失提示), +只有纯技术故障才是失败。仅 admin 用,不碰 C 端 / #209 落库。 +""" +from __future__ import annotations + +from sqlalchemy import func + +from app.models.comparison import ComparisonRecord + +# 记录级原始业务结局里算「成功(流程跑完)」的集合;其余(failed / 未知)才是技术故障。 +ADMIN_SUCCESS_OUTCOMES = frozenset({ + "success", "below_minimum", "store_closed", + "store_not_found", "items_not_found", "no_delivery", "unsupported", +}) +# 有缺失的成功 → 感叹号 hover 提示;success 本身无提示。 +OUTCOME_HINTS = { + "below_minimum": "未满起送", + "store_closed": "门店打烊", + "store_not_found": "未找到店", + "items_not_found": "未找到菜", + "no_delivery": "单点不配送", + "unsupported": "平台·场景不支持", +} + + +def derive_admin_outcome(raw_payload: dict | None, status: str) -> tuple[str, str | None]: + """(admin_status, outcome_hint)。列表 Python 层派生(raw_payload 已随 ORM 加载)。""" + if status in ("cancelled", "running"): + return status, None + raw = raw_payload or {} + # 原始结局:优先 raw.record_status,其次 raw.status,兜底 status 列 + # (兼容迁移未覆盖、细分值残留在 status 列的老记录;与下方 SQL 口径一致)。 + original = raw.get("record_status") or raw.get("status") or status + if original in ADMIN_SUCCESS_OUTCOMES: + return "success", OUTCOME_HINTS.get(original) + return "failed", None + + +def _original_expr(): + """SQL:原始结局 = coalesce(raw.record_status, raw.status, status 列)。跨方言(as_string,#209 迁移已验证)。""" + return func.coalesce( + ComparisonRecord.raw_payload["record_status"].as_string(), + ComparisonRecord.raw_payload["status"].as_string(), + ComparisonRecord.status, + ) + + +def admin_success_sql(): + """SQL 层 admin 成功判定(概览 / 大盘的 case / where 共用)。 + + 排除 cancelled / running(生命周期态,不看结局);其余按原始结局 ∈ S。 + coalesce 兜底 status 列 → original 永非 NULL、且兼容 status 列残留的细分值。 + """ + return ComparisonRecord.status.notin_(("cancelled", "running")) & _original_expr().in_( + tuple(ADMIN_SUCCESS_OUTCOMES) + ) diff --git a/tests/test_admin_comparison_outcome.py b/tests/test_admin_comparison_outcome.py new file mode 100644 index 0000000..f0eb196 --- /dev/null +++ b/tests/test_admin_comparison_outcome.py @@ -0,0 +1,33 @@ +"""admin 展示口径派生单测:记录级原始结局 → (admin_status, outcome_hint)。""" +from __future__ import annotations + +import pytest + +from app.admin.repositories.comparison_outcome import derive_admin_outcome + + +@pytest.mark.parametrize( + ("raw_payload", "status", "expected"), + [ + # 真实形态:status 已 normalize,细分在 raw_payload.record_status + ({"record_status": "success"}, "success", ("success", None)), + ({"record_status": "below_minimum"}, "success", ("success", "未满起送")), + ({"record_status": "store_closed"}, "failed", ("success", "门店打烊")), + ({"record_status": "store_not_found"}, "failed", ("success", "未找到店")), + ({"record_status": "items_not_found"}, "failed", ("success", "未找到菜")), + ({"record_status": "no_delivery"}, "failed", ("success", "单点不配送")), + ({"record_status": "unsupported"}, "failed", ("success", "平台·场景不支持")), + ({"record_status": "failed"}, "failed", ("failed", None)), # 纯技术故障 + # POST 路径:细分在 raw_payload.status + ({"status": "store_not_found"}, "failed", ("success", "未找到店")), + # 兜底 status 列:raw_payload 缺失(极老记录)或残留细分值 + (None, "success", ("success", None)), + (None, "failed", ("failed", None)), + (None, "store_closed", ("success", "门店打烊")), # 迁移未覆盖的残留 + # 生命周期态优先,不看结局 + ({}, "cancelled", ("cancelled", None)), + ({"record_status": "success"}, "running", ("running", None)), + ], +) +def test_derive_admin_outcome(raw_payload, status, expected): + assert derive_admin_outcome(raw_payload, status) == expected