Files
live-hub-py/web/backend/services/login_failure.py
T
yml2213 4eef45efe4 fix(accounts): 完善手机号必绑与账号注销等登录异常分型识别
- 补充必须绑定手机号与账号注销/封禁/冻结等错误文案的识别规则
- 增加数据修正迁移 20261008_0052,回写历史账号和登录任务的失败分型
- 增加列表查询与导出时根据错误文案自动兜底细化 failure_kind 的逻辑
- 增加并完善相关分型单元测试
2026-10-08 11:26:19 +08:00

86 lines
3.4 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.
"""登录失败分型:把「失败消息」映射成稳定的型别。
## 为什么要单独一个模块
这个分类原先只活在 `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 "", "")