- 高价值套装精确匹配「套装-兔兔白日梦」与「套装-狂战萌兔」,排除头盔/背包/手榴弹散件干扰 - 增加 is_suit_queried_expr,未查询或 NULL 账号严禁误判为双套均可兑换 - 修复前后端序列化空值保持,完善列表与详情页准入状态和判定依据展示 - 新增头盔排除与 NULL 统计防错回归单测,全量通过 689 个用例
120 lines
4.0 KiB
Python
120 lines
4.0 KiB
Python
"""账号价值分档。
|
||
|
||
列表上的「高价值待救」和巡检 / 重登共用这里的口径,避免两套规则各说各的。
|
||
|
||
分档只读账号上已经有的字段(兑换记录、积分、鱼翅、宝典),不看标签。
|
||
数字越小越值钱:
|
||
|
||
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)
|