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: