03129e059f
背景 admin 比价记录页 / 概览 / 大盘此前把「未找到店、未找到菜、门店打烊、单点不配送、平台不支持、未满起送」统统显示/统计为失败——根因是 #209 把这些业务结局归一化成记录级 status='failed' 落库。但它们其实是比价流程正常跑完、只是外部原因导致结果缺失;与「系统技术故障」混为一谈后,管理员排查时无法区分「是我们系统的锅」还是「目标平台本来就没这家店/这些菜」。 方案 admin 后台改用技术完成率口径:流程跑完(非 running)且非纯技术故障 → 记为成功,有外部缺失的前端标绿「成功」+ ⚠(hover 看具体原因);只有真正的技术故障 failed 才是失败。 口径收敛到新模块 app/admin/repositories/comparison_outcome.py,被列表下发 / 概览 / 大盘 / 状态筛选共同消费(单一真相源)。 原始业务结局取自 raw_payload.record_status(3 级 coalesce 兜底,兼容历史残留;Python 派生与 SQL 判定 bit 一致)。 仅 admin,不碰 C 端 / #209 落库 / 奖励逻辑——admin 关心「系统跑没跑成」,C 端关心「省没省到钱」,刻意分层。 口径映射 原始 record_status | admin 状态 | hover 提示 -- | -- | -- success | 🟢 成功 | — below_minimum | 🟢 成功 ⚠ | 未满起送 store_closed | 🟢 成功 ⚠ | 门店打烊 store_not_found | 🟢 成功 ⚠ | 未找到店 items_not_found | 🟢 成功 ⚠ | 未找到菜 no_delivery | 🟢 成功 ⚠ | 单点不配送 unsupported | 🟢 成功 ⚠ | 平台·场景不支持 failed(纯技术故障) | 🔴 失败 | — cancelled / running | ⚪ 中途退出 / 🔵 进行中 | — 改动清单 新增 comparison_outcome.py:derive_admin_outcome(列表 Python 派生) + admin_success_sql(聚合/筛选 SQL 判定)。 列表/详情 下发 admin_status + outcome_hint(瞬态挂载,零额外查询)。 概览 comparison_records_summary:success / completed / 耗时分位改 admin 口径。 大盘 dashboard_overview:比价成功率改 admin 口径(顺带补齐 #209 未同步大盘的 below_minimum)。 状态筛选 _comparison_status_condition:筛「成功」含 6 类、筛「失败」仅纯技术故障;清理 #209 遗留死常量。 文档 补 admin 口径说明。 --------- Co-authored-by: guke <guke@autohome.com.cn> Reviewed-on: #217
63 lines
2.9 KiB
Python
63 lines
2.9 KiB
Python
"""admin 展示口径派生单测:记录级原始结局 → (admin_status, outcome_hint)。"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from sqlalchemy import select
|
|
|
|
from app.admin.repositories.comparison_outcome import admin_success_sql, derive_admin_outcome
|
|
from app.db.session import SessionLocal
|
|
from app.models.comparison import ComparisonRecord
|
|
|
|
|
|
@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)), # 纯技术故障
|
|
# 空字符串视作缺失,兜到下一级(与 SQL nullif 对齐)
|
|
({"record_status": "", "status": "store_closed"}, "failed", ("success", "门店打烊")),
|
|
# 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
|
|
|
|
|
|
def test_admin_success_sql_matches_python_on_empty_string() -> None:
|
|
"""record_status 为空串时,SQL 侧(nullif)与 Python 侧(or)都应兜到 status 列结局、判为成功。"""
|
|
db = SessionLocal()
|
|
try:
|
|
rec = ComparisonRecord(
|
|
trace_id="outcome-empty-record-status",
|
|
status="failed",
|
|
raw_payload={"record_status": "", "status": "store_closed"},
|
|
)
|
|
db.add(rec)
|
|
db.flush()
|
|
matched = db.execute(
|
|
select(ComparisonRecord.id).where(
|
|
ComparisonRecord.trace_id == "outcome-empty-record-status",
|
|
admin_success_sql(),
|
|
)
|
|
).scalar_one_or_none()
|
|
assert matched is not None # SQL 侧判成功
|
|
assert derive_admin_outcome(rec.raw_payload, rec.status) == ("success", "门店打烊") # Python 侧一致
|
|
finally:
|
|
db.rollback()
|
|
db.close()
|