Files
shaguabijia-app-server/tests/test_comparison_admin_summary.py
T
guke 03129e059f feat(admin): 比价记录改「技术成功/失败」口径,外部缺失记为成功 (#217)
背景
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
2026-08-04 18:49:30 +08:00

128 lines
5.2 KiB
Python

"""比价记录页后端概览聚合(admin 口径:外部缺失记为成功)。"""
from __future__ import annotations
from datetime import UTC, date, datetime
import pytest
from sqlalchemy.dialects import postgresql
from app.admin.repositories import queries
from app.admin.repositories.comparison_outcome import admin_success_sql
from app.db.session import SessionLocal
from app.models.comparison import ComparisonRecord
def test_postgresql_duration_summary_uses_ordered_set_aggregates() -> None:
stmt = queries._comparison_duration_aggregate_stmt(
[], admin_success_sql(), (0.05, 0.5, 0.95, 0.99)
)
sql = str(
stmt.compile(
dialect=postgresql.dialect(),
compile_kwargs={"literal_binds": True},
)
)
assert sql.count("percentile_cont") == 4
# 口径按原始结局 coalesce,而非直接读 status 列
assert "coalesce" in sql.lower()
def test_summary_counts_external_gaps_as_success() -> None:
db = SessionLocal()
try:
# (trace_id, status 列[已 normalize], record_status[原始结局], total_ms, cost, saved)
rows = [
("sum-success", "success", "success", 1000, 1.0, 100),
("sum-below-min", "success", "below_minimum", 2000, 2.0, 0),
("sum-store-closed", "failed", "store_closed", 3000, None, 0),
("sum-store-not-found", "failed", "store_not_found", 4000, None, 0),
("sum-failed", "failed", "failed", 100_000, 3.0, 0),
("sum-cancelled", "cancelled", None, 5000, None, 0),
("sum-running", "running", None, 6000, None, 0),
]
for trace_id, status, record_status, total_ms, cost, saved in rows:
db.add(ComparisonRecord(
trace_id=trace_id,
status=status,
total_ms=total_ms,
llm_cost_yuan=cost,
saved_amount_cents=saved,
raw_payload={"record_status": record_status} if record_status else None,
created_at=datetime(2038, 1, 15, 12, tzinfo=UTC),
))
db.flush()
summary = queries.comparison_records_summary(
db, date_from=date(2038, 1, 15), date_to=date(2038, 1, 15)
)
# admin 成功 = success + below_minimum + store_closed + store_not_found = 4
assert summary["started"] == 7
assert summary["success"] == 4
assert summary["completed"] == 5 # 4 成功 + 1 纯 failed
assert summary["cancelled"] == 1
assert summary["success_rate"] == pytest.approx(4 / 6) # 分母 started - cancelled
assert summary["avg_token_cost"] == pytest.approx(2.0) # (1+2+3)/3
assert summary["lower_price_rate"] == pytest.approx(1 / 4) # 仅 sum-success saved>0
# 耗时统计集 = admin 成功的 total_ms [1000,2000,3000,4000]
assert summary["avg_duration_ms"] == 2500
assert summary["p5_duration_ms"] == 1150
assert summary["p50_duration_ms"] == 2500
assert summary["p95_duration_ms"] == 3850
assert summary["p99_duration_ms"] == 3970
assert summary["cancelled_p50_ms"] == 5000
finally:
db.rollback()
db.close()
def test_list_status_filter_uses_admin_outcome() -> None:
"""列表「状态」筛选走 admin 口径:筛成功含 6 类、筛失败仅纯技术故障;并验日期边界排除。"""
db = SessionLocal()
try:
rows = [
("flt-success", "success", "success"),
("flt-below-min", "success", "below_minimum"),
("flt-store-not-found", "failed", "store_not_found"),
("flt-failed", "failed", "failed"),
("flt-cancelled", "cancelled", None),
]
for trace_id, status, record_status in rows:
db.add(ComparisonRecord(
trace_id=trace_id,
status=status,
raw_payload={"record_status": record_status} if record_status else None,
created_at=datetime(2039, 3, 10, 12, tzinfo=UTC),
))
# 日期边界:窗口外一条 admin 成功记录(次日),应被日期筛选排除
db.add(ComparisonRecord(
trace_id="flt-out-of-window",
status="success",
raw_payload={"record_status": "success"},
created_at=datetime(2039, 3, 11, 12, tzinfo=UTC),
))
db.flush()
succ, _c1, succ_total = queries.list_comparison_records(
db, status="success", date_from=date(2039, 3, 10), date_to=date(2039, 3, 10)
)
# 3 条当天 admin 成功(含 store_not_found);窗口外 flt-out-of-window 被日期排除
assert succ_total == 3
assert {it.trace_id for it in succ} == {
"flt-success", "flt-below-min", "flt-store-not-found",
}
fail, _c2, fail_total = queries.list_comparison_records(
db, status="failed", date_from=date(2039, 3, 10), date_to=date(2039, 3, 10)
)
assert fail_total == 1
assert {it.trace_id for it in fail} == {"flt-failed"}
_canc, _c3, canc_total = queries.list_comparison_records(
db, status="cancelled", date_from=date(2039, 3, 10), date_to=date(2039, 3, 10)
)
assert canc_total == 1
finally:
db.rollback()
db.close()