- 补充必须绑定手机号与账号注销/封禁/冻结等错误文案的识别规则 - 增加数据修正迁移 20261008_0052,回写历史账号和登录任务的失败分型 - 增加列表查询与导出时根据错误文案自动兜底细化 failure_kind 的逻辑 - 增加并完善相关分型单元测试
86 lines
3.4 KiB
Python
86 lines
3.4 KiB
Python
"""登录失败分型:把「失败消息」映射成稳定的型别。
|
||
|
||
## 为什么要单独一个模块
|
||
|
||
这个分类原先只活在 `automation/jobs/ck_relogin.py`(自动重登作业)里。但
|
||
**手动/批量登录**同样需要它:
|
||
|
||
1. 账号时间线要写「登录失败」事件,写的时候得带型别,否则以后想筛
|
||
「验证码失败的账号」只能去猜中文;
|
||
2. 导出 CSV 要给运营一列可直接筛选的型别。
|
||
|
||
放 service 而不是留在 job 里,是沿用仓库已有的分层决定
|
||
(`MANUAL_KINDS` 当初就是从 job 挪到 `account_lifecycle.py` 的,
|
||
注释写着「service 不该反过来依赖 job」)。
|
||
|
||
## 一条重要推论:`interrupted` ≠ 失败
|
||
|
||
分型把「已停止 / 被中断 / 服务重启」都归到 `interrupted`。**这不是账号的失败**:
|
||
调用方(`_mark_attempt_failed`)据此决定**不往账号时间线写失败事件** ——
|
||
否则运营按一次停止就能把几十个账号的时间线刷满假失败。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
#: 型别 → 中文标签(导出 CSV 与页面上用同一份,避免两处各写一遍漂移)。
|
||
FAILURE_KIND_LABELS: dict[str, str] = {
|
||
"must_bind_mobile": "必须绑定手机号",
|
||
"captcha": "极验/验证码拦截",
|
||
"credential": "账号凭证问题",
|
||
"email_code": "邮箱取码失败",
|
||
"email_quota": "邮箱配额已满",
|
||
"network": "网络/未知异常",
|
||
"interrupted": "被中断(停止/重启)",
|
||
}
|
||
|
||
#: 不写进账号时间线的型别:这些是**人的动作或进程重启**,不是账号状况。
|
||
NOT_AN_ACCOUNT_FAULT: frozenset[str] = frozenset({"interrupted"})
|
||
|
||
|
||
def classify_failure(message: str | None) -> str:
|
||
"""把重登/登录失败消息映射成分型(见规划 §7.4.3)。
|
||
|
||
顺序很重要:
|
||
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):
|
||
return "captcha"
|
||
if "验证码" in text or "取码" in text:
|
||
return "email_code"
|
||
if "中断" in text or "已停止" in text or "任务已停止" in text:
|
||
return "interrupted"
|
||
if any(
|
||
k in text
|
||
for k in (
|
||
"密码",
|
||
"账号不存在",
|
||
"缺少密码或邮箱",
|
||
"注销",
|
||
"已注销",
|
||
"封禁",
|
||
"冻结",
|
||
"限制登录",
|
||
"禁止登录",
|
||
)
|
||
):
|
||
return "credential"
|
||
# 请求失败/超时/连接异常,以及未知文案:都按可重试处理。
|
||
# 宁可多给一次机会(有每日上限兜底),也不要把可能只是抖动的账号推给人工。
|
||
return "network"
|
||
|
||
|
||
def is_account_fault(kind: str | None) -> bool:
|
||
"""这个型别算不算「账号出了问题」——不算就不该写进账号一生。"""
|
||
return (kind or "") not in NOT_AN_ACCOUNT_FAULT
|
||
|
||
|
||
def failure_kind_label(kind: str | None) -> str:
|
||
return FAILURE_KIND_LABELS.get(kind or "", "")
|