`status` 列从「空壳」变成可信状态:S1 建了列、S2 让账号独立,本提交让状态真正
反映账号处境,并把「哪些账号要人处理」变成可筛选的事实。
## 状态机(7 → 6 个状态)
`imported` / `active` / `invalid` / `manual` / `disabled` / `archived`
**砍掉 `relogin`**:它完全等价于 `invalid + relogin_next_at is not None`,
独立成状态就多出一个必须与 `relogin_next_at` 同步的字段 —— 正是要消灭的「双份真相」。
## 唯一推导入口(防「双份真相」)
新增 `services/account_lifecycle.py`:
- `derive_status(account)` —— **纯函数**,按 cookie / ck_check_status / relogin_*
推导状态;人工态(disabled/archived)优先级最高,不被覆盖;
- `sync_status(db, account)` —— 推导 + 变了才写,**幂等**(没变则不写库、不发事件,
否则巡检每轮都往时间线刷一条);
- `set_manual_state(db, account, state)` —— 停用/归档/`auto`(恢复自动推导);
- `on_login_success(db, account)` —— **「登录成功对账号意味着什么」只定义一次**:
作废本轮重登退避痕迹 + 重算状态。于是**「待人工」的账号被人工登录救回来时状态
会自动恢复**,而不是卡在 manual。
- `MANUAL_KINDS` 从 `jobs/ck_relogin.py` 挪到这里(service 不该反向依赖 job)。
**9 处底层判据写入点全部接线**:登录成功、CK 清除(单个/批量)、ck_health 巡检、
人工 CK 检测、工作台执行前检测、重登复检、重登结算(成功/失败退避)。
## 消费端:补上一个真缺口 + 删掉两处临时派生
- ⚠️ **缺口**:`ck_health` / `ck_relogin` / `count_pending_relogin` 此前只按底层判据
选行,**没排除 disabled/archived** —— S2 提交里已标注,状态一上线就会变成
「停用的账号照样被自动重登」。现已全部排除。
- `automation.manual_queue` 原本用 4 个条件自己拼「需要人工」,现在一句
`status == 'manual'`。同一判据从「3 处各写一遍」收敛成「1 处推导」。
## 接口与前端
- `PUT /api/accounts/{id}/lifecycle`(disabled/archived/auto + 原因,写审计)
- `PUT /api/accounts/{id}/automation`(**接上死开关**:`auto_managed` 此前没有任何
接口/前端能改,永远是默认 True)
- 新增权限点 `account:manage`(运营默认拥有)
- 列表支持 `?status=` 筛选;**归档默认不出现**(要看需显式筛选)
- `AccountOut` / 概览接口补 status / status_label / status_reason / auto_managed
- 前端:账号列表加「状态」列与状态筛选;详情页状态卡 + 停用/归档/恢复/自动化开关
## 两个自查出来的问题(都已修)
1. **幂等键反而丢事件**:状态事件一开始用 `status:{id}:{target}:{秒级时间戳}` 作
`source_ref`,结果同一账号在同一秒内两次切到同一状态就撞唯一键,`emit` 静默吞掉
—— **状态改了、时间线却少一条**,正是本模块要消灭的「悄悄不一致」。
改为不传 `source_ref`:`sync_status` 本身已幂等,不存在重复写入路径。已加用例。
2. **迁移与运行时规则分叉**:迁移 0042 的状态回填 CASE 原本会写出已被砍掉的
`relogin`,且判据与 `derive_status` 不一致(次数用尽的会回填成 invalid 而非 manual)。
已改成逐字对应,并加一条用例**真跑那段 SQL 与纯函数逐条比对**(已验证:
临时改回旧 CASE 该用例立刻红)。
## 验证
- `TZ=Asia/Shanghai`:**404 passed / 6 skipped**(S2 后 373 → 新增 31 条)
- 迁移链 3 passed;ruff check/format 全绿;前端 `tsc -b && vite build` 通过
- 真实开发库核对:落库状态分布 active 4 / imported 8,**与 derive_status 逐账号比对无漂移**
- 新增用例覆盖:derive 真值表(每状态入口 + 人工态优先)、sync 幂等、
人工态切换、**全生命周期走查**(导入→登录→失效→重登→转人工→人工救回→清 CK→停用)、
自动化排除 disabled/archived、列表状态筛选、接口
未做(后续阶段):S4 事件发射点补齐、S5 业务事实直发事件、S6 账号页重构。
150 lines
4.8 KiB
Python
150 lines
4.8 KiB
Python
"""权限点定义与角色默认权限"""
|
|
|
|
# ---- 权限点 ----
|
|
PERMISSIONS = {
|
|
# 用户管理
|
|
"user:view": "查看用户列表",
|
|
"user:create": "创建用户",
|
|
"user:edit": "编辑用户",
|
|
"user:rename": "修改用户名",
|
|
"user:delete": "删除用户",
|
|
"user:assign_permissions": "分配权限",
|
|
# 账号管理
|
|
"account:view_all": "查看所有账号(仅用户名、标签、分配)",
|
|
"account:view_full": "查看账号完整字段(密码/邮箱,仅管理员)",
|
|
"account:view_assigned": "查看分配给自己的账号",
|
|
"account:import": "导入账号",
|
|
"account:tag": "修改账号标签",
|
|
"account:check": "账号检测",
|
|
"account:assign": "分配账号给客服",
|
|
"account:delete": "删除账号",
|
|
"account:manage": "停用/归档账号、开关自动化",
|
|
# 登录任务
|
|
"login:batch": "发起批量登录",
|
|
"login:view_all": "查看所有登录任务",
|
|
"login:view_assigned": "查看自己账号的登录任务",
|
|
# Cookie
|
|
"cookie:view": "查看 Cookie",
|
|
"cookie:operate": "检测/重登已分配账号 Cookie(不含查看)",
|
|
"cookie:export": "导出 Cookie",
|
|
# 斗鱼活动
|
|
"douyu:task": "斗鱼任务管理",
|
|
"douyu:config": "斗鱼配置管理",
|
|
"yyb:session": "应用宝扫码登录与角色查询",
|
|
"yyb:recharge": "应用宝充值与付款",
|
|
"yyb:manage": "接管并操作他人的应用宝任务",
|
|
"yyb:history": "查看应用宝充值历史",
|
|
# 虎牙
|
|
"huya:account": "虎牙账号管理(兼容旧权限)",
|
|
"huya:view_all": "查看所有虎牙账号",
|
|
"huya:view_assigned": "查看分配给自己的虎牙账号",
|
|
"huya:import": "导入/登录虎牙账号",
|
|
"huya:tag": "修改虎牙账号标签",
|
|
"huya:assign": "分配虎牙账号",
|
|
"huya:delete": "删除虎牙账号",
|
|
"huya:cookie:view": "查看虎牙 Cookie",
|
|
"huya:cookie:export": "导出/清除虎牙 Cookie",
|
|
"huya:task": "虎牙任务管理",
|
|
"huya:bind": "虎牙绑定操作",
|
|
"huya:recharge": "虎牙充值操作",
|
|
"huya:config": "虎牙配置管理",
|
|
# 代理 & 白名单
|
|
"proxy:manage": "代理配置管理",
|
|
"whitelist:manage": "白名单配置管理",
|
|
"whitelist:test": "测试白名单",
|
|
# 自动化
|
|
"automation:view": "查看自动化作业与运行历史",
|
|
"automation:manage": "修改自动化配置、手动触发作业",
|
|
# 系统
|
|
"system:settings": "系统设置",
|
|
"audit:view": "查看审计日志",
|
|
}
|
|
|
|
# ---- 角色默认权限 ----
|
|
ROLE_PERMISSIONS = {
|
|
"super_admin": list(PERMISSIONS.keys()), # 全部权限
|
|
"operation": [
|
|
"account:view_all",
|
|
"account:import",
|
|
"account:tag",
|
|
"account:check",
|
|
"account:assign",
|
|
"account:manage",
|
|
"login:batch",
|
|
"login:view_all",
|
|
"cookie:view",
|
|
"cookie:export",
|
|
"douyu:task",
|
|
"douyu:config",
|
|
"yyb:session",
|
|
"yyb:recharge",
|
|
"yyb:manage",
|
|
"yyb:history",
|
|
"huya:account",
|
|
"huya:view_all",
|
|
"huya:import",
|
|
"huya:tag",
|
|
"huya:assign",
|
|
"huya:delete",
|
|
"huya:cookie:view",
|
|
"huya:cookie:export",
|
|
"huya:task",
|
|
"huya:bind",
|
|
"huya:recharge",
|
|
"huya:config",
|
|
"proxy:manage",
|
|
"whitelist:manage",
|
|
"whitelist:test",
|
|
"automation:view",
|
|
],
|
|
"support": [
|
|
"account:view_assigned",
|
|
"account:tag",
|
|
"cookie:operate",
|
|
"douyu:task",
|
|
"yyb:session",
|
|
"yyb:history",
|
|
"huya:view_assigned",
|
|
"huya:tag",
|
|
"huya:task",
|
|
],
|
|
}
|
|
|
|
# 角色中文映射
|
|
ROLE_LABELS = {
|
|
"super_admin": "超级管理员",
|
|
"operation": "运营",
|
|
"support": "客服",
|
|
}
|
|
|
|
|
|
def get_role_permissions(role: str) -> list[str]:
|
|
"""获取角色拥有的权限列表。"""
|
|
return ROLE_PERMISSIONS.get(role, [])
|
|
|
|
|
|
def get_user_permissions(user) -> list[str]:
|
|
"""获取用户的实际权限列表。
|
|
优先使用用户自定义权限(custom_permissions),
|
|
若为 None 则回退到角色默认权限。
|
|
"""
|
|
if user.custom_permissions is not None:
|
|
return user.custom_permissions
|
|
return get_role_permissions(user.role)
|
|
|
|
|
|
def has_permission(role: str, permission: str) -> bool:
|
|
"""检查角色是否拥有某权限(仅检查角色默认权限,不考虑自定义权限)。
|
|
|
|
注意:对于需要考虑用户自定义权限的场景,请使用 user_has_permission()。
|
|
"""
|
|
return permission in get_role_permissions(role)
|
|
|
|
|
|
def user_has_permission(user, permission: str) -> bool:
|
|
"""检查用户是否拥有某权限(考虑自定义权限)。
|
|
|
|
优先使用用户的 custom_permissions,若为 None 则回退到角色默认权限。
|
|
"""
|
|
return permission in get_user_permissions(user)
|