文档更新

This commit is contained in:
Shen Wei
2026-05-02 11:35:20 +08:00
parent 464c5fce51
commit ca33cc141f
21 changed files with 5906 additions and 2908 deletions

View File

@@ -122,7 +122,7 @@ apps/property/
## 7. 客户端发布技术栈Desktop Client
> **完整方案**见:`TECH_STACK/客户端发布管理技术方案.md`(实现口径)与 `PRD/发布管理/客户端发布管理模块PRD.md`(需求口径)。本节仅列最终结论。
> **完整方案**见:`TECH_STACK/平台管理后台技术方案.md`(实现口径)与 `PRD/平台管理后台/平台管理后台PRD.md`(需求口径)。本节仅列最终结论。
- **框架**Electron稳定版 + Chromium 内核(随版本固定,不依赖系统浏览器)
- **渲染层**:直接加载 Fonrey Web URL**100% 复用 HTMX + Alpine + Tailwind**,渲染层零新增框架
@@ -146,8 +146,8 @@ apps/property/
| 客源管理 | [`客源管理技术方案.md`](./客源管理技术方案.md) | `PRD/客源管理/` | `DATA_MODEL/DATA_MODEL_CLIENT.md` | `tests/integration/client/test_us_client.py` | `v1.0` |
| 楼盘管理 | [`楼盘管理技术方案.md`](./楼盘管理技术方案.md) | `PRD/房源管理/`(含楼盘) | `DATA_MODEL/DATA_MODEL_COMPLEX.md` | `tests/integration/complex/test_us_complex.py` | `v1.0` |
| 组织人事 | [`组织人事技术方案.md`](./组织人事技术方案.md) | `PRD/组织人事管理/` | `DATA_MODEL/DATA_MODEL_ORG.md` | `tests/integration/org/test_us_org.py` | `v1.0` |
| 系统设置 | [`系统设置技术方案.md`](./系统设置技术方案.md) | `PRD/系统配置/``PRD/系统管理/` | `DATA_MODEL/DATA_MODEL_SETTING.md` | `tests/integration/setting/test_us_setting.py` | `v1.2` |
| 客户端发布 | [`客户端发布管理技术方案.md`](./客户端发布管理技术方案.md) | `PRD/发布管理/客户端发布管理模块PRD.md` | `DATA_MODEL/DATA_MODEL_PUBLIC.md``client_releases` / `client_heartbeats` | `tests/integration/release/test_us_release.py` | `v1.0` |
| 系统设置 | [`系统设置技术方案.md`](./系统设置技术方案.md) | `PRD/系统配置/` | `DATA_MODEL/DATA_MODEL_SETTING.md` | `tests/integration/setting/test_us_setting.py` | `v1.2` |
| 平台管理后台 | [`平台管理后台技术方案.md`](./平台管理后台技术方案.md) | `PRD/平台管理后台/平台管理后台PRD.md` | `DATA_MODEL/DATA_MODEL_PUBLIC.md``tenants` / `platform_admins` / `client_releases` / `client_heartbeats` / `tenant_backups` / `tenant_data_exports` / `audit_logs` / `feature_flags` | `tests/integration/admin_console/``tests/integration/release/test_us_release.py` | `v1.0` |
**总览数据模型**[`DATA_MODEL/DATA_MODEL.md`](../DATA_MODEL/DATA_MODEL.md)
**全局 API 契约**[`API_CONTRACT.md`](./API_CONTRACT.md)
@@ -191,7 +191,7 @@ apps/property/
| 楼盘管理 | `楼盘管理技术方案.md` | 15/15 | 完全覆盖 |
| 组织人事 | `组织人事技术方案.md` | 15/15 | 完全覆盖 |
| 系统设置 | `系统设置技术方案.md` | 15/15 | 完全覆盖 |
| 客户端发布 | `客户端发布管理技术方案.md` | 15/15 | 新增,已覆盖 Electron/EV/Heartbeat/自动升级/R2/官网下载/便携版 |
| 客户端发布 | `平台管理后台技术方案.md` | 15/15 | 已合并入「平台管理后台技术方案」(`ADR-20260502-002`覆盖 Electron/EV/Heartbeat/自动升级/R2/官网下载/便携版 |
### 9.3 使用规则(对 AI Agent 生效)

View File

@@ -1,330 +0,0 @@
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
# Fonrey 客户端发布管理技术方案
**版本**: 1.0
**项目**: Fonrey 房产经纪管理系统
**技术栈**: Django 4.x + HTMX + PostgreSQL 16public schema+ Redis + Celery + Electron + electron-updater + Cloudflare R2/CDN
**关联 PRD**: `PRD/发布管理/客户端发布管理模块PRD.md`v1.2
**关联数据模型**: `DATA_MODEL/DATA_MODEL_PUBLIC.md``client_releases` / `client_heartbeats`
**关联枚举字典**: `DATA_MODEL/ENUMS.md`(含中文显示标签)
**关联契约规范**: `TECH_STACK/API_CONTRACT.md`
**最后更新**: 2026-04-30
---
## 变更历史
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
## 一、文档定位与边界
本文件定义客户端发布管理模块(`apps/release`)的实现口径:
1. Electron 客户端壳应用与运行时安全边界
2. EV 代码签名与构建发布链路
3. Heartbeat 上报与版本分布统计方案
4. 自动升级、强制升级与失败回退策略
5. 客户端下载完整性校验SHA256
6. R2 版本资产管理(对象键、状态流转、回滚)
7. 公司下载站点(官网)分发方案
8. 便携版Portable ZIP落地方案
> 本文件不重复 DDL。表结构与索引以 `DATA_MODEL_PUBLIC.md` 为唯一权威。
---
## 二、范围定义(以 PRD v1.2 为准)
### 2.1 P0 必须覆盖
- Windows 客户端win32下载安装与登录使用
- 平台运营后台版本管理(草稿/发布/下线、普通/强制)
- 自动更新(启动 + 每 4 小时检测)
- SHA256 完整性校验EXE/ZIP
- Heartbeat启动时上报与版本分布/租户活跃统计
- 官方下载页(公司站点)
- 便携版 ZIP可选上传、受控分发
### 2.2 非目标(本期不做)
- macOS / Linux 客户端
- 移动端 App
- 客户端离线模式
- 客户端反逆向加固v2
---
## 三、模块架构边界
### 3.1 模块职责(`apps/release`
- 管理端:客户端版本元数据管理、发布状态流转、回滚
- 公共接口客户端更新检测、下载引导、Heartbeat 上报
- 统计接口:版本分布、租户活跃数、历史装机总数
- 资产管理R2 对象键规范、发布包引用与审计
### 3.2 分层与鉴权
| 子能力 | Schema | 鉴权 | 说明 |
|---|---|---|---|
| 版本管理后台 API | public | Platform Admin 必须认证 | 跨租户统一运营能力 |
| 更新检测 API | public | 公开(客户端调用) | 仅返回当前发布版本,不暴露草稿/下线版本 |
| Heartbeat API | public | 已登录客户端会话或签名设备票据 | 防伪造上报、防刷统计 |
| 统计 API | public | Platform Admin 必须认证 | 提供 Story 5 所需聚合指标 |
### 3.3 外部依赖
| 依赖 | 用途 |
|---|---|
| Electron + electron-updater | 客户端壳应用、更新下载与安装 |
| electron-builder | 打包 NSIS EXE + Portable ZIP |
| EV 代码签名证书 | Windows SmartScreen 信任 |
| Cloudflare R2 + CDN | 发布包存储与分发 |
| Celery | 异步计算 checksum / 文件扫描 / 可选预热 |
---
## 四、API 设计原则
1. **路径以 PRD 为准**:统一使用 `/api/release/...` 命名空间。
2. **只允许单一生效版本**:同 `platform + arch` 仅 1 条 `published`
3. **公开接口最小暴露**:客户端仅获取更新必要字段,不返回后台内部字段。
4. **统计可信优先**Heartbeat 仅“启动上报”,按 `(tenant_id, device_id)` Upsert。
5. **完整性优先于安装**:校验失败禁止安装,保留当前版本可用。
6. **强制更新可控**`release_type=force` + `min_required_version` 双保险。
---
## 五、端点清单(核心)
### 5.1 页面路由(平台运营后台)
| 路径 | 方法 | 鉴权 | 说明 |
|---|---|---|---|
| `/platform/release/updates/` | GET | Platform Admin | 版本列表页 |
| `/platform/release/updates/new/` | GET | Platform Admin | 新建版本页 |
| `/platform/release/updates/{id}/edit/` | GET | Platform Admin | 编辑版本页 |
| `/platform/release/metrics/` | GET | Platform Admin | 版本分布与租户活跃榜 |
### 5.2 JSON API对齐 PRD + 数据模型)
| 端点 | 方法 | 说明 |
|---|---|---|
| `/api/release/updates/latest/` | GET | 客户端检查最新版本(公开) |
| `/api/release/updates/` | GET | 管理端查询版本列表 |
| `/api/release/updates/` | POST | 管理端创建版本(草稿/发布) |
| `/api/release/updates/{id}/` | PATCH | 修改状态、版本类型、日志 |
| `/api/release/updates/{id}/rollback/` | POST | 回滚至历史版本(原子切换 published |
| `/api/release/heartbeats/` | POST | 客户端启动上报Upsert |
| `/api/release/metrics/version-distribution/` | GET | 版本活跃分布 |
| `/api/release/metrics/tenant-installs/` | GET | 指定租户活跃安装数 + 历史装机数 |
| `/api/release/metrics/tenant-leaderboard/` | GET | 全平台租户活跃榜 |
> 说明:`heartbeats/metrics` 为实现 Story 5 与 `DATA_MODEL_PUBLIC` 聚合查询所需端点,归属同一模块。
---
## 六、关键 API 规范(请求/响应)
### 6.1 更新检测
`GET /api/release/updates/latest/?platform=win32&arch=x64&current_version=1.2.0`
响应(有更新):
```json
{
"has_update": true,
"latest_version": "1.3.0",
"force_update": false,
"min_required_version": "1.0.0",
"download_url": "https://download.fonrey.com/releases/system/v1.3.0/fonrey-setup-1.3.0-win.exe",
"portable_url": "https://download.fonrey.com/releases/system/v1.3.0/fonrey-portable-1.3.0-win.zip",
"checksum_sha256": "<exe_sha256>",
"portable_checksum_sha256": "<zip_sha256>",
"file_size_bytes": 157286400,
"release_notes": "## v1.3.0\n- ...",
"release_date": "2026-05-01"
}
```
### 6.2 Heartbeat 上报
`POST /api/release/heartbeats/`
```json
{
"device_id": "9e6de37b-8c49-4f9b-af47-52f4e5b8b7f2",
"client_version": "1.3.0",
"platform": "win32",
"arch": "x64",
"os_version": "Windows 11 23H2"
}
```
处理要求:
- 服务端从登录上下文解析 `tenant_id``user_id`
- 使用 `INSERT ... ON CONFLICT (tenant_id, device_id) DO UPDATE`
- 更新 `last_seen_at``launch_count = launch_count + 1`
### 6.3 版本发布(管理端)
`POST /api/release/updates/`
```json
{
"version": "1.3.0",
"platform": "win32",
"arch": "x64",
"release_type": "normal",
"min_required_version": "1.0.0",
"download_url": "https://download.fonrey.com/releases/system/v1.3.0/fonrey-setup-1.3.0-win.exe",
"portable_url": "https://download.fonrey.com/releases/system/v1.3.0/fonrey-portable-1.3.0-win.zip",
"checksum_sha256": "<exe_sha256>",
"portable_checksum_sha256": "<zip_sha256>",
"release_notes": "## v1.3.0\n- ...",
"status": "published"
}
```
---
## 七、HTMX 交互约定(平台运营后台)
- 列表筛选(状态/版本号)使用 HTMX 局刷表格区域
- 发布/下线/回滚操作使用确认弹窗 + 局部刷新
- 成功:`HX-Trigger: toast-success`
- 失败:`HX-Trigger: toast-error`(同时返回标准错误码)
模板建议:
- `templates/release/updates_list.html`
- `templates/release/fragments/updates_table.html`
- `templates/release/fragments/version_distribution_chart.html`
- `templates/release/fragments/tenant_leaderboard_table.html`
---
## 八、权限与数据范围
### 8.1 最小权限矩阵
| 能力 | permission_code |
|---|---|
| 客户端版本列表查看 | `platform.release.view.allow` |
| 创建/编辑发布版本 | `platform.release.edit.allow` |
| 发布/下线/回滚 | `platform.release.publish.allow` |
| 版本分布与租户统计查看 | `platform.release.metrics.view.allow` |
### 8.2 范围规则
- 所有管理能力仅 Platform Admin 可访问
- Tenant Admin / Agent 不可访问平台发布后台
- 数据物理存储在 public schema逻辑上属于平台级共享数据
---
## 九、异步任务与缓存策略
### 9.1 异步任务
| 任务 | 触发时机 | 说明 |
|---|---|---|
| `release_compute_checksum_task` | 文件上传后 | 计算 EXE/ZIP SHA256 并回填 |
| `release_publish_cdn_warmup_task` | 版本发布后 | 可选,预热热点下载节点 |
| `release_scan_artifact_task` | 文件上传后 | 可选,执行恶意文件扫描与审计 |
### 9.2 Redis Key 建议
| Key | TTL | 说明 |
|---|---|---|
| `release:latest:{platform}:{arch}` | 60s | 最新发布版本缓存 |
| `release:metrics:version_distribution` | 60s | 版本分布聚合缓存 |
| `release:metrics:tenant:{tenant_id}` | 60s | 单租户安装/活跃统计缓存 |
| `release:download:ratelimit:{ip}` | 60s | 下载链接接口限流 |
---
## 十、性能与可靠性约束
- 更新检测接口:`p95 < 120ms`(缓存命中)
- Heartbeat 写入:`p95 < 80ms`
- 版本列表页:`p95 < 200ms`
- 发布状态切换使用事务,保证“下线旧版 + 发布新版”原子完成
- 任意更新失败不影响当前版本继续运行(可恢复原则)
---
## 十一、安全与合规
1. Electron 必须启用:`contextIsolation=true``nodeIntegration=false``sandbox=true`
2. 更新包仅允许 HTTPS 下载;域名白名单固定为 `download.fonrey.com`
3. EV 证书私钥仅在 CI 密钥库中可用,禁止落盘到开发机。
4. 校验值由服务端生成并签名传输(至少 TLS + 服务端可信源)。
5. Heartbeat 接口必须防重放/防刷(鉴权 + 频控 + 审计)。
6. 管理端操作(发布、回滚、下线)全部记录审计日志。
---
## 十二、错误码建议
| code | HTTP | 中文含义 |
|---|---|---|
| `RELEASE_VERSION_INVALID` | 400 | 版本号不符合 SemVer |
| `RELEASE_PUBLISHED_CONFLICT` | 409 | 当前平台架构已存在发布版本 |
| `RELEASE_ARTIFACT_NOT_FOUND` | 404 | 发布包不存在或不可访问 |
| `RELEASE_CHECKSUM_MISMATCH` | 400 | 安装包完整性校验失败 |
| `RELEASE_HEARTBEAT_INVALID` | 400 | 心跳参数非法 |
| `RELEASE_PERMISSION_DENIED` | 403 | 权限不足 |
| `RELEASE_RATE_LIMITED` | 429 | 请求过于频繁 |
---
## 十三、测试映射P0
| Story | 测试关注点 |
|---|---|
| Story 1 下载安装 | 官网下载链接可用、签名有效、安装步骤 ≤3、首次启动直达登录 |
| Story 2 客户端使用 | Chromium 内核能力、HTMX/Alpine/Tailwind 渲染一致性、文件上传下载 |
| Story 3 自动升级 | 启动 + 4h 检测、普通更新/强制更新分支、失败可恢复 |
| Story 4 发布管理 | 版本创建/发布/下线/回滚、唯一 published 约束 |
| Story 5 版本分布 | Heartbeat Upsert、活跃统计24h、租户活跃榜排序 |
测试文件建议:`tests/integration/release/test_us_release.py`
---
## 十四、落地顺序建议
1. `apps/release` 模型/服务/API 基础骨架(先打通 `/api/release/updates/latest/`
2. 平台运营后台版本管理页(列表 + 新建 + 发布/下线)
3. Electron 壳应用最小可运行版本(加载 Web + 标题版本)
4. 自动更新链路electron-updater + 后端 latest API
5. Heartbeat 上报 + 统计 API + 后台图表
6. 官网下载页上线(公司域名)
7. 便携版 ZIP 与企业无安装权限场景验收
---
## 十五、文档同步规则
- PRD 发布管理模块变更:同步本文件
- `client_releases/client_heartbeats` 字段变更:同步 `DATA_MODEL_PUBLIC.md`
- 枚举值变更:同步 `DATA_MODEL/ENUMS.md`(含中文标签)
- API 包络/错误契约变更:同步 `TECH_STACK/API_CONTRACT.md`
- 若将来新增独立测试用例文档:同步 `TECH_STACK/测试规范.md` 与测试用例注册表
---
## 附:你关注的 8 个专题落地结论
1. **Electron 方案**:采用 Electron + electron-updater客户端坚持“壳应用”原则。
2. **EV 证书方案**CI 自动签名,证书私钥仅在密钥管理系统。
3. **Heartbeat 方案**:仅启动上报,`(tenant_id, device_id)` Upsert支撑活跃与版本分布。
4. **自动升级方案**:启动 + 每 4h 轮询,普通/强制双模式,失败可恢复。
5. **完整性验证**:下载后先 SHA256 校验,再安装;失败禁止覆盖当前版本。
6. **R2 版本管理**`releases/system/v{version}/...` 路径规范,发布状态驱动可见性。
7. **公司网站下载**`download.fonrey.com` 静态下载页 + 版本信息 + 更新日志。
8. **便携版实现**electron-builder 输出 ZIP首次运行写入用户目录配置不修改系统级安装。

View File

@@ -0,0 +1,986 @@
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
# Fonrey 平台管理后台技术方案
**版本**: v1.0
**项目**: Fonrey 房产经纪管理系统
**模块**: 平台管理后台(`apps/admin_console` + `apps/release`
**关联 PRD**: [`PRD/平台管理后台/平台管理后台PRD.md`](../PRD/平台管理后台/平台管理后台PRD.md)v1.0
**关联数据模型**: [`DATA_MODEL/DATA_MODEL_PUBLIC.md`](../DATA_MODEL/DATA_MODEL_PUBLIC.md)
**关联 ADR**: `ADR-20260502-001``ADR-20260502-002``ADR-20260430-006``ADR-20260430-007``ADR-20260430-008``ADR-20260430-009`
**最后更新**: 2026-05-02
> **关键定位**:本文件是「平台管理后台」的统一技术方案,**取代**已删除的 `客户端发布管理技术方案.md` 与 `系统管理技术文档.md`(详见 `ADR-20260502-002`。两份原文档涉及的全部技术口径API 命名空间、`client_heartbeats` 表结构、SHA256 校验、Argon2id、TOTP、`admin_ops` 队列、`pub:` 缓存前缀、`django.contrib.admin` 全环境弃用等)原样保留并整合到本文件,无任何技术决策变更。
---
## 变更历史
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-05-02 | Sisyphus | 初版:合并原『客户端发布管理技术方案.md』与原『系统管理技术文档.md』统一三大维度技术选型 / 页面路由表 / API 设计),新增 `ADR-20260502-002` |
---
## 1. 文档定位与边界
### 1.1 范围
本文件覆盖「平台管理后台」全部实现口径,受众为所有为该后台编码、测试、运维的工程师与 AI Agent
- 技术选型Django CBV + django-tenantspublic schema + HTMX + Alpine.js + Tailwind + Celery + Redis + R2 + Electron + electron-updater
- 页面路由表16 张页面 + HTMX Partial 子路由 + 路由守卫 Mixin 链 + 懒加载约定
- API 设计:双命名空间(`/admin/...` 后台业务 + `/api/release/...` 客户端运行时的具体路径、HTTP 方法、请求/响应、错误码、版本控制、认证方式
- 安全与合规MFA 强制、IP 白名单、CSRF、CSP、审计不可变、Django Admin 全环境弃用
- 异步任务、缓存策略、文件上传、监控集成、测试规范、部署规范
### 1.2 边界
- **本文件不重复 DDL**。`tenants` / `domains` / `tenant_status_logs` / `platform_admins` / `admin_mfa_devices` / `admin_sessions` / `ip_whitelist` / `platform_audit_logs` / `backup_schedules` / `backup_records` / `export_tasks` / `system_versions` / `upgrade_events` / `client_releases` / `client_heartbeats` / `feature_flag_definitions` / `feature_flag_change_log`(合计 17 张表)字段以 `DATA_MODEL_PUBLIC.md` 为唯一权威。
- **本文件不描述租户业务模块**`apps.property` / `apps.client` 等),仅在跨域操作(备份、恢复、导出、升级编排)中通过 `with schema_context(tenant.schema_name):` 显式切换 schema。
- **本文件不重复 PRD 业务规则**。租户状态机、角色权限矩阵、页面清单的产品口径见 PRD §5.4 / §6 / §7。
### 1.3 与原两份文档的对应关系
| 原文档章节 | 本文件章节 |
|---|---|
| 原『系统管理技术文档.md』§1 模块边界 | §2.1、§2.2、§2.4 |
| 原『系统管理技术文档.md』§2 目录结构 | §2.3 |
| 原『系统管理技术文档.md』§3 路由命名空间 | §2.5、§3 |
| 原『系统管理技术文档.md』§4 API 端点设计 | §3、§4.1 |
| 原『系统管理技术文档.md』§5 权限与认证 | §3.4、§7.1§7.3 |
| 原『系统管理技术文档.md』§6 缓存策略 | §6.2 |
| 原『系统管理技术文档.md』§7 文件上传 | §6.3 |
| 原『系统管理技术文档.md』§8 Celery + 升级 A/B/C | §6.1、§6.4§6.5 |
| 原『系统管理技术文档.md』§9 监控集成 | §8 |
| 原『系统管理技术文档.md』§10 测试规范 | §9 |
| 原『系统管理技术文档.md』§11 安全要点 | §7 |
| 原『系统管理技术文档.md』§12 部署规范 | §10 |
| 原『客户端发布管理技术方案.md』§3 模块架构 | §2.6 |
| 原『客户端发布管理技术方案.md』§5 端点清单 | §4.2 |
| 原『客户端发布管理技术方案.md』§6 关键 API | §4.2 |
| 原『客户端发布管理技术方案.md』§9 异步与缓存 | §6.1、§6.2 |
| 原『客户端发布管理技术方案.md』§10 性能 | §5.4 |
| 原『客户端发布管理技术方案.md』§11 安全 | §7.4 |
| 原『客户端发布管理技术方案.md』§12 错误码 | §4.4 |
---
## 2. 技术选型
### 2.1 核心技术栈
| 层级 | 选型 | 用途 | 选型理由 |
|---|---|---|---|
| **路由 + 视图** | Django 4.xASGI+ Class-Based Views | 后端路由、页面渲染、JSON API | 与租户业务同栈,复用 `django-tenants` schema 切换CBV Mixin 组合权限/审计/MFA |
| **多租户编排** | `django-tenants` 1.4+`SHARED_APPS` | `public` schema 注册、`schema_context()` 切换 | 物理 schema 隔离 + 后台无感切换 |
| **前端交互** | HTMX 1.9+ | 局部刷新、表单提交、轮询 | 无重前端框架AGENTS.md §5单进程返回 partial HTML |
| **前端状态** | Alpine.js 3.x | 弹窗开关、Tab 切换、MFA Modal、表单字数统计 | 轻量、属性式声明,配合 CSP `script-src 'self'` |
| **样式** | Tailwind CSS 3.x | 全部样式 | 与租户业务共用设计系统 |
| **REST API客户端** | Django Views手写 JSON | `/api/release/...` 客户端运行时接口 | 端点少(≤ 10 个)、无需 DRF 全套;`JsonResponse` + 手写序列化即可 |
| **认证(后台)** | 自建 `PlatformAdminBackend` + Django Session + Argon2id + django-otp/TOTP | 平台管理员独立账号体系 | 不复用 `django.contrib.auth.User`;强制 MFA |
| **认证(客户端)** | 设备签名票据Token in Header | 客户端 Heartbeat 鉴权 | 防伪造上报;与租户登录态解耦 |
| **数据库** | PostgreSQL 16 + PgBouncer | 数据落 `public` schema`SHARED_APPS` | 与租户业务同实例不同 schema |
| **缓存** | Redis | 后台 session 反查、IP 白名单、任务进度、版本分布聚合 | Key 前缀 `pub:`,与租户业务 `{schema}:` 严格隔离 |
| **异步任务** | Celery 5.x + Celery Beat | 备份/恢复/导出/升级编排/Heartbeat 聚合 | 独立队列 `admin_ops` + `migration` 双队列 |
| **对象存储** | Cloudflare R2S3 兼容) | 升级包 / 备份产出 / 导出产出 / 客户端安装包 | 后端写入,禁用前端直传(合规 + SHA256 完整性) |
| **CDN** | Cloudflare CDN | 客户端安装包分发 `download.fonrey.com` | 与 R2 原生集成 |
| **客户端壳应用** | Electron + electron-updater + electron-builder | Windows 桌面客户端 | 壳应用原则:不内嵌业务逻辑,仅渲染 Web URL |
| **代码签名** | EV Code Signing Certificate | 客户端 EXE / ZIP 签名 | Windows SmartScreen 信任 |
| **完整性校验** | SHA-256 | 客户端安装包校验(强制) | 详见 `ADR-20260430-008` |
| **服务器** | Gunicorn + Uvicorn workers + Nginx | ASGI 部署 | 与租户应用共用进程,按 `Host` 路由 |
| **监控** | Sentry独立 DSN+ Grafana iframe + Flower | 错误追踪 + 平台指标 + Celery 队列健康 | 与租户业务监控分离 |
**禁止项(违反视为 Bug**
- ❌ 引入 Django REST Framework 仅为本模块(端点少,开销过大)。
- ❌ 引入 React/Vue/Angular 等重前端AGENTS.md §5
- ❌ 注册 `django.contrib.admin`(全环境弃用,详见 §2.4)。
- ❌ 复用 `django.contrib.auth.User` 作为平台管理员主体(必须独立 `platform_admins`)。
- ❌ 客户端渲染进程开启 `nodeIntegration: true`(壳应用安全边界)。
- ❌ 前端直传 Presigned URL 上传升级包/备份/导出(必须后端中转 + SHA-256 校验)。
### 2.2 部署边界
| 维度 | 说明 |
|---|---|
| 部署域名 | `admin.fonrey.com`独立子域Nginx 层 IP 白名单 + 应用层 `IpWhitelistMiddleware` 双重保险) |
| Schema 归属 | `public``SHARED_APPS`),所有 ORM 查询走 `public_schema_urlconf` |
| 客户端运行时域名 | `download.fonrey.com`CDN 边缘)+ 业务接口走 `app.fonrey.com/api/release/...` |
| URL 前缀(后台业务) | `/admin/...` |
| URL 前缀(客户端 API | `/api/release/...`(沿用 `ADR-20260430-009` |
| Celery 队列 | `admin_ops`(默认)、`migration`(独立限并发,仅 B 类升级使用) |
| Cookie 域 | `admin.fonrey.com`Strict禁止跨子域|
### 2.3 Django App 与目录结构
本后台跨两个 App均在 `SHARED_APPS`
- `apps/admin_console/` — 系统管理主体(租户/备份/导出/升级/审计/告警/平台管理员设置/Feature Flag
- `apps/release/` — 客户端发布(系统版本元数据、客户端 Heartbeat、版本分布统计、自动更新接口
两者共用:`apps.admin_console.permissions`(角色 Mixin`apps.admin_console.middleware`IP 白名单 + Session`apps.admin_console.services.audit_service`(统一审计入口)。`apps/release` **不得**反向依赖 `apps.admin_console.views`,仅依赖其权限与审计基础件。
```
apps/admin_console/
├── apps.py
├── urls.py # 注册到 PUBLIC_SCHEMA_URLCONFnamespace='admin_console'
├── auth_backends.py # PlatformAdminBackend独立认证
├── middleware.py # IpWhitelistMiddleware / AdminSessionMiddleware
├── permissions.py # AdminRole 枚举 / Mixin / ACTION_REQUIRED_ROLE
├── signals.py
├── forms.py
├── models/ # tenant / platform_admin / audit / backup / export / version / feature_flag
├── views/ # 全部 CBVauth/dashboard/tenants/backups/exports/versions/monitoring/audit/settings/feature_flags
├── tasks/ # tenant_lifecycle / backup / restore / export / upgrade / notifications / housekeeping
├── services/ # tenant_service / audit_service / mfa_service / permission_service / backup_service / version_service / feature_flags
├── tests/
└── templates/admin_console/ # base.html + 各页面 + partials/
apps/release/
├── apps.py
├── urls.py # 同时挂到 /admin/client-releases/(后台 UI与 /api/release/(客户端 API
├── models/ # client_release / client_heartbeat
├── views/
│ ├── admin.py # 后台 CBV与 admin_console 同款 Mixin 链)
│ └── api.py # 客户端 JSON APIlatest / heartbeats / metrics
├── serializers.py # 极简 dataclass + asdict(),不引入 DRF
├── tasks/ # release_compute_checksum / release_publish_cdn_warmup / release_scan_artifact
├── services/ # release_service / heartbeat_service / metrics_service
├── tests/
└── templates/release/ # 后台 UI partials与 admin_console templates/ 同风格)
```
**目录约定**
- `models/` 一表一文件。
- `views/` 全部 CBV`ListView` / `DetailView` / `FormView` / `View`);禁止函数视图。
- `tasks/` 是 Celery 入口(薄壳),业务逻辑落在 `services/`,便于单测。
- `templates/.../partials/` 命名以 `partials/` 区分完整页 vs HTMX 局部模板。
### 2.4 与 `django.contrib.admin` 的关系(强制全环境弃用)
理由(沿用原『系统管理技术文档.md』§1.5
| 冲突点 | 说明 |
|---|---|
| 多租户编排 | Django Admin 假设单 schema无 schema 切换钩子 |
| 认证体系 | Admin 强绑定 `auth.User`;本模块要求独立 `platform_admins` + 强制 TOTP |
| 审计强度 | Admin `LogEntry` 允许 UPDATE/DELETE且不覆盖读操作 |
| 交互范式 | Admin 模板整页刷新;本模块要求 HTMX 局刷 + Alpine 二次确认 Modal |
| 业务流页面 | 升级灰度进度、备份恢复 MFA step-up、监控大盘等无法用 ModelAdmin 表达 |
**强制措施**
- `INSTALLED_APPS` 不注册 `django.contrib.admin`
- `urls_public.py` 不导入 `django.contrib.admin`,无 `admin.site.urls` 路由。
- `config/settings/base.py` 启动断言:
```python
assert 'django.contrib.admin' not in INSTALLED_APPS, \
"Django Admin 已全环境弃用,平台后台请走 apps.admin_console"
```
- CI 检查:`grep -rn "from django.contrib import admin\|admin.site.register" apps/ config/` 命中即构建失败。
- 紧急数据修复一律走 `manage.py shell_plus` + 由超管在本模块 `/admin/audit-logs/` 手工补录审计条目(`source='manual_shell'`**不开后门**。
### 2.5 Settings 关键配置
```python
# config/settings/base.py
SHARED_APPS = [
'django_tenants',
'apps.tenant',
'apps.admin_console',
'apps.release',
'django.contrib.contenttypes',
'django.contrib.staticfiles',
# 注意:不注册 'django.contrib.admin'
]
assert 'django.contrib.admin' not in SHARED_APPS, \
"Django Admin 已全环境弃用,平台后台请走 apps.admin_console"
PUBLIC_SCHEMA_URLCONF = 'config.urls_public' # 平台后台 URL
ROOT_URLCONF = 'config.urls_tenant' # 租户业务 URL
ADMIN_CONSOLE_HOSTS = ['admin.fonrey.com', 'admin.localhost']
RELEASE_DOWNLOAD_HOST = 'download.fonrey.com'
CELERY_TASK_ROUTES = {
'apps.admin_console.tasks.*': {'queue': 'admin_ops'},
'apps.release.tasks.*': {'queue': 'admin_ops'},
'apps.admin_console.tasks.upgrade.migrate_single_tenant': {'queue': 'migration'},
'apps.admin_console.tasks.upgrade.rollback_single_tenant': {'queue': 'migration'},
}
# Cookie / CSRF / CSP
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Strict'
SESSION_COOKIE_DOMAIN = '.fonrey.com' # 但 admin Cookie 走独立 Cookie 名 + 限定 admin.fonrey.com详见 §7.2
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = False # HTMX 需读取
CSRF_COOKIE_SAMESITE = 'Strict'
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
# 不允许降级 PBKDF2/SHA1
]
```
### 2.6 与租户业务的隔离原则
- ❌ 严禁本模块代码导入 `apps.property` / `apps.client` / `apps.org` 等租户 App 的 Model。
- ❌ 严禁本模块视图、任务直接访问租户 schema 中的表。
- ✅ 跨租户数据操作(备份、恢复、导出、升级编排)必须 `with schema_context(tenant.schema_name):` 显式切换。
- ✅ Celery 任务必须在参数中传入 `tenant_schema_name`,任务开头切换 schema**不得依赖外部上下文传递**AGENTS.md §4.1)。
---
## 3. 页面路由表
### 3.1 路由组织与注册
```python
# config/urls_public.py
from django.urls import path, include
# 严禁 from django.contrib import admin
urlpatterns = [
path('admin/', include(('apps.admin_console.urls', 'admin_console'), namespace='admin_console')),
path('admin/client-releases/', include(('apps.release.admin_urls', 'release_admin'), namespace='release_admin')),
path('api/release/', include(('apps.release.api_urls', 'release_api'), namespace='release_api')),
]
```
`apps.admin_console.urls` 内顶层 `app_name = 'admin_console'`;反向解析使用 `admin_console:tenants:list` 等命名空间。
### 3.2 16 张主页面路由表(与 PRD §5.4.1 对齐)
| # | PRD 页面 | 后端 URL 模式 | 视图类 | 模板 | 路由守卫Mixin 链) |
|---|---|---|---|---|---|
| 1 | 登录页 | `/admin/login/` | `AdminLoginView`GET/POST| `admin_console/auth/login.html` | 匿名(中间件 `IpWhitelistMiddleware` 兜底) |
| 1.5 | MFA 校验 | `/admin/login/mfa/` | `MfaChallengeView` | `admin_console/auth/mfa_challenge.html` | 已通过密码校验session 标志) |
| 1.6 | MFA 首次绑定 | `/admin/login/mfa/setup/` | `MfaSetupView` | `admin_console/auth/mfa_setup.html` | 首登态(`platform_admin.mfa_enabled=False` |
| 1.7 | MFA Step-up | `/admin/login/mfa/step-up/` | `MfaStepUpView`POST| —(仅写 session.mfa_confirmed_at | `AdminLoginRequiredMixin` |
| 2 | 仪表盘 | `/admin/` | `DashboardView` | `admin_console/dashboard.html` | `AdminLoginRequiredMixin` |
| 3 | 租户列表 | `/admin/tenants/` | `TenantListView` | `admin_console/tenants/list.html` | `AdminLoginRequiredMixin` |
| 4 | 新建租户 | `/admin/tenants/new/` | `TenantCreateView` | `admin_console/tenants/create.html` | `RoleRequiredMixin(OPS)` + `AuditedActionMixin` |
| 5 | 租户详情:基本信息 | `/admin/tenants/<uuid:pk>/` | `TenantDetailView` | `admin_console/tenants/detail.html` | `AdminLoginRequiredMixin` |
| 6 | 租户详情:用户管理 | `/admin/tenants/<uuid:pk>/users/` | `TenantUserTabView` | `admin_console/tenants/detail.html`Tab| 同上 |
| 7 | 租户详情:套餐信息 | `/admin/tenants/<uuid:pk>/plan/` | `TenantPlanTabView` | 同上 | 同上 |
| 8 | 租户详情:监控 | `/admin/tenants/<uuid:pk>/monitoring/` | `TenantMonitoringTabView` | 同上 | 同上 |
| 9 | 租户详情:备份记录 | `/admin/tenants/<uuid:pk>/backups/` | `TenantBackupTabView` | 同上 | 同上(恢复操作另需 `MfaConfirmedRequiredMixin + RoleRequiredMixin(SUPER)` |
| 10 | 租户详情:操作历史 | `/admin/tenants/<uuid:pk>/history/` | `TenantHistoryTabView` | 同上 | 同上 |
| 11 | 系统版本管理 | `/admin/system/versions/` | `SystemVersionListView` | `admin_console/versions/list.html` | `AdminLoginRequiredMixin`(写操作另需 `RoleRequiredMixin(SUPER) + MfaConfirmedRequiredMixin`|
| 12 | 备份管理 | `/admin/system/backups/` | `BackupListView` | `admin_console/backups/list.html` | `AdminLoginRequiredMixin` |
| 13 | 监控与告警 | `/admin/monitoring/` | `MonitoringView` | `admin_console/monitoring/index.html` | `AdminLoginRequiredMixin` |
| 14 | **客户端版本管理** | `/admin/client-releases/` | `ClientReleaseListView` | `release/admin/list.html` | `AdminLoginRequiredMixin`(写操作另需 `RoleRequiredMixin(SUPER) + MfaConfirmedRequiredMixin`|
| 15 | 审计日志 | `/admin/audit-logs/` | `AuditLogListView` | `admin_console/audit/list.html` | `ReadOnlyAuditorAllowedMixin`(审计员可读) |
| 16 | 管理员设置 | `/admin/settings/admins/` | `AdminAccountListView` | `admin_console/settings/admins.html` | `RoleRequiredMixin(SUPER)` |
> 动态参数:`<uuid:pk>` 用于租户 ID、版本 ID、备份 ID、导出任务 ID、客户端发布 ID`<uuid:event_id>` 用于升级事件 ID。统一使用 UUID v4禁止自增整数AGENTS.md §4.4)。
### 3.3 HTMX Partial 子路由(页面内局部刷新)
> 命名约定:`<父页面>/rows/`(列表筛选/翻页)、`<父页面>/<id>/<动作>/`(行内动作)。所有 partial 视图必须校验 `request.htmx``django-htmx`),非 HTMX 直访返回 404 或重定向到父页。
#### 3.3.1 仪表盘(懒加载 + 轮询)
| URL | 方法 | 视图 | 触发 | 响应 |
|---|---|---|---|---|
| `/admin/dashboard/health/` | GET | `HealthStatusPartialView` | `hx-trigger="load, every 30s"` 轮询服务健康 | HTML(Partial) |
| `/admin/dashboard/recent-actions/` | GET | `RecentActionsPartialView` | `hx-trigger="revealed"` 懒加载(首屏不阻塞) | HTML(Partial) |
| `/admin/dashboard/stats/` | GET | `DashboardStatsPartialView` | `hx-trigger="load"` 单次加载 | HTML(Partial) |
| `/admin/dashboard/client-coverage/` | GET | `ClientCoveragePartialView` | `hx-trigger="revealed"` 懒加载 | HTML(Partial) |
#### 3.3.2 租户管理(详情页 Tab 与行内动作)
| URL | 方法 | 视图 | 触发 | 权限 |
|---|---|---|---|---|
| `/admin/tenants/rows/` | GET | `TenantRowsPartialView` | HTMX 筛选/翻页/搜索 | `AdminLoginRequiredMixin` |
| `/admin/tenants/<uuid:pk>/edit/` | POST | `TenantUpdateView` | 内联编辑 | `RoleRequiredMixin(OPS)` |
| `/admin/tenants/<uuid:pk>/suspend/` | POST | `TenantSuspendView` | 挂起 | `RoleRequiredMixin(OPS)` |
| `/admin/tenants/<uuid:pk>/resume/` | POST | `TenantResumeView` | 恢复 | `RoleRequiredMixin(OPS)` |
| `/admin/tenants/<uuid:pk>/soft-delete/` | POST | `TenantSoftDeleteView` | 软删除 | `RoleRequiredMixin(OPS)` |
| `/admin/tenants/<uuid:pk>/hard-delete/` | POST | `TenantHardDeleteView` | 硬删除 | `MfaConfirmedRequiredMixin + RoleRequiredMixin(SUPER)` |
| `/admin/tenants/<uuid:pk>/restore-deletion/` | POST | `TenantRestoreDeletionView` | 撤销软删除 | `RoleRequiredMixin(OPS)` |
| `/admin/tenants/<uuid:pk>/users/<uuid:user_id>/reset-password/` | POST | `TenantUserResetPasswordView` | 重置租户用户密码 | `RoleRequiredMixin(OPS)` |
| `/admin/tenants/<uuid:pk>/admins/grant/` | POST | `TenantAdminGrantView` | 赋予 Tenant Admin | `RoleRequiredMixin(OPS)` |
| `/admin/tenants/<uuid:pk>/admins/revoke/` | POST | `TenantAdminRevokeView` | 撤销 Tenant Admin | `RoleRequiredMixin(OPS)` |
| `/admin/tenants/<uuid:pk>/plan/upgrade/` | POST | `TenantPlanUpgradeView` | 套餐升级 | `RoleRequiredMixin(OPS)` |
| `/admin/tenants/<uuid:pk>/plan/license/` | POST | `TenantLicenseUpdateView` | 调整 License 到期/上限 | `RoleRequiredMixin(OPS)` |
#### 3.3.3 备份与恢复
| URL | 方法 | 视图 | 触发 | 权限 |
|---|---|---|---|---|
| `/admin/system/backups/rows/` | GET | `BackupRowsPartialView` | 筛选/翻页 | `AdminLoginRequiredMixin` |
| `/admin/system/backups/schedule/` | GET/POST | `BackupScheduleView` | 全局策略 | `RoleRequiredMixin(SUPER)` |
| `/admin/tenants/<uuid:pk>/backups/trigger/` | POST | `TenantBackupTriggerView` | 手动触发备份 | `RoleRequiredMixin(OPS)` |
| `/admin/system/backups/<uuid:pk>/status/` | GET | `BackupStatusPartialView` | `hx-trigger="every 5s"` 轮询任务进度;任务终态返回去 trigger 的 HTML | `AdminLoginRequiredMixin` |
| `/admin/system/backups/<uuid:pk>/restore/` | POST | `BackupRestoreView` | 数据恢复 | `MfaConfirmedRequiredMixin + RoleRequiredMixin(SUPER)` |
#### 3.3.4 数据导出
| URL | 方法 | 视图 | 触发 | 权限 |
|---|---|---|---|---|
| `/admin/system/exports/` | GET | `ExportListView` | 列表(首屏) | `AdminLoginRequiredMixin` |
| `/admin/system/exports/new/` | GET/POST | `ExportCreateView` | 新建导出 | `RoleRequiredMixin(OPS)` |
| `/admin/system/exports/<uuid:pk>/status/` | GET | `ExportStatusPartialView` | `hx-trigger="every 5s"` 轮询;终态去 trigger | `AdminLoginRequiredMixin` |
| `/admin/system/exports/<uuid:pk>/download/` | GET | `ExportDownloadRedirectView` | 302 跳 R2 Presigned URL | 触发人 + `RoleRequiredMixin(SUPER)` |
#### 3.3.5 系统升级A / B / C 三类,详见 §6.4
| URL | 方法 | 视图 | 触发 | 权限 |
|---|---|---|---|---|
| `/admin/system/versions/upgrade/` | GET/POST | `UpgradeFormView` | 升级表单(含类型/灰度/批次参数) | `MfaConfirmedRequiredMixin + RoleRequiredMixin(SUPER)`POST |
| `/admin/system/versions/<uuid:event_id>/progress/` | GET | `UpgradeProgressPartialView` | `hx-trigger="every 3s"` 轮询;状态 `halted/succeeded/failed` 终态后去 trigger | `AdminLoginRequiredMixin` |
| `/admin/system/versions/<uuid:event_id>/rollback/` | POST | `RollbackView` | 全量/单租户回滚 | `MfaConfirmedRequiredMixin + RoleRequiredMixin(SUPER)` |
| `/admin/system/versions/<uuid:event_id>/incident/` | GET | `IncidentReportView` | 事件报告 | `AdminLoginRequiredMixin` |
| `/admin/feature-flags/` | GET | `FeatureFlagListView` | Flag 定义列表 | `AdminLoginRequiredMixin` |
| `/admin/feature-flags/new/` | POST | `FeatureFlagCreateView` | 新增 Flag | `RoleRequiredMixin(SUPER)` |
| `/admin/feature-flags/<key>/rollout/` | POST | `FeatureFlagRolloutView` | 调整百分比 | `RoleRequiredMixin(SUPER)` |
| `/admin/feature-flags/<key>/archive/` | POST | `FeatureFlagArchiveView` | 归档 | `RoleRequiredMixin(SUPER)` |
| `/admin/tenants/<uuid:pk>/feature-flags/toggle/` | POST | `TenantFlagToggleView` | 租户级 Flag 覆盖 | `RoleRequiredMixin(OPS)` |
#### 3.3.6 监控与审计
| URL | 方法 | 视图 | 触发 | 权限 |
|---|---|---|---|---|
| `/admin/monitoring/alerts/` | GET | `AlertRuleListView` | 告警规则 | `RoleRequiredMixin(OPS)` |
| `/admin/monitoring/alerts/<uuid:pk>/edit/` | POST | `AlertRuleUpdateView` | 编辑规则 | `RoleRequiredMixin(OPS)` |
| `/admin/monitoring/grafana-webhook/` | POST | `GrafanaWebhookView` | Grafana 推送HMAC 校验) | 公开HMAC 鉴权) |
| `/admin/audit-logs/rows/` | GET | `AuditLogRowsPartialView` | 筛选/翻页 | `ReadOnlyAuditorAllowedMixin` |
| `/admin/audit-logs/export/` | POST | `AuditLogExportView` | 异步导出 CSV | `ReadOnlyAuditorAllowedMixin` |
#### 3.3.7 管理员设置
| URL | 方法 | 视图 | 权限 |
|---|---|---|---|
| `/admin/settings/admins/new/` | POST | `AdminAccountCreateView` | `RoleRequiredMixin(SUPER)` |
| `/admin/settings/admins/<uuid:pk>/deactivate/` | POST | `AdminDeactivateView` | `RoleRequiredMixin(SUPER)` |
| `/admin/settings/admins/<uuid:pk>/sessions/revoke/` | POST | `ForceLogoutView` | `RoleRequiredMixin(SUPER)` |
| `/admin/settings/ip-whitelist/` | GET | `IpWhitelistListView` | `RoleRequiredMixin(SUPER)` |
| `/admin/settings/ip-whitelist/new/` | POST | `IpWhitelistCreateView` | `RoleRequiredMixin(SUPER)` |
| `/admin/settings/ip-whitelist/<uuid:pk>/toggle/` | POST | `IpWhitelistToggleView` | `RoleRequiredMixin(SUPER)` |
| `/admin/settings/sessions/` | GET | `MyActiveSessionListView` | `AdminLoginRequiredMixin` |
#### 3.3.8 客户端版本管理(`apps.release` 后台 UI
| URL | 方法 | 视图 | 触发 | 权限 |
|---|---|---|---|---|
| `/admin/client-releases/rows/` | GET | `ClientReleaseRowsPartialView` | 筛选/翻页 | `AdminLoginRequiredMixin` |
| `/admin/client-releases/new/` | GET/POST | `ClientReleaseCreateView` | 新建版本(草稿/发布) | `RoleRequiredMixin(SUPER) + MfaConfirmedRequiredMixin`POST |
| `/admin/client-releases/<uuid:pk>/edit/` | POST | `ClientReleaseUpdateView` | 修改元数据 | `RoleRequiredMixin(SUPER)` |
| `/admin/client-releases/<uuid:pk>/publish/` | POST | `ClientReleasePublishView` | 发布 | `MfaConfirmedRequiredMixin + RoleRequiredMixin(SUPER)` |
| `/admin/client-releases/<uuid:pk>/unpublish/` | POST | `ClientReleaseUnpublishView` | 下线 | `MfaConfirmedRequiredMixin + RoleRequiredMixin(SUPER)` |
| `/admin/client-releases/<uuid:pk>/rollback/` | POST | `ClientReleaseRollbackView` | 回滚到该版本 | `MfaConfirmedRequiredMixin + RoleRequiredMixin(SUPER)` |
| `/admin/client-releases/<uuid:pk>/force-update/` | POST | `ClientReleaseForceUpdateView` | 推送强制更新标记 | `MfaConfirmedRequiredMixin + RoleRequiredMixin(SUPER)` |
| `/admin/client-releases/metrics/version-distribution/` | GET | `VersionDistributionPartialView` | `hx-trigger="revealed"` 懒加载 | `AdminLoginRequiredMixin` |
| `/admin/client-releases/metrics/tenant-leaderboard/` | GET | `TenantLeaderboardPartialView` | `hx-trigger="revealed"` 懒加载 + 翻页 | `AdminLoginRequiredMixin` |
### 3.4 路由守卫(视图层 Mixin 链)
```python
# apps/admin_console/permissions.py
class AdminRole:
SUPER = 'super_admin'
OPS = 'ops_operator'
AUDITOR = 'read_only_auditor'
ROLE_RANK = {AdminRole.AUDITOR: 1, AdminRole.OPS: 2, AdminRole.SUPER: 3}
class AdminLoginRequiredMixin:
"""检查 request.platform_admin否则 302 /admin/login/。
HTMX 请求返回 401 + HX-Redirect: /admin/login/"""
class RoleRequiredMixin(AdminLoginRequiredMixin):
required_role: str = None # AdminRole.SUPER / OPS / AUDITOR
# ROLE_RANK[user.role] >= ROLE_RANK[required_role] 才放行;否则 403 Partial + Toast
class ReadOnlyAuditorAllowedMixin(AdminLoginRequiredMixin):
"""允许 AUDITOR 访问 GET其他角色无差别放行非 GET 方法对 AUDITOR 返回 403"""
class MfaConfirmedRequiredMixin(AdminLoginRequiredMixin):
"""要求 session.mfa_confirmed_at 在最近 5 分钟内。
未通过 → 401 + HX-Trigger: {"fonrey:mfa-required":{"action":"...","return_to":"..."}}"""
class AuditedActionMixin:
"""form_valid / 成功响应后调用 audit_service.write_audit()
自动从 request 提取 operator/ip/user_agent/payload_summary"""
```
**典型组合**
```python
class TenantHardDeleteView(MfaConfirmedRequiredMixin, RoleRequiredMixin, AuditedActionMixin, FormView):
required_role = AdminRole.SUPER
audit_action = 'HARD_DELETE_TENANT'
```
**MFA Step-up 流程**(用户点击「硬删除」):
```
1. HTMX POST /admin/tenants/<id>/hard-delete/
2. MfaConfirmedRequiredMixin 检测 session.mfa_confirmed_at 已过 5 min
3. 401 + HX-Trigger: {"fonrey:mfa-required":{"action":"hard_delete","return_to":"<原 URL>"}}
4. 前端 Alpine.js 监听该事件 → 打开 MFA Modal
5. 用户输入 TOTP → POST /admin/login/mfa/step-up/ → 后端写 session.mfa_confirmed_at = now()
6. 前端拿到成功响应 → 重新发起原始 POST带 X-Mfa-Step-Up: 1→ 通过执行
```
### 3.5 懒加载策略
| 场景 | HTMX 实现 | 触发时机 |
|---|---|---|
| 详情页 Tab 内容 | 容器 `<div hx-get="..." hx-trigger="revealed" hx-swap="innerHTML">`Tab 激活后第一次进入视口才请求 | 首屏不阻塞 |
| 仪表盘图表 | `hx-trigger="revealed"` + 加载占位骨架屏 | 滚动到位时加载 |
| 客户端版本分布 / 租户活跃榜 | 同上 | 同上 |
| 长轮询任务(备份/导出/升级) | `hx-trigger="every Ns"`;后端在终态 partial 中**移除** `hx-trigger="every"`(替换为 `hx-trigger="load"`),避免持续轮询 | 进入页面 → 终态停 |
| 列表懒加载(追加) | Keyset 分页 + `hx-trigger="revealed" hx-swap="afterend"` 在最后一行触发 | 滚动到底部加载下页 |
| 模态框组件MFA Modal、删除确认 | `hx-get` 拉 partial → swap 到 `#dialog`;不预渲染 | 用户点击触发 |
**反模式**
- ❌ 全量预渲染所有 Tab首屏慢、浪费请求
- ❌ 终态后仍 `every Ns` 轮询(资源泄漏)。
- ❌ OFFSET 分页AGENTS.md §4.51000+ 数据集禁用)。
---
## 4. API 设计
### 4.1 双命名空间策略
| 命名空间 | 用途 | 受众 | 认证方式 | 响应类型 | 版本控制 |
|---|---|---|---|---|---|
| `/admin/...` | 平台管理后台业务CRUD + HTMX 局刷) | 平台管理员浏览器 | Django SessionHttpOnly Cookie + CSRF + TOTP MFA | HTML(Page) / HTML(Partial) / 偶发 JSON仅 Celery 任务状态轮询) | 不带版本号;通过 Mixin 灰度 |
| `/api/release/...` | 客户端运行时接口(更新检测 / Heartbeat / 统计) | Electron 客户端 + 平台后台(统计) | 公开(更新检测) / 设备签名票据Heartbeat / Session管理端 | JSON | URL 路径携带 `v1`(沿用 `ADR-20260430-009` |
**版本控制策略**
- `/admin/...`:内部接口,不提供向后兼容承诺,随发版滚动升级。
- `/api/release/v1/...`:客户端长期使用,必须遵守向后兼容;破坏性变更必须新增 `/api/release/v2/...` 并允许 `v1` 至少共存 6 个月。
### 4.2 客户端运行时 API`/api/release/v1/...`
> 沿用 `ADR-20260430-009`(统一命名空间)+ `ADR-20260430-008`SHA-256 强制)+ `ADR-20260430-007`Heartbeat Upsert + 24h 活跃口径)。
#### 4.2.1 端点清单
| 端点 | 方法 | 鉴权 | 说明 |
|---|---|---|---|
| `/api/release/v1/updates/latest/` | GET | 公开 | 客户端检查最新版本(仅返回 `published` 版本) |
| `/api/release/v1/heartbeats/` | POST | 设备票据 | 客户端启动上报Upsert |
| `/api/release/v1/metrics/version-distribution/` | GET | SessionPlatform Admin | 全平台版本活跃分布 |
| `/api/release/v1/metrics/tenant-installs/` | GET | SessionPlatform Admin | 指定租户活跃安装数 + 历史装机数 |
| `/api/release/v1/metrics/tenant-leaderboard/` | GET | SessionPlatform Admin | 全平台租户活跃榜 |
| `/admin/api/client-releases/` | POST | SessionSUPER + MFA | 管理端创建版本 |
| `/admin/api/client-releases/<uuid:pk>/` | PATCH | SessionSUPER + MFA | 修改状态 / 类型 / 日志 |
| `/admin/api/client-releases/<uuid:pk>/rollback/` | POST | SessionSUPER + MFA | 原子切换 published |
> 管理端写操作走 `/admin/api/client-releases/...`(与 `/admin/...` HTMX 路由共享 Session/CSRF/MFA 链路,但返回 JSON与客户端 `/api/release/v1/` 解耦:客户端永远只读公开版本。
#### 4.2.2 请求 / 响应规范
##### `GET /api/release/v1/updates/latest/`
**请求参数querystring**
| 参数 | 必填 | 说明 |
|---|---|---|
| `platform` | 是 | `win32`MVP 仅 Windows |
| `arch` | 是 | `x64` / `x86` |
| `current_version` | 是 | SemVer `X.Y.Z` |
**响应200有更新**
```json
{
"has_update": true,
"latest_version": "1.3.0",
"force_update": false,
"min_required_version": "1.0.0",
"download_url": "https://download.fonrey.com/releases/system/v1.3.0/fonrey-setup-1.3.0-win.exe",
"portable_url": "https://download.fonrey.com/releases/system/v1.3.0/fonrey-portable-1.3.0-win.zip",
"checksum_sha256": "<exe_sha256>",
"portable_checksum_sha256": "<zip_sha256>",
"file_size_bytes": 157286400,
"release_notes": "## v1.3.0\n- ...",
"release_date": "2026-05-01"
}
```
**响应200无更新**
```json
{ "has_update": false, "latest_version": "1.3.0" }
```
**性能要求**:缓存命中 `p95 < 120ms`(缓存键 `pub:release:latest:{platform}:{arch}`TTL 60s
##### `POST /api/release/v1/heartbeats/`
**请求头**
- `Authorization: Bearer <device_token>`(设备票据,由客户端登录时由租户业务后端签发,包含 `tenant_id` + `user_id` + `device_id``exp` 7 天)
- `Content-Type: application/json`
**请求体**
```json
{
"device_id": "9e6de37b-8c49-4f9b-af47-52f4e5b8b7f2",
"client_version": "1.3.0",
"platform": "win32",
"arch": "x64",
"os_version": "Windows 11 23H2"
}
```
**处理要求**
- 服务端从 `Authorization` 解析 `tenant_id``user_id`,与请求体的 `device_id` 一起作为唯一键。
- `INSERT ... ON CONFLICT (tenant_id, device_id) DO UPDATE SET last_seen_at=NOW(), launch_count=launch_count+1, client_version=EXCLUDED.client_version, os_version=EXCLUDED.os_version`
- 频控:单 `device_id` 每分钟 ≤ 12 次(启动场景),超限 429。
**响应202 Accepted**
```json
{ "ok": true }
```
**性能要求**:写入 `p95 < 80ms`
##### `GET /api/release/v1/metrics/version-distribution/`
**响应200**
```json
{
"as_of": "2026-05-02T03:21:00Z",
"active_window_hours": 24,
"total_active_devices": 12480,
"items": [
{ "version": "1.3.0", "active_devices": 9824, "share": 0.787 },
{ "version": "1.2.0", "active_devices": 1856, "share": 0.149 },
{ "version": "1.1.0", "active_devices": 800, "share": 0.064 }
]
}
```
##### `POST /admin/api/client-releases/`
**请求头**`Cookie: sessionid=...; csrftoken=...``X-CSRFToken: ...``X-Mfa-Step-Up: 1`
**请求体**
```json
{
"version": "1.3.0",
"platform": "win32",
"arch": "x64",
"release_type": "normal",
"min_required_version": "1.0.0",
"download_url": "https://download.fonrey.com/releases/system/v1.3.0/fonrey-setup-1.3.0-win.exe",
"portable_url": "https://download.fonrey.com/releases/system/v1.3.0/fonrey-portable-1.3.0-win.zip",
"checksum_sha256": "<exe_sha256>",
"portable_checksum_sha256": "<zip_sha256>",
"release_notes": "## v1.3.0\n- ...",
"status": "draft"
}
```
**响应201**:返回创建的资源;`status='published'` 必须在事务中将旧 published 版本切为 `unpublished`(确保单一生效约束)。
### 4.3 后台业务接口(`/admin/...`HTMX 局刷为主)
> 完整路径见 §3.3。所有路径返回 HTML(Partial)**不返回 JSON**(除轮询任务进度的 Celery 状态接口)。
#### 4.3.1 HTMX 响应规范
| 场景 | HTTP | 响应内容 | 响应头 |
|---|---|---|---|
| 操作成功 | 200 | 更新后的 HTML Partial | `HX-Trigger: {"fonrey:toast":{"type":"success","message":"..."}}` |
| 表单校验失败 | 422 | 含错误信息的表单 Partial保留用户输入 | 不发 Toast |
| 业务规则拒绝(如未导出就硬删) | 422 | 表单 Partial + 顶部 Alert 块 | 可选 `HX-Trigger` warning Toast |
| 权限不足 | 403 | `<div class="alert alert-danger">无权限</div>` Partial | `HX-Trigger: {"fonrey:toast":{"type":"error","message":"权限不足"}}` |
| 需要 MFA 二次确认 | 401 | 空 Partial | `HX-Trigger: {"fonrey:mfa-required":{"action":"...","return_to":"..."}}` |
| 未登录 / Session 过期 | 401 | 空 Partial | `HX-Redirect: /admin/login/` |
| 服务器异常 | 500 | 错误页 Partial | `HX-Trigger: {"fonrey:toast":{"type":"error","message":"系统异常,请重试"}}` |
#### 4.3.2 Celery 任务前端协议
```http
POST /admin/system/exports/new/
HX-Request: true
Content-Type: application/x-www-form-urlencoded
200 OK
HX-Trigger: {"fonrey:toast":{"type":"info","message":""}}
<tr id="export-row-{task_id}"
hx-get="/admin/system/exports/{task_id}/status/"
hx-trigger="every 5s" hx-swap="outerHTML">
<td>{{ task.modules }}</td><td></td><td></td>
</tr>
```
**轮询规约**
- 轮询间隔:备份/导出 = 5s升级进度 = 3s。
- 终态后端必须**移除** `hx-trigger="every"`(替换为 `hx-trigger="load"` 或不附 trigger
- 进度展示字段统一来自 Celery `AsyncResult` + DB 状态DB 优先,避免 Celery 结果过期丢失)。
### 4.4 错误码
> 所有 JSON 响应遵循 [`API_CONTRACT.md`](./API_CONTRACT.md) 包络规范:`{"error": "...", "code": "SNAKE_CASE_CODE"}`AGENTS.md §4.7)。
#### 4.4.1 客户端运行时(`/api/release/v1/...`
| code | HTTP | 中文含义 |
|---|---|---|
| `RELEASE_VERSION_INVALID` | 400 | 版本号不符合 SemVer |
| `RELEASE_PUBLISHED_CONFLICT` | 409 | 当前 `platform + arch` 已存在 published 版本 |
| `RELEASE_ARTIFACT_NOT_FOUND` | 404 | 发布包不存在或不可访问 |
| `RELEASE_CHECKSUM_MISMATCH` | 400 | 安装包完整性校验失败 |
| `RELEASE_HEARTBEAT_INVALID` | 400 | 心跳参数非法 |
| `RELEASE_DEVICE_TOKEN_INVALID` | 401 | 设备票据无效 / 过期 |
| `RELEASE_PERMISSION_DENIED` | 403 | 权限不足 |
| `RELEASE_RATE_LIMITED` | 429 | 请求过于频繁 |
#### 4.4.2 后台业务(`/admin/...` 与 `/admin/api/...`
| code | HTTP | 中文含义 |
|---|---|---|
| `ADMIN_LOGIN_REQUIRED` | 401 | 未登录或 Session 过期 |
| `ADMIN_MFA_REQUIRED` | 401 | 高危操作需 MFA step-up |
| `ADMIN_MFA_INVALID` | 422 | TOTP 校验失败 |
| `ADMIN_ROLE_DENIED` | 403 | 当前角色不足以执行此操作 |
| `ADMIN_IP_NOT_WHITELISTED` | 403 | 来源 IP 未在白名单(中间件层返回静态 403 页,不暴露后台存在) |
| `TENANT_NOT_FOUND` | 404 | 租户不存在 |
| `TENANT_INVALID_TRANSITION` | 422 | 状态机非法迁移 |
| `TENANT_HAS_ACTIVE_USERS` | 422 | 软删除前需先清空活跃用户 |
| `TENANT_NOT_EXPORTED` | 422 | 硬删除前必须先完成数据导出 |
| `BACKUP_IN_PROGRESS` | 409 | 同租户存在进行中的备份 |
| `RESTORE_PRECHECK_FAILED` | 422 | 恢复前置检查失败(备份哈希不匹配 / 目标 schema 异常) |
| `EXPORT_TOO_LARGE` | 422 | 单次导出数据量超限(建议拆分) |
| `UPGRADE_HEALTH_GATE_FAILED` | 422 | 批后健康门控指标不通过,已进入 `halted` 态 |
| `UPGRADE_INVALID_TYPE` | 422 | `upgrade_type` 与字段组合非法(如 A 类填了 `gray_tenant_ids` |
| `AUDIT_IMMUTABLE` | 422 | 审计日志禁止修改/删除DB trigger 兜底) |
| `FEATURE_FLAG_DEFINITION_ARCHIVED` | 422 | Flag 已归档,禁止变更 |
| `RATE_LIMITED` | 429 | 请求过于频繁 |
---
## 5. 性能与可靠性约束
### 5.1 客户端运行时 API
- `GET /api/release/v1/updates/latest/``p95 < 120ms`(缓存命中)。
- `POST /api/release/v1/heartbeats/`:写入 `p95 < 80ms`;同 `device_id` 限频 12 次/分钟。
- 任意更新失败不影响当前版本继续运行(可恢复原则)。
- SHA-256 校验失败禁止安装并保留当前版本可用(`ADR-20260430-008`)。
### 5.2 后台业务
- 列表页(租户 / 备份 / 审计 / 客户端版本):`p95 < 200ms`(首屏,`tenants` < 5000 行级别;超过需走 Keyset 分页 + Redis 计数)。
- HTMX Partial 响应:`p95 < 150ms`(不含轮询接口)。
- 任务进度轮询:`p95 < 50ms`(命中 `pub:backup:status:{id}` / `pub:export:status:{id}` 缓存)。
- 升级状态切换使用事务,保证「下线旧版 + 发布新版」原子完成。
- 客户端发布状态切换使用事务,保证「同 `(platform, arch)` 单一 published」约束。
### 5.3 跨租户操作
- 备份单租户1 min 2h取决数据量异步 + 进度上报。
- 恢复单租户530 min**不重试**,失败自动回滚到前置快照。
- 单租户 schema 迁移110s 快照 + `migrate` 主体。
- 整体硬删除110 minDROP SCHEMA 必须事务 + SAVEPOINT
---
## 6. 异步任务、缓存与文件上传
### 6.1 Celery 任务清单
> 队列:`admin_ops`(默认)、`migration`(独立限并发,仅 B 类升级使用)。
| 任务 | 触发场景 | 队列 | 重试 | 失败处理 |
|---|---|---|---|---|
| `provision_tenant` | 创建租户后异步执行 schema 创建 + 迁移 + 默认数据 | `admin_ops` | 不重试 | 标记 `tenants.status='failed'`,事务回滚,邮件告警 |
| `auto_resume_suspended` | Beat 每 10 min 扫描 `suspended_until <= NOW()` | `admin_ops` | 3 次 / 60s | Sentry 告警 |
| `purge_pending_delete` | Beat 每天 03:00 扫描冷静期到期 | `admin_ops` | 不重试 | 标记 `failed_to_purge` |
| `hard_delete_tenant` | 视图触发 | `admin_ops` | 不重试 | 部分删除标记 + 告警 |
| `run_backup` | 调度器 + 升级前 + 手动 | `admin_ops` | 2 次 / 5+30 min 指数退避 | `backup_records.status='failed'` |
| `cleanup_old_backups` | Beat 每天 04:00 | `admin_ops` | 3 次 | 告警 |
| `run_restore` | 视图触发(高危) | `admin_ops` | **不重试** | 自动回滚到恢复前快照 |
| `run_export` | 视图触发 | `admin_ops` | 2 次 / 60s | 标记 `failed`,邮件触发人 |
| `expire_export_links` | Beat 每小时 | `admin_ops` | 3 次 | 告警 |
| `health_check` | 升级前 | `admin_ops` | 1 次 | 阻断升级,前端 422 |
| `orchestrate_upgrade` | 升级表单触发 | `admin_ops` | **不重试** | 失败 → `halted` |
| `migrate_single_tenant` | 编排器派发 | `migration` | 不重试 | 单租户回滚到快照 |
| `tenant_smoke_test` | 单租户迁移后 | `admin_ops` | 不重试 | 计入失败率 |
| `post_batch_health_gate` | 批后门控 | `admin_ops` | 不重试 | 任一指标失败 → `halted` |
| `rollback_single_tenant` | 单租户回滚 | `migration` | 不重试 | 写 incident |
| `rollback_upgrade` | 整体回滚 | `admin_ops` | 不重试 | 写 incident |
| `release_compute_checksum_task` | 客户端发布包上传后 | `admin_ops` | 3 次 | 标记发布包不可用 |
| `release_publish_cdn_warmup_task` | 版本发布后(可选) | `admin_ops` | 3 次 | 仅告警,不阻塞 |
| `release_scan_artifact_task` | 客户端发布包上传后(可选) | `admin_ops` | 3 次 | 命中即标记不可发布 |
| `aggregate_dashboard_stats` | Beat 每 1 min | `admin_ops` | 2 次 | 仪表盘读旧缓存 |
| `aggregate_release_metrics` | Beat 每 1 min | `admin_ops` | 2 次 | 版本分布读旧缓存 |
| `cleanup_admin_sessions` | Beat 每 30 min | `admin_ops` | 3 次 | 告警 |
| `send_welcome_email` / `send_export_ready` / `send_suspend_notice` | 业务事件后 | `admin_ops` | 5 次 / 指数退避 | 仅告警,不阻塞 |
**通用约定**
- 所有任务 `bind=True`,前置统一 `audit_service.write_audit()`(成功/失败均落审计)。
- 涉及租户 schema 操作的任务必须 `with schema_context(tenant.schema_name):`
- 长任务(> 5 min必须周期性 `task.update_state(state='PROGRESS', meta={...})`
- 不重试任务必须显式 `acks_late=True, autoretry_for=()`
### 6.2 Redis 缓存策略
> Key 前缀 `pub:`public schema与租户业务 `{schema}:` 严格隔离AGENTS.md §4.6)。
| 缓存对象 | Key 格式 | TTL | 失效条件 |
|---|---|---|---|
| 平台管理员对象(含 role | `pub:admin:{admin_id}` | 30 min | 角色变更、停用、强制登出 |
| 管理员 Session 反查 | `pub:session:{session_token}` | 30 min与 expires_at 同步) | 强制登出、活动续期 |
| IP 白名单CIDR 列表) | `pub:ipwl:active` | 5 min | 新增/启停白名单 |
| MFA step-up 时间戳 | `pub:mfa:stepup:{session_token}` | 5 min | 自然过期 |
| 登录失败计数 | `pub:login:fail:{username}` | 15 min | 自然过期或登录成功清零 |
| 同 IP 失败计数 | `pub:login:fail:ip:{ip}` | 1 h | 同上 |
| 租户列表筛选总数 | `pub:tenant:count:{filter_hash}` | 30s | 短 TTL |
| 租户基本信息 | `pub:tenant:{tenant_id}` | 10 min | 编辑、状态变更后主动清 |
| 系统当前版本 | `pub:sys:current_version` | 1 h | 升级 / 回滚成功后清 |
| 备份/导出/升级任务进度 | `pub:backup:status:{id}` / `pub:export:status:{id}` / `pub:upgrade:progress:{event_id}` | 5s / 5s / 3s | 任务结束后立即清 |
| 全局备份策略 | `pub:backup:schedule:global` | 1 h | 策略保存后清 |
| 仪表盘统计 | `pub:dashboard:stats` | 1 min | 自然过期 |
| 服务健康状态 | `pub:health:{service}` | 30s | 自然过期 |
| 客户端最新发布版本 | `pub:release:latest:{platform}:{arch}` | 60s | 发布/下线/回滚后立即清 |
| 客户端版本分布聚合 | `pub:release:metrics:version_distribution` | 60s | 自然过期 |
| 单租户安装/活跃统计 | `pub:release:metrics:tenant:{tenant_id}` | 60s | 自然过期 |
| 客户端下载接口限流 | `pub:release:download:ratelimit:{ip}` | 60s | 自然过期 |
| Feature Flag 全局定义 | `pub:ff:def:{flag_key}` | 1 min | 定义变更后清 |
| 租户 Flag 覆盖 | `pub:ff:tenant:{tenant_id}` | 5 min | 租户 Flag 变更后清 |
**失效策略**
- Django Signals 在 `post_save` / `post_delete` 时主动 `cache.delete_many([...])`
- 任务进度类缓存仅作"减少 DB 压力",前端**仍以 DB 状态为准**:缓存 miss 直接查 DB。
- IP 白名单缓存命中失败时**不能放行**,必须 fail-closed拒绝访问
### 6.3 文件上传规范Cloudflare R2
| 场景 | 上传方 | Bucket | 路径模板 |
|---|---|---|---|
| 系统升级包 artifact | 超级管理员 → 后端 → R2 | `releases-system` | `releases/system/{version}/{filename}` |
| 客户端发布包EXE/ZIP| 超级管理员 → 后端 → R2 | `releases-client` | `releases/system/v{version}/fonrey-setup-{version}-win.exe``fonrey-portable-{version}-win.zip` |
| 备份产出 | Celery worker → R2 | `backups` | `backups/{tenant_schema}/{record_id}.tar.gz` |
| 导出产出 | Celery worker → R2 | `exports` | `exports/{tenant_schema}/{task_id}.zip` |
| 审计日志导出 CSV | Celery worker → R2 | `exports` | `exports/audit/{task_id}.csv` |
**强制规则**
- 所有 R2 写入由后端 Celery 完成;**禁用**前端直传 Presigned URL合规 + SHA-256 完整性 + 频次极低)。
- 升级包 / 客户端发布包必须 SHA-256 双校验:上传完成后由 `release_compute_checksum_task` 计算并落库(`system_versions.checksum_sha256` / `client_releases.checksum_sha256`);客户端拉取后必须校验,失败禁止安装(`ADR-20260430-008`)。
- 升级包视图使用 `python-magic` 读取头部字节做 MIME 校验,**不信任** `Content-Type` header 或文件扩展名。
- 下载链接(导出/备份)使用 R2 Presigned GET URLTTL = 24 小时;视图 302 重定向,不返回链接给前端。
- Nginx `client_max_body_size`:升级包/发布包路径 600M其他路径 10M。
### 6.4 系统升级 A / B / C 三类编排
> 沿用原『系统管理技术文档.md』§8.5§8.6。摘要:
| 类型 | 内容 | 是否可分批到租户级 | 编排路径 |
|---|---|---|---|
| **A. 应用代码升级** | Python 代码、模板、JS/CSS、Worker 镜像 | ❌ 单进程多租户架构下物理上不可分批;蓝绿切换 | 运维侧 K8s/Compose本模块仅记录 `system_versions` 元数据 |
| **B. 租户 Schema 迁移** | `apps.property` / `apps.client``migrations/*.py` | ✅ 按 `schema_name` 分批 | 本模块 `orchestrate_upgrade` 编排 |
| **C. Feature Flag 灰度** | 运行时启停(双路径分支) | ✅ 按租户/用户/百分比 | 本模块 `feature_flags` 服务 |
**B 类状态机**`draft → pre_check → pre_backup → batch_running ⇄ batch_done → succeeded`;任一批次失败 → `halted`,超管二选一「继续 / 回滚」。
**B 类批次参数(`upgrade_events` 字段)**`upgrade_type``batch_size`(默认 5`batch_concurrency`(默认 2`batch_interval_seconds`(默认 300`failure_policy``halt_batch` / `continue`)、`health_gate_config`jsonb 阈值覆盖)。
**批后健康门控**`error_rate_5xx_5m < 0.5%``p95_latency_5m < 2000ms``celery_queue_pending < 1000``sentry_new_issues_5m < 5``migrated_tenant_smoke_pass_rate = 100%` 才进入下一批。
**DDL 兼容性纪律**:所有租户 App migration 必须向后兼容(`ADD COLUMN`/`CREATE INDEX CONCURRENTLY`/`ADD CONSTRAINT NOT VALID` 安全;`DROP COLUMN`/`RENAME`/`ALTER TYPE 不兼容` 必须拆两次发布CI `django-migration-linter` 兜底;强制注释 `# UPGRADE_TYPE: expand|cleanup`
### 6.5 Feature Flag 服务
```python
def is_enabled(tenant, flag_key: str, *, user=None) -> bool:
if flag_key in tenant.feature_flags:
return bool(tenant.feature_flags[flag_key])
definition = _get_definition_cached(flag_key)
if definition is None or definition.archived_at:
return False
if definition.rollout_strategy == 'percentage':
bucket = stable_hash(f"{flag_key}:{tenant.id}") % 100
return bucket < definition.rollout_config.get('percentage', 0)
if definition.rollout_strategy == 'user' and user:
bucket = stable_hash(f"{flag_key}:{user.id}") % 100
return bucket < definition.rollout_config.get('percentage', 0)
return definition.default_value
```
**约束**
- 业务代码必须用 `is_enabled(...)`**严禁**直接读 `tenant.feature_flags[...]`
- `stable_hash` 使用 `xxhash`,租户 ID 长期稳定,避免百分比策略下租户被频繁挤进/挤出。
- 写操作必填 `reason`,写入 `feature_flag_change_log``platform_audit_logs`
---
## 7. 安全与合规
### 7.1 认证与会话
| 项 | 要求 |
|---|---|
| 独立 Auth Backend | `PlatformAdminBackend`,校验 `platform_admins.password_hash`Argon2id登录成功后写 `admin_sessions` |
| 不复用 `auth.User` | 租户业务用 `apps.account.User`(租户 schema平台后台完全独立 |
| Session 超时 | 滚动续 30 min`AdminSessionMiddleware` 每次请求把 `expires_at = NOW() + 30min` |
| Cookie | `Secure``HttpOnly``SameSite=Strict`Cookie domain 限定 `admin.fonrey.com`(不允许跨子域,与租户业务 Cookie 物理分离) |
| CSRF | 所有写操作启用HTMX 通过 `hx-headers='{"X-CSRFToken": "..."}'` 在 base 模板注入 |
| MFA 强制 | `platform_admins.mfa_enabled=False` 时除 `MfaSetupView` 外所有视图 302 强制跳设置 |
| TOTP 密钥 | `admin_mfa_devices.totp_secret` AES-256-GCM 加密存储,密钥来自环境变量 `ADMIN_MFA_KEY`,不与租户加密密钥共用 |
| 高危操作 | 硬删除/恢复/升级/回滚/客户端发布/下线/强制更新推送必须 MFA step-up5 min 时效) |
| 暴力破解防护 | 登录失败 5 次锁账号 15 min`pub:login:fail:{username}`);同 IP 失败 20 次锁 IP 1h |
### 7.2 IP 白名单
- `IpWhitelistMiddleware` 仅在 `request.host in ADMIN_CONSOLE_HOSTS` 时启用。
-`ip_whitelist`Redis 缓存 `pub:ipwl:active`);未命中返回 403 静态页(不暴露后台存在)。
- Nginx 层 `allow / deny` + 应用层中间件双重保险。
### 7.3 审计不可变
- DB trigger`BEFORE UPDATE OR DELETE ON platform_audit_logs ... RAISE EXCEPTION`
- ORM 层 `PlatformAuditLog` Manager 重写 `update()` / `delete()``IntegrityError`
- 只允许 `objects.append_only_create()`
- 紧急数据修复走 `manage.py shell_plus`,事后由超管在 `/admin/audit-logs/` 手工补录条目(`source='manual_shell'`),不开后门。
### 7.4 客户端运行时安全
1. Electron 必须启用:`contextIsolation=true``nodeIntegration=false``sandbox=true`
2. 更新包仅允许 HTTPS 下载;域名白名单固定为 `download.fonrey.com`
3. EV 证书私钥仅在 CI 密钥库中可用,禁止落盘到开发机。
4. 校验值由服务端生成并签名传输(至少 TLS + 服务端可信源)。
5. Heartbeat 接口必须防重放/防刷(设备票据 + 频控 + 审计)。
6. 管理端操作(发布、回滚、下线、强制更新)全部记录审计日志。
### 7.5 CSP 与跨域名隔离
- `Content-Security-Policy: default-src 'self'`Grafana iframe 域加入 `frame-src` 白名单;禁止 `unsafe-inline`HTMX/Alpine 已用 attribute 模式)。
- 跨域名严禁串台:租户 host 上访问 `/admin/...` 必须 404管理 host 上访问租户 URL 必须 404。由 `IpWhitelistMiddleware` + URLConf 双重保证。
- Django Admin 全环境弃用(详见 §2.4)。
---
## 8. 监控集成
| 维度 | 实现 |
|---|---|
| Grafana 嵌入 | `MonitoringView` 渲染含 Grafana iframe 的页面URL 含短期签名 token避免暴露 Grafana 公网入口 |
| 告警接收 | Grafana → Webhook → `GrafanaWebhookView`HMAC 签名校验) |
| Sentry | 独立 DSN与租户业务分离 |
| Celery 队列健康 | Flower 部署在 `admin.fonrey.com/flower/`,仅超管可访问 |
| 审计告警 | 任意 `result='FAILED'` 的高危操作HARD_DELETE / RESTORE / UPGRADE / ROLLBACK / FORCE_UPDATE_PUSH实时推送企业微信 + 邮件 |
| 客户端发布告警 | 发布失败 / SHA-256 校验失败 / 客户端版本分布异常波动24h 内某版本占比骤降 > 30%)实时告警 |
监控数据采集来源PRD §5.1.5CPU/内存来自 Prometheus node_exporter存储/API/活跃数来自应用埋点写 PostgreSQL `tenant_metrics_daily` 物化视图(属租户业务模块范畴,本模块仅消费)。
---
## 9. 测试规范
### 9.1 覆盖矩阵
| 类别 | 工具 | 必测内容 |
|---|---|---|
| Model | pytest-django + factory_boy | UUID 默认值、状态机 CHECK、唯一索引`schema_name` / `email` / `(platform,arch,status='published')` 单一约束、append-only Manager、软删除标记 |
| Service | pytest-django + Mock R2 / Mock Celery | `tenant_service.provision()` 失败回滚、`audit_service.write_audit()` 字段完整、状态机非法迁移抛错、`release_service.publish()` 单一 published 原子切换 |
| ViewHTTP | `django_tenants.test.client.TenantClient`(公共 schema+ `HTTP_HOST='admin.fonrey.com'` | 三角色 × 关键端点的 200/403/401未登录三场景MFA step-up 拦截CSRF |
| ViewHTMX | 同上 + `HTTP_HX_REQUEST='true'` | 验证返回 partial不含 `<html>` 根标签),且响应头包含约定的 `HX-Trigger` |
| 客户端 API | `Client(HTTP_HOST='app.fonrey.com')` | `/api/release/v1/updates/latest/` 公开访问;`/api/release/v1/heartbeats/` 设备票据校验;版本分布 Session |
| Middleware | pytest-django | IP 白名单命中/未命中Session 滚动续期;过期 302fail-closed |
| Celery 任务 | `CELERY_TASK_ALWAYS_EAGER=True` + R2/邮件 Mock | 主流程 + 失败回滚 + 重试次数 + 不重试任务的 `acks_late` 行为 |
| 安全回归 | 集成测试 | 跨域名(`*.fonrey.com` host 访问 `/admin/...` → 404租户用户身份不能登入管理后台管理后台 Cookie 不能在租户域名下生效;`'django.contrib.admin' not in settings.INSTALLED_APPS` 断言CI grep 守门 |
### 9.2 关键测试约束AGENTS.md §6
- 禁止使用 Django 原生 `Client()`,统一使用 `TenantClient`
- 所有受角色保护的 View 必须覆盖超级200/204、运营200 或 403、审计员GET 200 / 非 GET 403、未登录302 → /admin/login/)。
- 高危操作测试必须包含 MFA step-up 已通过 / 未通过两个分支。
- `platform_audit_logs` 测试:写操作后断言审计行存在;尝试 UPDATE / DELETE 必须抛 `IntegrityError`Manager + DB trigger 双重保险)。
- 客户端 API 集成测试SHA-256 校验失败禁止安装;强制更新分支;启动 + 4h 检测周期;失败可恢复。
- Celery 异步测试覆盖率:`tasks/` ≥ 85%`services/` ≥ 90%`views/` ≥ 75%。
### 9.3 测试文件路径
- `tests/integration/admin_console/test_us_admin_console.py`(租户/备份/导出/升级/审计/管理员设置)
- `tests/integration/release/test_us_release.py`(客户端发布管理 + 客户端运行时 API + Heartbeat
### 9.4 测试映射(与 PRD Story 对齐)
| PRD Persona / Story | 测试关注点 |
|---|---|
| Persona A — 运营人员 Lily | 租户 CRUD、挂起/恢复、备份触发、导出 |
| Persona B — David系统升级 | A/B/C 三类升级、灰度名单可填性、健康门控、回滚 |
| Persona C — David客户端发布 | 版本创建/发布/下线/回滚、单一 published 约束、强制更新推送、SHA-256 校验 |
| Persona D — 审计员 Carol | 审计日志只读、导出 CSV、非 GET → 403 |
| 客户端 HeartbeatPRD 5.5.5 + ADR-20260430-007| Upsert 幂等、24h 活跃口径、租户活跃榜排序 |
| 客户端自动升级 | 启动 + 4h 检测、普通/强制分支、失败可恢复、SHA-256 失败禁止安装 |
---
## 10. 部署规范
| 项 | 配置 |
|---|---|
| 域名 | `admin.fonrey.com` 解析到与租户应用相同的 Gunicorn/Uvicorn 集群(共用进程,按 Host 路由) |
| 客户端下载域名 | `download.fonrey.com`Cloudflare CDN + R2 origin |
| Nginx | `server_name admin.fonrey.com` 单独 server block① IP 白名单 `allow / deny`(与应用层双重保险)② `client_max_body_size 600M` 仅限 `/admin/system/versions/upgrade/``/admin/client-releases/new/`;其他端点 10M |
| Celery workeradmin_ops | 独立部署 worker`--concurrency=2 --max-tasks-per-child=50`(任务多为 IO 密集长任务) |
| Celery workermigration | 独立部署 worker`--concurrency=2 --prefetch-multiplier=1`(避免并发 migrate 打爆连接池) |
| Celery beat | 单实例运行;调度任务:`auto_resume_suspended` / `purge_pending_delete` / `cleanup_old_backups` / `expire_export_links` / `aggregate_dashboard_stats` / `aggregate_release_metrics` / `cleanup_admin_sessions` |
| 密钥管理 | `ADMIN_MFA_KEY` / `R2_ADMIN_KEY` / `GRAFANA_SIGN_KEY` / `EV_SIGN_KEY` 通过 Docker Secret 注入;不出现在 `.env` 文件 |
| 日志 | Web 访问日志 / 审计日志 / Sentry 三路独立审计日志同时落库DB+ 落对象存储R2 月归档) |
| 备份的备份 | 备份元数据(`backup_records`)随 `public` schema 每日 02:00 全量 dump 到独立 R2 桶 `meta-backups`,灾难场景下用于重建 |
| 客户端 EV 签名 | EV 证书私钥仅在 CI 密钥库签名步骤electron-builder 输出 → CI 调用 `signtool.exe` → 上传 R2 |
---
## 11. 落地顺序建议
1. `apps/admin_console` 模型/服务/Mixin 基础骨架 + 独立认证 + MFA 设置 + 仪表盘空壳(页面 12
2. 租户管理(页面 310+ 审计基础 + IP 白名单(页面 16 子集)。
3. 备份 + 导出 + 监控(页面 12 / 13
4. 系统升级(页面 11 + Feature Flag
5. `apps/release` 后台 UI页面 14+ 客户端运行时 API`/api/release/v1/updates/latest/``/heartbeats/`)。
6. Electron 壳应用最小可运行版 + electron-updater 接入 + EV 签名 CI。
7. 官网下载页(公司域名)+ 便携版 ZIP。
8. 完整审计页面 + 平台管理员设置(页面 15 / 16 全集)。
---
## 12. 文档同步规则
- PRD 平台管理后台变更:同步本文件。
- `tenants` / `platform_admins` / `client_releases` / `client_heartbeats` / `upgrade_events` / `feature_flag_*` 字段变更:同步 `DATA_MODEL_PUBLIC.md`
- 枚举值变更:同步 `DATA_MODEL/ENUMS.md`(含中文标签)。
- API 包络/错误契约变更:同步 `TECH_STACK/API_CONTRACT.md`
- 测试用例新增:同步 `TECH_STACK/测试规范.md``TEST_CASES/TEST_CASE_REGISTRY.md`
---
## 13. 待解决问题
| 编号 | 级别 | 问题描述 |
| ------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **TS-PA-01** | 🟢 Minor | 客户端管理 API 是否将 `/admin/api/client-releases/...` 升级为 `/admin/api/v1/client-releases/...``/api/release/v1/...` 对齐版本号?决议:**保持现状**(管理端非长期对外契约)。 |
| **TS-PA-02** | 🟢 Minor | Feature Flag 与 `tenants.feature_flags` 字段的 DDL 是否需要 ADR 单列?决议:合并到 B 类升级 ADR`ADR-20260430-007` 范围内),不另列。 |
| **TS-PA-03** | 🟠 Major | 旧 PRD 的反向引用README、其他 TECH_STACK、ADR 早期条目尚未全量清理35+ 处);按用户决定本轮不处理,后续单独发起治理。 |

View File

@@ -2,22 +2,23 @@
# Fonrey 登录管理技术方案
**版本**: 4.0
**版本**: 4.1
**项目**: Fonrey 房产经纪管理系统
**技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery
**关联 PRD**: `PRD/登录管理/用户登录管理模块PRD.md`v2.0
**关联 PRD**: `PRD/登录管理/用户登录管理模块PRD.md`v3.0
**关联数据模型**: `DATA_MODEL/DATA_MODEL_LOGIN.md`(本方案不重复 DDL
**关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威)
**关联测试规范**: `TECH_STACK/测试规范.md``TEST_CASES/TEST_CASES_LOGIN_MODULE.md`
**最后更新**: 2026-04-30
**最后更新**: 2026-05-02
---
## 变更历史
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-04-30 | Atlas | 补充变更历史章节(文档治理) |
| 日期 | 变更人 | 变更内容 |
| ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-04-30 | Atlas | 补充"变更历史"章节(文档治理) |
| 2026-05-02 | Sisyphus | 按 `ADR-20260502-003` 承接从 PRD v3.0 迁出的实现细节①§5.3 新增 Tenant Verify 请求/响应 JSON Schema②§5.3 新增预留 Wechat 端点 `GET /api/auth/wechat/qrcode/``POST /api/auth/wechat/callback/`仅占位MVP 不开放);③新增 §十三 Electron 客户端约定Tenant Code 存储/Session/登录页加载/多标签页/登出/窗口关闭/强制更新);④§六 关键流程约束补全密码错误锁定阈值、滑块容差、找回密码错误次数等数值口径,统一以本文件为准 |
## 一、文档定位与边界
@@ -96,26 +97,26 @@
---
## 五、端点清单(对齐 PRD
## 五、端点清单
### 5.1 页面路由SSR
| 路径 | 方法 | 鉴权 | 说明 |
|---|---|---|---|
| `/auth/tenant/identify/` | GET | 否 | Tenant 识别页 |
| `/auth/login/` | GET | 否 | 登录页(密码登录/验证码登录 Tab |
| `/auth/password/forgot/` | GET | 否 | 找回密码三步页 |
| `/auth/password/change-initial/` | GET | 是 | 首次登录强制改密页 |
| 路径 | 方法 | 鉴权 | 说明 |
| -------------------------------- | --- | --- | ------------------- |
| `/auth/tenant/identify/` | GET | 否 | Tenant 识别页 |
| `/auth/login/` | GET | 否 | 登录页(密码登录/验证码登录 Tab |
| `/auth/password/forgot/` | GET | 否 | 找回密码三步页 |
| `/auth/password/change-initial/` | GET | 是 | 首次登录强制改密页 |
> 说明:`/auth/wechat/*` 仅预留,不在 MVP 开放。
### 5.2 HTMX 片段端点
| 路径 | 方法 | 用途 | 返回 |
|---|---|---|---|
| `/auth/fragments/captcha/` | GET | 刷新滑块区块 | HTML 片段 |
| `/auth/fragments/login-form/` | GET | 登录 Tab 内容局刷 | HTML 片段 |
| `/auth/fragments/forgot-step/` | GET | 找回密码步骤局刷 | HTML 片段 |
| 路径 | 方法 | 用途 | 返回 |
| ------------------------------ | --- | ----------- | ------- |
| `/auth/fragments/captcha/` | GET | 刷新滑块区块 | HTML 片段 |
| `/auth/fragments/login-form/` | GET | 登录 Tab 内容局刷 | HTML 片段 |
| `/auth/fragments/forgot-step/` | GET | 找回密码步骤局刷 | HTML 片段 |
### 5.3 JSON APIMVP
@@ -132,6 +133,46 @@
| `/api/auth/password/change-initial/` | POST | 首次登录强制改密提交 |
| `/api/auth/logout/` | POST | 登出销毁会话 |
#### 5.3.1 Tenant Verify 请求/响应 Schema
请求体:
```json
{ "tenant_code": "202500010001" }
```
成功响应HTTP 200
```json
{
"valid": true,
"tenant_name": "XX房产经纪有限公司",
"tenant_logo_url": "https://cdn.fonrey.com/tenants/xxx/logo.png",
"login_url": "https://xxx.fonrey.com/auth/login/"
}
```
失败响应HTTP 200业务态失败
```json
{
"valid": false,
"error_code": "AUTH_TENANT_NOT_FOUND",
"message": "识别码无效"
}
```
限流超限响应HTTP 429遵循 `API_CONTRACT.md` 统一错误格式,`code = AUTH_TENANT_RATE_LIMITED`
### 5.4 预留端点MVP 不开放)
| 端点 | 方法 | 状态 | 说明 |
|---|---|---|---|
| `/api/auth/wechat/qrcode/` | GET | 仅占位 | 微信扫码登录二维码获取v2 实现) |
| `/api/auth/wechat/callback/` | POST | 仅占位 | 微信扫码回调换取系统 Tokenv2 实现) |
> MVP 内 URLConf 中**不注册**这两个路由,登录页 UI 入口以禁用态展示v2 启用时再按本表落地路由与视图。
---
## 六、关键流程约束
@@ -274,3 +315,42 @@
- 数据结构调整:同步 `DATA_MODEL_LOGIN.md`
- 测试用例新增/变更:同步 `TEST_CASES/TEST_CASE_REGISTRY.md` 与登录用例文档
- API 契约调整:同步 `TECH_STACK/API_CONTRACT.md`
---
## 十三、Electron 客户端约定(实现口径)
> 承接自原 PRD §5.7(按 `ADR-20260502-003` 迁入本文件)。本节为 Electron 渲染层与主进程在登录链路上的实现规约,开发时必须严格遵循。
### 13.1 存储与会话
| 约定项 | 实现规格 |
|---|---|
| Tenant Code 存储 | `electron-store``app.getPath('userData')` 下的配置文件,**必须 AES 加密**,禁止明文落盘 |
| Session Token 存储 | 内存(主进程 `global` 变量)+ Chromium 管理的 `session` Cookie`HttpOnly` + `Secure` + `SameSite=Strict`**禁止写入磁盘明文文件** |
| 多标签页 | 同一 `BrowserWindow` 内,所有页面共享同一 `session.defaultSession`,复用同一 Session Cookie |
| 窗口关闭 | Session 保留(不自动登出);下次启动客户端时,若 Cookie 未过期且 `/api/auth/whoami/` 校验通过,直接进入系统 |
### 13.2 登录页加载流程
1. 主进程读取本地缓存的 `tenant_code`
- 不存在 → `BrowserWindow.loadURL('app://tenant-identify')`(本地静态页或专用 Tenant 识别 URL
- 存在 → 调用 `POST /api/auth/tenant/verify/` 复核(防止租户被禁用 / 改名)
- 成功 → 构建 `https://{tenant_slug}.fonrey.com/auth/login/`,通过 `BrowserWindow.loadURL()` 加载
- 失败 → 清除本地 `tenant_code` 缓存 → 跳回 Tenant 识别页
2. 切换公司:用户在登录页点击「切换公司」 → 主进程清缓存 → 关闭当前 `BrowserWindow` → 重新走步骤 1。
### 13.3 登出与强制更新
| 场景 | 客户端动作 |
|---|---|
| 用户登出 | 调用 `POST /api/auth/logout/` → 清除 Chromium Session Cookie`session.defaultSession.clearStorageData({ storages: ['cookies'] })`)→ 跳转登录页 |
| 服务端 Session 过期401 | 渲染层捕获 401 → 主进程清 Cookie → 跳转登录页并提示"登录已过期,请重新登录" |
| 客户端版本低于 `min_required_version` | 加载登录页前先展示"请更新客户端"模态,阻断登录流程;联动 `平台管理后台技术方案.md` 客户端发布章节 |
### 13.4 安全约束(与 AGENTS.md §5 对齐)
- 渲染进程必须 `nodeIntegration: false``contextIsolation: true``sandbox: true`
- 渲染层禁止内嵌业务逻辑或本地数据库(壳应用原则);
- 主进程与渲染层之间通过 `contextBridge` 暴露的最小白名单 IPC 接口通信;
- 仅允许加载 `https://*.fonrey.com``app://` 协议,其他 URL 在 `will-navigate` 中拦截。

