diff --git a/docs/superpowers/specs/2026-08-04-admin-comparison-outcome-display-design.md b/docs/superpowers/specs/2026-08-04-admin-comparison-outcome-display-design.md index 3de3930..670ea1a 100644 --- a/docs/superpowers/specs/2026-08-04-admin-comparison-outcome-display-design.md +++ b/docs/superpowers/specs/2026-08-04-admin-comparison-outcome-display-design.md @@ -3,7 +3,7 @@ - **日期**:2026-08-04 - **范围**:后端 `shaguabijia-app-server`(admin 层,**方案 A**:无 schema 变更、无迁移、不碰 #209 落库与 C 端)+ 前端 `shaguabijia-admin-web` - **涉及文件**: - - 后端:`app/admin/repositories/queries.py`(列表派生 + 概览口径)、`app/admin/schemas/comparison.py`(新增两字段)、`app/admin/repositories/stats.py`(大盘成功率分子口径) + - 后端:`app/admin/repositories/comparison_outcome.py`(**新建**:共享常量 + Python 派生 + SQL 判定)、`app/admin/repositories/queries.py`(列表派生 + 概览口径)、`app/admin/schemas/comparison.py`(新增两字段)、`app/admin/repositories/stats.py`(大盘成功率分子口径) - 前端:`src/app/(main)/comparison-records/page.tsx`、`src/lib/types.ts` - 文档:`shaguabijia-app-server/docs/guides/比价结果卡片-状态口径与交互参考.md`(补 admin 口径说明) @@ -56,29 +56,46 @@ outcome_hint: str | None = None # 缺失提示文案;None = 无缺失 ## 后端实现(方案 A) -### 1. 常量 + 派生(`queries.py`,紧邻既有 `_COMPARISON_STATUS_ALIASES`) +### 1. 新模块 `app/admin/repositories/comparison_outcome.py`(供 `queries.py` 与 `stats.py` 共用,避免循环 import) ```python -_ADMIN_SUCCESS_OUTCOMES = frozenset({ +from sqlalchemy import func +from app.models.comparison import ComparisonRecord + +ADMIN_SUCCESS_OUTCOMES = frozenset({ "success", "below_minimum", "store_closed", "store_not_found", "items_not_found", "no_delivery", "unsupported", }) -_OUTCOME_HINTS = { +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]: +def derive_admin_outcome(raw_payload: dict | None, status: str) -> tuple[str, str | None]: + """Python 层派生(列表用;raw_payload 已随 ORM 加载,零额外查询)。""" if status in ("cancelled", "running"): return status, None raw = raw_payload or {} original = raw.get("record_status") or raw.get("status") if original is None: # 极老记录兜底 return ("success" if status == "success" else "failed"), None - if original in _ADMIN_SUCCESS_OUTCOMES: - return "success", _OUTCOME_HINTS.get(original) + if original in ADMIN_SUCCESS_OUTCOMES: + return "success", OUTCOME_HINTS.get(original) return "failed", None + +def _original_expr(): + return func.coalesce( + ComparisonRecord.raw_payload["record_status"].as_string(), + ComparisonRecord.raw_payload["status"].as_string(), + ) + +def admin_success_sql(): + """SQL 层 admin 成功判定(概览/大盘的 case/where 共用)。""" + original = _original_expr() + return original.in_(tuple(ADMIN_SUCCESS_OUTCOMES)) | ( + original.is_(None) & (ComparisonRecord.status == "success") + ) ``` ### 2. 列表 `list_comparison_records` @@ -101,17 +118,17 @@ _original_expr = func.coalesce( ComparisonRecord.raw_payload["status"].as_string(), ) # admin 成功 = original ∈ S OR (original IS NULL AND status == 'success') -# completed = admin 成功 + 纯 failed(admin 口径失败) -# success_rate = admin 成功 / completed +# completed = admin 成功 + 纯 failed(展示字段;admin 口径) +# success_rate = admin 成功 / (started - cancelled) # 分母不变,见 queries.py:532 ``` **耗时分位**(avg / p50 / p95,`_comparison_duration_aggregate*`):统计集从「`status == "success"`」改为「admin 成功集」——用户已确认**统一口径纳入**这 6 类。改动点:`_comparison_status_condition` 系的耗时过滤条件改用 `_original_expr ∈ S`(或复用一个 `_admin_success_condition()` 表达式,列表/概览/大盘共用)。 ### 4. 大盘 `stats.py` `dashboard_overview` -`period_comparison_success`(分子,[stats.py:288](../../../shaguabijia-app-server/app/admin/repositories/stats.py) 的聚合 case)改用 `_original_expr ∈ S` 口径,与概览一致。 +`period_comparison_stats` 的 success 分子([stats.py:278-281](../../../shaguabijia-app-server/app/admin/repositories/stats.py) 的 `case(status=='success')`)现在**不含 below_minimum**(#209 只改了概览、没同步大盘)。改用 `admin_success_sql()`(`original ∈ S`)口径,与概览一致。 -**范围界定(重要)**:大盘成功率分母是 `total - cancelled`([stats.py:290](../../../shaguabijia-app-server/app/admin/repositories/stats.py)),与比价记录页概览的 `completed`(success+failed)分母**本就不同**(running 的处理差异)。**此既有差异不在本次范围**——本次只统一「6 类算成功」这个**分子**口径;两页分母口径保持现状,避免波及大盘既有数字。 +**范围界定**:两页成功率**分母都是 `total - cancelled`**(概览 [queries.py:532](../../src/../../../shaguabijia-app-server/app/admin/repositories/queries.py)、大盘 [stats.py:290](../../../shaguabijia-app-server/app/admin/repositories/stats.py)),口径一致、本次不动。真正的既有差异在**分子**:概览 success 含 `below_minimum`、大盘不含。本次把两处分子都统一为 `original ∈ S`,改完两页口径**完全一致**。副作用:大盘成功率因补上 `below_minimum` + 5 类而上升,概览因补上 5 类上升——均属口径调整、非数据异常。 ## 前端实现