fix(overview): 锁定核心高价值套装并修复未查询空值误判

- 高价值套装精确匹配「套装-兔兔白日梦」与「套装-狂战萌兔」,排除头盔/背包/手榴弹散件干扰
- 增加 is_suit_queried_expr,未查询或 NULL 账号严禁误判为双套均可兑换
- 修复前后端序列化空值保持,完善列表与详情页准入状态和判定依据展示
- 新增头盔排除与 NULL 统计防错回归单测,全量通过 689 个用例
This commit is contained in:
yml2213
2026-10-07 15:27:30 +08:00
parent 5405d4b1d1
commit 44b762ea8c
11 changed files with 202 additions and 112 deletions
+55 -5
View File
@@ -69,15 +69,28 @@ class TestOwnedJudgement:
def test_owned_comes_from_official_records(self):
got = extract_high_value_goods(
[GOODS_AVAILABLE], [RABBIT], [{"commodityName": "破片手榴弹-兔兔白日梦"}]
[GOODS_AVAILABLE], [RABBIT], [{"commodityName": "套装-兔兔白日梦"}]
)
assert got[0]["owned"] is True
assert got[0]["obtained_names"] == ["破片手榴弹-兔兔白日梦"]
assert got[0]["obtained_names"] == ["套装-兔兔白日梦"]
def test_not_owned_when_records_empty(self):
got = extract_high_value_goods([GOODS_AVAILABLE], [RABBIT], [])
assert got[0]["owned"] is False
def test_accessories_never_imply_suit_owned(self):
"""散件配件(手榴弹/头盔/背包)绝不能误判为拥有了高价值套装。"""
got = extract_high_value_goods(
[GOODS_AVAILABLE],
[RABBIT],
[
{"commodityName": "破片手榴弹-兔兔白日梦"},
{"commodityName": "兔兔白日梦头盔"},
],
)
assert got[0]["owned"] is False
assert got[0]["obtained_names"] == []
def test_global_stock_never_implies_owned(self):
"""回归用例:全局库存为 0 或售罄,但 status==0 时,不能推出「这个账号已拥有」"""
sold_out = {
@@ -99,12 +112,12 @@ class TestOwnedJudgement:
assert got[0]["owned"] is True
assert got[0]["count"] == 0 # 全局库存如实展示
def test_records_match_by_substring_family(self):
"""同一件东西有多个商品名(套装- / 破片手榴弹-),都要算数。"""
def test_records_match_suit_names(self):
"""兑换记录精确匹配套装名,过滤无关商品。"""
got = extract_high_value_goods(
[GOODS_AVAILABLE],
[RABBIT],
[{"commodityName": "破片手榴弹-兔兔白日梦"}, {"commodityName": "别的"}],
[{"commodityName": "套装-兔兔白日梦"}, {"commodityName": "别的商品"}],
)
assert got[0]["owned"] is True
assert len(got[0]["obtained_names"]) == 1
@@ -1966,3 +1979,40 @@ class TestHighValueAccountFilters:
assert hv["unqueried"] == 1 # acc_unqueried
assert hv["high_points"] == 2 # 500, 800
assert hv["high_value_broken"] == 1 # acc_broken
def test_helmet_and_backpack_never_match_over_suit(self):
"""当商品列表中头盔排在前面时,必须精准匹配到套装-兔兔白日梦,严禁命中头盔。"""
items = [
{"commodityName": "兔兔白日梦头盔", "commodityId": "helmet_1", "status": 0},
{"commodityName": "兔兔白日梦背包", "commodityId": "bag_1", "status": 0},
{"commodityName": "套装-兔兔白日梦", "commodityId": "suit_1", "status": 1},
]
got = extract_high_value_goods(items, ["套装-兔兔白日梦"], [])
assert len(got) == 1
assert got[0]["name"] == "套装-兔兔白日梦"
assert got[0]["commodity_id"] == "suit_1"
assert got[0]["owned"] is True
assert got[0]["reason"] == "官方商城显示已达兑换上限"
def test_null_high_value_owned_not_treated_as_both_available(self):
"""high_value_owned 为 None 的账号(如被重置过),绝不能误判为双套均可兑换。"""
from datetime import datetime, UTC
from web.backend.models import Account
from web.backend.routers.accounts import accounts_summary
acc = Account(
username="acc_reset_null",
password="p",
email="e@test.com",
email_password="ep",
high_value_owned=None,
overview_queried_at=datetime.now(UTC), # 虽然时间不为空,但字段是 None
)
self.session.add(acc)
self.session.commit()
res = accounts_summary(db=self.session, current=self.admin)
hv = res.get("high_value", {})
# 绝不计入 both_available,应计入 unqueried
assert hv["both_available"] == 2
assert hv["unqueried"] == 2