修复迁移中断重跑、旧快照回填覆盖、降级将 unknown 误写成可兑换,以及迁移前 CK 已失效账号未作废旧结论的问题。统一 CK error 的展示与筛选口径,补齐旧快照迁移和兑换兜底反向用例,并同步高价值结论时间语义。同时纳入 .gitignore 更新。
192 lines
7.0 KiB
Python
192 lines
7.0 KiB
Python
"""账号价值分档。
|
||
|
||
列表上的「高价值待救」和巡检 / 重登共用这里的口径,避免两套规则各说各的。
|
||
|
||
分档只读账号上已经有的当前字段(官方准入三态、积分、鱼翅、宝典),不看标签。
|
||
数字越小越值钱:
|
||
|
||
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)
|