Files
live-hub-py/web/backend/services/account_value.py
T
yml2213 4c1aff47da feat(douyu): 高价值套装改用官方 storehome 三态判定
高价值套装的准入判定改为只认官方精英宝典 storehome 里账号级的
status:0=可兑换、1=已达兑换上限,商品缺失/接口异常=unknown。
兑换记录和积分记录只作历史展示,不参与是否拥有的判定。

原来的 high_value_owned(JSON 名单)无法区分「确认可兑换」和
「官方接口没返回商品」,也无法表达部分失败。改为两套独立状态 +
检查时间(迁移 20261106_0065),旧快照保守回填:status 缺失的
一律 unknown,不猜。

CK 判定失效时在数据层作废结论(降为 unknown),不是展示层降级。
官方 status 是账号维度的,但 CK 失效那段时间账号可能被别人手工
登录并兑换(这些号共享给客服),缓存里的「可兑换」就不成立了;
只在展示层降级的话,CK 恢复后那条陈旧的「可换」会重新变绿。
error(查不到)不作废 —— 查不到不等于账号出问题了。

高价值结论有自己的时钟:资料刷新除了看 overview_queried_at,还看
high_value_checked_at。两者会脱钩(商品接口单独失败时资料时间被刷新、
高价值时间停在原地),少了这一条这类账号会永久停在旧结论。作废的
结论按 6 小时慢速重试,但「从来没查到过」不因此重选 —— 那是接口对
该账号稳定失败,每轮都重选只会反复撞墙。

商城商品列表改用精英宝典的 manualID 拉取,不再寄生在电竞手册分支里
(否则没配电竞手册的配置就永远拉不到商城,三态全部停在待确认)。

筛选拆成三类「查不到」:unqueried(未查资料,旧口径不变)、
high_value_never(从未读到商城)、high_value_unknown(查过但结论
未知/过期)。之前第二类无处可寻 —— 它既不满足 unqueried 也不满足
high_value_unknown。
2026-11-06 12:25:08 +08:00

