Merge branch 'main' into dev: 同步分配管理优化、登录异常分型及历史数据刷正
This commit is contained in:
@@ -71,7 +71,7 @@ AUTOMATABLE_STATES: tuple[str, ...] = DERIVED_STATES
|
||||
|
||||
# 重登失败分型 → 需要人工(自动重试只会加重风控 / 重试多少次都一样)。
|
||||
# 放在这里而不是 job 里:`derive_status` 要用它,service 不该反过来依赖 job。
|
||||
MANUAL_KINDS = frozenset({"captcha", "credential"})
|
||||
MANUAL_KINDS = frozenset({"captcha", "credential", "must_bind_mobile"})
|
||||
|
||||
STATUS_LABELS = {
|
||||
STATUS_IMPORTED: "未登录",
|
||||
|
||||
@@ -24,6 +24,7 @@ from __future__ import annotations
|
||||
|
||||
#: 型别 → 中文标签(导出 CSV 与页面上用同一份,避免两处各写一遍漂移)。
|
||||
FAILURE_KIND_LABELS: dict[str, str] = {
|
||||
"must_bind_mobile": "必须绑定手机号",
|
||||
"captcha": "极验/验证码拦截",
|
||||
"credential": "账号凭证问题",
|
||||
"email_code": "邮箱取码失败",
|
||||
@@ -39,20 +40,36 @@ NOT_AN_ACCOUNT_FAULT: frozenset[str] = frozenset({"interrupted"})
|
||||
def classify_failure(message: str | None) -> str:
|
||||
"""把重登/登录失败消息映射成分型(见规划 §7.4.3)。
|
||||
|
||||
顺序很重要:「邮箱验证码发送次数已达上限」既含「验证码」也含「上限」,
|
||||
必须先在 ``email_quota`` 命中,否则会被归到可重试的 ``email_code`` 上,
|
||||
导致次日才该重试的账号被按小时反复撞配额。
|
||||
顺序很重要:
|
||||
1. 「必须绑定手机号 / 跳过手机号绑定失败」优先归入 ``must_bind_mobile``;
|
||||
2. 「邮箱验证码发送次数已达上限」既含「验证码」也含「上限」,必须先在 ``email_quota`` 命中;
|
||||
3. 极验滑块命中 ``captcha``。
|
||||
"""
|
||||
text = message or ""
|
||||
if "必须绑定手机号" in text or "跳过手机号绑定失败" in text or "绑定手机" in text:
|
||||
return "must_bind_mobile"
|
||||
if "上限" in text or "配额" in text:
|
||||
return "email_quota"
|
||||
if "极验" in text or "验证" in text and "滑块" in text:
|
||||
if "极验" in text or ("验证" in text and "滑块" in text):
|
||||
return "captcha"
|
||||
if "验证码" in text:
|
||||
if "验证码" in text or "取码" in text:
|
||||
return "email_code"
|
||||
if "中断" in text or "已停止" in text or "任务已停止" in text:
|
||||
return "interrupted"
|
||||
if "密码" in text or "账号不存在" in text or "缺少密码或邮箱" in text:
|
||||
if any(
|
||||
k in text
|
||||
for k in (
|
||||
"密码",
|
||||
"账号不存在",
|
||||
"缺少密码或邮箱",
|
||||
"注销",
|
||||
"已注销",
|
||||
"封禁",
|
||||
"冻结",
|
||||
"限制登录",
|
||||
"禁止登录",
|
||||
)
|
||||
):
|
||||
return "credential"
|
||||
# 请求失败/超时/连接异常,以及未知文案:都按可重试处理。
|
||||
# 宁可多给一次机会(有每日上限兜底),也不要把可能只是抖动的账号推给人工。
|
||||
|
||||
@@ -27,7 +27,7 @@ from .account_events_service import (
|
||||
EVENT_RELOGIN_OK,
|
||||
emit,
|
||||
)
|
||||
from .account_lifecycle import on_ck_verdict, on_login_success
|
||||
from .account_lifecycle import on_ck_verdict, on_login_success, sync_status
|
||||
from .cookie_check_service import check_douyu_cookie
|
||||
from .douyu_service import LOGIN_ATTEMPT_IN_FLIGHT_STATUSES
|
||||
from .login_failure import classify_failure, failure_kind_label, is_account_fault
|
||||
@@ -155,6 +155,9 @@ def _mark_attempt_failed(
|
||||
if db is None or account is None:
|
||||
# 没有会话(纯内存构造的用例)或没有账号:跳过旁路事件,绝不影响主流程。
|
||||
return
|
||||
account.relogin_failure_kind = kind
|
||||
account.relogin_last_error = (message or "")[:512]
|
||||
sync_status(db, account, occurred_at=completed_at)
|
||||
emit(
|
||||
db,
|
||||
account_id=account.id,
|
||||
|
||||
Reference in New Issue
Block a user