Files
live-hub-py/web/backend/services/login_failure.py
T
yml2213 f101414f5e feat(accounts): 新增登录异常分型统计、多选/全量导出、筛选记忆及页面组件模块化拆分
- 新增登录失败异常分型统计:支持「必须绑定手机号」、「极验滑块」、「凭证错误」、「邮箱取码失败」、「邮箱配额已满」、「网络异常」分型识别、生命周期联动(待人工)与一键筛选;
- 新增账号名单与明细导出功能:支持勾选多选或按当前筛选条件全选,可导出纯用户名名单 (TXT) 或 UTF-8 BOM 结构化明细报表 (CSV),并支持一键复制到剪贴板;
- 优化筛选条件记忆机制:支持页面切换/返回后自动保留所有筛选与排序状态,新增顶部「重置全部筛选」快捷按钮;
- 模块化拆分 AccountsPage:将 2100+ 行的大文件拆解至 pages/accounts/ 子目录(包含状态卡片、筛选面板、工具栏、操作弹窗与表格构建器),提升可维护性;
- 验证方式:新增 5 个后端自动化单测 (test_account_failure_and_export.py),全量 694 个测试用例通过,前端 TypeScript 编译与 pre-commit 代码规范检查全部通过。
2026-10-07 17:23:39 +08:00

73 lines
3.2 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:
return "email_code"
if "中断" in text or "已停止" in text or "任务已停止" in text:
return "interrupted"
if "密码" in text or "账号不存在" in text or "缺少密码或邮箱" in text:
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 "", "")