From e6dcad7c7d5f130e6b9d6cfaeecb32af0a12bbfa Mon Sep 17 00:00:00 2001 From: guke Date: Thu, 30 Jul 2026 10:52:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(comparison):=20best=20=E5=85=9C=E5=BA=95?= =?UTF-8?q?=E5=80=99=E9=80=89=E5=90=AB=E6=BA=90,=E6=BA=90=E6=9C=80?= =?UTF-8?q?=E4=BE=BF=E5=AE=9C=E6=97=B6=E5=9B=9E=E8=90=BD=E6=BA=90=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E8=B4=9F=E7=9C=81=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 无 is_best 兜底原本排除源、强选最低目标;当源本身最便宜时(全平台 dish-diff 相似替换),会误选更贵目标 → saved 变负,倒扣 get_stats 的 「累计发现可省」(该聚合按 status=success 求和、不带 >0 过滤)。 改为在含源的有价行里取最低价,与老派生函数 _derive「全目标缺菜回落源、 不虚报省」同一语义:源最便宜 → best=源、saved=0、is_source_best=True。 补回归测试:源最便宜场景(旧逻辑 best 误选更贵目标、saved 为负)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/repositories/comparison.py | 18 +++++++++--------- tests/test_compare_harvest.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/app/repositories/comparison.py b/app/repositories/comparison.py index 945d38e..39ccf4b 100644 --- a/app/repositories/comparison.py +++ b/app/repositories/comparison.py @@ -376,15 +376,15 @@ def _derive_from_platforms( src = next((p for p in rows if p.get("role") == "source"), None) best = next((p for p in rows if p.get("is_best")), None) if best is None: - # pricebot 没标 is_best(如全平台 has_dish_diff「相似替换/价格仅供参考」→ 不认定权威 - # 最低价)但仍有有价目标 → 兜底取有价目标里最低价当参考 best,避免记录级 best_*/saved - # 整条落 NULL(否则首页价 0.00 / 记录页无最低红框 / 省额丢失 /「累计发现可省」漏计)。 - priced_targets = [ - p for p in rows - if p.get("role") != "source" and p.get("price") is not None - ] - if priced_targets: - best = min(priced_targets, key=lambda p: (p["price"], p.get("display_order") or 0)) + # pricebot 没标 is_best(如全平台 has_dish_diff「相似替换/仅供参考」→ 不认定权威最低价) + # 但仍有有价行 → 兜底取有价行里最低价当参考 best,避免记录级 best_*/saved 整条落 NULL + # (否则首页价 0.00 / 记录页无最低红框 / 省额丢失 /「累计发现可省」漏计)。**候选含源**: + # 源常年全菜、价可信,源本身最便宜时 best 回落源(saved=0、is_source_best=True),与 _derive + # 「全目标缺菜回落源、不虚报省」同一语义;若排除源强选更贵目标,saved 会变负、倒扣「累计 + # 发现可省」(get_stats 对 saved_amount_cents 求和不带 >0 过滤)。 + priced = [p for p in rows if p.get("price") is not None] + if priced: + best = min(priced, key=lambda p: (p["price"], p.get("display_order") or 0)) source_price_cents = _yuan_to_cents(src.get("price")) if src else None best_price_cents = _yuan_to_cents(best.get("price")) if best else None saved_amount_cents = None diff --git a/tests/test_compare_harvest.py b/tests/test_compare_harvest.py index 6cf16e9..5a42e3a 100644 --- a/tests/test_compare_harvest.py +++ b/tests/test_compare_harvest.py @@ -175,6 +175,40 @@ def test_harvest_done_platforms_no_isbest_falls_back_to_cheapest_target(client) assert rec.is_source_best is False # 兜底选的是目标,非源 +def _done_params_no_isbest_source_cheapest() -> dict: + """无 is_best 且源本身最便宜:源淘宝闪购 50.0 < 全部 dish-diff 目标(美团 57.8 / 京东 74.9)。 + 此时不能强选更贵的目标当 best(否则 saved 变负、污染「累计发现可省」),应回落源。""" + return { + "platforms": [ + {"role": "source", "platform_id": "eleme", "platform_name": "淘宝闪购", + "package": "me.ele", "price": 50.0, "is_best": False, "has_dish_diff": False, + "store_name": "窑鸡王", "items": [{"name": "招牌窑鸡 整只-香辣", "qty": 1}]}, + {"role": "target", "platform_id": "meituan_waimai", "platform_name": "美团外卖", + "package": "com.sankuai.meituan.takeoutnew", "price": 57.8, "is_best": False, + "has_dish_diff": True}, + {"role": "target", "platform_id": "jd_waimai_standalone", "platform_name": "京东外卖", + "package": "com.jd.waimai", "price": 74.9, "is_best": False, "has_dish_diff": True}, + ], + "information": "源平台已是最低(其余为相似替换)", + } + + +def test_harvest_done_no_isbest_source_cheapest_falls_back_to_source(client) -> None: + """回归:无 is_best 且源最便宜 → best 回落源(与 _derive「全目标缺菜回落源、不虚报省」同语义), + saved=0、is_source_best=True,绝不因强选更贵目标而让 saved 变负、倒扣「累计发现可省」。""" + tid = _tid() + with SessionLocal() as db: + crud.harvest_running(db, trace_id=tid, user_id=None) + rec, _ = crud.harvest_done(db, trace_id=tid, user_id=None, + done_params=_done_params_no_isbest_source_cheapest()) + assert rec.status == "success" + assert rec.best_platform_id == "eleme" # 回落到源(源最便宜) + assert rec.best_price_cents == 5000 # 源 50.0 + assert rec.source_price_cents == 5000 + assert rec.saved_amount_cents == 0 # 没省到,绝不为负 + assert rec.is_source_best is True # 源就是最便宜 + + def test_harvest_abort_cancels_running(client) -> None: tid = _tid() with SessionLocal() as db: