Files
live-hub-py/web/backend/services/account_value.py
T
yml2213 578e259eea fix(douyu): 加固高价值三态迁移与状态口径
修复迁移中断重跑、旧快照回填覆盖、降级将 unknown 误写成可兑换,以及迁移前 CK 已失效账号未作废旧结论的问题。统一 CK error 的展示与筛选口径,补齐旧快照迁移和兑换兜底反向用例,并同步高价值结论时间语义。同时纳入 .gitignore 更新。
2026-11-06 13:25:45 +08:00

192 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`` 说「上次形成高价值结论是什么时候」;
结论可以是 ``unknown``。
两者会脱钩 —— 比如一键查询里商品接口单独失败(其它项都拿到了),
资料时间被刷新了,高价值时间却停在原地。再比如 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)