From 28820fe995c8e6e610e69596cfb81938a6cb8f02 Mon Sep 17 00:00:00 2001 From: guke Date: Tue, 4 Aug 2026 15:13:17 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=20spec=20=E6=88=90?= =?UTF-8?q?=E5=8A=9F=E7=8E=87=E5=8F=A3=E5=BE=84=20+=20=E6=B4=BE=E7=94=9F?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E8=90=BD=E4=B8=BA=E5=85=B1=E4=BA=AB=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 两页成功率分母其实都是 total-cancelled(概览 queries.py:532、大盘 stats.py:290), 一致、本次不动;真正的既有差异在分子(概览含 below_minimum、大盘不含), 本次统一为 original∈S,顺带对齐两页口径 - 派生逻辑落为独立模块 comparison_outcome.py,供 queries/stats 共用避免循环 import Co-Authored-By: Claude Opus 4.8 (1M context) --- ...admin-comparison-outcome-display-design.md | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) 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 类上升——均属口径调整、非数据异常。 ## 前端实现