191 lines
7.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
from datetime import UTC, datetime, timedelta
from sqlalchemy import and_, case, or_
from ..models import Account
from .high_value_service import (
HIGH_VALUE_FRESH_SECONDS,
HIGH_VALUE_STATUS_AVAILABLE,
HIGH_VALUE_STATUS_OWNED,
HIGH_VALUE_STATUS_UNKNOWN,
HIGH_VALUE_UNKNOWN_RETRY_SECONDS,
high_value_fresh_cutoff,
)
HIGH_VALUE_POINTS = 300
# 第 1、2 档都是「还值得抢救」的号。
HIGH_VALUE_TIER_MAX = 2
# 高价值账号的巡检间隔。作业上配置的间隔更短时,跟那个更短的走。
HIGH_VALUE_RECHECK_SECONDS = 6 * 3600
def high_value_fresh_expr(*, now: datetime | None = None):
"""高价值三态是否仍在有效期内。"""
cutoff = high_value_fresh_cutoff(now=now or datetime.now(UTC))
return Account.high_value_checked_at >= cutoff
def suit_status_expr(key: str):
return getattr(Account, f"high_value_{key}_status")
def suit_available_expr(key: str, *, now: datetime | None = None):
return and_(
high_value_fresh_expr(now=now),
suit_status_expr(key) == HIGH_VALUE_STATUS_AVAILABLE,
)
def suit_owned_expr(key: str, *, now: datetime | None = None):
return and_(
high_value_fresh_expr(now=now),
suit_status_expr(key) == HIGH_VALUE_STATUS_OWNED,
)
def high_value_unknown_expr(*, now: datetime | None = None):
"""已检查过,但结果过期或至少一套官方状态未知。"""
return and_(
Account.high_value_checked_at.isnot(None),
or_(
~high_value_fresh_expr(now=now),
suit_status_expr("bairimeng").is_(None),
suit_status_expr("bairimeng") == HIGH_VALUE_STATUS_UNKNOWN,
suit_status_expr("mengtu").is_(None),
suit_status_expr("mengtu") == HIGH_VALUE_STATUS_UNKNOWN,
),
)
def high_value_needs_refresh_expr(
*,
now: datetime | None = None,
max_seconds: int = HIGH_VALUE_FRESH_SECONDS,
unknown_seconds: int = HIGH_VALUE_UNKNOWN_RETRY_SECONDS,
):
"""高价值结论**已经过期** —— 需要再拉一次官方 ``storehome``。
和「资料是否过期」是**两件事**:
- ``overview_queried_at`` 说「上次一键查询是什么时候」;
- ``high_value_checked_at`` 说「上次**成功读到官方商品状态**是什么时候」。
两者会脱钩 —— 比如一键查询里商品接口单独失败(其它项都拿到了),
资料时间被刷新了,高价值时间却停在原地。再比如 CK 失效那段时间查不动,
账号恢复后高价值结论还挂着「查不到」。
所以定时刷新必须看**后者**:高价值结论过期就该再查一次 —— 哪怕账号的
整体资料看起来还很新。
三种情况:
- **过期**(查过但超过 ``max_seconds``):尽快重拉 —— 结论太旧,
而「可兑换」随时会变;
- **结论被作废**(状态是 ``unknown``,比如 CK 失效期间被作废):
按 ``unknown_seconds`` 慢速重试;
- **从来没查到过**(``checked_at IS NULL``):**不算**。那种情况要么
账号确实没查过资料(本来就该被选中),要么商品接口对这个账号稳定失败
(没配 manualID、账号没有宝典)—— 后者若每轮都重选,同一批账号会
反复撞墙,挤掉真正有新欠账的账号。它由「资料过期」那条兜着就够了。
``max_seconds`` 由调用方给:默认 7 天,与列表上「待确认」的判定同源。
"""
seconds = max(int(max_seconds or 0), 60)
unknown = max(int(unknown_seconds or 0), 60)
current = now or datetime.now(UTC)
cutoff = current - timedelta(seconds=seconds)
unknown_cutoff = current - timedelta(seconds=unknown)
unknown_status = or_(
suit_status_expr("bairimeng").is_(None),
suit_status_expr("bairimeng") == HIGH_VALUE_STATUS_UNKNOWN,
suit_status_expr("mengtu").is_(None),
suit_status_expr("mengtu") == HIGH_VALUE_STATUS_UNKNOWN,
)
return and_(
Account.high_value_checked_at.isnot(None),
or_(
Account.high_value_checked_at < cutoff,
and_(
unknown_status,
Account.high_value_checked_at < unknown_cutoff,
),
),
)
def value_tier_expr(*, now: datetime | None = None):
"""1–5 档。没查过、也没有任何资产信号时落在第 5 档。"""
available_bairimeng = suit_available_expr("bairimeng", now=now)
available_mengtu = suit_available_expr("mengtu", now=now)
both_available = and_(available_bairimeng, available_mengtu)
one_available = or_(
available_bairimeng,
available_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(*, now: datetime | None = None):
"""第 1、2 档:巡检加密、重登插队、列表「高价值待救」都用这一条。"""
return value_tier_expr(now=now) <= HIGH_VALUE_TIER_MAX
def suit_sort_rank_expr(*, now: datetime | None = None):
"""列表「高价值套装」列的排序,和格子上看到的一致。数字越小越值钱。
0 查过、两套都还能换
1 查过、只剩一套
2 查过、两套都换过
3 还没查过(未知,不和「还能换」混在一起)
不掺积分、鱼翅。那是巡检分档的事,点这一列的人是在排套装。
"""
available_bairimeng = suit_available_expr("bairimeng", now=now)
available_mengtu = suit_available_expr("mengtu", now=now)
owned_bairimeng = suit_owned_expr("bairimeng", now=now)
owned_mengtu = suit_owned_expr("mengtu", now=now)
both_available = and_(available_bairimeng, available_mengtu)
one_available = or_(
available_bairimeng,
available_mengtu,
)
both_owned = and_(owned_bairimeng, owned_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)