Files
live-hub-py/web/backend/services/account_value.py
T
yml2213 44b762ea8c fix(overview): 锁定核心高价值套装并修复未查询空值误判
- 高价值套装精确匹配「套装-兔兔白日梦」与「套装-狂战萌兔」,排除头盔/背包/手榴弹散件干扰
- 增加 is_suit_queried_expr,未查询或 NULL 账号严禁误判为双套均可兑换
- 修复前后端序列化空值保持,完善列表与详情页准入状态和判定依据展示
- 新增头盔排除与 NULL 统计防错回归单测,全量通过 689 个用例
2026-10-07 15:27:30 +08:00

120 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""账号价值分档。
列表上的「高价值待救」和巡检 / 重登共用这里的口径,避免两套规则各说各的。
分档只读账号上已经有的字段(兑换记录、积分、鱼翅、宝典),不看标签。
数字越小越值钱:
1. 查过资料,兔兔白日梦和狂战萌兔都还没换
2. 查过资料、还剩一套能换,或者积分 ≥ 300(没查过但积分已经 ≥ 300 也算)
3. 有鱼翅,或宝典已开通
4. 查过资料,但上面都不沾
5. 还没查过资料
「高价值」= 第 1、2 档。巡检对这两档用更短的重检间隔,重登也先拿这两档。
"""
from __future__ import annotations
import json
from sqlalchemy import String, and_, case, cast, or_
from ..models import Account
SUIT_BAIRIMENG = "兔兔白日梦"
SUIT_MENGTU = "狂战萌兔"
HIGH_VALUE_POINTS = 300
# 第 1、2 档都是「还值得抢救」的号。
HIGH_VALUE_TIER_MAX = 2
# 高价值账号的巡检间隔。作业上配置的间隔更短时,跟那个更短的走。
HIGH_VALUE_RECHECK_SECONDS = 6 * 3600
def suit_owned_expr(column, text: str):
"""JSON 列里是否包含这个套装名。MySQL 与 SQLite 都按字符串匹配。"""
escaped = json.dumps(text).strip('"')
return or_(
cast(column, String).like(f"%{text}%"),
cast(column, String).like(f"%{escaped}%"),
)
def is_suit_queried_expr():
"""判断账号是否有效完成过高价值套装查询。
必须同时满足:
1. 有查询时间 overview_queried_at
2. high_value_owned 必须是有效 JSON 数组(排除 SQL NULL 与 JSON NULL)
"""
return and_(
Account.overview_queried_at.isnot(None),
Account.high_value_owned.isnot(None),
cast(Account.high_value_owned, String).like("[%"),
)
def value_tier_expr():
"""1–5 档。没查过、也没有任何资产信号时落在第 5 档。"""
hv_queried = is_suit_queried_expr()
owns_bairimeng = suit_owned_expr(Account.high_value_owned, SUIT_BAIRIMENG)
owns_mengtu = suit_owned_expr(Account.high_value_owned, SUIT_MENGTU)
both_available = and_(hv_queried, ~owns_bairimeng, ~owns_mengtu)
one_available = and_(
hv_queried,
or_(
and_(~owns_bairimeng, owns_mengtu),
and_(owns_bairimeng, ~owns_mengtu),
),
)
return case(
(both_available, 1),
(or_(one_available, Account.points >= HIGH_VALUE_POINTS), 2),
(or_(Account.gold_balance > 0, Account.elite_opened.is_(True)), 3),
(Account.overview_queried_at.isnot(None), 4),
else_=5,
)
def is_high_value_expr():
"""第 1、2 档:巡检加密、重登插队、列表「高价值待救」都用这一条。"""
return value_tier_expr() <= HIGH_VALUE_TIER_MAX
def suit_sort_rank_expr():
"""列表「高价值套装」列的排序,和格子上看到的一致。数字越小越值钱。
0 查过、两套都还能换
1 查过、只剩一套
2 查过、两套都换过
3 还没查过(未知,不和「还能换」混在一起)
不掺积分、鱼翅。那是巡检分档的事,点这一列的人是在排套装。
"""
hv_queried = is_suit_queried_expr()
owns_bairimeng = suit_owned_expr(Account.high_value_owned, SUIT_BAIRIMENG)
owns_mengtu = suit_owned_expr(Account.high_value_owned, SUIT_MENGTU)
both_available = and_(hv_queried, ~owns_bairimeng, ~owns_mengtu)
one_available = and_(
hv_queried,
or_(
and_(~owns_bairimeng, owns_mengtu),
and_(owns_bairimeng, ~owns_mengtu),
),
)
both_owned = and_(hv_queried, owns_bairimeng, owns_mengtu)
return case(
(both_available, 0),
(one_available, 1),
(both_owned, 2),
else_=3,
)
def high_value_recheck_seconds(interval_seconds: int) -> int:
"""高价值账号多久重检一次。不会比作业配置的间隔更长。"""
interval = max(int(interval_seconds or 0), 60)
return min(HIGH_VALUE_RECHECK_SECONDS, interval)