# 平台 Admin 后台 + 注册邀请码 · 落地进度 > 计划:`C:/Users/Air-work/.claude/plans/gleaming-sleeping-ullman.md`(Phase 0–9 全做)。 > 每个 Phase 走完「★测试底座」(后端单测 + 无头浏览器 e2e + tsc/build + 回归)才 commit。 > 全程本地、不 push;测试服 DB 可动。 --- ## 测试基线(回归红线) - `apps.accounts / apps.billing / apps.products`:全绿(Phase 0 后 15/15 OK)。 - `apps.projects`:**进 Phase 0 前已有 3 failures + 1 error**(均为 AI provider `image_edit` mock 相关,与本工程无关)——**非本次引入**,Phase 0 改动仅在 accounts + 前端,未触碰 projects/ai。后续 Phase 不得新增 fail。 --- ## ✅ Phase 0 · 基础(角色 + 壳 + 审计)— 完成 2026-06-19 **后端** - `User.is_platform_admin`(bool)+ migration `0004_user_is_platform_admin_adminauditlog`(已 apply 到测试 MySQL)。 - `apps/accounts/permissions.py` · `IsPlatformAdmin`(超管放行 / 普通拒 / 停用拒)。 - `apps/accounts/management/commands/create_platform_admin.py` · 建 **admin/admin123**(幂等;`--reset-password` 才重置;也置 `is_staff`)。已对测试 MySQL 执行,真账号可登。 - `AdminAuditLog` 模型(operator/operator_name/action/target_*/before/after/ip)+ `apps/accounts/audit.py` `log_admin_action()`(失败不阻断主流程)。 - `me` / `login` 对**无团队的平台超管**优雅返回 `team=null`(新增 `current_team_or_none`);`UserSerializer` 暴露 `is_platform_admin`。 **前端** - 新 `routes/admin/admin-app.tsx`:超管后台外壳(折叠分组侧栏 6 组 11 项 + 顶栏 + 概览 + 占位页),全程复用 restraint 共享类。 - 路由:`route-config.ts` 解析 `/admin`、`/admin/
`;`App.tsx` 分流渲染 `AdminApp` + gating(超管无团队登录直落 /admin、非超管访问 /admin 纠回工作台)。 - `app-shell.tsx` 侧栏「平台」入口(仅超管可见)。 - `admin-page.css`(仅 token,无裸 hex)+ `IconKitSvg` 补 8 个 Lucide-line 图标。 **测试底座(全过)** - 后端单测:`apps.accounts` **11/11 OK**(新增 `PlatformAdminTests`:建超管命令幂等/权限类放行拒绝/登录+me 返回标志且 team 为空/普通注册非超管)。 - 无头 e2e:`core/qa/visual-parity/_admin-p0.mjs` —— **全部断言通过 + 0 console error**: - admin/admin123 真登录 → 落 /admin; - 超管壳:6 分组 / 11 导航 / `[ PLATFORM ADMIN ]` 徽标 / 概览标题 / 点「邀请码」→ /admin/invites 占位「模块即将上线」; - 普通用户访问 /admin → 纠回 /dashboard、admin 壳不渲染。 - 截图:`output/admin/p0-*.png`。 - 类型/构建:`tsc --noEmit` + `npm run build` 全绿。 - 视觉:概览截图核对 —— 冷灰底 + 单橙 accent + 8px 圆角 + 准星,符合 design.md 克制调性。 **测试账号**:平台超管 `admin/admin123`(测试 MySQL 已建);普通 demo `airshelf/Restraint2026`。 > **e2e 端口提示**:5173 被同机另一项目(AirDrama)抢占且不稳;AirShelf e2e 改用**专用端口 5188**(`npx vite --port 5188 --strictPort`)。e2e 脚本默认 BASE 已指向 5188。 --- ## ✅ Phase 1 · 注册邀请码逻辑 — 完成 2026-06-19 **后端** - `Invitation.kind`(join_team / create_team)+ `team` 可空 + migration `0005_invitation_kind_alter_invitation_team`(已 apply 测试 MySQL)。 - `RegisterSerializer`:**invite_code 必填**;按 kind 分流 —— create_team 开新团队当 owner + 发试用额度(回填 invite.team),join_team 幂等加入该团队、不重复发额度;**删除「空码=开团队」**。 - `validate-invite` 端点(免登录,POST `/api/auth/validate-invite/`):验码 → `{valid, kind, role, team_name?}`,无效始终 200+`valid:false`。 - 团管发码恒为 `kind=join_team`;`InvitationSerializer` 增 `kind`/`team`/`team_name`。 **前端** - 注册页重做(码优先):先填邀请码 → 点「验证」(或 Enter)→ 验过才展开;create_team 显示「团队名」输入,join_team 显示**只读团队名**(不暴露团队列表);CTA 文案随 kind 切换(创建团队 / 加入团队)。 - 邀请链接 `?invite=CODE` 进页自动回填 + 验码;改码即重置验证态。 - `api.validateInvite`;`field-note.ok/err` + `invite-verify-row` + 只读团队样式(仅 token)。 **测试底座(全过)** - 后端单测:`apps.accounts` **22/22 OK**(新增 `RegisterInviteCodeTests` 6 项 + `ValidateInviteTests` 5 项:无码拒/空码拒/create_team 开团队+试用额度/join_team 入伙不开新团队/已用·过期拒/验码返回正确 kind+team_name)。 - 无头 e2e:`_admin-p1.mjs`(端口 5188)—— **全断言过 + 0 console error**:create_team 验码→展开团队名→注册落工作台;join_team 验码→只读团队名「演示团队」→入伙;错码不展开+报错;URL 自动验码。截图 `output/admin/p1-*.png`。 - 类型/构建:`tsc --noEmit` + `npm run build` 全绿。 - 视觉:两态注册截图核对 —— 黑块等高、验证按钮对齐、绿色 ok 提示、只读团队名灰底,符合 restraint。 --- ## ✅ Phase 2 · 邀请码发放管理(Admin)— 完成 2026-06-19 **后端(新建 `apps/adminpanel/`)** - 新 Django app `apps.adminpanel`(跨团队后台端点容器),注册进 INSTALLED_APPS + 主 urls `path("api/admin/", ...)`。 - `GET/POST /api/admin/invitations/`:列所有邀请码(跨团队,按 kind/status/search 过滤 + 分页);POST 平台超管发 create_team「开团队码」(写审计 `invite.issue_create_team`)。 - `POST /api/admin/invitations//revoke/`:撤销待用码(写审计 `invite.revoke`)。 - 全程 `IsPlatformAdmin`,非超管 403。 **前端** - `adminApi`(api.ts 分层):invitations / createInvite / revokeInvite。 - Admin「邀请码」页(`routes/admin/admin-invites.tsx`):类型筛选段控(全部/开团队码/加入码)+ 表格(码/类型/团队/状态徽章/创建时间/使用者)+「发开团队码」弹窗 + 复制邀请链接(剪贴板 + 反馈)+ 撤销;接入 AdminApp(本地 toast notify)。 - `Invitation` 类型补 kind/team/team_name;admin-page.css 补段控/表格/弹窗样式(仅 token)。 **测试底座(全过)** - 后端单测:`apps.adminpanel` **9 项 + apps.accounts 22 = 31 OK**(列权限 403/发码+审计/跨团队列表/kind 过滤/撤销+审计/端到端凭发出的码注册开团队/撤销后码失效)。 - 无头 e2e:`_admin-p2.mjs`(5188)—— **6 断言全过 + 0 console error**:页面加载、发码弹窗、新码入列、复制链接反馈「已复制」、**端到端用 UI 发的码 API 注册开团队 201**。截图 `output/admin/p2-*.png`。 - 类型/构建:`tsc --noEmit` + `npm run build` 全绿。 - 视觉:列表截图核对 —— 类型/状态橙/中性 pill、段控黑激活、成功 toast,符合 restraint。 - 回归:accounts 22 仍全绿;live 后端自动 reload 后 `/api/admin/invitations/` 200(admin)/401(匿名)。 --- ## ✅ Phase 3 · 平台团队 + 用户管理(Admin)— 完成 2026-06-19 **后端(apps/adminpanel 扩展)** - 团队:`GET teams/`(列跨团队 + status/search 过滤分页,带 owner/成员数/余额)、`GET teams//`(详情含成员)、`POST teams//toggle/`(启停 + 审计)。 - 用户:`GET users/`(列跨团队 + 过滤分页,带所属团队)、`POST users//toggle/`(启停,停用清 token;**不允许停用平台超管**)、`POST users//reset-password/`(强制改密,清 token)。 - **团队停用拦登录**:`login` 校验当前团队 `status`,停用团队的成员一律拒登(平台超管无团队不受影响)。 - 新 `apps/adminpanel/serializers.py`(AdminTeam/AdminTeamMember/AdminUser);全程 IsPlatformAdmin + 写审计。 **前端** - `adminApi`:teams/teamDetail/toggleTeam/users/toggleUser/resetUserPassword;类型 AdminTeam/AdminTeamDetail/AdminUser。 - Admin「团队」页(状态筛选 + 搜索 + 表格 + 详情弹窗含成员 + 启停)、「用户」页(筛选 + 搜索 + 表格 + 启停 + 改密弹窗;超管行带「超管」标记且禁操作)。 - admin-page.css 补 search/宽弹窗/详情元信息样式(仅 token)。 **测试底座(全过)** - 后端单测:`apps.adminpanel` **19 项 + accounts 22 = 41 OK**(团队列字段/详情/启停拦登录再恢复/用户列含团队/启停拦登录/禁停超管/强制改密生效旧密失效/权限 403)。 - 无头 e2e:`_admin-p3.mjs`(5188,用一次性 P3 团队不动 demo)—— **7 断言全过 + 0 console error**:团队页+搜索+详情弹窗;**UI 停用团队→该成员 API 登录 400→启用→200**;用户页超管标记;**UI 改密→新密码登录 200**。截图 `output/admin/p3-*.png`。 - 类型/构建:`tsc --noEmit` + `npm run build` 全绿。 - 视觉:团队/用户表格、绿/红状态 pill、详情弹窗,符合 restraint。