feat(db): S1 迁移 0042 —— 账号全生命周期落库(登录态 + 状态机 + 事件台账)

账号不再依赖登录记录的第一步:把「账号自己的东西」搬到账号上。本提交**只加列、
只读源表**,不改任何行为,单独部署安全。

accounts 新增 15 列:
- 登录态:cookie / cookie_updated_at / cookie_source
- CK 健康:ck_check_status / ck_check_result / ck_checked_at
- 重登进度:relogin_attempts / relogin_next_at / relogin_last_error / relogin_failure_kind
- 生命周期:status / status_reason / status_changed_at / disabled_at / archived_at
- 索引:ck_checked_at、status、与 (status, relogin_next_at)

account_events 新增 5 列(category / subject_type / subject_key / actor_type / actor_id)
与台账复合索引 (account_id, event_type, subject_key, occurred_at) —— 支撑
「365 天同皮肤」这类需要检索客体的规则,让事件表同时承担业务台账。

login_tasks 补 (account_id, attempt_status) 索引:CK 搬到账号后,「在途尝试」只能留在
登录记录上,巡检/重登每轮都要按它过滤。

回填做成生产级:
- 只读 login_tasks、分块 200 账号、幂等(只在 accounts.cookie 为空时写);
- 窗口函数取「最近一条持 CK 的行」,不用 GROUP_CONCAT(受 max_len 限制会静默截断);
- 自校验 fail closed:回填不完整直接抛错让应用起不来,宁可起不来也不带半份 CK 上线;
- 外加值级校验,防张冠李戴(条数相等仍可能 A 账号拿到 B 账号的 CK)。

踩坑留档:cookie 是 TEXT,**不能有 server_default**(MySQL 1101:
BLOB/TEXT/GEOMETRY/JSON column can not have a default value),模型与迁移两侧都要去掉。

验证:
- 空库整链 upgrade head + 模型对齐 + 索引存在 → 通过;
- 本机开发库实跑 0041→0042:记录侧 4 个持 CK 账号 = 账号侧 4 个,值级不一致 0,
  CK 仍是密文(enc:v1: 前缀),状态分布 active 4 / imported 8;
- downgrade 到 0041 再 upgrade:数据完整还原(这是 CK 丢失场景的安全网);
- 全量 pytest 364 passed / 6 skipped。
This commit is contained in:
yml2213
2026-10-04 13:45:59 +08:00
parent 3a227ffec1
commit e916a7015d
4 changed files with 432 additions and 1 deletions
+4 -1
View File
@@ -26,7 +26,7 @@ TESTS_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = TESTS_DIR.parent
VERSIONS_DIR = PROJECT_ROOT / "web" / "backend" / "migrations" / "versions"
HEAD_REVISION = "20261003_0041"
HEAD_REVISION = "20261003_0042"
# 迁移新增、但不声明在模型里的复合查询索引。
EXTRA_INDEXES = (
@@ -36,6 +36,9 @@ EXTRA_INDEXES = (
"ix_login_tasks_status_finished_at_id",
# 账号时间线按「账号 + 时间倒序」查,模型里不声明这个复合索引。
"ix_account_events_account_occurred",
# CK 搬到账号后,「在途尝试」只能留在登录记录上,巡检/重登每轮按
# (account_id, attempt_status) 过滤 —— 模型里不声明这个索引。
"ix_login_tasks_account_attempt_status",
)