View File

@@ -1,895 +0,0 @@
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
# Fonrey 系统管理模块 — 技术方案
**版本**: v1.2 **最后更新**: 2026-04-26
**模块**: 系统管理Admin Console / Platform Operations
**范围依据**:
- PRD: [`PRD/系统管理/系统管理模块PRD.md`](../PRD/系统管理/系统管理模块PRD.md)
- 数据模型: [`DATA_MODEL/DATA_MODEL_PUBLIC.md`](../DATA_MODEL/DATA_MODEL_PUBLIC.md)
- 技术总纲: [`TECH_STACK.md`](./TECH_STACK.md)
> **关键定位**:本模块是 **平台运营后台**`admin.fonrey.com`),数据全部存于 PostgreSQL `public` schema归属 `django-tenants` 的 `SHARED_APPS`**不参与租户 schema 切换**。所有功能仅限平台管理员(`platform_admins`)访问,与租户应用(`*.fonrey.com`的认证体系、Session、URL 命名空间完全隔离。
---
## 变更历史
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
## 1. 模块边界与定位
| 维度 | 说明 |
|------|------|
| 部署域名 | `admin.fonrey.com`独立子域Nginx 层 IP 白名单) |
| Schema 归属 | `public``SHARED_APPS`),所有 ORM 查询走 `public_schema_urlconf` |
| 认证体系 | `platform_admins` 独立账号 + 强制 TOTP MFA与租户 `staff` 不共享 |
| 受众 | 超级管理员 / 运营人员 / 只读审计员(详见 PRD §9.2 权限矩阵) |
| URL 前缀 | `/admin/...`(自建 CBV 后台,**Django 自带 `django.contrib.admin` 全环境弃用** |
| Celery 队列 | 独立队列 `admin_ops`,与租户业务队列隔离避免相互干扰 |
**与租户业务模块的隔离原则**
- ❌ 严禁本模块代码导入 `apps.property` / `apps.client` / `apps.org` 等租户 App 的 Model
- ❌ 严禁本模块视图、任务直接访问租户 schema 中的表
- ✅ 跨租户数据操作(备份、恢复、导出)必须通过 `tenant_context(tenant)` 显式切换 schema 后再操作
### 1.5 与 `django.contrib.admin` 的关系(强制弃用)
**全环境弃用 Django Admin**,包括开发、预发、生产。理由:
| 冲突点 | 说明 |
|---|---|
| 多租户编排 | Django Admin 假设单 schema无 schema 切换钩子;本模块需跨 `public` ↔ 租户 schema 编排(备份/恢复/重置租户用户密码Admin 框架无法承载 |
| 认证体系 | Admin 强绑定 `django.contrib.auth.User`;本模块要求独立 `platform_admins` 表 + 强制 TOTP二者不可共存于同一登录入口 |
| 审计强度 | Admin 自带 `LogEntry` 仅记录 add/change/delete且允许 UPDATE/DELETE本模块要求 append-only + 覆盖读操作PRD §3.2 |
| 交互范式 | Admin 模板基于整页表单刷新;本模块要求 HTMX 局部刷新 + Alpine.js 二次确认 ModalUI_SYSTEM 规范) |
| 业务流页面 | 升级灰度进度、备份恢复 MFA step-up、监控大盘等非 CRUD 页面无法用 ModelAdmin 表达 |
| 受众 | Admin 面向「懂 Django ORM 概念」的开发者;本模块运营人员为非技术背景,需要业务化 UI |
**强制措施**
- `INSTALLED_APPS` **不注册** `django.contrib.admin``django.contrib.admin.apps.SimpleAdminConfig`
- `INSTALLED_APPS` **不注册** `django.contrib.auth.AuthenticationMiddleware` 之外的 Admin 相关中间件
- `urls_public.py` **不导入** `django.contrib.admin`,无 `admin.site.urls` 路由
- 项目根 `settings/base.py` 增加启动断言:`assert 'django.contrib.admin' not in INSTALLED_APPS, "Django Admin 已弃用,请使用 apps.admin_console"`
- CI 检查:`grep -rn "from django.contrib import admin\|admin.site.register" apps/ config/` 命中即构建失败
- 紧急数据修复一律走 `python manage.py shell_plus` + 审计日志手工补录,**不开后门**
---
## 2. Django App 目录结构
本模块对应单个 App`apps/admin_console/`,归属 `SHARED_APPS`。受 PRD §5 划分驱动,内部按子域拆分 `views/` `tasks/` `services/`,避免单文件膨胀。
```
apps/admin_console/
├── apps.py
├── urls.py # 仅注册到 PUBLIC_SCHEMA_URLCONF
├── signals.py # 状态变更 → 写 audit_log备份完成 → 发邮件
├── forms.py # 所有表单TenantForm / BackupScheduleForm / AdminForm ...
├── serializers.py # 仅给 Celery 任务状态轮询的极少 JSON 端点用
├── permissions.py # 角色 Mixin / 装饰器 / IP 白名单 Middleware 接口
├── middleware.py # IpWhitelistMiddleware / AdminSessionMiddleware
├── auth_backends.py # PlatformAdminBackend独立认证不复用 Django auth User
├── models/
│ ├── __init__.py
│ ├── tenant.py # Tenant / Domain / TenantStatusLog继承 django-tenants
│ ├── platform_admin.py # PlatformAdmin / AdminMfaDevice / AdminSession / IpWhitelist
│ ├── audit.py # PlatformAuditLogappend-only Manager
│ ├── backup.py # BackupSchedule / BackupRecord
│ ├── export.py # ExportTask
│ └── version.py # SystemVersion / UpgradeEvent
├── views/
│ ├── __init__.py
│ ├── auth.py # 登录 / MFA 校验 / 登出
│ ├── dashboard.py # DashboardView
│ ├── tenants.py # TenantListView / TenantDetailView / TenantCreateView / SuspendView / DeleteView ...
│ ├── tenant_users.py # 租户内 Tenant Admin 列表 / 密码重置
│ ├── backups.py # BackupScheduleView / BackupListView / TriggerBackupView / RestoreView
│ ├── exports.py # ExportTaskCreateView / ExportTaskListView / DownloadView签名链接
│ ├── versions.py # SystemVersionListView / UpgradeView / RollbackView
│ ├── monitoring.py # MonitoringView嵌入 Grafana iframe
│ ├── audit.py # AuditLogListView / AuditLogExportView
│ └── settings.py # AdminAccountView / RoleView / IpWhitelistView / SessionView
├── tasks/
│ ├── __init__.py
│ ├── tenant_lifecycle.py # provision_tenant / suspend_tenant / hard_delete_tenant / auto_resume_suspended
│ ├── backup.py # run_backup / cleanup_old_backups
│ ├── restore.py # run_restore含前置自动快照
│ ├── export.py # run_exportCSV/JSON/SQL Dump
│ ├── upgrade.py # run_upgrade / run_rollback / health_check
│ ├── notifications.py # send_welcome_email / send_suspend_notice / send_export_ready
│ └── housekeeping.py # purge_pending_delete / expire_export_links / cleanup_admin_sessions
├── services/
│ ├── __init__.py
│ ├── tenant_service.py # 业务逻辑状态机迁移、Schema 创建/销毁
│ ├── audit_service.py # 统一写审计日志的入口write_audit
│ ├── mfa_service.py # TOTP 生成 / 校验 / 二维码
│ ├── permission_service.py # 三角色权限矩阵决策
│ ├── backup_service.py # 调度计划解析、保留策略
│ └── version_service.py # 升级状态机、灰度租户进度合成
├── tests/
│ ├── __init__.py
│ ├── factories.py # factory_boy 工厂
│ ├── test_models.py # 字段约束、状态机、append-only
│ ├── test_views_tenants.py # 三角色 200/403 矩阵
│ ├── test_views_audit.py # 审计日志只读、导出
│ ├── test_views_settings.py # MFA、IP 白名单、强制登出
│ ├── test_tasks_tenant.py # provision / suspend / hard_delete
│ ├── test_tasks_backup.py
│ ├── test_tasks_export.py
│ ├── test_tasks_upgrade.py
│ ├── test_middleware.py # IP 白名单、Session 滚动超时
│ └── test_audit_service.py
└── templates/admin_console/
├── base.html # 管理后台独立 base不与租户 base 共享)
├── auth/
│ ├── login.html
│ └── mfa_challenge.html
├── dashboard.html
├── tenants/
│ ├── list.html
│ ├── detail.html # 含 Tab基本信息/用户/套餐/监控/备份/操作历史
│ ├── create.html
│ └── partials/
│ ├── row.html # 列表行HTMX swap target
│ ├── filter_bar.html
│ ├── pagination.html
│ ├── status_badge.html
│ ├── suspend_form.html
│ ├── delete_confirm.html # Confirm Modal partial
│ └── tenant_admins_table.html
├── backups/
│ ├── list.html
│ ├── schedule_form.html
│ └── partials/
│ ├── record_row.html
│ ├── progress_cell.html # HTMX 轮询任务状态
│ └── restore_confirm.html
├── exports/
│ ├── list.html
│ └── partials/
│ ├── task_row.html
│ └── progress_cell.html
├── versions/
│ ├── list.html
│ ├── upgrade_form.html
│ └── partials/
│ ├── tenant_progress_table.html # 灰度升级实时进度
│ └── rollback_confirm.html
├── monitoring/
│ └── index.html # Grafana iframe 容器
├── audit/
│ ├── list.html
│ └── partials/
│ ├── log_row.html
│ └── filter_bar.html
├── settings/
│ ├── admins.html
│ ├── ip_whitelist.html
│ ├── sessions.html
│ └── partials/
│ ├── admin_row.html
│ ├── mfa_setup_modal.html
│ └── session_row.html
└── components/
├── confirm_modal.html # Danger Confirm删除/回滚/恢复)
├── mfa_challenge_modal.html # 高危操作二次 MFA
├── toast.html # HX-Trigger payload 渲染
└── stat_card.html
```
**目录约定**
- `models/` 一表一文件,对应 DATA_MODEL_PUBLIC.md §2.x 章节
- `views/` 全部使用 Class-Based Views`ListView` / `DetailView` / `FormView`),禁止函数视图
- `tasks/``services/` 分离:`tasks/` 是 Celery 入口(薄壳),业务逻辑落在 `services/`,便于单测
- `templates/admin_console/partials/` 命名以 `_partial`/`partials` 区分完整页 vs HTMX 局部模板
---
## 3. 路由命名空间与设置
### 3.1 settings 关键配置
```python
# config/settings/base.py
SHARED_APPS = [
'django_tenants',
'apps.tenant', # 租户路由 App
'apps.admin_console', # 本模块(自建后台,主体)
'apps.release', # 客户端发布
'django.contrib.contenttypes',
'django.contrib.staticfiles',
# ⚠️ 注意:不注册 'django.contrib.admin',全环境弃用
]
# 启动期硬约束:防止任何人误把 Admin 加回来
assert 'django.contrib.admin' not in SHARED_APPS, \
"Django Admin 已全环境弃用,平台后台请走 apps.admin_console"
assert 'django.contrib.admin' not in (TENANT_APPS if 'TENANT_APPS' in dir() else []), \
"Django Admin 不应在租户 App 中出现"
PUBLIC_SCHEMA_URLCONF = 'config.urls_public' # 管理后台 URL 注册位置
ROOT_URLCONF = 'config.urls_tenant' # 租户业务 URL
# 管理后台域名识别Nginx 已按 host 路由到同一 Django 进程,由中间件区分)
ADMIN_CONSOLE_HOSTS = ['admin.fonrey.com', 'admin.localhost']
# Celery 队列隔离
CELERY_TASK_ROUTES = {
'apps.admin_console.tasks.*': {'queue': 'admin_ops'},
}
```
### 3.2 URL 命名空间
```python
# config/urls_public.py
from django.urls import path, include
# ⚠️ 严禁 from django.contrib import admin —— Django Admin 全环境弃用
urlpatterns = [
path('admin/', include(('apps.admin_console.urls', 'admin_console'),
namespace='admin_console')),
]
```
`apps/admin_console/urls.py` 内顶层 `app_name = 'admin_console'`;所有反向解析使用 `admin_console:tenants:list` 等命名空间。
---
## 4. API 端点设计
> 端点全部映射 PRD §4 用户故事 + §5.3 页面规格 + §9.3 路由表。
> **响应类型约定**:标 `HTML(Page)` 表示返回完整模板(首屏/直链);`HTML(Partial)` 表示 HTMX 局部刷新(仅返回片段,前端 swap`JSON` 仅用于 Celery 任务状态轮询。
### 4.1 认证与会话
| URL Pattern | HTTP | 视图 | 触发场景 | 响应 | 权限 |
|---|---|---|---|---|---|
| `/admin/login/` | GET | `AdminLoginView` | 登录页 | HTML(Page) | 匿名 |
| `/admin/login/` | POST | `AdminLoginView` | 提交账号密码 | HTML(Partial) 跳转或表单错误422 | 匿名 |
| `/admin/login/mfa/` | GET | `MfaChallengeView` | 提示输入 TOTP | HTML(Page) | 已通过密码校验 |
| `/admin/login/mfa/` | POST | `MfaChallengeView` | 校验 TOTP | 302 跳转 dashboard / 422 | 同上 |
| `/admin/login/mfa/setup/` | GET | `MfaSetupView` | 首次绑定 TOTP强制 | HTML(Page) + 二维码 | 首登态 |
| `/admin/login/mfa/setup/` | POST | `MfaSetupView` | 确认绑定 | 302 | 同上 |
| `/admin/logout/` | POST | `AdminLogoutView` | 主动登出 | 302 | 已登录 |
### 4.2 仪表盘
| URL Pattern | HTTP | 视图 | 触发场景 | 响应 | 权限 |
|---|---|---|---|---|---|
| `/admin/` | GET | `DashboardView` | 进入仪表盘 | HTML(Page) | 已登录 |
| `/admin/dashboard/health/` | GET | `HealthStatusPartialView` | 卡片每 30s 轮询服务健康 | HTML(Partial) | 已登录 |
| `/admin/dashboard/recent-actions/` | GET | `RecentActionsPartialView` | 最近 10 条审计 | HTML(Partial) | 已登录 |
### 4.3 租户管理
| URL Pattern | HTTP | 视图 | 触发场景 | 响应 | 权限 |
|---|---|---|---|---|---|
| `/admin/tenants/` | GET | `TenantListView` | 列表页(首屏) | HTML(Page) | 已登录 |
| `/admin/tenants/rows/` | GET | `TenantRowsPartialView` | HTMX 筛选/翻页/搜索 | HTML(Partial) `<tbody>` | 已登录 |
| `/admin/tenants/new/` | GET | `TenantCreateView` | 新建表单 | HTML(Page) | 运营+ |
| `/admin/tenants/new/` | POST | `TenantCreateView` | 提交开通 | HTML(Partial) 表单/422 + `HX-Trigger: showToast` | 运营+ |
| `/admin/tenants/<uuid:pk>/` | GET | `TenantDetailView` | 进入详情 | HTML(Page) | 已登录 |
| `/admin/tenants/<uuid:pk>/edit/` | POST | `TenantUpdateView` | 编辑可变字段 | HTML(Partial) | 运营+ |
| `/admin/tenants/<uuid:pk>/suspend/` | POST | `TenantSuspendView` | 挂起 | HTML(Partial) 状态徽章 + Toast | 运营+ |
| `/admin/tenants/<uuid:pk>/resume/` | POST | `TenantResumeView` | 恢复 | HTML(Partial) | 运营+ |
| `/admin/tenants/<uuid:pk>/soft-delete/` | POST | `TenantSoftDeleteView` | 软删除 | HTML(Partial) | 运营+ |
| `/admin/tenants/<uuid:pk>/hard-delete/` | POST | `TenantHardDeleteView` | 硬删除(需 MFA 二次) | HTML(Partial) | **超级管理员** |
| `/admin/tenants/<uuid:pk>/restore-deletion/` | POST | `TenantRestoreDeletionView` | 撤销软删除(冷静期内) | HTML(Partial) | 运营+ |
| `/admin/tenants/<uuid:pk>/users/` | GET | `TenantUserListPartialView` | 详情页 Tab用户 | HTML(Partial) | 已登录 |
| `/admin/tenants/<uuid:pk>/users/<uuid:user_id>/reset-password/` | POST | `TenantUserResetPasswordView` | 重置租户用户密码 | HTML(Partial) | 运营+ |
| `/admin/tenants/<uuid:pk>/admins/` | GET | `TenantAdminListPartialView` | Tenant Admin 列表 | HTML(Partial) | 运营+ |
| `/admin/tenants/<uuid:pk>/admins/grant/` | POST | `TenantAdminGrantView` | 赋予管理员角色 | HTML(Partial) | 运营+ |
| `/admin/tenants/<uuid:pk>/admins/revoke/` | POST | `TenantAdminRevokeView` | 撤销管理员 | HTML(Partial) | 运营+ |
| `/admin/tenants/<uuid:pk>/plan/upgrade/` | POST | `TenantPlanUpgradeView` | 套餐升级 | HTML(Partial) | 运营+ |
| `/admin/tenants/<uuid:pk>/monitoring/` | GET | `TenantMonitoringPartialView` | 监控 TabGrafana iframe | HTML(Partial) | 已登录 |
| `/admin/tenants/<uuid:pk>/history/` | GET | `TenantHistoryPartialView` | 操作历史 Tab | HTML(Partial) | 已登录 |
### 4.4 备份与恢复
| URL Pattern | HTTP | 视图 | 触发场景 | 响应 | 权限 |
|---|---|---|---|---|---|
| `/admin/system/backups/` | GET | `BackupListView` | 备份任务列表 | HTML(Page) | 已登录 |
| `/admin/system/backups/rows/` | GET | `BackupRowsPartialView` | 筛选/翻页 | HTML(Partial) | 已登录 |
| `/admin/system/backups/schedule/` | GET | `BackupScheduleView` | 全局策略表单 | HTML(Page) | 超级管理员 |
| `/admin/system/backups/schedule/` | POST | `BackupScheduleView` | 提交策略 | HTML(Partial) | 超级管理员 |
| `/admin/tenants/<uuid:pk>/backups/` | GET | `TenantBackupListPartialView` | 详情页 Tab备份 | HTML(Partial) | 已登录 |
| `/admin/tenants/<uuid:pk>/backups/trigger/` | POST | `TenantBackupTriggerView` | 手动触发备份 | HTML(Partial) + `HX-Trigger: showToast` | 运营+ |
| `/admin/system/backups/<uuid:pk>/status/` | GET | `BackupStatusPartialView` | HTMX 每 5s 轮询任务进度 | HTML(Partial) 行 | 已登录 |
| `/admin/system/backups/<uuid:pk>/restore/` | POST | `BackupRestoreView` | 数据恢复(需 MFA 二次) | HTML(Partial) | **超级管理员** |
### 4.5 数据导出
| URL Pattern | HTTP | 视图 | 触发场景 | 响应 | 权限 |
|---|---|---|---|---|---|
| `/admin/system/exports/` | GET | `ExportListView` | 导出任务列表 | HTML(Page) | 已登录 |
| `/admin/system/exports/new/` | GET | `ExportCreateView` | 表单 | HTML(Page) | 运营+ |
| `/admin/system/exports/new/` | POST | `ExportCreateView` | 提交导出 | HTML(Partial) + Toast | 运营+ |
| `/admin/system/exports/<uuid:pk>/status/` | GET | `ExportStatusPartialView` | 轮询任务进度 | HTML(Partial) | 已登录 |
| `/admin/system/exports/<uuid:pk>/download/` | GET | `ExportDownloadRedirectView` | 跳转 R2 签名链接 | 302 | 触发人 + 超级管理员 |
### 4.6 系统升级
| URL Pattern | HTTP | 视图 | 触发场景 | 响应 | 权限 |
|---|---|---|---|---|---|
| `/admin/system/versions/` | GET | `SystemVersionListView` | 版本列表 | HTML(Page) | 已登录 |
| `/admin/system/versions/upgrade/` | GET | `UpgradeFormView` | 升级表单(选灰度名单) | HTML(Page) | 超级管理员 |
| `/admin/system/versions/upgrade/` | POST | `UpgradeFormView` | 触发升级(需 MFA 二次) | HTML(Partial) + Toast | 超级管理员 |
| `/admin/system/versions/<uuid:event_id>/progress/` | GET | `UpgradeProgressPartialView` | HTMX 每 3s 轮询进度 | HTML(Partial) 表格 | 已登录 |
| `/admin/system/versions/<uuid:event_id>/rollback/` | POST | `RollbackView` | 回滚(需 MFA 二次) | HTML(Partial) | 超级管理员 |
| `/admin/system/versions/<uuid:event_id>/incident/` | GET | `IncidentReportView` | 查看事件报告 | HTML(Page) | 已登录 |
### 4.7 监控与审计
| URL Pattern | HTTP | 视图 | 触发场景 | 响应 | 权限 |
|---|---|---|---|---|---|
| `/admin/monitoring/` | GET | `MonitoringView` | 监控大盘(嵌 Grafana | HTML(Page) | 已登录 |
| `/admin/monitoring/alerts/` | GET | `AlertRuleListView` | 告警规则 | HTML(Page) | 运营+ |
| `/admin/monitoring/alerts/<uuid:pk>/edit/` | POST | `AlertRuleUpdateView` | 编辑规则 | HTML(Partial) | 运营+ |
| `/admin/audit-logs/` | GET | `AuditLogListView` | 审计日志列表 | HTML(Page) | 已登录(含审计员) |
| `/admin/audit-logs/rows/` | GET | `AuditLogRowsPartialView` | HTMX 筛选/翻页 | HTML(Partial) | 同上 |
| `/admin/audit-logs/export/` | POST | `AuditLogExportView` | 异步导出 CSV | HTML(Partial) + Toast任务 ID | 同上 |
### 4.8 管理员设置
| URL Pattern | HTTP | 视图 | 触发场景 | 响应 | 权限 |
|---|---|---|---|---|---|
| `/admin/settings/admins/` | GET | `AdminAccountListView` | 管理员列表 | HTML(Page) | 超级管理员 |
| `/admin/settings/admins/new/` | POST | `AdminAccountCreateView` | 新增管理员 | HTML(Partial) | 超级管理员 |
| `/admin/settings/admins/<uuid:pk>/deactivate/` | POST | `AdminDeactivateView` | 停用 | HTML(Partial) | 超级管理员 |
| `/admin/settings/admins/<uuid:pk>/sessions/revoke/` | POST | `ForceLogoutView` | 强制登出该管理员 | HTML(Partial) | 超级管理员 |
| `/admin/settings/ip-whitelist/` | GET | `IpWhitelistListView` | 白名单 | HTML(Page) | 超级管理员 |
| `/admin/settings/ip-whitelist/new/` | POST | `IpWhitelistCreateView` | 新增 CIDR | HTML(Partial) | 超级管理员 |
| `/admin/settings/ip-whitelist/<uuid:pk>/toggle/` | POST | `IpWhitelistToggleView` | 启停 | HTML(Partial) | 超级管理员 |
| `/admin/settings/sessions/` | GET | `MyActiveSessionListView` | 当前管理员的活跃会话 | HTML(Page) | 已登录 |
### 4.9 HTMX 响应规范
| 场景 | HTTP 状态 | 响应内容 | 响应头 |
|------|----------|---------|--------|
| 操作成功 | 200 | 更新后的 HTML Partial | `HX-Trigger: {"fonrey:toast":{"type":"success","message":"..."}}` |
| 表单校验失败 | 422 | 含错误信息的表单 Partial保留用户输入 | 不发 Toast错误信息已嵌在表单内 |
| 业务规则拒绝(如未导出就硬删) | 422 | 表单 Partial + 顶部 Alert 块 | 可选 `HX-Trigger: {"fonrey:toast":{"type":"warning",...}}` |
| 权限不足 | 403 | 极简 Partial`<div class="alert alert-danger">无权限</div>` | `HX-Trigger: {"fonrey:toast":{"type":"error","message":"权限不足"}}` |
| 需要 MFA 二次确认 | 401 | 触发前端打开 MFA Modal | `HX-Trigger: {"fonrey:mfa-required":{"action":"hard_delete_tenant","target":"<id>"}}` |
| 未登录 / Session 过期 | 401 | 空 Partial | `HX-Redirect: /admin/login/` |
| 服务器异常 | 500 | 错误页 Partial | `HX-Trigger: {"fonrey:toast":{"type":"error","message":"系统异常,请重试"}}` |
**HTMX 相关请求头约定**
- 所有视图必须区分 `request.htmx`(来自 `django-htmx`):是 → 返回 partial 模板;否 → 返回完整页面或 302 跳转 list
- `hx-target` 命名以 `#tenant-list-tbody``#tenant-row-{id}``#dialog` 等结构化 ID 为约定
### 4.10 Celery 异步任务前端协议
```http
POST /admin/system/exports/new/
HX-Request: true
Content-Type: application/x-www-form-urlencoded
200 OK
HX-Trigger: {"fonrey:toast":{"type":"info","message":""}}
<tr id="export-row-{task_id}" hx-get="/admin/system/exports/{task_id}/status/"
hx-trigger="every 5s" hx-swap="outerHTML">
<td>{{ task.modules }}</td><td></td><td></td>
</tr>
```
**轮询规约**
- 轮询间隔:备份/导出 = 5s升级进度 = 3s
- 终态后端必须**移除** `hx-trigger="every"` 避免持续轮询:`hx-trigger="load"` 或不附 trigger
- 进度展示字段统一来自 Celery `AsyncResult` + DB 状态DB 优先,避免 Celery 结果过期丢失)
---
## 5. 权限与认证实现
### 5.1 角色体系PRD §9.2
```python
# apps/admin_console/permissions.py
class AdminRole:
SUPER = 'super_admin'
OPS = 'ops_operator'
AUDITOR = 'read_only_auditor'
# 操作 → 最低角色 映射
ACTION_REQUIRED_ROLE = {
'tenant.create': AdminRole.OPS,
'tenant.suspend': AdminRole.OPS,
'tenant.soft_delete': AdminRole.OPS,
'tenant.hard_delete': AdminRole.SUPER,
'tenant.restore': AdminRole.SUPER,
'system.upgrade': AdminRole.SUPER,
'system.rollback': AdminRole.SUPER,
'admin.manage': AdminRole.SUPER,
'admin.force_logout': AdminRole.SUPER,
'ip_whitelist.manage': AdminRole.SUPER,
'audit_log.read': AdminRole.AUDITOR,
'audit_log.export': AdminRole.AUDITOR,
'export.create': AdminRole.OPS,
'backup.trigger': AdminRole.OPS,
'backup.schedule_edit': AdminRole.SUPER,
# ... 完整映射详见 9.2 矩阵
}
```
### 5.2 认证后端
**独立 Auth Backend**`PlatformAdminBackend`,校验 `platform_admins.password_hash`Django PBKDF2/Argon2登录成功后写 `admin_sessions`,并把 `request.session['admin_id']``session_token` 关联。
**不复用** `django.contrib.auth.User`
- 租户业务用 `django.contrib.auth` + `apps.account.User`(在租户 schema
- 平台管理后台完全独立,避免角色/Session 跨域污染
### 5.3 中间件链(顺序敏感)
```python
MIDDLEWARE = [
'django_tenants.middleware.main.TenantMainMiddleware',
'apps.admin_console.middleware.IpWhitelistMiddleware', # 仅对 admin.* 域名生效
'apps.admin_console.middleware.AdminSessionMiddleware', # 校验 session_token滚动续 30min
'django.middleware.security.SecurityMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django_htmx.middleware.HtmxMiddleware',
# ...
]
```
- `IpWhitelistMiddleware`:只在 `request.host in ADMIN_CONSOLE_HOSTS` 时启用,查 `ip_whitelist`Redis 缓存),未命中返回 403 静态页(不暴露后台存在)
- `AdminSessionMiddleware`:每次请求把 `admin_sessions.expires_at = NOW() + 30min`,过期则清 cookie 并 302 到 `/admin/login/`
### 5.4 视图层 Mixin
```python
class AdminLoginRequiredMixin:
"""检查 request.platform_admin 是否存在;否则 302 /admin/login/"""
class RoleRequiredMixin(AdminLoginRequiredMixin):
required_role = None # AdminRole.SUPER / OPS / AUDITOR
# AUDITOR < OPS < SUPERSUPER 可访问全部
class ReadOnlyAuditorAllowedMixin(AdminLoginRequiredMixin):
"""允许审计员访问只读端点(仅 GET 安全方法)"""
class MfaConfirmedRequiredMixin(AdminLoginRequiredMixin):
"""要求当前 session 在最近 5 分钟内通过 MFA step-up 验证。
未通过 → 返回 401 + HX-Trigger: fonrey:mfa-required前端弹 Modal"""
class AuditedActionMixin:
"""form_valid 成功后通过 audit_service.write_audit() 记录日志,
自动从 self.request 提取 operator/ip/payload_summary"""
```
**典型组合**
```python
class TenantHardDeleteView(MfaConfirmedRequiredMixin, RoleRequiredMixin, AuditedActionMixin, FormView):
required_role = AdminRole.SUPER
audit_action = 'HARD_DELETE_TENANT'
```
### 5.5 数据范围控制
平台管理后台**不存在租户层数据范围限制**(与租户业务"经纪人只看自己名下"不同):
- 超级 / 运营:可见所有租户
- 审计员:可见所有数据但全局只读(通过 Mixin 拦截非 GET 请求 → 403
- Manager 层无需自动过滤;`PlatformAuditLog` 的 Manager 强制 `objects.append_only_create()`,禁用 `update`/`delete`
### 5.6 高危操作二次 MFA 流程
```
用户点击「硬删除」
→ HTMX POST /admin/tenants/<id>/hard-delete/
→ MfaConfirmedRequiredMixin 检测 session.mfa_confirmed_at 已过 5 min
→ 返回 401 + HX-Trigger: {"fonrey:mfa-required":{"action":"hard_delete","return_to":"<原 URL>"}}
→ 前端 Alpine.js 监听该事件 → 打开 MFA Modal
→ 用户输入 TOTP → POST /admin/login/mfa/step-up/ → 后端写 session.mfa_confirmed_at = now()
→ 前端拿到成功响应 → 重新发起原始 POST带 X-Mfa-Step-Up: 1→ 通过执行
```
---
## 6. 缓存策略
> **Key 规范**:本模块所有 Key 以 `pub:` 前缀public schema与租户业务的 `{schema}:` 前缀严格隔离。
| 缓存对象 | Key 格式 | TTL | 失效条件 |
|---|---|---|---|
| 平台管理员对象(含 role | `pub:admin:{admin_id}` | 30 min | 角色变更、停用、强制登出 |
| 管理员 Session 反查 | `pub:session:{session_token}` | 30 min与 expires_at 同步) | 强制登出、活动续期 |
| IP 白名单CIDR 列表) | `pub:ipwl:active` | 5 min | 新增/启停白名单 |
| MFA step-up 时间戳 | `pub:mfa:stepup:{session_token}` | 5 min | 自然过期 |
| 租户列表筛选总数 | `pub:tenant:count:{filter_hash}` | 30s | 短 TTL避免 COUNT(*) 全扫 |
| 租户基本信息 | `pub:tenant:{tenant_id}` | 10 min | 编辑、状态变更后主动清除 |
| 系统当前版本 | `pub:sys:current_version` | 1 h | 升级 / 回滚成功后清除 |
| 全局备份策略 | `pub:backup:schedule:global` | 1 h | 策略保存后清除 |
| 备份任务进度(前端轮询热点) | `pub:backup:status:{record_id}` | 5s | 任务结束后立即清 |
| 导出任务进度 | `pub:export:status:{task_id}` | 5s | 同上 |
| 升级事件聚合进度 | `pub:upgrade:progress:{event_id}` | 3s | 任务结束后清 |
| 仪表盘统计(总租户/活跃/本月新增) | `pub:dashboard:stats` | 1 min | 自然过期 |
| 服务健康状态 | `pub:health:{service}` | 30s | 自然过期 |
**失效策略**
- 通过 Django Signals 在 model `post_save` / `post_delete` 时主动 `cache.delete_many([...])`
- 任务进度类缓存(备份/导出/升级)只用作"减少 DB 压力",前端**仍以 DB 状态为准**:缓存 miss 直接查 DB
- IP 白名单缓存命中失败时不能放行,必须 fail-closed拒绝访问
---
## 7. 文件上传规范Cloudflare R2
### 7.1 本模块涉及的文件流向
| 场景 | 上传方 | 存储桶 | 路径模板 |
|---|---|---|---|
| 升级包 artifact | 超级管理员 → 后端 → R2 | `releases` | `releases/system/{version}/{filename}` |
| 备份产出pg_dump + R2 文件清单) | Celery worker → R2 | `backups` | `backups/{tenant_schema}/{record_id}.tar.gz` |
| 导出产出CSV/JSON/SQL Dump 压缩包) | Celery worker → R2 | `exports` | `exports/{tenant_schema}/{task_id}.zip` |
| 审计日志导出 CSV | Celery worker → R2 | `exports` | `exports/audit/{task_id}.csv` |
> 系统管理模块**不接收用户图片上传**(管理员头像可选,用 Gravatar/字母头像即可)。所有 R2 写入由后端 Celery 完成,**不使用前端直传 Presigned URL**。
> **选型理由**
> - 升级包/备份/导出均为大文件且涉及合规与完整性校验SHA256必须由可信后端校验后写入前端直传无法保证文件完整性与权限链
> - 频次极低(每日 < 100 次),中转带宽成本可忽略
> - 反之,租户业务模块(房源图片)才是高频次小文件,使用前端直传 Presigned URL属于其他模块的范畴
### 7.2 下载链接Presigned GET URL
- 导出包/备份包对外下载使用 R2 Presigned GET URLTTL = **24 小时**(与 `export_tasks.expires_at` 一致)
- 视图 `ExportDownloadRedirectView` 不返回链接给前端,而是 302 重定向到当时生成的签名 URL避免链接被前端日志采集泄漏
- 链接生成使用 boto3-S3 兼容 API密钥仅注入到 Celery worker 容器(管理后台 Web 容器无写权限)
### 7.3 文件命名
`{bucket}/{tenant_schema}/{model_id}/{uuid}.{ext}` —— 与 TECH_STACK.md 总纲一致;其中 `tenant_schema` 在 public schema 数据中对应 `tenants.schema_name`
### 7.4 类型与大小限制
| 文件类型 | MIME 二次校验 | Django 视图大小限制 | Nginx `client_max_body_size` |
|---|---|---|---|
| 升级包 `.zip` / `.tar.gz` | `application/zip` / `application/gzip` | 500 MB | 600 MB |
| 备份产出 | 后端生成,无上传 | — | — |
| 导出产出 | 后端生成,无上传 | — | — |
- 升级包视图使用 `python-magic` 读取头部字节做 MIME 校验,**不信任** `Content-Type` header 或文件扩展名
- 升级包 SHA256 在上传完成后由后端计算并落库(`system_versions.artifact_url` 同时写入校验码),客户端拉取时校验
---
## 8. Celery 异步任务规范
队列:所有任务统一进入 `admin_ops` 队列,避免与租户业务队列竞争。
| 任务名称 | 触发场景 | 预估耗时 | 重试策略 | 失败处理 |
|---|---|---|---|---|
| `provision_tenant` | 创建租户后异步执行 Schema 创建 + 迁移 + 默认数据注入 | 3060s | 不重试(失败必须人工介入) | 标记 `tenants.status='failed'`,事务回滚已创建资源,发邮件通知管理员;写审计 `CREATE_TENANT result=FAILED` |
| `auto_resume_suspended` | Celery Beat 每 10 min 扫描 `suspended_until <= NOW()` | < 5s | 最多 3 次60s 间隔 | Sentry 告警,保留 `suspended` 状态由人工兜底 |
| `purge_pending_delete` | Beat 每天 03:00 扫描冷静期到期租户 | 取决于租户大小110 min | 不重试 | 标记 `failed_to_purge`,告警 |
| `hard_delete_tenant` | 视图触发 | 110 min | 不重试 | 部分删除标记告警DROP SCHEMA 必须用事务 + SAVEPOINT |
| `run_backup` | 调度器 + 升级前 + 手动 | 1 min 2h取决数据量 | 最多 2 次指数退避5/30 min | 标记 `backup_records.status='failed'`,发邮件 |
| `cleanup_old_backups` | Beat 每天 04:00 | < 5 min | 最多 3 次 | 告警 |
| `run_restore` | 视图触发(高危) | 530 min | **不重试** | 失败 → 自动回滚到恢复前快照;事件报告写 `upgrade_events.incident_report` |
| `run_export` | 视图触发 | 115 min | 最多 2 次60s 间隔 | 标记 `failed`,邮件通知触发人 |
| `expire_export_links` | Beat 每小时 | < 1 min | 最多 3 次 | 告警 |
| `health_check` | 升级前 | < 30s | 最多 1 次 | 失败阻断升级,返回前端 422 |
| `run_upgrade` | 升级表单触发 | 5 min 2h | **不重试** | `upgrade_events.status='failed'`,前端按钮自动转换为「立即回滚」 |
| `run_rollback` | 升级失败 / 手动 | 530 min | **不重试** | 写 `incident_report`,自动 + 人工双重告警 |
| `send_welcome_email` | 租户开通成功后 | < 5s | 最多 5 次,指数退避 | 失败仅告警,不阻塞主流程 |
| `send_export_ready` | 导出完成后 | < 5s | 最多 5 次 | 同上 |
| `cleanup_admin_sessions` | Beat 每 30 min | < 5s | 最多 3 次 | 告警 |
| `aggregate_dashboard_stats` | Beat 每 1 min | < 10s | 最多 2 次 | 失败时仪表盘读旧缓存 |
**通用约定**
- 所有任务使用 `bind=True`,前置统一 `audit_service.write_audit()`(成功/失败均落审计)
- 涉及租户 schema 操作的任务必须 `with schema_context(tenant.schema_name):`
- 长任务(> 5 min必须周期性 `task.update_state(state='PROGRESS', meta={...})`,前端轮询读取
- 重试策略统一通过装饰器 `@retry(max_retries=N, backoff='exponential', initial_delay=...)` 实现
- 不重试任务必须显式 `acks_late=True, autoretry_for=()`
### 8.5 升级类型分级A / B / C
「升级」必须先按内容分类,不同类型的分批能力天然不同。混淆三者会导致设计错误。
| 类型 | 内容 | 是否可分批到租户级 | 编排路径 |
|---|---|---|---|
| **A. 应用代码升级** | Python 代码、模板、JS/CSS 包、Celery Worker 镜像 | ❌ 单进程多租户架构下物理上不可分批;只能整体蓝绿切换 | 运维侧K8s/Compose 切流),不在本模块编排;本模块仅记录 `system_versions` 元数据 |
| **B. 租户 Schema 迁移** | `apps.property` / `apps.client` 等租户 App 的 `migrations/*.py` | ✅ 按 `schema_name` 分批迁移 | 本模块 `run_upgrade` 编排(详见 §8.6 |
| **C. Feature Flag 灰度** | 新功能的运行时启停(双路径分支) | ✅ 按租户 / 用户 / 百分比 | 本模块 `feature_flags` 服务(详见 §8.7 |
**强制纪律**
- A 类的「灰度名单」字段(`upgrade_events.gray_tenant_ids`)在 PRD §5.1.6 表单中**必须置灰并提示**:「应用代码升级影响全部租户,本字段仅对 schema 迁移类型升级生效」
- 系统升级表单上必须先选择类型 `upgrade_type ∈ {A_app, B_schema, C_feature}`UI 据此切换可填字段
- 真实生产中绝大多数版本是 A+B 混合A 部分先全量切换蓝绿B 部分按本模块编排分批迁移C 部分(功能开关)独立于版本号
### 8.6 B 类Schema 迁移)分批编排详解
#### 8.6.1 编排状态机
```
[draft] ──提交──→ [pre_check] ──健康通过──→ [pre_backup] ──备份完成──→ [batch_running]
批次成功 ↓ │ 批次失败
[batch_done] ↓
↓ [halted]──人工──→ [rollback] / [resume]
下一批 / 完成 ↓
[succeeded]
```
`upgrade_events.status` 字段取值与上图一致;前端 §4.6 进度页根据状态控制按钮可见性halted 状态显示「继续 / 回滚」二选一)。
#### 8.6.2 Celery 任务结构
新增/细化以下任务(替换 §8 表中粗粒度 `run_upgrade`
| 任务名称 | 职责 | 队列 | 重试 |
|---|---|---|---|
| `orchestrate_upgrade(event_id)` | 顶层编排器:跑 pre-check → pre-backup → 按批次循环派发 → 健康门控 → 终态 | `admin_ops` | 不重试(失败 → halted |
| `migrate_single_tenant(event_id, tenant_id)` | 单租户:创建轻量快照 → `schema_context``call_command('migrate')` → smoke test → 失败回滚该租户 | `migration`(独立队列限并发) | 不重试 |
| `tenant_smoke_test(tenant_id)` | 单租户健康检查:跑预定义关键 ORM 查询、HTTP 探活 | `admin_ops` | 不重试 |
| `post_batch_health_gate(event_id, batch_no)` | 批后门控:从 Prometheus + Sentry + Flower 拉指标,判断是否进入下一批 | `admin_ops` | 不重试 |
| `rollback_single_tenant(tenant_id, snapshot_id)` | 单租户回滚到本次升级前快照 | `migration` | 不重试 |
| `rollback_upgrade(event_id)` | 整体回滚:对所有 `progress.status='success'` 的租户依次调用 `rollback_single_tenant` | `admin_ops` | 不重试 |
**专用队列 `migration`**:与 `admin_ops` 分开,限制 `--concurrency=2 --prefetch-multiplier=1`,避免并发 `migrate` 打爆 PostgreSQL 连接池或触发 DDL 锁竞争。
#### 8.6.3 批次与并发参数(`upgrade_events` 表字段建议)
数据模型补充建议(提交给 DATA_MODEL_PUBLIC.md 维护者):
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `upgrade_type` | varchar(16) | `B_schema` | A_app / B_schema / C_feature |
| `batch_size` | int | 5 | 每批包含的租户数 |
| `batch_concurrency` | int | 2 | 批内并发执行的租户数(≤ batch_size |
| `batch_interval_seconds` | int | 300 | 批间观察窗口(秒),让监控指标稳定 |
| `failure_policy` | varchar(16) | `halt_batch` | `halt_batch`(任一租户失败即中断本批)/ `continue`(其他租户继续,仅标记失败) |
| `health_gate_config` | jsonb | `{}` | 门控阈值覆盖(默认值见 §8.6.5 |
#### 8.6.4 单租户快照策略
不能用全局 `pg_dump`(恢复粒度太粗)。分级方案:
| 时机 | 方案 | 用途 |
|---|---|---|
| 升级开始前(一次) | 全租户 `pg_dump`(即 `pre_backup_record_id` | 兜底;用于「整体灾难回滚」 |
| 单租户迁移开始前 | `pg_dump -n {schema}` 快照到独立文件,约 110s | 用于该租户失败时秒级回滚drop schema + restore |
| 迁移完成后 7 天 | 自动清理单租户快照 | 缩短保留期,节省存储 |
实现:`migrate_single_tenant` 第一步调用 `backup_service.snapshot_tenant(tenant)`,返回 `snapshot_id` 写入 `upgrade_events.progress[tenant_id].snapshot_id`
#### 8.6.5 批后健康门控Health Gate
每批结束后,`post_batch_health_gate` 任务检查以下指标,**任一不通过即 halt**
| 指标 | 来源 | 默认阈值 | 含义 |
|---|---|---|---|
| `error_rate_5xx_5m` | Prometheus | < 0.5% | 近 5 分钟 5xx 比例 |
| `p95_latency_5m` | Prometheus | < 2000 ms | 近 5 分钟 P95 延迟 |
| `celery_queue_pending` | Flower | < 1000 | 任务队列积压 |
| `sentry_new_issues_5m` | Sentry API | < 5 | 近 5 分钟新错误数 |
| `migrated_tenant_smoke_pass_rate` | DBprogress 字段) | = 100% | 本批所有租户 smoke test 通过 |
阈值可在 `upgrade_events.health_gate_config` 中按本次升级覆盖。门控不通过时:
- `event.status = 'halted'``halted_reason` 写入失败指标快照
- 推送企业微信 + 邮件给所有超级管理员
- 前端进度页弹出 Modal超管二选一「继续下一批」/「立即回滚已升级租户」
#### 8.6.6 DDL 兼容性纪律(**最重要的工程约束**
分批升级期间,新代码会同时面对**新旧两种 schema**(已迁移租户用新 schema未迁移租户仍是旧 schema。这要求**所有 migration 必须向后兼容**。
| 类型 | 是否安全 | 备注 |
|---|---|---|
| `ADD COLUMN` 带 NULL 或默认值 | ✅ | 默认值不要用大表 `UPDATE`,改用应用层填充 |
| `CREATE INDEX CONCURRENTLY` | ✅ | 必须 `CONCURRENTLY`,否则锁表 |
| `ADD CONSTRAINT ... NOT VALID` + 后续 `VALIDATE` | ✅ | 拆两次发布 |
| 新增表 / 新增视图 | ✅ | 旧代码不感知即可 |
| `DROP COLUMN` | ❌ | 旧代码可能仍在写;必须拆两次发布 |
| `RENAME COLUMN` / `RENAME TABLE` | ❌ | 同上 |
| `ALTER COLUMN TYPE` 不兼容类型 | ❌ | 必须用「新增列 + 双写 + 切读 + 删旧列」分四步 |
| 删除唯一约束 / 主键 | ❌ | 必须拆 |
**两阶段发布范式**
1. **v_n扩展**:新增字段 / 表 / 索引;代码层「双写双读」(同时维护新旧字段,读写都兼容)
2. **v_{n+1}(清理)**:在 v_n 全量上线 ≥ 1 周且监控正常后,再发布迁移删除旧字段;此阶段允许出现破坏性 DDL但因为旧代码已经下线安全
**强制门禁**
- 所有租户 App 的 `migrations/*.py` 必须由 `engineering-backend-architect` 在 PR review 时检查兼容性
- CI 增加迁移静态扫描(`django-migration-linter`),命中破坏性操作直接阻断 merge
- migration 文件提交时强制附带 `# UPGRADE_TYPE: expand|cleanup` 注释CI 据此区分门禁规则
### 8.7 C 类Feature Flag灰度体系
#### 8.7.1 适用场景
- 新业务功能上线(如「房源 AI 描述生成」),先开 5 个租户 → 50 个 → 全量
- 重构高风险模块(如搜索算法),需要 A/B 对比
- 商业策略(如「企业版独享某功能」)
- B 类升级双写阶段切读:先用新字段服务部分租户,验证后全量
#### 8.7.2 数据模型(建议提交 DATA_MODEL_PUBLIC.md
```sql
-- public.tenants 表新增列
ALTER TABLE public.tenants
ADD COLUMN feature_flags JSONB NOT NULL DEFAULT '{}'::jsonb;
-- 全局 Flag 注册表(控制平面,非按租户)
CREATE TABLE public.feature_flag_definitions (
key varchar(64) PRIMARY KEY, -- 如 'ai_description_v2'
description text NOT NULL,
default_value boolean NOT NULL DEFAULT false, -- 未在 tenant.feature_flags 显式覆盖时的默认
rollout_strategy varchar(16) NOT NULL DEFAULT 'tenant', -- tenant | percentage | user
rollout_config jsonb NOT NULL DEFAULT '{}', -- e.g. {"percentage": 30}
owner_admin_id uuid REFERENCES public.platform_admins(id),
created_at timestamptz NOT NULL DEFAULT NOW(),
archived_at timestamptz NULL -- 归档时间;归档后视为永久关闭
);
-- Flag 变更历史append-only与 platform_audit_logs 一致约束)
CREATE TABLE public.feature_flag_change_log (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
flag_key varchar(64) NOT NULL,
tenant_id uuid NULL REFERENCES public.tenants(id), -- NULL 表示全局变更
old_value jsonb,
new_value jsonb NOT NULL,
operator_id uuid NOT NULL REFERENCES public.platform_admins(id),
reason text NOT NULL, -- 强制填写变更原因
created_at timestamptz NOT NULL DEFAULT NOW()
);
```
#### 8.7.3 服务层 API
```python
# apps/admin_console/services/feature_flags.py
def is_enabled(tenant, flag_key: str, *, user=None) -> bool:
"""运行时查询 Flag。
优先级tenant.feature_flags 显式覆盖 > 全局 rollout_strategy > default_value
"""
# 1. 租户级显式覆盖
if flag_key in tenant.feature_flags:
return bool(tenant.feature_flags[flag_key])
# 2. 全局策略
definition = _get_definition_cached(flag_key)
if definition is None or definition.archived_at:
return False
if definition.rollout_strategy == 'percentage':
# 稳定哈希:同一 tenant 永远落到同一桶,避免抖动
bucket = stable_hash(f"{flag_key}:{tenant.id}") % 100
return bucket < definition.rollout_config.get('percentage', 0)
if definition.rollout_strategy == 'user' and user:
bucket = stable_hash(f"{flag_key}:{user.id}") % 100
return bucket < definition.rollout_config.get('percentage', 0)
return definition.default_value
```
**关键约束**
- `_get_definition_cached` 走 Redis 缓存(`pub:ff:def:{key}`TTL 1 min避免每个请求查 DB
- 业务代码必须用 `is_enabled(...)` 接口,**严禁**直接读 `tenant.feature_flags[...]`(绕过策略层)
- `stable_hash` 使用 `xxhash`,租户 ID 在内的 key 长期稳定,避免百分比策略下租户被频繁挤进/挤出
#### 8.7.4 缓存策略(补充 §6 缓存表)
| 缓存对象 | Key | TTL | 失效条件 |
|---|---|---|---|
| Flag 全局定义 | `pub:ff:def:{flag_key}` | 1 min | 定义变更后立即清 |
| 租户 Flag 覆盖 | `pub:ff:tenant:{tenant_id}` | 5 min | 租户 Flag 变更后清 |
#### 8.7.5 管理界面(补充 §4 路由表)
| URL Pattern | HTTP | 视图 | 权限 |
|---|---|---|---|
| `/admin/feature-flags/` | GET | `FeatureFlagListView`(列出所有 Flag 定义) | 已登录 |
| `/admin/feature-flags/new/` | POST | `FeatureFlagCreateView`(新增 Flag | 超级管理员 |
| `/admin/feature-flags/<key>/rollout/` | POST | `FeatureFlagRolloutView`(调整百分比 / 策略) | 超级管理员 |
| `/admin/feature-flags/<key>/archive/` | POST | `FeatureFlagArchiveView`(归档) | 超级管理员 |
| `/admin/tenants/<uuid:pk>/feature-flags/` | GET | `TenantFlagsPartialView`(详情页 Tab租户级覆盖 | 已登录 |
| `/admin/tenants/<uuid:pk>/feature-flags/toggle/` | POST | `TenantFlagToggleView`(覆盖某 Flag | 运营+ |
所有写操作必填 `reason`(变更原因),写入 `feature_flag_change_log``platform_audit_logs`
#### 8.7.6 与 B 类的最佳实践组合
发布破坏性变更(如重命名字段、改变行为)的标准 4 步流程:
1. **B 类(扩展)**:新增字段,代码双写双读(旧字段为权威)
2. **C 类(切读灰度)**:开 Flag `read_from_new_field`,按租户 5% → 50% → 100% 灰度
3. **C 类(切写灰度)**:开 Flag `write_to_new_field_only`,按租户灰度
4. **B 类(清理)**:在 v_{n+1} 删除旧字段;归档相关 Flag
这样 B 类只承担「结构准备」低风险、可分批C 类承担「行为切换」(可即时关停),是 SaaS 多租户系统最稳健的演进模式。
---
## 9. 监控集成
| 维度 | 实现 |
|---|---|
| Grafana 嵌入 | `MonitoringView` 渲染含 Grafana iframe 的页面URL 含短期签名 token避免暴露 Grafana 公网入口 |
| 告警接收 | Grafana → Webhook → `apps.admin_console.views.alerts.GrafanaWebhookView`HMAC 签名校验) |
| Sentry | 独立 DSN与租户业务分离方便定位平台层 Bug |
| Celery 队列健康 | Flower 部署在 `admin.fonrey.com/flower/`,仅超级管理员可访问 |
| 审计日志告警 | 任意 `result='FAILED'` 的高危操作HARD_DELETE / RESTORE / UPGRADE / ROLLBACK实时推送企业微信 / 邮件 |
监控数据采集来源PRD §5.1.5CPU/内存来自 Prometheus node_exporter存储/API/活跃数来自应用埋点写 PostgreSQL `tenant_metrics_daily` 物化视图(属租户业务模块范畴,本模块仅消费)。
---
## 10. 测试规范
### 10.1 覆盖矩阵
| 类别 | 工具 | 必测内容 |
|------|------|---------|
| Model | pytest-django + factory_boy | UUID 默认值、状态机 CHECK 约束、唯一索引(`schema_name` / `email` / `published current`、append-only Manager、软删除标记 |
| Service | pytest-django + Mock R2 / Mock Celery | `tenant_service.provision()` 失败回滚、`audit_service.write_audit()` 字段完整、状态机非法迁移抛错 |
| ViewHTTP | `django-tenants` 公共 schema TestCase + `Client(HTTP_HOST='admin.fonrey.com')` | 三角色 × 关键端点的 200/403/401未登录三场景MFA step-up 拦截CSRF |
| ViewHTMX | 同上 + `HTTP_HX_REQUEST='true'` | 验证返回为 partial不含 `<html>` 根标签),且响应头包含约定的 `HX-Trigger` |
| Middleware | pytest-django | IP 白名单命中/未命中Session 滚动续期;过期 302 |
| Celery 任务 | `CELERY_TASK_ALWAYS_EAGER=True` + R2/邮件 Mock | 主流程 + 失败回滚 + 重试次数 + 不重试任务的 `acks_late` 行为 |
| 安全回归 | 集成测试 | 跨域名访问(用 `*.fonrey.com` host 访问 `/admin/...` → 404 或重定向);租户用户身份不能登入管理后台;管理后台 Cookie 不能在租户域名下生效;`'django.contrib.admin' not in settings.INSTALLED_APPS` 断言;`apps/``config/` 全文 grep 不应命中 `from django.contrib import admin`CI 守门) |
### 10.2 关键测试约束
- 禁止使用 Django 原生 `Client()`,统一使用 `django_tenants.test.client.TenantClient` 配合 `public` schema fixture
- 所有受角色保护的 View 必须覆盖超级200/204、运营200 或 403、审计员GET 200 / 非 GET 403、未登录302 → /admin/login/
- 高危操作测试必须包含 MFA step-up 已通过 / 未通过两个分支
- `platform_audit_logs` 测试:执行任意写操作后断言审计行存在且字段一致;尝试 UPDATE / DELETE 该表必须抛 `IntegrityError`(通过 Manager 限制 + 数据库 trigger 双重保险)
- Celery 异步测试覆盖率:`tasks/` 模块 ≥ 85%`services/` 模块 ≥ 90%`views/` 模块 ≥ 75%
### 10.3 测试数据约定
- `factories.py` 提供 `PlatformAdminFactory(role=...)` / `TenantFactory(status=...)` / `AuditLogFactory()` / `BackupRecordFactory()`
- 租户工厂创建后自动调用 `provision_tenant.delay()`(在 EAGER 模式下同步执行 Schema 创建),便于跨 schema 测试
---
## 11. 安全要点(强制执行)
| 项 | 要求 |
|---|---|
| MFA 强制 | `platform_admins.mfa_enabled=False` 时除 `MfaSetupView` 外所有视图 302 强制跳转设置 |
| TOTP 密钥 | `admin_mfa_devices.totp_secret` AES-256-GCM 加密存储,密钥来自环境变量 `ADMIN_MFA_KEY`,不与租户加密密钥共用 |
| 密码哈希 | Argon2idDjango `ARGON2_PASSWORD_HASHER`),不允许降级 PBKDF2 |
| 暴力破解防护 | 登录失败 5 次锁定账号 15 minRedis 计数器Key `pub:login:fail:{username}`);同 IP 失败 20 次锁定 IP 1h |
| Session 安全 | Cookie `Secure``HttpOnly``SameSite=Strict`Cookie domain 限定 `admin.fonrey.com`(不允许跨子域) |
| CSRF | 所有写操作启用 CSRFHTMX 通过 `hx-headers='{"X-CSRFToken": "..."}'` 在 base 模板注入 |
| CSP | `default-src 'self'`Grafana iframe 域加入 `frame-src` 白名单;禁止 `unsafe-inline` 脚本HTMX/Alpine 的内联事件已用 attribute 模式,符合) |
| 高危操作 | 硬删除/恢复/升级/回滚必须 MFA step-up5 min 时效) |
| 审计日志不可变 | DB 层 trigger`BEFORE UPDATE OR DELETE ON platform_audit_logs ... RAISE EXCEPTION`ORM 层 Manager 重写 `update()` `delete()` 抛错 |
| 跨域名严禁串台 | 租户 host 上访问 `/admin/...` 必须 404管理 host 上访问租户 URL 必须 404`IpWhitelistMiddleware` + URLConf 双重保证 |
| Django Admin 全环境弃用 | `INSTALLED_APPS` 不包含 `django.contrib.admin``urls_public.py` / `urls_tenant.py` 不导入 `admin.site`;启动期 `assert` 兜底CI 步骤 `grep` 命中 `from django.contrib import admin` 即构建失败。原因详见 §1.5 |
| 紧急数据修复流程 | 不开 Admin 后门;统一走 `manage.py shell_plus` 在堡垒机执行,操作前后由超管在本模块 `/admin/audit-logs/` 手工补录审计条目(`source='manual_shell'` |
---
## 12. 部署规范
| 项 | 配置 |
|---|---|
| 域名 | `admin.fonrey.com` 解析到与租户应用相同的 Gunicorn/Uvicorn 集群(共用进程,省运维成本) |
| Nginx | `server_name admin.fonrey.com` 单独 server block① IP 白名单 `allow / deny`(与应用层双重保险)② `client_max_body_size 600M` 仅限 `/admin/system/versions/upgrade/`;其他端点 10M |
| Celery worker | 独立部署 worker 监听 `admin_ops` 队列,`--concurrency=2 --max-tasks-per-child=50`(任务多为 IO 密集长任务) |
| Celery beat | 单实例运行所有调度任务auto_resume / purge / cleanup_old_backups / expire_export_links / aggregate_dashboard_stats注册于此 |
| 密钥管理 | `ADMIN_MFA_KEY` / `R2_ADMIN_KEY` / `GRAFANA_SIGN_KEY` 通过 Docker Secret 注入;不出现在 `.env` 文件 |
| 日志 | Web 访问日志 / 审计日志 / Sentry 三路独立审计日志同时落库DB+ 落对象存储R2 月归档) |
| 备份的备份 | 备份元数据(`backup_records`)随 `public` schema 每日 02:00 全量 dump 到独立 R2 桶 `meta-backups`,灾难场景下用于重建 |
---
## 13. 文档变更记录
| 版本 | 日期 | 变更 |
|------|------|------|
| v1.0 | 2026-04-26 | 初稿。基于 PRD v1.0 + DATA_MODEL_PUBLIC v1.1 编制 |
| v1.1 | 2026-04-26 | 明确全环境弃用 `django.contrib.admin`;新增 §1.5 章节、settings 启动断言、安全要点 2 条、CI 守门测试 |
| v1.2 | 2026-04-26 | 新增 §8.5§8.7:升级类型 A/B/C 分级、B 类 Schema 迁移分批编排(状态机/任务表/快照/健康门控/DDL 兼容性纪律、C 类 Feature Flag 灰度体系(数据模型 + 服务 API + 管理界面 + 与 B 类组合最佳实践) |