文档更新

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

@@ -11,6 +11,8 @@
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-04-30 | Atlas | 初始化 ADR 动态决策记录;补录当日关键技术与需求决策 |
| 2026-05-02 | Sisyphus | 新增 ADR-20260502-001合并系统管理与客户端发布两份 PRD 为统一的『平台管理后台 PRD』原文件删除 |
| 2026-05-02 | Sisyphus | 新增 ADR-20260502-003定义『PRD 与 Tech 文档职责边界』规则PRD 管 what/why、Tech 管 how首次落地于登录管理 PRD v3.0 |
## 一、记录规范(必须遵守)
@@ -155,6 +157,96 @@
---
## 2026-05-02
### ADR-20260502-001
- **类型**REQ
- **模块**平台管理后台PRD 治理)
- **状态**accepted
- **背景**:原 `PRD/系统管理/系统管理模块PRD.md`v1.3)与 `PRD/发布管理/客户端发布管理模块PRD.md`v1.2)虽分别归口「系统管理」与「客户端发布」,但二者实际受众均为**平台管理员**Platform Admin / 运营人员 / 只读审计员),分别描述会导致:① 页面路由与权限矩阵在两份 PRD 中各自演化、容易割裂;② API 操作清单缺乏统一规划;③ 客户端发布 v1.1 已明确归属"平台运营后台",与系统管理同处一域。
- **决策**
1. 合并为单一文档 `PRD/平台管理后台/平台管理后台PRD.md`v1.0),定位「面向平台管理员的统一后台 PRD」以产品视角统一规划页面清单、访问权限、页面间导航逻辑、业务 API 操作清单(不绑定具体路径)。
2. 原两份 PRD 文件**直接删除**,由新文档完全取代;新文档头部声明 supersede 关系。
3. 客户端运行时与平台之间的接口(如查询最新版本、上报心跳)维持在 `TECH_STACK/客户端发布管理技术方案.md` 中描述,本 PRD 仅描述平台管理员可执行的业务操作。
4. 角色权限矩阵、租户状态机、客户端版本治理(含全平台租户活跃榜)等内容统一并入新 PRD 第 57 章。
- **影响范围**
- PRD删除 `PRD/系统管理/系统管理模块PRD.md``PRD/发布管理/客户端发布管理模块PRD.md`;新增 `PRD/平台管理后台/平台管理后台PRD.md`
- README模块入口索引同步更新原「系统管理」「客户端发布」入口指向新 PRD客户端技术方案文档保留
- TECH_STACK`客户端发布管理技术方案.md` 关联 PRD 的引用需同步指向新 PRD
- **关联文档**
- `PRD/平台管理后台/平台管理后台PRD.md`
- `README.md`
- `TECH_STACK/客户端发布管理技术方案.md`
- `TECH_STACK/系统设置技术方案.md`
- **备注**本决策不改变任何技术口径API 命名空间、`client_heartbeats` 表结构、SHA256 校验等),仅是 PRD 文档治理层面的合并与归属调整。`ADR-20260430-006/007/008/009` 全部继续生效。
### ADR-20260502-002
- **类型**TECH
- **模块**平台管理后台TECH_STACK 治理)
- **状态**accepted
- **背景**:随 `ADR-20260502-001` 完成 PRD 合并后TECH_STACK 域仍存在两份独立技术方案:`TECH_STACK/客户端发布管理技术方案.md`v1.0)与 `TECH_STACK/系统管理技术文档.md`v1.x。两者受众一致平台管理员后台却分别演化路由表、API 命名空间、Mixin 守卫链与错误码,存在以下风险:① 路由守卫与 step-up MFA 协议在两文档中各自定义,易割裂;② API 命名空间(`/admin/...` 后台 vs `/api/release/...` 客户端运行时)缺乏统一约束;③ Celery 队列、Redis Key、错误码族无法横向对齐。
- **决策**
1. 合并为单一文档 `TECH_STACK/平台管理后台技术方案.md`v1.0),覆盖 PRD 要求的三维度——**技术选型**(实现路由与 API 的框架)、**页面路由表**(路径定义、动态参数、路由守卫、懒加载)、**API 设计**RESTful 路径、HTTP 方法、请求/响应、错误码、版本控制、认证方式)。
2.`TECH_STACK/客户端发布管理技术方案.md``TECH_STACK/系统管理技术文档.md` **直接删除**,由新文档完全取代。
3. 双 API 命名空间最终落地:`/admin/...`HTMX PartialSession+CSRF+TOTP无路径版本号`/admin/api/client-releases/...`(管理端 JSON 写操作)、`/api/release/v1/...`(客户端运行时,路径版本号 `v1`)。
4. App 拆分:`apps/admin_console`(租户/备份/导出/升级/审计/管理员/Feature Flag+ `apps/release`(客户端发布 + 客户端运行时 API后者仅依赖前者的 `permissions / middleware / services.audit_service`
5. `TECH_STACK/TECH_STACK.md` §8 模块表合并「系统设置」「客户端发布」相关行为单行「平台管理后台」入口README 模块入口同步指向新文档。
- **影响范围**
- TECH_STACK新增 `平台管理后台技术方案.md`;删除 `客户端发布管理技术方案.md``系统管理技术文档.md`
- TECH_STACK`TECH_STACK.md` §8 模块表
- README模块入口索引平台管理后台一行
- **关联文档**
- `TECH_STACK/平台管理后台技术方案.md`
- `TECH_STACK/TECH_STACK.md`
- `PRD/平台管理后台/平台管理后台PRD.md`
- `ADR-20260502-001``ADR-20260430-006/007/008/009`
- **备注**本决策不变更任何技术口径API 命名空间、`/api/release/v1/...` 版本号、SHA256 校验、`client_heartbeats` Upsert + 24h 活跃口径、审计不可变约束等),仅在 TECH_STACK 文档治理层面执行合并与归属调整。
### ADR-20260502-003
- **类型**REQ
- **模块**:文档治理(全局)
- **状态**accepted
- **背景**:历史 PRD`PRD/登录管理/用户登录管理模块PRD.md` v2.0、`PRD/平台管理后台/平台管理后台PRD.md` v1.0)混杂大量实现细节——具体 API 路径与 HTTP 方法、Redis Key 格式与 TTL、Django 字段类型与中间件类名、Electron API 名、CSS 类名、Cookie 属性等。这导致:① PRD 评审被技术细节淹没,业务边界讨论失焦;② 实现口径出现"PRD 写一份、Tech 写一份"的双源头,长期产生漂移(参见 `ADR-20260430-004` 已为登录模块单独修过一次);③ 业务变更与技术调整混在同一份文档里,变更范围与责任人难以拆分。
- **决策**:自本 ADR 起,全项目 PRD 与 Tech 文档严格遵循以下职责边界,任何新建/修订必须自检通过。
**PRD 应包含("是什么 / 为什么"**
1. **页面与导航**:列出页面清单、访问权限(角色矩阵)、用户视角的页面间跳转逻辑、登录态/未登录态分支。✅ "/dashboard 需登录后访问";✅ "用户点击『详情』后跳转到订单详情页"。
2. **业务操作清单**:以业务动词描述用户/角色可执行的能力。✅ "用户可查询自己的订单列表";✅ "管理员可批量更新商品状态"。
3. **业务规则与数据约束**:抽象层面的规则与数值阈值(如『密码连续错误 5 次锁定 30 分钟』『短信验证码有效期 10 分钟』),但仅以业务语言表达,不绑定 Redis Key 或字段名。
4. **状态机**:业务状态枚举与合法跃迁。
5. **验收标准**:可由产品/QA 验证的用户可见行为。
**PRD 必须移出(移交 Tech / DATA_MODEL**
1. ❌ 具体 API 路径(如 `POST /api/auth/login/phone/`)与 HTTP 方法。
2. ❌ 请求/响应 JSON Schema、字段名、错误码常量。
3. ❌ Redis Key 格式、TTL、缓存策略、消息队列名。
4. ❌ 数据库字段名、字段类型(`CharField(30)``OneToOneField`)、表名、索引名、中间件类名。
5. ❌ 前端框架/库的 API 名(`BrowserWindow.loadURL``electron-store``HX-Request`、CSS 类名、Cookie 属性(`SameSite=Strict``HttpOnly`)。
6. ❌ 实现选型与库依赖清单(除非业务上明确强制,如"必须由具备短信资质的服务商发送")。
**PRD 引用 Tech 的标准格式**当业务规则需要技术细节落地时PRD 用以下任一方式引用:
- "(实现细节详见 `TECH_STACK/<模块>技术方案.md` §X.Y"
- "(数据结构见 `DATA_MODEL/<模块>.md`"
- "(端点契约见 `TECH_STACK/API_CONTRACT.md`"
**Tech 文档应承接**:上述被移出的全部内容;若 PRD 移出某项时 Tech 文档尚未承接,**必须在同一次提交内同步补齐 Tech**,禁止信息丢失。
**测试用例不受本 ADR 约束**:测试用例本质上需要可执行的实现细节(路径、字段、错误码),保留细节不动;但其引用 PRD 章节时应同步更新章节号。
- **影响范围**
- PRD所有 PRD 文档需按本规则审视并修订(首批落地:登录管理 PRD → v3.0;后续模块按需推进)
- TECH_STACK所有 Tech 文档需承接对应实现细节,缺口必须同步补齐
- 未来所有新建 PRD必须自检本规则
- CI/Review建议在 PR Review checklist 增加"PRD 是否含具体 API 路径/Redis Key/字段类型"的反向检查项
- **关联文档**
- `AGENTS.md` §9.1ADR 治理联动规则)
- `PRD/登录管理/用户登录管理模块PRD.md`v3.0 首批落地)
- `TECH_STACK/登录管理技术方案.md`(同步承接被移出细节)
- `ADR-20260430-004`(登录接口路径以 PRD 为准 → 本 ADR 后语义升级为:业务能力以 PRD 为准,具体路径以 Tech 为准;不冲突,互补)
- **备注**:本 ADR 不更改任何已实现的技术口径,仅约束**文档承载位置**。`ADR-20260430-004` 关于"登录接口最终路径"的权威源仍以两份文档同步为准PRD 描述业务操作、Tech 描述具体路径),二者不应再次出现漂移。
---
## 三、按模块分类记录(视图索引)
## 3.1 测试治理(全局)
@@ -167,7 +259,7 @@
- `ADR-20260430-005`MVP 范围冻结REQ
## 3.3 客户端发布管理
- `ADR-20260430-006`新增独立技术方案TECH
- `ADR-20260430-006`新增独立技术方案TECH *(superseded by `ADR-20260502-002`,技术方案已合并至『平台管理后台技术方案』)*
- `ADR-20260430-008`SHA256 完整性校验强制TECH
- `ADR-20260430-009`API 命名空间统一 `/api/release/...`TECH
@@ -176,6 +268,11 @@
## 3.5 文档治理(全局)
- `ADR-20260430-010`变更历史章节统一规则TECH
- `ADR-20260502-003`PRD 与 Tech 文档职责边界REQ
## 3.6 平台管理后台
- `ADR-20260502-001`:合并系统管理 PRD 与客户端发布 PRD 为统一的『平台管理后台 PRD』REQ
- `ADR-20260502-002`合并系统管理技术文档与客户端发布技术方案为统一的『平台管理后台技术方案』TECH
---
@@ -195,6 +292,9 @@
| ADR-20260430-008 | 2026-04-30 | 客户端发布安全 | TECH | accepted | SHA256 校验失败禁止安装 | `TECH_STACK/客户端发布管理技术方案.md` |
| ADR-20260430-009 | 2026-04-30 | 客户端发布 API | TECH | accepted | 统一 `/api/release/...` 路径 | `TECH_STACK/TECH_STACK.md` |
| ADR-20260430-010 | 2026-04-30 | 文档治理 | TECH | accepted | 变更历史章节位置统一规范 | `TECH_STACK/*.md` `DATA_MODEL/*.md` `TEST_CASES/*.md` |
| ADR-20260502-001 | 2026-05-02 | 平台管理后台 | REQ | accepted | 合并系统管理 PRD + 客户端发布 PRD 为『平台管理后台 PRD』原文件删除 | `PRD/平台管理后台/平台管理后台PRD.md` |
| ADR-20260502-002 | 2026-05-02 | 平台管理后台 | TECH | accepted | 合并系统管理技术文档 + 客户端发布技术方案为『平台管理后台技术方案』(覆盖技术选型/页面路由表/API 设计三维度),原文件删除 | `TECH_STACK/平台管理后台技术方案.md` |
| ADR-20260502-003 | 2026-05-02 | 文档治理 | REQ | accepted | PRD 管 what/why、Tech 管 howPRD 必须移出 API 路径/Redis Key/字段类型/框架 API 等实现细节,由 Tech 与 DATA_MODEL 承接 | `PRD/登录管理/用户登录管理模块PRD.md` v3.0 |
---

View File

@@ -2,10 +2,10 @@
# Fonrey — Public Schema 数据模型
> **作者**: Backend Architect
> **版本**: v1.5
> **日期**: 2026-04-30
> **版本**: v1.6
> **日期**: 2026-05-02
> **权威源**: 本文件是 `public` schema 所有表的唯一权威定义
> **设计依据**: 系统管理模块 PRD`PRD/系统管理/系统管理模块PRD.md`);客户端发布管理模块 PRD`PRD/发布管理/客户端发布管理模块PRD.md`
> **设计依据**: 平台管理后台 PRD`PRD/平台管理后台/平台管理后台PRD.md`
> **索引文档**: [`DATA_MODEL.md §三`](./DATA_MODEL.md)(仅保留摘要索引,开发以本文件为准)
---
@@ -14,6 +14,7 @@
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-05-02 | Atlas | 对齐平台管理后台 PRD v1.0`tenants` 增加 License 授权用户数上限字段 `license_user_limit`(含约束、索引、查询示例);`suspended_reason` 增加 `license_expired`;审计动作补充 `UPDATE_LICENSE_USER_LIMIT``AUTO_SUSPEND_LICENSE_EXPIRED``domains` 标注为统一 `tenant=` 路由下的兼容保留;同步更新元信息与版本历史。 |
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
## 一、概览
@@ -53,7 +54,7 @@ PostgreSQL Instance
| 表名 | 说明 | 节 |
|------|------|----|
| `public.tenants` | 租户主表(每家房产公司一条记录) | §2.1 |
| `public.domains` | 域名↔租户映射(多域名支持 | §2.1 |
| `public.domains` | 域名↔租户映射(兼容保留;主路径已切换统一 `tenant=` 参数路由 | §2.1 |
| `public.tenant_status_logs` | 租户状态变更不可变审计日志 | §2.1 |
| `public.platform_admins` | 平台管理员账号3 种角色) | §2.2 |
| `public.admin_mfa_devices` | 管理员 TOTP MFA 设备(强制启用) | §2.2 |
@@ -81,7 +82,7 @@ PostgreSQL Instance
-- ============================================================
-- 文件: shared_schema.sql
-- 用途: django-tenants 公共 Schema存放平台运营层数据
-- 设计依据: 系统管理模块 PRD v1.0
-- 设计依据: 平台管理后台 PRD v1.0
-- ============================================================
-- ────────────────────────────────────────────────────────────
@@ -111,11 +112,13 @@ CREATE TABLE public.tenants (
CHECK (status IN ('creating','active','suspended','pending_delete','deleted','failed')),
suspended_until TIMESTAMPTZ, -- NULL = 永久挂起,非 NULL = Celery Beat 定时恢复
suspended_reason VARCHAR(50)
CHECK (suspended_reason IN ('overdue','violation','requested','other')),
CHECK (suspended_reason IN ('overdue','violation','requested','other','license_expired')),
deleted_at TIMESTAMPTZ, -- 软删除时间戳;硬删除直接物理删除行
-- 订阅
paid_until DATE, -- 订阅到期日
paid_until DATE, -- 订阅到期日License 到期日期)
license_user_limit INTEGER NOT NULL DEFAULT 50
CHECK (license_user_limit > 0), -- License 授权用户数上限(含 Tenant Admin + Agent
on_trial BOOLEAN NOT NULL DEFAULT TRUE,
-- 灰度升级
@@ -139,11 +142,14 @@ CREATE INDEX idx_tenants_suspended_until ON public.tenants(suspended_until)
CREATE INDEX idx_tenants_canary ON public.tenants(is_canary) WHERE is_canary = TRUE;
CREATE INDEX idx_tenants_pending_delete ON public.tenants(deleted_at)
WHERE status = 'pending_delete';
CREATE INDEX idx_tenants_license_user_limit ON public.tenants(license_user_limit);
CREATE INDEX idx_tenants_paid_until_active ON public.tenants(paid_until)
WHERE status = 'active';
-- Feature Flag 租户级覆盖的 GIN 索引(详见 §2.7
-- 同时在 §2.7 中以 ALTER TABLE ADD COLUMN IF NOT EXISTS 兜底,便于按模块独立部署
CREATE INDEX idx_tenants_feature_flags_gin ON public.tenants USING gin (feature_flags);
-- 域名映射表(支持多域名绑定一个租户,子域名创建后不可修改)
-- 域名映射表(兼容保留:主路径已切换统一 `tenant=` 参数路由;子域名创建后不可修改)
CREATE TABLE public.domains (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
domain VARCHAR(253) UNIQUE NOT NULL, -- 含子域名的完整域名(如 abc.platform.com
@@ -257,6 +263,7 @@ CREATE TABLE public.platform_audit_logs (
operator_name VARCHAR(100), -- 快照(防止账号删除后失去溯源)
action_type VARCHAR(50) NOT NULL,
-- CREATE_TENANT | SUSPEND_TENANT | RESUME_TENANT | DELETE_TENANT | HARD_DELETE_TENANT
-- UPDATE_LICENSE | UPDATE_LICENSE_USER_LIMIT | AUTO_SUSPEND_LICENSE_EXPIRED
-- RESTORE_DATA | TRIGGER_BACKUP | SYSTEM_UPGRADE | ROLLBACK
-- RESET_PASSWORD | CREATE_ADMIN | DEACTIVATE_ADMIN | FORCE_LOGOUT
-- UPDATE_IP_WHITELIST | UPDATE_BACKUP_SCHEDULE | EXPORT_DATA | ...
@@ -709,7 +716,7 @@ CREATE INDEX idx_ff_log_operator ON public.feature_flag_change_log(operator_id,
| `tenant_status_logs` append-only | 禁止 UPDATE / DELETE状态机变更只追加新行 |
| `platform_audit_logs` append-only | 禁止 UPDATE / DELETE建议按月 RANGE 分区 |
| `public.tenants.schema_name` 不可修改 | 创建后禁止 UPDATEPG schema 绑定 |
| `public.domains.domain` 不可修改 | 子域名创建后禁止 UPDATE |
| `public.domains.domain` 不可修改(兼容保留) | `tenant=` 统一路由方案下,`domains` 仅用于历史兼容与迁移窗口;子域名创建后禁止 UPDATE |
| `system_versions` 唯一 current | `idx_system_versions_current` 部分唯一索引保证全局只有一个 `status='current'` |
| `backup_schedules.tenant_id` UNIQUE | 每个租户最多一条独立计划;`NULL` 全局计划由应用层保证唯一 |
| `platform_admins``staff` 完全独立 | 不共享表、不共享 auth 系统 |
@@ -801,16 +808,25 @@ draft ──提交──► pre_check ──健康检查通过──► pre_back
```sql
-- 查询所有活跃租户
SELECT id, name, plan, paid_until
SELECT id, name, plan, paid_until, license_user_limit
FROM public.tenants
WHERE status = 'active'
ORDER BY created_at DESC;
-- 查询即将到期的租户(7 天内)
SELECT id, name, contact_email, paid_until
-- 查询即将到期的租户(15 天内,对齐平台管理后台 PRD
SELECT id, name, contact_email, paid_until, license_user_limit
FROM public.tenants
WHERE status = 'active'
AND paid_until BETWEEN CURRENT_DATE AND CURRENT_DATE + 7;
AND paid_until BETWEEN CURRENT_DATE AND CURRENT_DATE + 15;
-- 查询“用户数已满/超限”的租户(供运营筛选)
-- current_user_count 由租户侧聚合任务回填至统计视图或物化视图,此处示例为联表查询形态
SELECT t.id, t.name, t.tenant_code, t.license_user_limit, u.current_user_count
FROM public.tenants t
JOIN public.v_tenant_user_counts u ON u.tenant_id = t.id
WHERE t.status = 'active'
AND u.current_user_count >= t.license_user_limit
ORDER BY u.current_user_count DESC, t.created_at DESC;
-- 查询灰度租户canary 升级目标)
SELECT id, schema_name, name
@@ -1009,7 +1025,7 @@ ORDER BY created_at DESC;
| `DELETE FROM public.platform_audit_logs` | append-only 审计表 |
| `UPDATE public.tenants SET schema_name = ...` | schema 名绑定 PG 物理 schema |
| `UPDATE public.tenants SET tenant_code = ...` | 12 位识别码一旦发放给用户不可更改,避免经纪人无法登录 |
| `UPDATE public.domains SET domain = ...` | 域名路由不可变 |
| `UPDATE public.domains SET domain = ...` | 域名路由不可变`domains` 为兼容保留表,主路径使用 `tenant=` 参数) |
| `UPDATE public.client_releases SET version = ...` | 版本号创建后不可修改,变更须新建记录 |
| 在租户 schema 中创建 `client_releases` 副本 | 本表属于 public schema多租户共享禁止下沉到租户层 |
| `UPDATE public.feature_flag_change_log` / `DELETE FROM ...` | append-only 审计表 |
@@ -1030,3 +1046,4 @@ ORDER BY created_at DESC;
| v1.3 | 2026-04-30 | 配合登录管理 PRD v2.0 / 系统管理 PRD§2.1 `tenants` 新增 `tenant_code` (CHAR(12), 全局唯一对外识别码) 与 `contact_phone` (CHAR(11)Tenant Admin 账号创建数据来源)`contact_email` 改为可 NULL同步更新约束、查询模式 |
| v1.4 | 2026-04-30 | 配合客户端发布管理 PRD v1.1 §5.5 Story 5§2.6 新增 `client_heartbeats` 表(启动时 Upsert by `tenant_id + device_id`,活跃定义为最近 24h 内心跳);同步更新表清单、§三 约束Upsert 锚点 / 启动上报 / 不可下沉到租户 schema、§5.1 查询模式Upsert 模板 + 版本活跃分布 JOIN + 平台总活跃数 + 租户维度排查MVP 不做升级趋势图v2 规划) |
| v1.5 | 2026-04-30 | 配合客户端发布管理 PRD v1.2 §5.5 Story 5 验收标准追加(按租户统计安装数):① §2.6 `client_heartbeats.tenant_id` 注释强化,明确为"按租户统计安装数 / 活跃数 / 版本分布"的核心维度字段;② §5.1 新增"租户维度安装/活跃统计"查询专区4 个查询:某租户活跃安装数 / 历史装机总数 / 租户内版本分布 / 全平台租户活跃榜) |
| v1.6 | 2026-05-02 | 对齐平台管理后台 PRD v1.0:① `tenants` 增加 `license_user_limit``INTEGER NOT NULL DEFAULT 50 CHECK (>0)`)并新增 `idx_tenants_license_user_limit`;② `suspended_reason` 增加 `license_expired` 枚举;③ 新增 `idx_tenants_paid_until_active` 并将“即将到期”查询窗口由 7 天调整为 15 天;④ 新增“用户数已满/超限”查询示例(`v_tenant_user_counts` 联表);⑤ 审计动作补充 `UPDATE_LICENSE_USER_LIMIT``AUTO_SUSPEND_LICENSE_EXPIRED`;⑥ `domains` 标记为统一 `tenant=` 路由下的兼容保留。 |

View File

@@ -1,420 +0,0 @@
# PRD: 客户端发布管理模块
**状态**: Draft
**作者**: 产品经理
**最后更新**: 2026-04-30v1.2
**版本**: 1.2
**所属系统**: Fonrey 房产经纪管理系统
**关联模块**: 系统管理
**干系人**: 工程负责人、运维负责人、Platform Admin平台超级管理员
## 变更历史
| 版本 | 日期 | 作者 | 变更说明 |
|------|------|------|---------|
| v1.0 | 2026-04-24 | 产品经理 | 初稿 |
| v1.1 | 2026-04-30 | 产品经理 | Persona 与命名修正:① Story 4 / Story 5 责任角色由 Tenant Admin租户管理员修正为 Platform Admin平台超级管理员客户端版本上传与版本分布监控属于平台运营职责跨租户统一管理② §5.3 版本管理后台从"租户系统管理模块"迁移至"平台运营后台",明确 public schema 跨租户、Tenant Admin 与 Agent 无访问权限、不配置 RBAC permission_code③ 全部 API 路径 `/api/client/updates/...` 重命名为 `/api/release/updates/...`避免与客源管理模块apps/client/)命名空间冲突,与 AGENTS.md §3 的 `apps/release/` 目录约定对齐;④ 头部"关联模块"由"系统管理、权限管理"修订为"系统管理"(不再涉及租户 RBAC⑤ 干系人列表 Tenant Admin → Platform Admin。 |
| v1.2 | 2026-04-30 | 产品经理 | Story 5 验收标准追加"按租户统计安装数"能力:① 新增验收项——可查看任意租户的"当前活跃安装数(最近 24h"与"历史装机总数";② 新增验收项——全平台租户活跃榜视图,按 `tenant_code` + 租户名称 + 活跃安装数 + 历史装机数列表展示,按活跃安装数降序。底层数据由 `public.client_heartbeats` 表的 `tenant_id` 维度聚合提供(详见 DATA_MODEL_PUBLIC v1.5 §5.1 租户维度安装/活跃统计查询专区)。 |
---
## 1. 问题陈述
### 背景
Fonrey 房产经纪管理系统当前为纯 Web 应用,依赖用户自行通过浏览器访问。然而在实际部署场景中,经纪公司的终端设备环境高度复杂:
- **浏览器版本参差不齐**:经纪人使用的 Windows 设备可能运行 IE11、旧版 Edge、或未更新的 Chrome导致 HTMX + Alpine.js 等现代前端技术出现兼容性问题,系统体验碎片化
- **交付和部署门槛高**IT 能力薄弱的经纪公司无法独立配置浏览器访问方式URL 记忆成本高,容易访问错误版本
- **版本管理缺失**:后端服务升级后,用户仍可能使用旧版缓存页面操作,导致接口不兼容和功能异常
- **无官方入口**:用户通过私发链接访问系统,存在钓鱼仿冒风险,且无法统一品牌形象
### 目标用户
| 角色 | 使用场景 | 使用频率 |
|------|---------|----------|
| Agent经纪人 | 下载安装客户端、日常登录使用系统、接受自动更新 | 每日 |
| 店长/经理 | 同上 | 每日 |
| Platform Admin平台超级管理员 | 发布新版本、管理安装包下载地址、监控客户端版本分布 | 按需 |
| IT 运维人员 | 维护更新服务器、签名证书、构建发布流水线 | 按发布周期 |
### 核心痛点
1. **无法控制用户使用的浏览器环境**,兼容性问题无法从根源解决
2. **升级依赖用户主动刷新浏览器**,后端 API 变更时旧客户端可能造成数据错误
3. **缺乏官方分发渠道**,无法向终端用户传递信任感和版本一致性保障
4. **SaaS 多租户管理系统需要统一、可控的客户端入口**,避免因客户端环境差异导致的支持成本上升
---
## 2. 目标与成功指标
| 目标 | 指标 | 当前基准 | 目标值 | 衡量周期 |
|------|------|---------|--------|---------|
| 消除浏览器兼容性问题 | 因浏览器兼容产生的支持工单数 | 待统计 | 降低 ≥ 90% | 上线后 60 天 |
| 提升版本一致性 | 在线用户中使用最新版本客户端的比例 | 0%(无客户端) | ≥ 95% | 版本发布后 7 天 |
| 降低部署门槛 | 新客户从获取安装包到完成首次登录的时间 | 无基准 | ≤ 10 分钟 | 上线后首批客户反馈 |
| 自动更新成功率 | 客户端自动更新完成率(收到更新通知 → 升级完成) | 无基准 | ≥ 98% | 每次版本发布后 48 小时 |
---
## 3. 非目标(本期不做)
- **不支持 macOS / Linux 客户端**:目标用户群体 99% 使用 WindowsmacOS 版本为后续规划
- **不支持移动端 AppiOS / Android**:移动端为 v2 规划,本期不涉及
- **不开发私有化部署的离线安装方案**:本期聚焦 SaaS 在线版,私有化部署另行规划
- **不包含客户端内置的离线模式**:系统需联网使用,客户端不缓存业务数据供离线访问
- **不包含客户端层面的安全加固(如代码混淆、反逆向)**:本期以功能交付为优先,安全加固列入后续迭代
---
## 4. 用户故事与验收标准
---
### Story 1经纪人下载并安装客户端
**As** Agent经纪人**I want** 通过公司提供的网址下载一个安装程序并完成安装,**So that** 我可以立即打开登录界面使用 Fonrey 系统,无需手动配置浏览器。
**验收标准**
- [ ] 官方下载页面可通过指定 URL 访问,页面展示最新版本号、发布日期及下载按钮
- [ ] 下载产物为单一 `.exe` 安装包(或免安装便携版 `.zip`),文件大小控制在合理范围内
- [ ] 双击安装包后,安装向导步骤不超过 3 步(下一步 → 选择安装路径 → 安装),无需勾选额外组件
- [ ] 安装完成后,桌面自动生成快捷方式(图标为 Fonrey 品牌 Logo
- [ ] 首次启动后直接显示登录界面,无需用户手动输入任何 URL
- [ ] 安装包经过代码签名Windows SmartScreen 不弹出"无法识别的应用"警告
- [ ] 安装过程无需管理员权限(支持用户级安装到 `%APPDATA%` 目录),降低企业 IT 审批障碍
---
### Story 2经纪人使用客户端正常登录并使用系统
**As** Agent经纪人**I want** 打开客户端后直接访问 Fonrey 系统的完整功能,**So that** 我的日常使用体验与使用 Chrome 浏览器无差异,且不受本机安装的浏览器版本影响。
**验收标准**
- [ ] 客户端内嵌现代 Chromium 内核(如基于 Electron 或 WebView2版本不低于 Chromium 100支持现代 Web 标准ES2020、CSS Grid、Fetch API 等)
- [ ] HTMX 局部刷新、Alpine.js 状态交互、Tailwind CSS 样式在客户端中渲染效果与 Chrome 最新版一致
- [ ] 支持 Cookie / Session 存储,登录状态在客户端关闭后保留(复用 Django Session 机制)
- [ ] 文件上传图片、附件、文件下载Excel 导出)在客户端中正常工作
- [ ] 客户端窗口支持最大化、最小化、拖拽调整大小,支持多显示器
- [ ] 客户端标题栏显示应用名称和当前版本号(如:`Fonrey 房睿 v1.2.3`
- [ ] 客户端不显示浏览器默认的地址栏、书签栏、扩展工具栏,保持沉浸式应用体验
---
### Story 3客户端感知新版本并自动升级
**As** Agent经纪人**I want** 客户端在有新版本时自动提示并完成升级,**So that** 我无需手动下载安装,始终使用最新版本,不会因版本落后导致功能异常。
**验收标准**
- [ ] 客户端启动时及运行期间(每隔 4 小时)自动向更新服务器检查最新版本
- [ ] 有新版本时,客户端右下角弹出非阻断式通知:"发现新版本 vX.X.X点击立即更新",用户可选择"立即更新"或"稍后提醒"
- [ ] 点击"立即更新"后,客户端在后台静默下载更新包,进度条显示下载进度
- [ ] 下载完成后提示用户"更新已就绪,重启客户端完成安装",用户选择"立即重启"或"下次启动时安装"
- [ ] 重启后,新版本生效,标题栏版本号更新,历史会话自动恢复(用户无需重新登录)
- [ ] 支持强制更新模式:服务端可标记某版本为"强制升级",客户端不展示"稍后提醒"选项,必须升级后方可继续使用(用于重大 API 兼容性变更场景)
- [ ] 更新失败时(网络中断、磁盘空间不足等),客户端显示错误提示并保持当前版本正常运行,不影响用户当前操作
---
### Story 4Platform Admin平台超级管理员发布新版本
**As** Platform Admin平台超级管理员**I want** 通过平台运营后台上传新版客户端安装包并配置版本信息,**So that** 全平台所有租户的客户端能感知到更新并引导用户升级。
**验收标准**
- [ ] 平台运营后台提供"客户端版本管理"页面(位于平台运营后台下,不在租户系统管理模块)
- [ ] 支持上传 `.exe` 安装包,并填写版本号(遵循 SemVer`X.Y.Z`)、版本说明(更新日志,支持 Markdown、发布日期
- [ ] 支持设置版本类型:普通更新 / 强制更新
- [ ] 支持设置版本状态:草稿(不对外生效)/ 已发布 / 已下线
- [ ] 发布后,更新服务器 API 即时返回最新版本信息,客户端下次检测时可感知
- [ ] 支持版本回滚:将指定历史版本重新设为"已发布",自动将当前版本标记为已下线
- [ ] 支持查看各版本的下载量和活跃客户端版本分布统计
---
### Story 5Platform Admin平台超级管理员监控客户端版本分布
**As** Platform Admin平台超级管理员**I want** 跨租户查看当前所有在线客户端的版本分布情况,**So that** 了解全平台升级覆盖率,对仍在使用旧版本的客户端发出提醒或强制升级。
**验收标准**
- [ ] 客户端版本管理页面展示版本分布统计:各版本在线客户端数量及占比(饼图或条形图)
- [ ] 支持按租户维度查看版本分布(区分不同经纪公司的版本使用情况,便于平台超管识别落后租户)
- [ ] 支持**按租户统计安装数**:可查看任意租户的"当前活跃安装数(最近 24h"与"历史装机总数(不论是否活跃)",用于评估各租户的客户端覆盖率与渗透率
- [ ] 支持全平台租户活跃榜视图:以列表形式展示各 active 状态租户的 `tenant_code`、租户名称、活跃安装数、历史装机总数,按活跃安装数降序排列
- [ ] 支持对指定版本范围的用户推送"强制更新"通知(如:将所有低于 v1.5.0 的客户端标记为强制更新)
---
## 5. 功能详细说明
### 5.1 技术架构选型
#### 5.1.1 客户端技术方案
基于 Fonrey 现有技术栈Django + HTMX + Alpine.js + Tailwind CSS后端已采用 Docker Compose 部署),客户端本质是一个**内嵌现代 Chromium 内核的原生 Windows 应用外壳Shell**,其核心职责是:
1. 提供操作系统级原生窗口(标题栏、任务栏图标、托盘)
2. 内嵌高版本 Chromium 内核加载 Fonrey Web 应用 URL
3. 实现版本检测与自动更新逻辑
4. 处理文件下载、本地存储等 OS 级能力
**推荐方案Electron主选**
| 维度 | Electron | Tauri | WebView2 封装 |
|------|---------|-------|--------------|
| 内核控制 | ✅ 捆绑 Chromium100% 可控 | ❌ 依赖系统 WebView版本不可控 | ⚠️ 依赖 Windows 内置 WebView2 Runtime |
| 包体大小 | ~150MB可接受 | ~5MB | ~5MB |
| 生态成熟度 | ✅ 最成熟,社区最大 | ✅ 较新但活跃 | ⚠️ 微软官方但文档偏少 |
| 自动更新支持 | ✅ `electron-updater` 成熟方案 | ✅ 内置更新器 | ⚠️ 需自行实现 |
| 跨平台 | ✅ Win/Mac/Linux | ✅ | ❌ 仅 Windows |
| 团队技术匹配 | ✅ 主进程用 Node.js渲染层纯 Web | ⚠️ 主进程需 Rust | ✅ 主进程用 C# |
| **推荐度** | **✅ 主选** | 次选 | 备选 |
**选型决策**:采用 **Electron + electron-updater**。理由:
- 内嵌 Chromium 内核是本需求的核心约束Electron 是唯一能 100% 保证内核版本可控的主流方案
- `electron-updater` 配合 GitHub Releases 或自建 S3/R2 存储可实现完整的版本管理与自动更新流程,开发成本最低
- 渲染层完全复用 Fonrey 现有 Web 技术栈,无需新增前端框架学习成本
- 团队具备 JavaScript/Node.js 能力,主进程开发门槛可控
**技术决策**:客户端不内置任何业务逻辑,所有业务功能由服务端 Fonrey Web 应用提供。客户端仅负责加载 Web 应用、更新管理和 OS 级能力(窗口、托盘、文件下载路径)。
---
#### 5.1.2 更新服务架构
更新机制采用**差量检测 + 全量包下载**模式:
```
客户端启动 / 定时检测每4小时
GET /api/release/updates/latest?platform=win32&arch=x64&current_version=1.2.0
更新服务器Fonrey 后端 Django API
返回:{ latest_version, download_url, release_notes, force_update, checksum }
├── 无更新 → 继续正常运行
└── 有更新 → 弹出通知
├── 用户点击"立即更新" → 后台下载 .exe / NSIS 更新包
│ │
│ └── 下载完成 → 校验 SHA256 → 提示重启安装
└── 用户选择"稍后" → 下次启动再提示
```
**更新包存储**:上传至 Cloudflare R2与现有对象存储一致通过 Cloudflare CDN 加速下载,全国用户均可获得稳定下载速度。
**版本 API 端点**(新增至 Django 后端):
| 端点 | 方法 | 说明 |
|------|------|------|
| `/api/release/updates/latest/` | GET | 客户端查询最新版本,返回版本信息和下载 URL |
| `/api/release/updates/` | GET | 管理端查询版本列表(需认证) |
| `/api/release/updates/` | POST | 管理端发布新版本(需管理员权限) |
| `/api/release/updates/<id>/` | PATCH | 管理端修改版本状态(发布/下线/强制) |
---
#### 5.1.3 安装包签名与分发
**代码签名**
- 使用 EV 代码签名证书(推荐购买 DigiCert 或 Sectigo EV 证书)
- 通过 `electron-builder` 在 CI/CD 构建时自动签名
- 签名后安装包经 Windows SmartScreen 审核,用户安装时不触发安全警告
**安装包分发**
- 官方下载页:独立 HTML 页面托管于 Cloudflare Pages 或 Nginx 静态站
- 页面展示:最新版本号 + 发布日期 + 更新日志 + 下载按钮
- 下载 URL 格式:`https://download.fonrey.com/releases/v1.2.3/fonrey-setup-1.2.3-win.exe`
- 同时提供便携版Portable`fonrey-portable-1.2.3-win.zip`,供无安装权限的企业环境使用
---
### 5.2 客户端功能规格
#### 5.2.1 主窗口
| 属性 | 规格 |
|------|------|
| 默认窗口尺寸 | 1280 × 800最小1024 × 600 |
| 标题栏 | 显示 `Fonrey 房睿 v{version}`,含原生最小化/最大化/关闭按钮 |
| 内嵌 URL | 启动时加载 `https://{tenant}.fonrey.com`(或私有化部署地址,可配置) |
| 地址栏 | 不显示(沉浸式应用模式) |
| 右键菜单 | 仅保留"复制"/"粘贴"/"检查元素(仅开发模式)",移除"查看源代码"等浏览器默认项 |
| 外部链接 | 点击 `target="_blank"` 链接时,在系统默认浏览器中打开,不在客户端内新窗口打开 |
#### 5.2.2 系统托盘
| 功能 | 说明 |
|------|------|
| 托盘图标 | Fonrey Logo鼠标悬停显示 `Fonrey 房睿 - 已连接` / `- 离线` |
| 右键菜单 | 打开主窗口 / 检查更新 / 关于 / 退出 |
| 最小化行为 | 点击关闭按钮时最小化至托盘(不退出程序),用户通过托盘图标恢复窗口 |
#### 5.2.3 网络状态感知
| 状态 | 客户端行为 |
|------|-----------|
| 正常联网 | 加载 Fonrey Web 应用,状态栏显示"已连接" |
| 网络断开 | 显示全屏提示页:"网络连接已断开,请检查您的网络后重试",提供"重新连接"按钮 |
| 服务器维护 | 服务器返回 503 时,展示维护提示页(内容由服务端控制) |
#### 5.2.4 文件下载处理
- Excel 导出等文件下载触发时,客户端调用系统原生"另存为"对话框,用户选择保存路径
- 下载完成后,状态栏显示"下载完成,点击打开"提示,点击可直接打开文件
---
### 5.3 版本管理后台(平台运营后台新增页面)
**页面路径**:平台运营后台 → 客户端发布管理
> **归属说明**:本页面归属 Platform Admin平台超级管理员位于平台运营后台public schema 跨租户),不在租户的"系统管理"模块下。Tenant Admin 与 Agent 均无访问权限。
#### 5.3.1 版本列表
| 列 | 说明 |
|----|------|
| 版本号 | SemVer 格式,如 `v1.2.3` |
| 版本类型 | 普通更新 / 强制更新(红色标签) |
| 状态 | 草稿 / 已发布(绿色)/ 已下线(灰色) |
| 发布时间 | 版本设为已发布的时间 |
| 下载量 | 该版本安装包被下载次数 |
| 操作 | 发布 / 下线 / 编辑 / 复制下载链接 |
#### 5.3.2 新增/编辑版本表单
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 版本号 | 文本输入 | 是 | 格式:`X.Y.Z`,自动校验 SemVer 格式 |
| 版本类型 | 单选 | 是 | 普通更新 / 强制更新 |
| 最低兼容版本 | 文本输入 | 否 | 低于该版本的客户端将被强制更新(如填写 `1.0.0`,则低于此版本的客户端强制升级) |
| 安装包EXE | 文件上传 | 是 | 上传至 Cloudflare R2最大 500MB |
| 便携版ZIP | 文件上传 | 否 | 同上 |
| SHA256 校验值 | 文本输入(自动填充) | 是 | 上传后系统自动计算并填充,用于客户端下载完成后校验完整性 |
| 更新日志 | Markdown 文本区域 | 是 | 展示给用户看的版本说明,最多 2000 字 |
| 发布说明(内部) | 文本区域 | 否 | 仅内部查看的技术说明,不对外展示 |
| 状态 | 单选 | 是 | 草稿 / 立即发布 |
#### 5.3.3 版本分布统计
| 图表 | 说明 |
|------|------|
| 版本分布饼图 | 按客户端版本号统计当前活跃用户数量及占比 |
| 升级进度趋势图 | 新版本发布后,各天累计升级完成的用户比例(折线图) |
| 租户版本明细 | 按租户(经纪公司)展示其员工的客户端版本分布 |
---
### 5.4 更新 API 规格
#### GET `/api/release/updates/latest/`
**请求参数Query String**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `platform` | string | 是 | 平台标识,如 `win32` |
| `arch` | string | 是 | CPU 架构,如 `x64` / `arm64` |
| `current_version` | string | 是 | 客户端当前版本号,如 `1.2.0` |
**响应示例(有新版本)**
```json
{
"has_update": true,
"latest_version": "1.3.0",
"force_update": false,
"download_url": "https://download.fonrey.com/releases/v1.3.0/fonrey-setup-1.3.0-win.exe",
"portable_url": "https://download.fonrey.com/releases/v1.3.0/fonrey-portable-1.3.0-win.zip",
"checksum_sha256": "a1b2c3d4...",
"release_notes": "## v1.3.0 更新内容\n- 新增客源智能配房功能\n- 修复房源列表筛选条件保存异常",
"release_date": "2026-05-01"
}
```
**响应示例(已是最新)**
```json
{
"has_update": false,
"latest_version": "1.3.0"
}
```
---
## 6. 技术实现注意事项
### 6.1 依赖关系
| 依赖项 | 说明 | 负责方 | 风险等级 |
|--------|------|--------|---------|
| Electron 框架 | 客户端技术基础,需评估 LicenseMIT商业可用 | 前端/客户端工程师 | 低 |
| EV 代码签名证书 | 需提前申请EV 证书审核周期 1-2 周 | IT/运维 | 中(需提前排期) |
| Cloudflare R2 存储桶 | 存放安装包,利用现有账号新增 bucket | 运维 | 低 |
| `electron-updater` | 自动更新库,需配合更新 API 端点实现 | 客户端工程师 | 低 |
| Django 更新 API | 新增 `/api/release/updates/` 相关接口 | 后端工程师 | 低 |
| CI/CD 构建流水线 | 自动构建、签名、上传安装包 | 运维/DevOps | 中 |
### 6.2 已知风险
| 风险 | 可能性 | 影响 | 缓解措施 |
|------|--------|------|---------|
| EV 证书申请延迟 | 中 | 高(无签名包无法正常分发) | MVP 阶段可使用普通 OV 证书临时过渡,但需向用户说明安全警告原因 |
| Electron 包体过大导致下载放弃 | 低 | 中 | 使用 `electron-builder``asar` 压缩 + 分片下载;首包控制在 150MB 以内 |
| 企业网络拦截 CDN 下载 | 中 | 中 | 提供备用下载 URL直连服务器支持客户手动下载后本地安装 |
| 自动更新期间用户强制关闭 | 低 | 低 | 更新包下载完成后才替换原文件,下载中断不影响现有版本正常运行 |
| 多租户场景下 URL 配置问题 | 低 | 高 | 客户端启动时加载的 URL 通过配置文件指定支持定制化部署SaaS 版统一指向主域名 |
### 6.3 开放问题(开发启动前必须解决)
- [ ] **租户 URL 如何分发到客户端?** 选项 A客户端硬编码主域名由服务端重定向到租户子域`fonrey.com``{tenant}.fonrey.com`);选项 B安装包内置配置文件由销售/运维在分发给客户前填写租户子域。——**Owner**: 产品 + 工程 **Deadline**: 开发启动前
- [ ] **代码签名证书采购主体和预算是否确认?****Owner**: IT 负责人 **Deadline**: 立项后 1 周
- [ ] **CI/CD 平台选型是否确定?**GitHub Actions / Jenkins / 其他)— **Owner**: 运维负责人 **Deadline**: 开发启动前
- [ ] **便携版Portable ZIP是否纳入 v1 范围?** 便携版可解决企业无安装权限场景,但增加测试成本。— **Owner**: PM **Deadline**: 立项后 1 周
---
## 7. 发布计划
| 阶段 | 时间 | 受众 | 成功门槛 |
|------|------|------|---------|
| 内部 Alpha | 开发完成后 1 周 | 内部团队 + 1 家种子客户 | 核心流程无 P0 Bug自动更新机制验证通过 |
| 封闭 Beta | Alpha + 2 周 | 3-5 家头部客户 | 安装成功率 ≥ 95%,自动更新成功率 ≥ 95%,无 P0/P1 Bug |
| 正式发布GA | Beta + 1 周 | 全部客户 | Beta 阶段目标达成 |
**回滚标准**:若正式发布后 24 小时内出现以下情况,立即下线该版本并恢复上一稳定版本为"已发布"
- 自动更新失败率 > 5%
- 客户端白屏/崩溃率 > 2%
- 收到 P0 级安全漏洞报告
---
## 8. 附录
### 8.1 竞品参考
| 产品 | 客户端方案 | 更新机制 |
|------|-----------|---------|
| 企业微信 | Electron + 自研内核 | 强制更新,启动时自动下载 |
| 飞书 | Electron | 后台静默更新,重启生效 |
| 钉钉 | Electron | 同上 |
> 房产经纪行业的竞品(如房客多、云客优)均采用 Electron 方案,验证了技术路线的合理性。
### 8.2 术语表
| 术语 | 定义 |
|------|------|
| SemVer | 语义化版本控制Semantic Versioning`主版本号.次版本号.补丁号`,如 `1.2.3` |
| Electron | 由 GitHub 开发的开源框架,允许使用 Web 技术HTML/CSS/JS构建跨平台桌面应用内嵌 Chromium 和 Node.js |
| electron-updater | Electron 生态中成熟的自动更新库,支持增量更新和全量更新 |
| EV 证书 | Extended Validation 代码签名证书,由 CA 机构颁发,可消除 Windows SmartScreen 安全警告 |
| SHA256 | 安全散列算法,用于验证下载文件的完整性,防止篡改或下载损坏 |
| Portable | 便携版,无需安装,解压即用,适合无管理员权限的企业环境 |

View File

@@ -0,0 +1,835 @@
# PRD平台管理后台Platform Admin Console
**状态**Draft
**作者**:产品经理
**最后更新**2026-05-02
**版本**v1.0
**所属系统**Fonrey 房产经纪管理系统
**关联模块**:权限管理(仅平台侧管理员账号体系)、登录管理(管理员登录与 MFA、所有租户业务模块间接仅作运营/监控对象)
**利益相关方**工程负责人、运营团队、安全合规、客户成功团队、IT 运维
---
## 变更历史
| 版本 | 日期 | 作者 | 变更说明 |
|------|------|------|---------|
| v1.0 | 2026-05-02 | 产品经理 | 由 `PRD/系统管理/系统管理模块PRD.md` (v1.3) 与 `PRD/发布管理/客户端发布管理模块PRD.md` (v1.2) 合并而来;统一面向平台管理员视角,统一规划页面路由与业务 API 操作清单。原两份 PRD 文件同步删除,参见 `ADR-20260502-001`。 |
> **本 PRD 取代以下两份历史文档**
> - `PRD/系统管理/系统管理模块PRD.md`v1.3,已删除,被本 PRD 替代)
> - `PRD/发布管理/客户端发布管理模块PRD.md`v1.2,已删除,被本 PRD 替代)
>
> 决策依据:`ADR-20260502-001`REQ
---
## 1. 问题陈述
### 1.1 背景
Fonrey房睿是面向房产经纪公司的 B2B SaaS 平台,采用 `django-tenants` 实现 PostgreSQL Schema 级别多租户隔离,同时通过 Electron 桌面客户端为终端经纪人提供统一入口。随着平台商业化推进,**平台运营团队**需要一套独立、跨租户的「平台管理后台」Platform Admin Console来统一承担
1. 租户全生命周期管理开通、挂起、注销、License 续期)
2. 平台版本治理(基础数据版本、租户数据版本、灰度升级、回滚)
3. 数据备份与恢复(灾难场景应对)
4. 客户端发布治理(桌面客户端版本上线、强制升级、版本分布监控)
5. 操作审计与合规
6. 平台管理员账号与安全MFA、IP 白名单、强制登出)
平台管理后台部署于平台公共域名(与租户应用域名隔离),数据落在 `public` schema跨租户。Tenant Admin 与 Agent 一律无访问权限。
### 1.2 核心痛点
| 痛点 | 影响方 | 当前代价 |
|------|--------|---------|
| 无统一租户管理界面,开通 / 挂起 / 注销依赖人工脚本 | 运营团队 | 高错误风险,操作耗时 |
| 平台版本升级需停机,影响所有租户 | 全量用户 | SLA 违约风险 |
| 数据备份无策略,灾难恢复依赖人工 | 平台稳定性 | 数据丢失风险 |
| 高危操作无审计轨迹 | 管理层 / 合规 | 法律与客户信任风险 |
| 客户端浏览器版本碎片化,兼容性问题层出 | Agent / 客户成功 | 支持成本飙升 |
| 客户端无统一发布渠道,旧版本与新后端 API 不兼容 | 全量用户 | 数据错误风险 |
| 无法跨租户掌握客户端版本分布与覆盖率 | Platform Admin | 无法定向催更 |
### 1.3 目标用户
| 角色 | 使用场景 | 频率 |
|------|---------|------|
| Platform Admin平台超级管理员 | 全局配置、高危操作、版本上线、强制升级 | 低频(每周) |
| 运营人员Ops Operator | 日常租户管理、监控巡检、客户咨询响应 | 高频(每日) |
| 只读审计员Read-only Auditor | 日志查询、合规报告导出 | 中频(每周) |
> 本 PRD **不涉及**租户内部的 Tenant Admin、Agent 角色——他们对本后台无访问权限。
---
## 2. 目标与成功指标
| 目标 | 指标 | 当前基线 | 目标值 | 测量窗口 |
|------|------|---------|--------|---------|
| 租户开通效率 | 新租户开通耗时 | 人工脚本 ~30 分钟 | < 5 分钟(含自动初始化) | 上线后 30 天 |
| 平台升级零停机 | 升级期间受影响租户数 | 全量中断 | 灰度阶段 ≤ 5% 租户 | 每次升级 |
| 数据恢复能力 | RTO单租户恢复时间 | 无标准流程 | < 2 小时 | v1 上线即达标 |
| 操作合规覆盖 | 高危操作审计日志覆盖率 | 0% | 100% | 上线后 30 天 |
| 管理员安全 | 平台管理员 MFA 启用率 | 0% | 100%(强制) | 上线即达标 |
| 客户端兼容性问题消除 | 因浏览器兼容产生的支持工单数 | 待统计 | 降低 ≥ 90% | 客户端上线后 60 天 |
| 客户端版本一致性 | 在线用户使用最新客户端比例 | 0%(无客户端) | ≥ 95% | 版本发布后 7 天 |
| 客户端自动更新成功率 | 收到通知 → 升级完成的成功率 | 无基准 | ≥ 98% | 每次发布后 48 小时 |
---
## 3. 非目标Non-Goals
**不在 v1 实现**
- 自动化账单计费、多币种支持、Webhook 自定义集成市场
- 租户端自助迁移工具
- 客户响应数据导出请求Story「响应客户数据导出请求」暂缓列入后续迭代
**不属于本后台**
- 租户内业务权限的细粒度配置(见权限管理模块 PRD
- 客服工单系统、SLA 自动赔付
- macOS / Linux 桌面客户端、移动端 App
- 客户端离线模式、私有化离线安装方案
- 客户端代码混淆 / 反逆向加固
**不支持**
- 移动端浏览器访问平台管理后台(运营场景明确为 PC 桌面)
- 多语言界面(运营团队为内部人员,中文已满足)
---
## 4. 用户角色与核心故事
### Persona A — 运营人员 Lily日常租户管理
> 负责 Fonrey 的日常运营,每天处理新客户开通、异常租户处理、客户咨询。使用 PC 浏览器登录平台管理后台。
#### Story A1新租户开通
> 作为运营人员,我希望通过填写表单快速完成租户开通,并由系统自动完成数据库初始化与欢迎通知,无需手动执行脚本。
**验收标准**
- [ ] 表单提交后,系统在后台自动创建 PostgreSQL Schema 并注入默认配置(见 §5.1.2「默认配置内容」),完成耗时 < 60 秒
- [ ] 默认配置注入包含两部分:
- **权限定义PermissionDef**:注入平台所有权限码定义(`permission_code`、描述、模块归属),作为该租户 RBAC 体系基础
- **系统默认角色与权限绑定**:按「角色权限矩阵.md」注入 7 个系统内置业务角色(置业顾问、店管、区管、区总、副总、总经、其他职能),并完成角色—权限绑定
- [ ] 初始 Tenant Admin以联系人手机号创建**不通过业务角色赋权**,由系统在租户创建时直接写入「租户管理员」专属权限集合;该集合独立于 7 个业务角色之外,不在租户角色管理界面显示,不可由 Tenant Admin 自行修改或分配。新增/变更租户管理员须由平台运营方在本后台操作
- [ ] 新租户创建后:平台运营管理员收到站内消息;租户联系人收到欢迎邮件(见下方「欢迎邮件规范」)
- [ ] 联系人无邮箱时,运营人员可在租户详情页下载「入驻信息 PDF 文档」,通过微信等渠道转发
- [ ] 租户访问地址采用统一域名 + Tenant Code 参数形式(`https://app.fonrey.com/?tenant={Tenant Code}`),无需子域名;详情页展示该链接,可一键复制
- [ ] 创建失败时回滚所有已创建资源,并显示明确错误原因
**欢迎邮件规范**(联系人有邮箱时自动发送):
| 字段 | 内容 |
|------|------|
| 主题 | 【房睿平台】您的账号已开通,欢迎登录 |
| 收件人 | 租户联系人邮箱 |
| 正文 | 公司名称、**Tenant Code**、登录地址、Tenant Admin 手机号(脱敏后三位)、**系统初始密码**(明文,首次登录后强制修改)、客服联系方式 |
| 备注 | 初始密码由系统随机生成12 位,含大小写字母+数字),发送后立即标记为「首次登录强制修改」 |
**入驻信息 PDF**(无邮箱客户的备选方案):
- 包含与欢迎邮件相同的关键信息
- 入口:租户详情 → 基本信息 Tab → 「下载入驻信息」按钮
- 文件名:`{公司名称}_入驻信息_{日期}.pdf`
#### Story A2挂起问题租户
> 作为运营人员,我希望快速冻结欠费租户的访问,同时保证数据不丢失,并在欠费解决后一键恢复。
**验收标准**
- [ ] 挂起后,该租户所有用户登录跳转至「账号已暂停」提示页;平台后台对该租户数据访问不受影响
- [ ] 支持设置到期时间,到期后系统自动恢复租户状态,并发送通知邮件
- [ ] 所有挂起 / 恢复操作记录于审计日志,包含操作人、时间、原因
#### Story A3License 时效管理与到期自动挂起
> 作为运营人员(或 Platform Admin我希望每个租户能设置 License 有效期,到期后系统自动挂起租户,并在到期前提前预警。
**验收标准**
- [ ] 每个租户「基本信息」中包含 **License 到期日期**字段,由 Platform Admin 在创建或续费时设定
- [ ] 系统每日自动检查 License 到期到期后自动挂起挂起原因标注为「License 到期」
- [ ] 自动挂起后租户联系人收到通知邮件Platform Admin 续费后可手动解除挂起
- [ ] **提前 15 天预警**:租户内 Tenant Admin 登录后管理界面顶部出现倒计时横幅:「您的 License 将于 X 天后到期({到期日期}),请联系平台续费」
- [ ] 倒计时横幅仅对 Tenant Admin 可见,不影响普通 Agent
- [ ] License 到期日期在租户列表与详情页均可见支持「即将到期15 天内)」筛选
#### Story A4查看与控制租户用户数License 计费维度)
> 作为运营人员(或 Platform Admin我希望看到每个租户的当前用户数并能设置用户数上限以便根据 License 授权进行管控。
**验收标准**
- [ ] 租户列表新增「当前用户数」列,显示该租户当前有效用户总数(含 Tenant Admin + 全部 Agent不含已删除/离职)
- [ ] 租户详情页「基本信息」Tab 显示:当前用户数 / License 授权用户数上限12 / 50
- [ ] Platform Admin 可设置「License 授权用户数上限」字段达到上限时Tenant Admin 在该租户内无法继续创建新用户,并收到提示:「当前用户数已达 License 上限,请联系平台扩容」
- [ ] Platform Admin 可随时调整用户数上限(扩容 / 缩容),变更写入审计日志
- [ ] 租户列表支持按「用户数已满(≥ 上限)」筛选,便于平台运营主动识别需续费的租户
> **Story A5响应客户数据导出请求** — **状态**暂缓v1 不实现,列入后续迭代)。
---
### Persona B — Platform Admin David系统升级、回滚与版本治理
> 负责平台技术运维,周期性执行版本升级,关注升级稳定性与租户影响面。拥有所有高危操作权限。
#### Story B1灰度系统升级
> 作为 Platform Admin我希望先对内测租户升级新版本验证稳定后再全量推送避免一次性影响所有客户。
**验收标准**
- [ ] 升级前自动执行健康检查,存在异常服务时阻断升级并提示
- [ ] 支持指定目标租户进行灰度升级,灰度租户名单可编辑
- [ ] 升级过程实时展示进度(每个租户的升级状态),支持查看升级日志
- [ ] 升级失败时系统自动告警,并提供一键回滚入口
#### Story B2升级失败回滚
> 作为 Platform Admin我希望在升级出现问题时立即回滚至上一稳定版本并生成事件报告。
**验收标准**
- [ ] 回滚操作触发前自动保存当前状态快照
- [ ] 支持全量回滚或单租户回滚
- [ ] 回滚完成后生成事件报告:失败原因、回滚耗时、影响范围
- [ ] 回滚操作需二次身份验证确认MFA
#### Story B3查看平台与租户版本总览
> 作为 Platform Admin我希望在管理界面一眼看到整个平台的版本情况包括基础数据版本和每个租户各自的数据升级版本。
**验收标准**
「版本总览」页面分两部分:
- **Part 1平台基础数据版本**
- [ ] 展示当前平台基础数据(公共 Schema 中的 PermissionDef、系统配置等 seed 数据)的版本号
- [ ] 该版本对所有租户一致,每次平台升级为一次性全量升级
- [ ] 字段:版本号、最后升级时间、升级描述、升级执行人
- **Part 2租户数据升级版本**
- [ ] 列表展示每个租户当前数据版本号(即该租户 Schema 已完成的 migration 版本)
- [ ] 灰度升级下各租户版本可能不一致
- [ ] 字段:租户名称、当前数据版本、上次升级时间、升级状态(最新 / 待升级 / 升级中 / 升级失败)
- [ ] 支持按「待升级」「升级失败」筛选,快速定位异常租户
- [ ] 点击租户行可跳转至该租户详情的「备份记录」Tab
- [ ] 页面支持手动刷新;版本数据允许最多 5 分钟缓存延迟
---
### Persona C — Platform Admin David客户端发布治理
> 同 Persona B但聚焦桌面客户端版本上线、强制升级与跨租户版本分布。
#### Story C1发布新版本客户端
> 作为 Platform Admin我希望通过本后台上传新版客户端安装包并配置版本信息使全平台所有租户的客户端能感知到更新并引导升级。
**验收标准**
- [ ] 平台管理后台提供「客户端版本管理」页面(位于本后台一级菜单,与租户系统管理隔离)
- [ ] 支持上传 `.exe` 安装包,并填写:
- 版本号SemVer`X.Y.Z`,自动校验格式)
- 版本类型(普通更新 / 强制更新)
- 最低兼容版本(低于此版本的客户端将被强制升级)
- 更新日志Markdown最多 2000 字,对外展示)
- 内部发布说明(不对外)
- [ ] 支持版本状态:草稿(不对外生效)/ 已发布 / 已下线
- [ ] 上传成功后,系统自动计算 SHA256 校验值并填充到该版本元数据,作为客户端下载完成后的完整性校验依据
- [ ] 发布后,客户端下次检测时即可感知(无需等待)
- [ ] 支持版本回滚:将指定历史版本重新置为「已发布」,自动将当前版本标记为「已下线」
- [ ] 支持便携版Portable ZIP作为可选上传项
#### Story C2跨租户监控客户端版本分布
> 作为 Platform Admin我希望跨租户查看当前所有在线客户端的版本分布以了解全平台升级覆盖率对仍使用旧版本的客户端发出提醒或强制升级。
**验收标准**
- [ ] 「客户端版本管理」页展示版本分布统计:各版本在线客户端数量及占比(饼图或条形图)
- [ ] 支持按租户维度查看版本分布(区分不同经纪公司,便于识别落后租户)
- [ ] 支持按租户统计安装数:可查看任一租户的「当前活跃安装数(最近 24h」与「历史装机总数不论是否活跃
- [ ] 支持「全平台租户活跃榜」视图:以列表形式展示各 active 状态租户的 `tenant_code`、租户名称、活跃安装数、历史装机总数,按活跃安装数降序排列
- [ ] 支持对指定版本范围的客户端推送「强制更新」标记(如:将所有低于 v1.5.0 的客户端标记为强制更新)
- [ ] 支持升级进度趋势图:新版本发布后各天累计升级完成的用户比例
> **客户端侧用户故事不在本 PRD 范围**如「Agent 下载安装客户端」「Agent 自动升级」)。这些故事面向终端用户使用的桌面 App不属于平台管理后台职责。但本 PRD 通过 Story C1/C2 为客户端侧故事提供版本数据源与控制能力。
---
### Persona D — 只读审计员 Carol合规审计
> 负责平台合规审查,定期导出操作日志供法务或客户审查。无任何写权限。
#### Story D1审计日志查询与导出
> 作为审计员,我希望按操作人、时间范围、操作类型筛选操作日志,并导出为报告。
**验收标准**
- [ ] 日志列表支持多维度筛选:操作人、时间范围、操作对象、操作类型(创建 / 修改 / 删除 / 高危)
- [ ] 每条日志包含:操作人、操作时间、操作对象(租户/用户ID、内容摘要、结果成功/失败)、来源 IP
- [ ] 支持导出筛选结果为 CSV
---
## 5. 功能详细说明
### 5.1 租户与 License 管理
#### 5.1.1 租户生命周期
**新建租户表单字段**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 公司名称 | Text | ✅ | 最大 100 字符 |
| 联系人 | Text | ✅ | 主联系人姓名 |
| 联系人手机号 | Text | ✅ | 用于创建初始 Tenant Admin |
| 联系邮箱 | Email | ❌(可选) | 发送欢迎邮件;无邮箱改用 PDF 下载 |
| 所在地区 | Select | ✅ | 省市两级 |
| 订阅套餐 | Select | ✅ | Basic / Professional / Enterprise |
| License 到期日期 | Date | ✅ | 到期后自动挂起 |
| License 授权用户数上限 | Number | ✅ | 用户数计费维度 |
**创建流程**
1. 表单校验通过后,后台异步任务执行:
- 创建 PostgreSQL Schema`tenant_{id}`
- 执行 Migrate 初始化表结构
- 注入默认配置(见下方「默认配置内容」)
- 生成初始密码12 位随机),标记为「首次登录强制修改」
- 联系邮箱已填写时:发送欢迎邮件(含 Tenant Code、初始密码、平台访问链接
2. 任务完成后更新租户状态为 `active`;失败则全量回滚并标记为 `failed`
3. 生成唯一 Tenant IDUUID及 Tenant Code12位纯数字`202605023765`
#### 5.1.2 默认配置内容Schema 初始化时注入)
| 配置类型 | 内容 | 说明 |
|---------|------|------|
| 权限定义PermissionDef | 平台全量权限码 | 从 public schema 同步至租户 schema |
| 系统默认业务角色 | 7 个内置角色(置业顾问/店管/区管/区总/副总/总经/其他职能)+ 角色—权限绑定 | 按「角色权限矩阵.md」 |
| 初始 Tenant Admin 用户 | 以联系人手机号创建 | 写入「租户管理员」专属权限集合,独立于 7 个业务角色 |
#### 5.1.3 挂起Suspend
- **触发方式**
- 手动:运营人员选择挂起原因(欠费 / 违规 / 主动申请 / 其他)
- 自动:系统每日检查 License 到期日期到期自动挂起原因「License 到期」)
- 可设置挂起到期时间(留空表示永久挂起直至手动恢复)
- 挂起效果:租户用户访问被拒,重定向至暂停提示页;平台管理后台对该租户数据访问不受影响
- 自动恢复手动设置了到期时间的挂起会到期自动恢复License 到期挂起须 Platform Admin 手动恢复
- 通知:挂起 / 恢复均向租户联系邮箱发送通知(无邮箱跳过)
- License 到期前 15 天起,租户端 Tenant Admin 看到倒计时横幅
#### 5.1.4 删除Delete
| 模式 | 说明 |
|------|------|
| 软删除 | 标记删除状态,数据保留 30 天(默认,可配置)后自动清除 |
| 硬删除 | 立即清除所有数据、Schema、存储资源仅 Platform Admin 可操作 |
**删除前置条件**
1. 操作人必须确认数据导出已完成(勾选确认框)
2. 硬删除需 MFA 二次验证
3. 软删除冷静期内可在租户列表中执行「撤销删除」
删除完成后释放:访问域名 / Tenant Code、Cloudflare R2 存储桶、License 席位
#### 5.1.5 数据导出Export
> Story A5响应客户数据导出请求暂缓本节描述能力为运营内部数据核查使用。
- 触发:管理员手动,选择目标租户 + 模块 + 格式
- 异步执行状态实时刷新Pending → In Progress → Done / Failed
- 内容结构化数据CSV / JSON / SQL Dump+ 文件资产 URL 清单(**不打包文件实体**
- 模块选项:客户数据 / 房源数据 / 交易记录 / 系统配置 / 全量
- 存储:压缩后存于 Cloudflare R2 临时目录,签名下载链接 24 小时有效
**文件资产导出处理规则**v1 决策):
- R2 Bucket 配置为 public read文件通过 CDN 持久 URL 形式内嵌于导出数据
- 账号未硬删除前CDN URL 持续有效
- 迁移类需求(需要文件实体)走「完整备份」流程,不走「数据导出」
| 导出格式 | 图片字段示例 | 附件字段示例 |
|---------|------------|------------|
| CSV | `photos` 列:多个 CDN URL 以英文分号分隔 | `attachments` 列:`文件名\|CDN URL` 以分号分隔 |
| JSON | `"photos": [{"url": "...", "filename": "...", "created_at": "..."}]` | `"attachments": [{"url": "...", "filename": "..."}]` |
| SQL Dump | 文件元数据表原样导出,`file_url` 字段为 CDN URL | 同左 |
导出包附 `README.txt` 说明文件资产链接策略。
**数据导出 vs 完整备份**
| 维度 | 数据导出Export | 完整备份Backup |
|------|------------------|-----------------|
| 用途 | 合规审计、数据核查、业务分析 | 灾难恢复、租户迁移 |
| 文件资产 | CDN URL 清单 | 含 R2 文件实体 |
| 完成时间 | 分钟级 | 小时级 |
| 触发方式 | 运营人员手动 | 手动 / 系统自动(升级前) |
| 存储成本 | 极低 | 较高 |
#### 5.1.6 数据备份Snapshot
- 自动触发:升级前系统对参与租户全量备份
- 手动触发:管理员可在租户详情页发起
- 内容:数据库 Schemapg_dump+ R2 文件存储(附件、图片)
- 字段:备份时间、触发方式、备份大小、状态
- 保留策略:默认最近 10 个版本,可在全局配置中调整
- 存储:加密;目标可选(本地 / S3 / R2 / GCS
#### 5.1.7 数据恢复Restore
恢复流程:
```
选择目标备份版本
→ 二次确认(显示当前数据版本将被覆盖)
→ 自动对当前数据生成临时快照
→ 租户切换为维护模式
→ 执行恢复
→ 自动恢复服务 → 生成恢复报告
```
报告字段:操作人、操作时间、恢复前/后版本、耗时、结果。
#### 5.1.8 套餐升级
- 升级路径Basic → Professional → Enterprise
- 升级前展示差异对比表功能项、用户数上限、存储空间、API 额度)
- 生效模式:立即 / 下一账期
- 升级前自动备份;升级失败可一键回滚
- 升级历史:时间、操作人、升级前后套餐
#### 5.1.9 租户用户与权限管理
**Tenant Admin 管理**
- 每个租户可设置 1 至多名 Tenant Admin
- Platform Admin 可直接创建新用户并赋予 Tenant Admin或从租户现有用户中指定
- 支持新增 / 替换 / 撤销
**密码重置**(针对租户内任意用户):
- 方式一:发送重置链接至注册邮箱(用户自助)
- 方式二:管理员直接设置临时密码(首次登录强制修改)
- 所有操作进审计日志
#### 5.1.10 租户监控与统计
**资源监控指标**
| 指标 | 展示维度 |
|------|---------|
| CPU / 内存占用 | 实时折线图 |
| 存储用量 | 当前值 vs 套餐上限 |
| API 调用次数 | 当日 / 本月累计 |
| 活跃用户数 | 当日活跃 |
| 当日登录次数 | 累计折线图 |
| 异常请求数 | 4xx / 5xx 分类 |
| 慢查询数量 | > 500ms |
支持为每个指标配置阈值告警(邮件 / Webhook
**可用性 / SLA**
- 服务可用率统计:日 / 周 / 月
- 故障事件记录:开始 / 恢复时间、持续时长、影响描述
- SLA 达标率报告,可导出
---
### 5.2 平台版本治理
#### 5.2.1 系统升级流程
```
上传/拉取升级包
→ 自动健康检查(所有服务正常才允许)
→ 配置升级策略:全量 / 灰度(指定内测租户)
→ 升级前自动备份(参与本次升级的租户)
→ 执行升级
→ 实时展示升级进度(租户维度状态列表)
→ 完成通知(成功/失败详情)
```
**灰度升级策略**
- 维护「内测租户组」列表,由 Platform Admin 配置
- 灰度阶段仅对内测租户升级,其余租户保持原版本
- 内测验证通过(手动确认)后,触发全量升级
#### 5.2.2 升级回滚
- 触发:手动 / 自动(监控检测到错误率超阈值)
- 范围:全量回滚 / 单租户回滚
- 前置:自动保存当前状态快照
- 后续:生成事件报告(失败原因、回滚耗时、受影响租户列表)
- 必需MFA 二次验证
#### 5.2.3 定时备份策略
| 配置项 | 选项 |
|--------|------|
| 备份频率 | 每小时 / 每日 / 每周 |
| 执行时间窗口 | 默认每日 02:00 |
| 保留数量 | 最近 N 个版本(默认 10 |
| 存储目标 | 本地 / AWS S3 / Cloudflare R2 / GCS |
支持为单租户配置独立备份计划,覆盖全局策略。备份失败自动告警,支持手动重试。
#### 5.2.4 版本总览
见 Story B3 验收标准。该视图为运营提供「平台基础数据版本 + 各租户数据版本」双维度可见性,是灰度升级时代的版本治理核心入口。
---
### 5.3 客户端发布治理
> 本节面向 Persona C 的 Story C1 / C2。客户端本身的技术形态、自动更新机制、签名分发等实现细节属于技术方案范畴请参见 `TECH_STACK/平台管理后台技术方案.md`。本 PRD 仅描述产品视角的业务能力。
#### 5.3.1 版本管理
平台管理后台提供独立的「客户端版本管理」一级页面,承担:
**版本列表展示字段**
| 列 | 说明 |
|----|------|
| 版本号 | SemVer 格式,如 `v1.2.3` |
| 版本类型 | 普通更新 / 强制更新(后者标红) |
| 状态 | 草稿 / 已发布(绿)/ 已下线(灰) |
| 发布时间 | 设为已发布的时间 |
| 下载量 | 该版本安装包被下载次数 |
| 操作 | 发布 / 下线 / 编辑 / 复制下载链接 |
**新增/编辑版本表单字段**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 版本号 | Text | ✅ | SemVer自动校验 |
| 版本类型 | 单选 | ✅ | 普通更新 / 强制更新 |
| 最低兼容版本 | Text | ❌ | 低于该版本的客户端被强制升级 |
| 安装包EXE | File | ✅ | 上传至 R2最大 500MB |
| 便携版ZIP | File | ❌ | 同上 |
| SHA256 校验值 | Text自动 | ✅ | 上传后系统计算并填充 |
| 更新日志 | Markdown | ✅ | 对外展示,最多 2000 字 |
| 发布说明(内部) | Text | ❌ | 仅内部查看 |
| 状态 | 单选 | ✅ | 草稿 / 立即发布 |
#### 5.3.2 版本分布统计
| 图表 | 说明 |
|------|------|
| 版本分布饼图 | 按客户端版本号统计当前活跃用户数量及占比 |
| 升级进度趋势图 | 新版本发布后各天累计升级完成的用户比例 |
| 租户版本明细 | 按租户展示其员工的客户端版本分布 |
| 全平台租户活跃榜 | `tenant_code` + 租户名称 + 活跃安装数(最近 24h + 历史装机总数,按活跃安装数降序 |
底层数据由 `public.client_heartbeats` 表的 `tenant_id` 维度聚合提供。
#### 5.3.3 强制更新推送
- Platform Admin 可对指定版本范围的客户端打上「强制更新」标记(如所有低于 v1.5.0 的客户端强制升级)
- 客户端侧的强制更新行为(不展示「稍后提醒」、必须升级后方可继续使用)由客户端实现保障
---
### 5.4 平台管理后台总体规划
#### 5.4.1 页面清单与访问权限
> 以下为产品视角的页面清单。「页面路径」以业务可读形式表达,不约束技术实现(具体 URL 由技术方案决定。所有页面均位于平台管理后台域无访问权限的角色Tenant Admin、Agent 及未登录用户)一律重定向到登录页或 403。
| # | 页面 | 路径(业务可读) | 访问权限 | 说明 |
| --- | ----------- | -------------------------------- | --------------------------------------- | --------------------------------- |
| 1 | 登录页 | `/admin/login` | 公开(仅平台管理员账号可登录) | 强制 MFA 配置 |
| 2 | 仪表盘 | `/admin` | 全部三类管理员 | 平台总览、健康度、近期告警、最近高危操作 |
| 3 | 租户列表 | `/admin/tenants` | Platform Admin / 运营人员(写)/ 审计员(只读) | 检索、筛选、快捷操作 |
| 4 | 新建租户 | `/admin/tenants/new` | Platform Admin / 运营人员 | — |
| 5 | 租户详情:基本信息 | `/admin/tenants/{id}` | 同租户列表 | 含 License、用户数、状态等编辑入口 |
| 6 | 租户详情:用户管理 | `/admin/tenants/{id}/users` | 同上 | Tenant Admin 列表、用户列表、密码重置 |
| 7 | 租户详情:套餐信息 | `/admin/tenants/{id}/plan` | 同上 | 套餐详情、用量、升级入口 |
| 8 | 租户详情:监控 | `/admin/tenants/{id}/monitoring` | 同上 | 资源使用、SLA |
| 9 | 租户详情:备份记录 | `/admin/tenants/{id}/backups` | Platform Admin/ 运营人员(仅触发备份)/ 审计员(只读) | 备份列表、触发备份、恢复入口(仅 Platform Admin |
| 10 | 租户详情:操作历史 | `/admin/tenants/{id}/history` | 全部三类 | 该租户相关所有管理员操作日志 |
| 11 | 系统版本管理 | `/admin/system/versions` | Platform Admin/ 运营人员(只读)/ 审计员(只读) | 含「版本总览」(平台基础数据版本 + 各租户数据版本)、升级、回滚 |
| 12 | 备份管理 | `/admin/system/backups` | Platform Admin/ 运营人员(触发)/ 审计员(只读) | 全局备份计划、备份任务列表、恢复入口 |
| 13 | 监控与告警 | `/admin/monitoring` | Platform Admin / 运营人员(写)/ 审计员(只读) | 全局监控图表、告警规则、告警历史 |
| 14 | **客户端版本管理** | `/admin/client-releases` | Platform Admin/ 运营人员(只读)/ 审计员(只读) | 版本列表、发布、下线、回滚、版本分布、租户活跃榜 |
| 15 | 审计日志 | `/admin/audit-logs` | 全部三类(含审计员) | 多维度筛选、导出 CSV |
| 16 | 管理员设置 | `/admin/settings/admins` | 仅 Platform Admin | 管理员账号、角色、MFA、IP 白名单、登录会话 |
#### 5.4.2 页面间导航逻辑(用户故事视角)
- 用户访问任一受保护页面但未登录 → 跳转 `/admin/login`
- 登录成功且未配置 MFA → 强制跳转 MFA 配置向导后再进入仪表盘
- 仪表盘的「最近高危操作」区域,每条记录可点击跳转到「审计日志」并自动以该记录为筛选条件
- 仪表盘的「系统健康」区域,点击异常服务可跳转「监控与告警」对应租户/服务视图
- 租户列表点击「公司名称」→ 进入「租户详情:基本信息」(默认 Tab
- 租户详情各 Tab 之间通过页内 Tab 切换URL 同步变化(页面 510
- 租户详情「备份记录」中点击「恢复」→ 出现二次确认弹窗 + MFA 验证(仅 Platform Admin 可见)
- 「系统版本管理」中点击某租户行 → 跳转到该租户的「备份记录」Tab
- 「客户端版本管理」中点击「全平台租户活跃榜」中的租户名称 → 跳转到该租户「基本信息」Tab
- 客户端版本「发布 / 下线 / 强制更新推送」操作 → 二次确认;强制更新推送额外要求 MFA
- 任何高危操作(删除租户、数据恢复、系统回滚、客户端版本下线、强制更新推送)触发 MFA 二次确认弹窗
- 用户超过 30 分钟无操作 → 自动登出,下次操作跳转登录页
- 「管理员设置」中 Platform Admin 强制登出某管理员 → 该管理员的所有会话立即失效,下次请求跳转登录页
#### 5.4.3 仪表盘内容
| 模块 | 展示内容 |
|------|---------|
| 全局概览 | 总租户数、活跃租户数、本月新增 |
| 系统健康 | 各核心服务状态Django / PostgreSQL / Redis / Celery / R2 |
| 近期告警 | 最近 24 小时告警,按严重程度分类 |
| 资源概览 | 平台整体存储用量、API 调用量趋势 |
| 客户端覆盖 | 当前活跃客户端总数、最新版本占比 |
| 最近操作 | 最近 10 条高危操作审计记录 |
#### 5.4.4 租户列表
- 分页(默认 20 条/页)
- 搜索公司名称、Tenant Code、联系邮箱关键词
- 筛选状态Active / Suspended / Deleted、套餐、注册时间、**即将到期15 天内)**、**用户数已满**
-公司名称、Tenant Code、套餐、状态、注册时间、活跃用户数 / License 上限、客户端最新版本占比
- 快捷操作:查看详情、挂起、发起备份、数据导出
---
### 5.5 业务 API 操作清单(产品视角)
> 仅描述「平台管理员需要能完成哪些业务操作」;不定义具体 RESTful 路径、HTTP 方法、参数结构。具体接口设计见对应技术方案文档。
#### 5.5.1 租户管理
- 平台管理员需要能查询租户列表(支持多维度筛选)
- 平台管理员需要能查看单租户的完整详情基本信息、套餐、用户、License、备份、操作历史
- 平台管理员需要能创建新租户(含异步初始化 Schema 与默认配置)
- 平台管理员需要能编辑租户基本信息(公司名、联系人、邮箱、地区)
- 平台管理员需要能挂起 / 恢复租户(含挂起原因、到期时间)
- 平台管理员需要能软删除 / 硬删除租户(硬删除需 MFA
- 平台管理员需要能撤销软删除(冷静期内)
- 平台管理员需要能调整租户的 License 到期日期
- 平台管理员需要能调整租户的 License 授权用户数上限
- 平台管理员需要能升级租户套餐
- 平台管理员需要能下载租户的「入驻信息 PDF」
#### 5.5.2 租户用户管理
- 平台管理员需要能查询租户内的全部用户
- 平台管理员需要能新增 / 替换 / 撤销 Tenant Admin
- 平台管理员需要能为租户内任意用户重置密码(链接 / 临时密码两种方式)
#### 5.5.3 数据导出与备份
- 平台管理员需要能为指定租户触发数据导出(异步,可查询任务状态、获取下载链接)
- 平台管理员需要能为指定租户手动触发完整备份
- 平台管理员需要能查询某租户的备份记录列表
- 平台管理员需要能基于某个备份执行恢复操作(需 MFA
- 平台管理员需要能配置全局 / 单租户的定时备份策略
#### 5.5.4 平台版本与升级
- 平台管理员需要能上传 / 拉取系统升级包
- 平台管理员需要能配置升级策略(全量 / 灰度,含内测租户名单)
- 平台管理员需要能触发升级,并实时查询升级进度(租户维度)
- 平台管理员需要能查看升级日志
- 平台管理员需要能触发升级回滚(全量 / 单租户,需 MFA
- 平台管理员需要能查询「平台基础数据版本」与「各租户数据升级版本」总览
#### 5.5.5 客户端发布
- 平台管理员需要能查询客户端版本列表(含状态、下载量)
- 平台管理员需要能新增客户端版本(上传安装包,系统自动计算 SHA256
- 平台管理员需要能修改某版本元数据(更新日志、版本类型、最低兼容版本等)
- 平台管理员需要能切换某版本状态(草稿 → 已发布 / 已发布 → 已下线)
- 平台管理员需要能将某历史版本回滚为「已发布」(同时把当前版本置为已下线)
- 平台管理员需要能对指定版本范围的客户端打上「强制更新」标记
- 平台管理员需要能查询全平台客户端版本分布(饼图、趋势图)
- 平台管理员需要能查询全平台租户活跃榜(活跃安装数、历史装机总数)
- 平台管理员需要能查询任一租户的客户端版本分布与活跃数
> 客户端**自身**与更新服务的交互(如查询最新版本、上报心跳)属于客户端运行时与平台之间的接口,不属于平台管理员的操作;本 PRD 不在此章描述,由 `TECH_STACK/平台管理后台技术方案.md` 定义。
#### 5.5.6 监控与告警
- 平台管理员需要能查询全局 / 单租户的监控图表
- 平台管理员需要能配置告警规则(指标 + 阈值 + 通知渠道)
- 平台管理员需要能查询告警历史
#### 5.5.7 审计日志
- 平台管理员(含审计员)需要能按多维度筛选审计日志
- 平台管理员(含审计员)需要能导出筛选结果为 CSV
#### 5.5.8 平台管理员账号与安全
- Platform Admin 需要能创建 / 编辑 / 停用平台管理员账号
- Platform Admin 需要能配置管理员角色Platform Admin / 运营人员 / 只读审计员)
- 全部平台管理员需要能首次登录时配置 MFA无法跳过
- 全部平台管理员需要能在高危操作时通过 MFA 二次确认
- Platform Admin 需要能配置 IP 白名单
- Platform Admin 需要能查看活跃会话并强制登出指定管理员
---
### 5.6 安全与访问控制
**强制要求(不可降级)**
| 安全要求 | 说明 |
|---------|------|
| MFA 强制启用 | 所有管理员账号首次登录强制配置 TOTP不可跳过 |
| IP 白名单 | 仅允许指定 IP 范围访问平台管理后台 |
| 高危操作二次验证 | 删除租户、数据恢复、系统回滚、客户端版本下线、强制更新推送均触发 MFA 二次确认 |
| 会话超时 | 无操作 30 分钟自动登出Token 失效 |
| 强制登出 | Platform Admin 可在「管理员设置」中强制终止指定管理员的所有会话 |
| 与租户应用隔离 | 平台管理后台部署在独立平台域名,不与租户应用共享 Session / Cookie |
### 5.7 操作审计日志规范
所有写操作Create / Update / Delete及高危操作必须落审计日志字段
```
{
"id": "UUID",
"operator_id": "管理员用户 ID",
"operator_name": "管理员显示名",
"action_type": "CREATE_TENANT | SUSPEND_TENANT | RESUME_TENANT | DELETE_TENANT | HARD_DELETE_TENANT | RESTORE_DATA | SYSTEM_UPGRADE | ROLLBACK | RESET_PASSWORD | RELEASE_CLIENT_VERSION | OFFLINE_CLIENT_VERSION | FORCE_UPDATE_PUSH | UPDATE_LICENSE | UPDATE_LICENSE_USER_LIMIT | AUTO_SUSPEND_LICENSE_EXPIRED | ...",
"target_type": "Tenant | User | System | Backup | ClientRelease | Admin",
"target_id": "操作对象 ID",
"target_name": "操作对象可读名称",
"payload_summary": "操作内容摘要(非敏感字段)",
"result": "SUCCESS | FAILED",
"error_message": "失败原因(如有)",
"ip_address": "操作来源 IP",
"created_at": "ISO 8601 时间戳"
}
```
---
## 6. 角色权限矩阵
| 操作 | Platform Admin | 运营人员 | 只读审计员 |
|------|---------------|---------|-----------|
| 创建租户 | ✅ | ✅ | ❌ |
| 挂起 / 恢复租户 | ✅ | ✅ | ❌ |
| 软删除租户 | ✅ | ✅ | ❌ |
| 硬删除租户 | ✅ | ❌ | ❌ |
| 调整 License 到期日期 / 用户数上限 | ✅ | ✅ | ❌ |
| 数据导出 | ✅ | ✅ | ❌ |
| 手动触发备份 | ✅ | ✅ | ❌ |
| 数据恢复 | ✅ | ❌ | ❌ |
| 系统升级 | ✅ | ❌ | ❌ |
| 系统回滚 | ✅ | ❌ | ❌ |
| 查看版本总览 | ✅ | ✅ | ❌ |
| 配置告警规则 | ✅ | ✅ | ❌ |
| 发布 / 下线客户端版本 | ✅ | ❌ | ❌ |
| 客户端版本回滚 | ✅ | ❌ | ❌ |
| 推送强制更新标记 | ✅ | ❌ | ❌ |
| 查看客户端版本分布与活跃榜 | ✅ | ✅ | ✅ |
| 查看审计日志 | ✅ | ✅ | ✅ |
| 导出审计日志 | ✅ | ✅ | ✅ |
| 管理员账号管理 | ✅ | ❌ | ❌ |
| 强制登出管理员 | ✅ | ❌ | ❌ |
| 配置 IP 白名单 | ✅ | ❌ | ❌ |
---
## 7. 租户状态机
```
[新建中 Creating]
↓ 成功
[活跃 Active] ←──────────────────┐
↓ 手动 / License 到期挂起 │ 到期自动恢复(仅手动挂起且设置了到期时间)/ 手动恢复
[已挂起 Suspended] ───────────────┘
↓ 删除操作(软删除)
[待清除 Pending Delete](冷静期 30 天)
↓ 冷静期到期 / 硬删除
[已删除 Deleted]
```
---
## 8. 不构建清单What We're NOT Building
| 请求/功能 | 原因 | 重新评估条件 |
|----------|------|------------|
| 自动化账单与发票 | 财务模块独立立项 | 财务模块 PRD 完成后接入 |
| 租户端自助迁移工具 | 当前规模不需要 | 租户数 > 500 时重新评估 |
| 平台管理后台移动端 | 运营场景明确为 PC | v2 规划,用户调研支持时推进 |
| Webhook 事件推送市场 | 集成复杂度高,无客户驱动 | 有 3+ 客户明确需求时评估 |
| 多语言管理界面 | 内部团队中文已满足 | 国际化扩张时规划 |
| macOS / Linux 客户端 | 99% 用户为 Windows | 市场需求出现时评估 |
| 移动端客户端 App | v2 规划 | v2 启动时 |
| 客户端离线模式 | 联网使用为既定形态 | 暂不考虑 |
| 客户端代码混淆 / 反逆向 | 优先功能交付 | 安全审计要求时启动 |
| 响应客户数据导出请求 | 暂缓 | 后续迭代排期时细化 |
---
## 9. 发布计划
| 阶段 | 时间 | 范围 | 通过标准 |
|------|------|------|---------|
| 内部 Alpha | Week 14 | 平台内部团队 | 核心租户 CRUD 流程无 P0 BugMFA 可用;客户端发布流程闭环 |
| 封闭 Beta | Week 56 | 运营团队日常使用 + 1 家种子客户的客户端 | 备份/恢复完整可用;审计日志 100% 覆盖;客户端自动更新成功率 ≥ 95% |
| 正式上线 | Week 7 | 全量运营团队 + 全部客户的客户端 | 升级/回滚验证通过;监控告警规则配置完成;客户端版本一致性 ≥ 95% |
**回滚标准**
- 正式上线 72 小时内发现租户数据隔离漏洞或审计日志丢失 → 立即回滚平台管理后台
- 客户端版本上线 24 小时内:自动更新失败率 > 5%、白屏 / 崩溃率 > 2%、或收到 P0 安全报告 → 立即下线该版本并恢复上一稳定版本
---
## 10. 技术考量(指引性)
> 本节为产品视角对技术决策的影响概述,详细方案请见各技术文档。
| 维度 | 关键依赖 | 备注 |
|------|---------|------|
| 多租户隔离 | `django-tenants` | 平台管理后台数据落 `public` schema跨租户查询走 `public` 维度 |
| 异步任务 | Celery + Celery Beat | 创建租户、备份、导出、定时挂起检查等 |
| 数据库备份 | PostgreSQL `pg_dump`(待评估流式方案) | 大租户备份耗时风险 |
| 文件存储 | Cloudflare R2 | 备份、导出包、客户端安装包 |
| 监控 | Sentry + Grafana | 已在技术栈中规划 |
| MFA | `django-otp` + TOTP | 待最终选型 |
| 客户端 | Electron + electron-updater | 见 `TECH_STACK/平台管理后台技术方案.md` |
| 客户端心跳/活跃统计 | `public.client_heartbeats`Upsert + 24h 活跃口径,见 `ADR-20260430-007` | 是租户活跃榜与版本分布的数据源 |
| 客户端 API 命名空间 | 统一 `/api/release/...`(见 `ADR-20260430-009` | 与租户客源管理 `apps/client/` 命名空间隔离 |
### 待解决问题(开发启动前必须确认)
- [ ] 数据库备份方案:`pg_dump` 直接执行还是基于 WAL 的增量备份(如 pgBackRest— Owner: 工程负责人
- [ ] 监控数据来源Grafana 直接对接 PostgreSQL 还是通过 Prometheus Exporter— Owner: 运维团队
- [ ] MFA 库选型:`django-otp` + TOTP 还是集成第三方认证?— Owner: 工程负责人
- [ ] 审计日志存储:写入 `public` schema 还是独立日志服务(如 Elasticsearch— Owner: 工程负责人
- [ ] 客户端 EV 代码签名证书采购主体与预算 — Owner: IT 负责人
- [ ] CI/CD 平台选型(客户端构建签名流水线)— Owner: 运维负责人
- [ ] 客户端便携版Portable ZIP是否纳入 v1 — Owner: PM
- [ ] 客户端启动时租户 URL 分发方式(统一域名重定向 vs 内置配置文件)— Owner: 产品 + 工程
---
## 11. 附录
### 11.1 术语表
| 术语 | 定义 |
|------|------|
| Platform Admin | 平台超级管理员,拥有所有高危操作权限 |
| 运营人员 | 平台日常运营,承担租户管理、监控巡检 |
| 只读审计员 | 仅查询与导出审计日志,无写权限 |
| Tenant Admin | 租户内的最高权限用户,**对平台管理后台无访问权限** |
| Tenant Code | 租户唯一可读短码,用作租户访问 URL 中的 `tenant=` 参数 |
| 灰度升级 | 先对内测租户升级、验证通过后再全量推送 |
| 数据导出 | 输出结构化数据 + 文件 CDN URL 清单,分钟级,不含文件实体 |
| 完整备份 | 数据库 + 文件实体的完整副本,小时级,用于灾难恢复与租户迁移 |
| SemVer | 语义化版本控制 `主.次.补丁` |
| EV 证书 | Extended Validation 代码签名证书 |
| SHA256 | 用于校验客户端安装包完整性的散列算法 |
### 11.2 关联文档
| 类型 | 文档 |
| ------------------ | ----------------------------------------- |
| 项目入口 | `README.md` |
| 开发约束 | `AGENTS.md` |
| MVP 范围 | `PRD/PRD_MVP.md` |
| 客户端技术方案 | `TECH_STACK/平台管理后台技术方案.md` |
| Public Schema 数据模型 | `DATA_MODEL/DATA_MODEL_PUBLIC.md` |
| ADR | `ADR.md`(含 `ADR-20260502-001`:本 PRD 合并决策) |

View File

@@ -2,17 +2,27 @@
**状态**: Draft
**作者**: 产品经理
**最后更新**: 2026-04-30v2.0 根据 review 后的 §4 用户故事全面同步 §5 功能详细说明:删除找回用户名流程及邮件模板;找回密码改为纯短信流程;新增 §5.5 手机验证码登录详细说明§6 技术注意事项更新短信依赖/风险/开放问题§8.2 接口清单同步正式功能状态
**版本**: 2.0
**最后更新**: 2026-05-02v3.0 `ADR-20260502-003` 重写:剥离全部实现细节——具体 API 路径、HTTP 方法、JSON Schema、Redis Key、字段类型、Electron API 名、Cookie 属性、限流接口实现等——交由 Tech / DATA_MODEL 承接;本文件只保留业务能力、页面与导航、业务规则、状态机、验收标准
**版本**: 3.0
**所属系统**: Fonrey 房产经纪管理系统
**关联模块**: 组织人事管理、权限管理、系统管理
**关联模块**: 组织人事管理、权限管理、平台管理后台
## 关联文档(实现口径)
- 实现细节端点路径、HTTP 方法、JSON Schema、错误码、滑块/OTP 算法、限流策略、Electron 客户端约定):`TECH_STACK/登录管理技术方案.md`v4.1
- 数据结构(账号表、登录审计表、短信 OTP 表、历史密码表、Redis Key 命名、字段类型与索引):`DATA_MODEL/DATA_MODEL_LOGIN.md`
- 全局 API 契约(统一错误响应、分页、限流外显规范):`TECH_STACK/API_CONTRACT.md`
- 测试用例:`TEST_CASES/TEST_CASES_LOGIN_MODULE.md`
> 本 PRD 涉及的"具体路径 / 字段名 / Token TTL 数值 / Cookie 属性 / Redis Key / 框架 API 名"等实现口径,**均不在本文件出现**,请按上表查阅对应权威文档。
## 变更历史
| 版本 | 日期 | 作者 | 变更说明 |
|------|------|------|---------|
| v1.x | — | 产品经理 | 初稿至 v1.9(详见 git 历史) |
| 版本 | 日期 | 作者 | 变更说明 |
| ---- | ---------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| v1.x | — | 产品经理 | 初稿至 v1.9(详见 git 历史) |
| v2.0 | 2026-04-30 | 产品经理 | 根据 review 后的 §4 用户故事全面同步 §5 功能详细说明:删除找回用户名流程及邮件模板;找回密码改为纯短信流程;新增 §5.5 手机验证码登录详细说明§6 技术注意事项更新短信依赖/风险/开放问题§8.2 接口清单同步正式功能状态全文「Tenant ID」对外概念统一替换为「Tenant Code」 |
| v3.0 | 2026-05-02 | Sisyphus | 按 `ADR-20260502-003` 重写:剥离全部实现细节交由 Tech / DATA_MODEL 承接;删除 §5.1.4 接口规范代码块、§5.2.2 Redis Key 与传输层细节、§5.3.3 字段类型表、§5.6 数据模型字段清单、§5.7 Electron 客户端约定、§6.1 依赖技术选型表、§6.2 多租户实现细节、§8.2 接口清单汇总;保留页面/导航/业务规则/状态机/验收标准;具体口径全部以 `TECH_STACK/登录管理技术方案.md` 为准 |
---
@@ -21,14 +31,14 @@
### 1.1 背景
Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架构`django-tenants` + PostgreSQL Schema 隔离)。终端用户通过 **Windows 桌面客户端Electron** 使用系统,无需手动输入网址即可打开 Web 应用。
Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架构,每家经纪公司是一个独立租户。终端用户通过 **Windows 桌面客户端Electron 壳应用** 使用系统,无需手动输入网址即可打开 Web 应用。
在多租户环境下,用户的身份验证流程比单租户系统更复杂:
- 用户安装客户端后,系统必须先识别当前设备归属哪个租户,才能加载对应租户的登录界面和数据隔离环境
- 每家经纪公司作为独立租户,其员工账号、组织结构、数据均完全隔离
- 经纪人账号须与实名员工档案绑定,确保每一条操作记录可追溯至具体自然人
- 现阶段登录方式以账号密码为主;手机验证码登录、微信扫码登录需预留口,待移动端(小程序)上线后实现
- 现阶段登录方式以账号密码手机验证码为主;微信扫码登录需预留口,待移动端(小程序)上线后实现
### 1.2 核心痛点
@@ -36,7 +46,7 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架
|------|--------|---------|
| 多租户环境下,客户端不知道应该连接哪个租户的服务端 | 新用户首次安装后无法正常使用 | 系统无法启动,用户体验极差 |
| 账号密码裸露登录,缺乏验证码保护 | 所有用户 | 存在暴力破解、自动化恶意登录风险 |
| 用户忘记账号或密码无自助找回通道 | Agent经纪人 | 依赖管理员手动重置,效率低 |
| 用户忘记密码无自助找回通道 | Agent经纪人 | 依赖管理员手动重置,效率低 |
| 账号未与实名经纪人档案绑定 | Tenant Admin租户管理员、合规审计 | 操作行为无法追溯至自然人 |
| 无多因素认证,安全系数低 | 管理层、数据合规 | 存在账号冒用、数据泄露风险 |
@@ -56,45 +66,97 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架
| 目标 | 指标 | 当前基准 | 目标值 | 衡量周期 |
|------|------|----------|--------|----------|
| 降低登录失败率 | 账号密码正确情况下的登录成功率 | 待统计 | ≥ 99% | 上线后 30 天 |
| 防止恶意登录 | 每日验证码拦截异常登录请求数 | 0无保护 | 建立基线,IP异常次数 > 5次/分钟触发封锁 | 上线后持续监控 |
| 提升找回账号效率 | 用户自助找回密码耗时 | 依赖管理员(约1工作日) | < 3 分钟(短信验证码自助) | 上线后 30 天 |
| 防止恶意登录 | 每日验证码拦截异常登录请求数 | 0无保护 | 建立基线,对同 IP 异常突增触发封锁 | 上线后持续监控 |
| 提升找回账号效率 | 用户自助找回密码耗时 | 依赖管理员(约 1 工作日) | < 3 分钟(短信验证码自助) | 上线后 30 天 |
| 确保账号实名绑定率 | 拥有系统账号且未与员工档案绑定的账号比例 | 待统计 | 0%(强制绑定) | 上线即达标 |
| Tenant 识别成功率 | 首次安装后成功完成 Tenant 识别的用户比例 | 待统计 | ≥ 98% | 上线后 30 天 |
> 具体的同 IP 限流阈值与 Tenant Code 校验频控数值,详见 `TECH_STACK/登录管理技术方案.md`。
---
## 3. 非目标(本期不做)
- **手机验证码登录**:移动端小程序上线后实现,本期**接口预留**UI 入口以「即将开放」禁用态展示
> **注意**:本条已更新。手机验证码登录已升级为 **MVP 正式功能**(见 Story 5与密码登录并列提供。此非目标条目保留仅作版本记录已失效。
- **微信扫码登录**:移动端小程序上线后实现,本期**接口预留**UI 入口以「即将开放」禁用态展示
- **微信扫码登录**:移动端小程序上线后实现,本期**仅在登录页保留禁用入口**,不开放实际功能
- **单点登录SSO/ 企业微信集成**:后续版本规划
- **多设备并发登录的强制踢出策略**:本期允许同账号多端登录,后续安全策略模块规划
- **登录时段限制 / IP 白名单**:安全策略模块另行规划
- **管理后台Platform Admin登录**Tenant Admin租户管理员登录管理后台的流程属于系统管理模块,本 PRD 专注租户内用户登录
- **平台管理后台Platform Admin登录**属于平台管理后台模块,本 PRD 专注租户内用户登录
- **找回用户名流程**普通员工用户名固定为手机号无需找回Tenant Admin 如忘记用户名请联系平台运营线下处理(原 Story 4 已废弃)
---
## 4. 用户故事与验收标准
## 4. 页面清单与导航
> 本节描述用户视角的页面与跳转关系;具体的前端路由实现与 URL 形态由 `TECH_STACK/登录管理技术方案.md` 定义。
### 4.1 页面清单
| 页面 | 访问权限 | 用途 |
|---|---|---|
| Tenant 识别页 | 公开(无需登录) | 客户端首次启动或切换公司时输入租户识别码 |
| 登录页 | 公开(需先完成 Tenant 识别) | 提供「密码登录」/「验证码登录」两种方式,含微信扫码禁用入口 |
| 找回密码页(三步) | 公开 | 输入手机号 → 输入短信验证码 → 设置新密码 |
| 首次登录强制改密页 | 已登录且账号处于初始密码状态 | 强制用户修改初始密码后方可使用系统 |
| 系统首页 | 已登录且非初始密码状态 | 业务入口;不在本 PRD 范围 |
### 4.2 导航流程
```
[客户端启动]
├─ 本地无 Tenant 缓存 ──→ [Tenant 识别页]
│ │ 验证成功
│ ↓
│ 缓存 Tenant ──→ [登录页]
└─ 本地有 Tenant 缓存 ──→ [登录页]
[登录页]
├─ 密码 / 验证码登录成功
│ │
│ ├─ 账号为"初始密码"状态 ──→ [首次登录强制改密页] ──改密成功──→ [系统首页]
│ └─ 账号为"非初始密码"状态 ──→ [系统首页]
├─ 点击"忘记密码" ──→ [找回密码页(步骤一)] → [步骤二] → [步骤三] ──→ 回 [登录页]
└─ 点击"切换公司"(二次确认)──→ 清缓存 ──→ [Tenant 识别页]
[系统内]
├─ 主动登出 ──→ [登录页]
├─ Session 过期 ──→ [登录页](提示"登录已过期,请重新登录"
└─ 客户端版本过低 ──→ [更新提示页] → 阻断后续登录流程
```
---
## 5. 用户故事与验收标准
### Story 1新用户首次启动客户端——Tenant 识别
**As** 新安装 Fonrey 客户端的经纪人,**I want** 在首次启动时输入所属公司的 12位 Tenant Code 完成租户识别,**So that** 客户端能连接到正确的服务端,后续显示对应公司的登录界面和数据。
**As** 新安装 Fonrey 客户端的经纪人,**I want** 在首次启动时输入所属公司的 12 位 Tenant Code 完成租户识别,**So that** 客户端能连接到正确的服务端,后续显示对应公司的登录界面和数据。
**验收标准**
- [ ] 客户端首次启动时(本地无 Tenant Code 缓存自动呈现「Tenant 识别」面,而非直接显示登录界面
- [ ] 面包含:产品 Logo、产品名称「Fonrey 房睿」、说明文案「请输入您公司的专属识别码」、Tenant Code 输入框、「确认」按钮
- [ ] Tenant Code 输入框支持粘贴操作,自动去除前后空格
- [ ] 点击「确认」后客户端向服务端发起 Tenant 验证请求(`POST /api/auth/tenant/verify/`展示加载状态spinner
- [ ] **验证成功**:服务端返回租户名称及品牌信息(公司名称、Logo URL);客户端将 Tenant Code 写入本地持久化存储,自动跳转至该租户的登录界面;界面顶部展示「正在登录XX 房产」
- [ ] **验证失败Tenant Code 无效)**输入框下方显示红色错误提示「识别码无效请联系您的Tenant Admin租户管理员获取正确的识别码」Tenant Code 不写入本地缓存;用户可重新输入
- [ ] 客户端首次启动时(本地无 Tenant Code 缓存自动呈现「Tenant 识别」面,而非直接显示登录
- [ ] 面包含:产品 Logo、产品名称「Fonrey 房睿」、说明文案「请输入您公司的专属识别码」、Tenant Code 输入框、「确认」按钮、帮助文案「不知道识别码请联系您公司的Tenant Admin租户管理员
- [ ] Tenant Code 输入框支持粘贴操作,自动去除前后空格,仅接受数字字符(非数字自动过滤),固定 12 位
- [ ] 点击「确认」后展示加载状态spinner客户端向服务端发起 Tenant 验证
- [ ] **验证成功**服务端返回租户名称及品牌信息公司名称、Logo );客户端将 Tenant Code 持久化到本地(必须加密存储,自动跳转至该租户的登录页;登录页顶部展示「正在登录XX 房产」
- [ ] **验证失败**(识别码无效):输入框下方显示红色错误提示「识别码无效,请联系您的 Tenant Admin租户管理员获取正确的识别码」Tenant Code 不写入本地缓存;用户可重新输入
- [ ] **网络异常**:显示「网络连接失败,请检查网络后重试」,提供「重试」按钮
- [ ] 非首次启动(本地已有合法 Tenant Code 缓存):直接跳过识别界面,进入登录界面
- [ ] 登录界面提供「切换公司」入口(链接文字,非主要 CTA点击后清除本地 Tenant Code 缓存并重新显示 Tenant 识别界面;确认前弹出二次确认「切换公司将退出当前账号,是否继续?」
- [ ] Tenant 验证接口属于公开接口,无需鉴权;但需对单 IP 请求频率限制(每分钟 ≤ 10 次)以防止枚举攻击
- [ ] 非首次启动(本地已有合法 Tenant Code 缓存):直接跳过识别,进入登录
- [ ] 登录提供「切换公司」入口(链接文字,非主要 CTA点击后弹出二次确认「切换公司将退出当前账号,是否继续?」,确认后清除本地 Tenant Code 缓存并重新显示 Tenant 识别
- [ ] Tenant 验证为公开操作,无需登录态,但平台需对该接口做防枚举的频控保护(具体阈值见 Tech 文档)
**Tenant Code 业务规则**
- 格式12 位纯数字
- 生成:由平台运营在平台管理后台开通租户时自动生成,全局唯一,不允许手动指定
- 唯一性:全局唯一,同一 Tenant Code 不得分配给多个租户
- 客户端存储:必须加密落盘,禁止明文(实现细节见 Tech 文档 §十三 Electron 客户端约定)
---
@@ -102,35 +164,38 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架
**As** 已识别租户的经纪人,**I want** 通过手机号和密码完成登录,**So that** 进入系统开始工作。
> **说明**普通员工的登录账号即为其手机号由Tenant Admin(租户管理员)在新增员工时自动创建无需记忆额外用户名。Tenant Admin 账号的登录名为平台运营自定义字符串,不受此约束
> **说明**:普通员工的登录账号即为其手机号(由 Tenant Admin 在新增员工时自动创建无需记忆额外用户名。Tenant Admin 账号的登录名为该租户联系人手机号,规则一致
**验收标准**
- [ ] 登录界面展示:租户品牌标识(公司 Logo + 公司名称)、手机号输入框、密码输入框、滑块拼图验证区域、「登录」按钮
- [ ] 登录展示:租户品牌标识(公司 Logo + 公司名称)、登录方式 Tab「密码登录」/「验证码登录」,默认选中「密码登录」)、手机号输入框、密码输入框、滑块拼图验证区域、「登录」按钮、「忘记密码」链接、「切换公司」入口、微信扫码禁用入口
- [ ] 手机号输入框 Placeholder「请输入您的手机号」仅接受数字字符非数字自动过滤固定 11 位
- [ ] 密码输入框默认密文显示,右侧提供「显示/隐藏」图标切换明密文
- [ ] **行为验证码(滑块拼图)**:展示一张带缺口的背景图和一块可拖动的拼图碎片,用户通过拖动滑块将碎片移动至缺口位置完成验证;无需输入任何字符,操作直观快速
- [ ] 验证逻辑:前端记录滑动轨迹(坐标序列 + 耗时),与背景图缺口位置一同发送至服务端;服务端综合校验**位置偏差**(允许 ±5px 容差)和**轨迹特征**(是否存在人类滑动的加速/减速规律)以区分机器行为
- [ ] 验证失败(位置不准或轨迹异常):拼图区域抖动动画提示失败,自动刷新新的背景图,用户重新拖动;**不计入账号密码错误次数**
- [ ] **行为验证码(滑块拼图)**:展示一张带缺口的背景图和一块可拖动的拼图碎片,用户通过拖动滑块将碎片移动至缺口位置完成验证;无需输入任何字符
- [ ] 验证失败(位置不准或轨迹异常):拼图区域抖动提示,自动刷新新背景图,用户重新拖动;**不计入账号密码错误次数**
- [ ] 验证成功后,拼图区域显示绿色对勾 + 「验证通过」文案,状态持续至本次登录提交完成
- [ ] 提供「刷新」图标按钮,允许用户主动刷新背景图(针对图片模糊或缺口不清晰的情况)
- [ ] 背景图从预置图库中随机抽取,缺口位置每次随机生成,防止固定模式被预测
- [ ] 三项(手机号、密码、验证码)均有填写后,「登录」按钮才可点击(否则置灰)
- [ ] 提供「刷新」图标按钮,允许用户主动刷新背景图
- [ ] 三项(手机号、密码、滑块)均完成后,「登录」按钮才可点击(否则置灰)
- [ ] 点击「登录」触发前端格式校验:
- 手机号为空 → 输入框下方红色提示「请输入手机号」
- 手机号为空 → 提示「请输入手机号」
- 手机号不满 11 位 → 提示「请输入完整的 11 位手机号」
- 密码为空 → 提示「请输入密码」
- 验证码为空 → 提示「请完成滑块验证」
- [ ] 格式校验通过后,向服务端发起登录请求,按钮进入 loading 状态防止重复提交
- [ ] **登录成功(常规)**服务端返回 Session Token 及 `is_initial_password` 标记;客户端存储 Token
- `is_initial_password = False`:直接跳转系统首页,顶部显示欢迎信息「欢迎回来,{姓名}」
- `is_initial_password = True`**立即跳转「修改初始密码」强制页面**,不可关闭、不可跳过、不可访问任何其他功能页面(详见 §5.3.4
- [ ] **登录失败(手机号或密码错误)**:显示「手机号或密码错误,请重新输入」(不区分具体原因,防止枚举攻击);验证码自动刷新;密码输入框清空;手机号保留
- [ ] **登录失败(验证码错误**:显示「验证码有误,请重新输入」;验证码自动刷新;验证码输入框清空
- [ ] **账号被锁定**(同一账号密码连续错误 ≥ 5 次):显示「账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁」;锁定状态下「登录」按钮置灰
- 滑块未完成 → 提示「请完成滑块验证」
- [ ] 格式校验通过后提交登录,按钮进入 loading 状态防止重复提交
- [ ] **登录成功**
-账号处于"非初始密码"状态:直接进入系统首页,顶部显示欢迎信息「欢迎回来,{姓名}」
-账号处于"初始密码"状态:**立即跳转至「首次登录强制改密页」**,不可关闭、不可跳过、不可访问其他功能页面(详见 §6.4
- [ ] **登录失败(手机号或密码错误)**:显示「手机号或密码错误,请重新输入」(不区分具体原因,防止枚举攻击);滑块自动刷新;密码输入框清空;手机号保留
- [ ] **登录失败(滑块异常**:显示「验证码有误,请重新输入」;滑块自动刷新
- [ ] **账号被锁定**(同一账号密码连续错误达到上限):显示「账号已被临时锁定,请稍后再试,或联系管理员解锁」;锁定状态下「登录」按钮置灰
- [ ] **账号已停用**:显示「账号已停用,请联系您的管理员」
- [ ] **Session 过期**:用户在系统内操作时 Session 过期,自动跳转至登录界面,并提示「登录已过期,请重新登录」
- [ ] 登录界面底部提供:「忘记密码」链接(详见 Story 3移除「忘记用户名」入口普通员工用户名即手机号无需找回Tenant Admin 如忘记用户名请联系平台运营)
- [ ] **Session 过期**:用户在系统内操作时 Session 过期,自动跳转至登录,并提示「登录已过期,请重新登录」
**业务规则**(具体数值口径见 Tech 文档):
- 同一账号密码连续错误达到上限后,账号进入临时锁定状态,到期自动解锁;管理员可手动解锁
- 滑块验证失败不计入密码错误次数
- 所有登录请求强制 HTTPS
---
@@ -138,52 +203,50 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架
**As** 忘记密码的经纪人,**I want** 通过手机号 + 短信验证码完成身份核验,重新设定密码,**So that** 无需邮箱、无需联系管理员,独立完成密码重置。
> **说明**:考虑到大多数Agent经纪人没有常用邮箱,本期找回密码统一通过短信验证码实现,废弃邮箱找回方式。账号中 `email` 字段在本系统无任何必须业务用途,完全可选。
> **说明**:考虑到大多数经纪人没有常用邮箱,本期找回密码统一通过短信验证码实现,废弃邮箱找回方式。账号中邮箱字段在本系统无任何必须业务用途,完全可选。
**验收标准**
- [ ] 点击登录界面「忘记密码」链接,跳转至「找回密码」流程(Stepper 分步页面,共三步)
- [ ] 点击登录「忘记密码」链接,跳转至「找回密码」流程(三步)
**步骤一:输入手机号**
- [ ] 页面显示手机号输入框11 位数字,自动过滤非数字)、「获取验证码」按钮、「返回登录」链接
- [ ] 手机号为空或不足 11 位 → 点击「获取验证码」时输入框下方提示「请输入完整的 11 位手机号」
- [ ] 手机号为空或不足 11 位 → 点击「获取验证码」时输入框下方提示「请输入完整的 11 位手机号」
- [ ] 手机号格式合法后,点击「获取验证码」,按钮进入 60 秒倒计时冷却态(「重新获取(59s)」),倒计时结束后按钮恢复可点击
- [ ] 服务端收到请求后:
- 若该手机号**存在**且账号状态为 `active`:向该号码发送 6 位数字短信验证码,有效期 **10 分钟**
- 若手机号**不存在**或账号已停用:页面统一提示「如该手机号已注册,验证码将在 1 分钟内发送」(**不泄露账号是否存在**
- [ ] 同一手机号 1 小时内最多发送 **5 次**短信验证码,超限后提示「发送次数过多,请 1 小时后再试」
- [ ] 不论该手机号是否注册,前端文案统一显示「如该手机号已注册,验证码将在 1 分钟内发送」(**不泄露账号是否存在**
- [ ] 后台仅在手机号存在且账号处于可用状态时实际发送短信
- [ ] 同一手机号在一段时间窗口内发送次数有上限,超限后提示「发送次数过多,请稍后再试」
- [ ] 短信内容模板「【Fonrey 房睿】您的密码重置验证码为 {code}10 分钟内有效,请勿泄露。」
**步骤二:输入短信验证码**
- [ ] 页面显示6 位验证码输入框(支持分格输入)、「重新发送」倒计时链接、「下一步」按钮
- [ ] 「下一步」按钮6 位验证码全部输入后方可点击
- [ ] 服务端校验验证码
- 正确且未过期 → 进入步骤三,颁发一次性 `sms_reset_token`(有效期 15 分钟,一次性,服务端存储)
- 错误 → 提示「验证码有误,请重新输入」,错误次数 ≥ 5 次则本次验证码作废,需重新获取
- [ ] 校验结果
- 正确且未过期 → 进入步骤三
- 错误 → 提示「验证码有误,请重新输入」;同一验证码连续错误达到上限后作废,需重新获取
- 已过期 → 提示「验证码已过期,请重新获取」
**步骤三:重置密码**
- [ ] 步骤三依赖步骤二颁发的 `sms_reset_token`(通过 URL 参数或会话状态传递Token 无效或过期 → 显示「操作已超时,请重新发起找回密码」,跳回步骤一
- [ ] **本页面复用「设置新密码」公共组件**(与首次登录强制改密码页面为同一组件,详见 §5.3.4),保持 UI 与交互逻辑完全一致;入口上下文不同时,仅页面标题提示文案有所差异:
- [ ] 步骤三依赖步骤二颁发的一次性凭证;凭证无效或过期 → 显示「操作已超时,请重新发起找回密码」,跳回步骤一
- [ ] **本页面复用「设置新密码」公共组件**(与首次登录强制改密为同一组件,详见 §6.4),保持 UI 与交互逻辑完全一致;入口上下文不同时,仅页面标题提示文案差异:
| 元素 | 首次登录强制修改§5.3.4 | 找回密码步骤三(本 Story |
| 元素 | 首次登录强制改密§6.4 | 找回密码步骤三(本 Story |
|------|--------------------------|--------------------------|
| 页面标题 | 「欢迎使用 Fonrey请先设置您的登录密码」 | 「重置您的登录密码」 |
| 提示文案 | 「您当前使用的是初始密码,为保障账号安全,请立即设置新密码后开始使用」 | 「请输入您的新密码,设置完成后请使用新密码重新登录」 |
| 提交按钮文案 | 「确认并进入系统」 | 「确认重置密码」 |
| 提交后跳转 | `is_initial_password = False`Session 保持,直接进入首页 | 所有 Session 立即失效,跳转登录界面并提示「密码已重置,请使用新密码登录」 |
| 提交后跳转 | Session 保持,直接进入首页 | 该账号所有 Session 立即失效,跳转登录并提示「密码已重置,请使用新密码登录」 |
- [ ] 提交成功后:`is_initial_password` 置为 **`False`**(找回密码属于用户主动操作,已完成身份核验,无需再触发强制修改流程
> **注意**:与首次登录流程不同,找回密码时用户已通过短信验证码完成了身份核验,本次密码设置即视为"用户本人主动设置",不应再触发 `is_initial_password = True` 的二次强制修改。
- [ ] 提交成功后:账号"初始密码"标记置为否(找回密码属于用户主动操作,已完成身份核验,**不应再触发首次登录强制改密流程**
---
### Story 4经纪人找回用户名已废弃
> **状态**已废弃。普通员工用户名固定为手机号无需找回Tenant Admin 如忘记用户名请联系平台运营线下处理。本 Story 保留占位以维持版本记录,实现时跳过此 Story
> **状态**已废弃。普通员工用户名固定为手机号无需找回Tenant Admin 如忘记用户名请联系平台运营线下处理。本 Story 保留占位以维持版本记录,实现时跳过。
---
@@ -191,150 +254,103 @@ Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用多租户架
**As** 已有账号的经纪人,**I want** 通过手机号 + 短信验证码直接登录,**So that** 在忘记密码或不想输入密码时,仍能快速进入系统。
> **说明**:短信基础设施(`sms_otp_records` 表、OTP 发送/校验逻辑)已在 Story 3 找回密码中建设完成,本 Story 直接复用,实现成本极低。登录界面提供「密码登录」和「验证码登录」两个并列入口,用户自由切换,两种方式均为 MVP 正式功能。
> **说明**:短信能力在 Story 3 找回密码中建设完成,本 Story 复用。登录提供「密码登录」和「验证码登录」两个并列 Tab,用户自由切换,两种方式均为 MVP 正式功能。
**验收标准**
- [ ] 登录界面提供两种登录方式的切换 Tab**「密码登录」**(默认选中)和 **「验证码登录」**
- [ ] 登录提供两种登录方式的切换 Tab**「密码登录」**(默认选中)和 **「验证码登录」**
- [ ] 切换 Tab 时,输入区域平滑切换,已填内容清空,滑块验证状态重置
**「验证码登录」界面元素**
- [ ] 手机号输入框(规格同 Story 211 位数字,自动过滤非数字)
- [ ] 手机号输入框(规格同 Story 211 位数字)
- [ ] 验证码输入框6 位数字分格输入)+ 「获取验证码」按钮60 秒倒计时冷却态)
- [ ] 滑块拼图验证区域(规格同 Story 2**先通过滑块验证,再允许点击「获取验证码」**
- [ ] 「登录」按钮(手机号 + 验证码均填写后方可点击)
**获取验证码逻辑**
- [ ] 用户须先完成滑块验证,「获取验证码」按钮方可点击;未完成滑块时点击 → 提示「请先完成滑块验证」
- [ ] 点击「获取验证码」后,服务端:
- 手机号格式不合法 → 前端拦截,提示「请输入完整的 11 位手机号」
- 手机号存在且状态 `active` → 发送 6 位 OTP有效期 **5 分钟**,存入 `sms_otp_records``scene = 'login'`
- 手机号不存在或已停用 → 统一响应「如该手机号已注册,验证码将在 1 分钟内发送」(防止枚举攻击)
- [ ] 同一手机号 1 小时内最多发送 **10 次**登录验证码(找回密码为独立计数,两者不共享限额);超限后提示「发送次数过多,请 1 小时后再试」
- [ ] 手机号格式不合法 → 前端拦截,提示「请输入完整的 11 位手机号」
- [ ] 不论手机号是否注册,前端文案统一显示「如该手机号已注册,验证码将在 1 分钟内发送」(防止枚举攻击)
- [ ] 后台仅在手机号存在且账号处于可用状态时实际发送短信
- [ ] 同一手机号在一段时间窗口内的发送次数有上限(与找回密码独立计数,互不共享);超限后提示「发送次数过多,请稍后再试」
- [ ] 短信内容模板「【Fonrey 房睿】您的登录验证码为 {code}5 分钟内有效,请勿泄露。」
**登录校验逻辑**
- [ ] 点击「登录」,服务端校验 OTP
- 正确且未过期 → 登录成功,后续行为与 Story 2 密码登录完全一致(含 `is_initial_password` 判断)
- 错误 → 提示「验证码有误,请重新输入」;连续错误 ≥ 5 次 → 本次 OTP 作废,提示「验证码已失效,请重新获取」
- [ ] 校验结果:
- 正确且未过期 → 登录成功,后续行为与 Story 2 密码登录完全一致(含初始密码状态判断)
- 错误 → 提示「验证码有误,请重新输入」;连续错误达到上限 → 本次验证码作废,提示「验证码已失效,请重新获取」
- 已过期 → 提示「验证码已过期,请重新获取」
- [ ] **账号被锁定**(密码登录失败次数触发):验证码登录仍受账号锁定限制,锁定期间无法通过任何方式登录,提示「账号已被临时锁定,请 30 分钟后重试,或联系管理员解锁」
- [ ] **账号被锁定**(密码登录失败次数触发):验证码登录仍受账号锁定限制,锁定期间无法通过任何方式登录,提示与 Story 2 一致
> **设计说明**:账号锁定是账号维度的安全策略,不区分登录方式;否则锁定形同虚设。
- [ ] **账号已停用**:提示「账号已停用,请联系您的管理员」
**接口规范**
---
```
POST /api/auth/login/phone/
Request: { phone: string, sms_code: string }
Response: { token: string, is_initial_password: bool, user: {...} } | { error_code: string, message: string }
```
### Story 6预留——微信扫码登录v2 实现)
**As** 绑定了微信账号的经纪人,**I want** 在登录页扫描微信二维码完成登录,**So that** 免去输入账号密码的步骤,提升登录体验。
**当前状态**
- 本期登录页 UI 入口以「即将开放」禁用态展示,**不提供任何可点击的实际功能**
- 后端服务端在 MVP 中**不开放微信相关接口**,相关端点仅占位规划,详见 Tech 文档 §5.4
- v2 实现时的业务前置要求:
- 微信账号必须先在「个人设置」中与系统账号完成绑定
- 二维码有效期 3 分钟,过期后前端自动刷新二维码
- 同一微信账号在同一租户内只能绑定一个系统账号
---
### Story 6预留——微信扫码登录接口预留v2 实现)
## 6. 业务规则与功能详细说明
**As** 绑定了微信账号的经纪人,**I want** 在登录界面扫描微信二维码完成登录,**So that** 免去输入账号密码的步骤,提升登录体验。
**当前状态**:本期 UI 入口以「即将开放」禁用态展示于登录界面,接口定义预留,不开放实际功能。
**预留接口设计**(供后端提前规划):
```
GET /api/auth/wechat/qrcode/ # 获取微信扫码二维码(含 state + 有效期)
POST /api/auth/wechat/callback/ # 微信扫码确认后回调,换取系统 Token
```
**绑定条件**v2 实现时的前置要求):
- 微信账号必须先在「个人设置」中与用户名账号完成绑定
- 二维码有效期 3 分钟,过期后前端自动刷新二维码
- 微信账号只能绑定一个用户名账号(同一租户内)
---
## 5. 功能详细说明
### 5.1 客户端 Tenant 识别流程
#### 5.1.1 流程概述
### 6.1 客户端 Tenant 识别业务流程
```
客户端启动
├─ 本地有 Tenant Code 缓存?
│ │
│ YES ──→ 校验缓存 Tenant Code 是否仍有效(服务端 validate
│ YES ──→ 复核缓存 Tenant Code 是否仍有效
│ │
│ 有效 ──→ 直接进入登录界面
│ 有效 ──→ 直接进入登录
│ │
│ 无效 ──→ 清除缓存,进入 Tenant 识别界面
│ 无效 ──→ 清除缓存,进入 Tenant 识别
└─ NO ──→ 显示 Tenant 识别界面
└─ NO ──→ 显示 Tenant 识别
用户输入 Tenant Code → 发起验证
验证成功 ──→ 缓存 Tenant Code → 进入登录界面
验证成功 ──→ 缓存 Tenant Code → 进入登录
验证失败 ──→ 显示错误信息,保持识别界面
验证失败 ──→ 显示错误信息,保持识别
```
#### 5.1.2 Tenant 识别界面规范
#### Tenant 识别规范
| 元素 | 规格 |
| ------------- | --------------------------------------- |
| 页面背景 | 品牌色渐变(与登录界面保持一致的视觉风格) |
| Logo | Fonrey 产品 Logo居中显示 |
| 标题 | 「欢迎使用 Fonrey 房睿」 |
| 副标题 | 「请输入您公司的专属识别码以继续」 |
| 元素 | 规格 |
| ----------- | --------------------------------------- |
| 页面背景 | 品牌色渐变(与登录保持一致的视觉风格) |
| Logo | Fonrey 产品 Logo居中显示 |
| 标题 | 「欢迎使用 Fonrey 房睿」 |
| 副标题 | 「请输入您公司的专属识别码以继续」 |
| Tenant Code 输入框 | 单行数字输入,固定 12 位,支持粘贴;非数字字符自动过滤,超出 12 位截断 |
| 输入框 Label | 「公司识别码Tenant Code |
| 确认按钮 | 主色调按钮,文字「确认」 |
| 错误提示 | 输入框下方红色文字,固定区域占位(不影响布局抖动) |
| 帮助文案 | 「不知道识别码请联系您公司的Tenant Admin租户管理员 |
| 输入框 Label | 「公司识别码Tenant Code」 |
| 确认按钮 | 主色调按钮,文字「确认」 |
| 错误提示 | 输入框下方红色文字,固定区域占位(不影响布局抖动) |
| 帮助文案 | 「不知道识别码请联系您公司的Tenant Admin租户管理员」 |
#### 5.1.3 Tenant Code 格式规范
- **格式**:固定 **12 位纯数字**,如 `202500010001`
- **生成规则**(建议):由平台运营在系统管理后台开通租户时自动生成,不允许手动指定,确保全局唯一性;可采用时间戳前缀 + 随机后缀的方式生成(如 `YYYYMM` + 6 位随机数)
- **前端校验**:输入框仅接受数字字符(非数字自动过滤),输入满 12 位后自动触发格式完成状态;少于 12 位时点击「确认」弹出提示「识别码须为 12 位数字」
- **唯一性**:全局唯一(公共 Schema 层面),同一 Tenant Code 不可分配给多个租户
- **客户端存储**Electron `app.getPath('userData')` 目录下的配置文件(加密存储,防止明文读取)
#### 5.1.4 服务端 Tenant 验证接口规范
```
POST /api/auth/tenant/verify/
Request Body:
{
"tenant_code": "202500010001"
}
Response 200 (成功):
{
"valid": true,
"tenant_name": "XX房产经纪有限公司",
"tenant_logo_url": "https://cdn.fonrey.com/tenants/xxx/logo.png",
"login_url": "https://xxx.fonrey.com/auth/login/"
}
Response 200 (失败):
{
"valid": false,
"error_code": "TENANT_NOT_FOUND",
"message": "识别码无效"
}
```
> **注意**:该接口属于 `shared_apps` 范围,路由在公共 Schema 下,不需要租户鉴权,但需要限流保护(每 IP 每分钟 ≤ 10 次请求)。
> Tenant 验证接口的具体路径、请求/响应 Schema、限流阈值与 Schema 隔离实现:见 `TECH_STACK/登录管理技术方案.md` §5.3.1 与 §三。
---
### 5.2 登录界面设计规范
### 6.2 登录设计规范
#### 5.2.1 面布局
#### 6.2.1 面布局
登录界面顶部以 **Tab 切换**区分两种登录方式「密码登录」默认选中Tab 下方的表单区随当前选中 Tab 动态切换微信扫码作为独立的「其他登录」保持禁用。
登录顶部以 **Tab 切换** 区分两种登录方式「密码登录」默认选中Tab 下方的表单区随当前选中 Tab 动态切换微信扫码作为独立的「其他登录」保持禁用。
```
┌─────────────────────────────────────────┐
@@ -382,80 +398,83 @@ Response 200 (失败):
└─────────────────────────────────────────┘
```
#### 5.2.2 安全机制
#### 6.2.2 安全机制(业务视角)
| 机制 | 规格 |
| 机制 | 业务规则 |
|------|------|
| 验证码类型 | **滑块拼图行为验证码**:展示带缺口的背景图 + 可拖动的拼图碎片,用户动碎片至缺口完成验证,无需输入字符 |
| 验证逻辑 | 服务端综合校验**位置偏差**(缺口中心 ±5px 容差)+ **滑动轨迹特征**(加速/减速曲线、总耗时),双重判断是否为人类行为 |
| 背景图来源 | 预置图库随机抽取,缺口位置每次服务端随机生成,防止固定模式被预测 |
| 验证码有效期 | 单次验证会话有效,提交登录后服务端 Token 立即失效;超过 3 分钟未操作需重新加载 |
| 验证失败处理 | 拼图区域抖动动画提示,自动刷新新背景图;**不计入账号密码错误次数**(行为验证失败属独立事件) |
| 密码错误锁定 | 同一账号连续密码错误 ≥ 5 次,锁定 30 分钟;解锁方式:等待超时自动解锁 或 管理员手动解锁 |
| 密码错误计数 | 计数存于 RedisKey 格式:`login_fail:tenant_id:phone`phone 即用户名/手机号TTL 30 分钟 |
| 验证码刷新 | 登录失败(用户名/密码错误)后自动刷新拼图;用户亦可主动点击「刷新」图标重新加载背景图 |
| HTTPS | 所有登录相关请求强制 HTTPS不允许 HTTP 降级 |
| 密码传输 | 前端不做密码加密HTTPS 层保证传输安全;后端存储使用 `django.contrib.auth` 默认的 `PBKDF2+SHA256` 哈希 |
| Session 有效期 | 默认 8 小时(工作日单日使用场景);可由租户管理员在「系统设置」中调整 |
| 验证码类型 | 滑块拼图行为验证码用户动碎片至缺口完成验证,无需输入字符 |
| 验证判定 | 综合位置精度与滑动轨迹特征判断是否为人类行为;具体容差与算法见 Tech 文档 |
| 背景图来源 | 预置图库随机抽取,缺口位置每次随机生成,防止固定模式被预测 |
| 验证失败处理 | 拼图区域抖动提示,自动刷新新背景图;**不计入账号密码错误次数** |
| 密码错误锁定 | 同一账号连续密码错误达到阈值后自动锁定,到期自动解锁;管理员可手动解锁 |
| 验证码刷新 | 登录失败后自动刷新拼图;用户亦可主动点击「刷新」图标 |
| 传输安全 | 所有登录相关请求强制 HTTPS不允许 HTTP 降级 |
| 会话有效期 | 默认按工作日单日使用场景设定;可由 Tenant Admin 在「系统设置」中调整 |
> 滑块容差像素值、密码错误次数阈值、锁定时长、Session 默认时长、密码哈希算法、登录失败计数存储位置等实现口径,统一由 `TECH_STACK/登录管理技术方案.md` §六、§七、§八 定义;本 PRD 不重复。
---
### 5.3 账号与员工实名绑定规范
### 6.3 账号与员工实名绑定规范
#### 5.3.1 绑定原则
#### 6.3.1 绑定原则
- 每个系统登录账号必须与「组织人事管理」模块中的一条**员工档案Staff**绑定
- 账号与员工是 **1:1 关系**,一个员工对应一个账号,一个账号只能绑定一个员工
- **不支持用户自行注册**,所有账号均由有权限的管理角色创建
#### 5.3.2 账号创建权限分层
#### 6.3.2 账号创建权限分层
系统内共有两类账号创建场景,权限和规则各不相同:
** Tenant Admin 账号(每个租户的超级管理账号)**
** Tenant Admin 账号(每个租户的超级管理账号)**
| 项目 | 规格 |
| 项目 | 业务规则 |
| ---- | ------------------------------------------------------------ |
| 创建时机 | 平台运营在系统管理后台开通租户时,系统**自动**以该租户联系人手机号创建 Tenant Admin 账号,无需手动设置 |
| 用户名 | **固定为该租户联系人的手机号**11 位数字),全局唯一,创建后不可更改 |
| 初始密码 | **系统统一固定初始密码**(与普通员工相同,由平台在部署配置中设定,如 `Fonrey@2025` |
| 首次登录 | 强制修改初始密码,不可跳过 |
| 权限范围 | 拥有该租户内最高权限,可管理员工账号、角色、系统设置等 |
| 数量限制 | 每个租户仅限 1 个 Tenant Admin 账号(后续可扩展为多管理员v2 规划) |
| 数据来源 | 联系人手机号来自 `public.tenants.contact_phone` 字段,开通租户时由平台运营录入,必填 |
| 创建时机 | 平台运营在平台管理后台开通租户时,系统**自动**以该租户联系人手机号创建 Tenant Admin 账号,无需手动设置 |
| 用户名 | **固定为该租户联系人的手机号**11 位数字),全局唯一,创建后不可更改 |
| 初始密码 | 系统统一固定初始密码(由平台运营在系统配置中设定),与普通员工相同 |
| 首次登录 | 强制修改初始密码,不可跳过 |
| 权限范围 | 拥有该租户内最高权限,可管理员工账号、角色、系统设置等 |
| 数量限制 | 每个租户仅限 1 个 Tenant Admin 账号(v2 可扩展为多管理员 |
| 数据来源 | 联系人手机号开通租户时由平台运营录入,必填 |
** 普通员工账号(经纪人、店长、行政等)**
** 普通员工账号(经纪人、店长、行政等)**
| 项目 | 规格 |
| 项目 | 业务规则 |
|------|------|
| 创建时机 | Tenant Admin 在「组织人事管理 → 新增员工」时,系统自动为该员工创建登录账号 |
| 用户名 | **固定为该员工的手机号**11 位数字),同租户内唯一,创建后不可更改 |
| 初始密码 | **系统统一固定初始密码**(由平台在部署配置中设定,如 `Fonrey@2025`),所有新员工账号均使用同一初始密码 |
| 首次登录 | 强制修改初始密码,**不可跳过**(详见 5.3.4 |
| 密码重置 | Tenant Admin 可在员工管理界面对任意员工账号执行「重置密码」,重置后恢复为固定初始密码,触发首次登录强制改流程 |
| 初始密码 | 系统统一固定初始密码(与 Tenant Admin 共用同一配置) |
| 首次登录 | 强制修改初始密码,**不可跳过**(详见 §6.4 |
| 密码重置 | Tenant Admin 可在员工管理界面对任意员工账号执行「重置密码」,重置后恢复为固定初始密码,触发首次登录强制改流程 |
| 账号禁用 | 员工离职或被停用时,对应账号自动禁用;禁用账号无法登录,历史操作记录保留 |
#### 5.3.3 账号字段规范
#### 6.3.3 账号字段语义(业务视角)
| 字段 | 类型 | Tenant Admin | 普通员工账号 | 说明 |
|------|------|-------------|-------------|------|
| 用户名username | CharField(30) | **固定为联系人手机号**11 位数字) | **固定为员工手机号**11 位数字) | 登录 ID创建后不可更改;两类账号规则统一 |
| 密码password | CharField | **系统统一固定初始密码** | **系统统一固定初始密码** | PBKDF2+SHA256 哈希存储;首次登录强制修改 |
| 手机号phone | CharField(11) | **必填,同时作为用户名**,来源于 `public.tenants.contact_phone` | **必填,同时作为用户名**,加密存储,同租户内唯一 | 两类账号均用手机号登录v2 启用手机验证码后复用此字段 |
| 邮箱email | EmailField | 选填,同租户唯一 | 选填,同租户唯一 | 在本系统无必须业务用途,完全可选;普通员工忘记密码通过手机短信验证码自助找回,**与邮箱无关** |
| 员工档案关联staff_id | OneToOneField → `org.Staff` | 可选关联(平台运营账号) | 必须关联 | 实名绑定 |
| 账号状态status | CharField | `active` / `disabled` / `locked` | `active` / `disabled` / `locked` | locked 为密码错误锁定,30 分钟自动恢复 |
| 初始密码标记is_initial_password | BooleanField | True首次登录前 | True首次登录前 | True 时登录成功后强制跳转改密页 |
| 创建人created_by | ForeignKey → self | 平台运营(系统管理后台) | Tenant Admin | 审计追溯 |
| 字段 | Tenant Admin | 普通员工 | 业务说明 |
|------|------|------|------|
| 用户名 | 联系人手机号 | 员工手机号 | 登录 ID创建后不可更改 |
| 密码 | 系统统一初始密码 | 系统统一初始密码 | 必须安全哈希存储;首次登录强制修改 |
| 手机号 | 必填,同时作为用户名 | 必填,同时作为用户名 | 用于密码登录与短信验证码登录;存储须加密 |
| 邮箱 | 选填 | 选填 | 在本系统无必须业务用途,完全可选 |
| 员工档案关联 | 可选关联 | 必须关联 | 实名绑定 |
| 账号状态 | active / disabled / locked | active / disabled / locked | locked 为密码错误锁定,到期自动恢复 |
| 初始密码标记 | 是 / 否 | 是 / 否 | 为"是"时登录成功后强制跳转改密页 |
| 创建人 | 平台运营 | Tenant Admin | 审计追溯 |
#### 5.3.4 首次登录强制修改密码
> 字段类型、字段名、表名、索引、跨 App 依赖、Migration 顺序等实现口径,全部以 `DATA_MODEL/DATA_MODEL_LOGIN.md` 为唯一权威。
- 新员工账号创建后,`is_initial_password = True`,账号处于「初始密码」状态
- 员工使用手机号(用户名)+ 固定初始密码登录成功后,系统**立即跳转**至「修改初始密码」强制页面,**不可关闭、不可跳过**,任何其他系统功能页面均不可访问
- Tenant Admin 对员工账号执行「重置密码」后,`is_initial_password` 重置为 True该员工下次登录时再次触发强制修改流程
- 修改成功后,`is_initial_password` 更新为 False原 Session 保持有效,直接进入系统首页
---
**强制修改密码页面规范**
### 6.4 首次登录强制修改密码
- 新员工账号创建后,账号处于「初始密码」状态
- 员工使用手机号 + 固定初始密码登录成功后,系统**立即跳转**至「首次登录强制改密页」,**不可关闭、不可跳过**,任何其他系统功能页面均不可访问
- Tenant Admin 对员工账号执行「重置密码」后,账号重新进入「初始密码」状态,该员工下次登录时再次触发强制改密流程
- 修改成功后,账号脱离「初始密码」状态,原 Session 保持有效,直接进入系统首页
**首次登录强制改密页规范**
| 元素 | 规格 |
|------|------|
@@ -467,13 +486,13 @@ Response 200 (失败):
| 提交按钮 | 「确认并进入系统」 |
| 不可操作项 | 无「跳过」按钮;顶部导航栏、侧边菜单、关闭按钮均禁用 |
> 历史密码校验范围、密码强度算法等实现细节见 Tech 文档与 `DATA_MODEL_LOGIN.md` 中历史密码记录表设计。
---
### 5.4 找回密码详细说明
### 6.5 找回密码业务流程
> **说明**Story 4「找回用户名」已废弃。普通员工用户名固定为手机号无需找回Tenant Admin 如忘记用户名请联系平台运营线下处理
#### 5.4.1 找回密码流程
> 实现细节(一次性凭证 TTL、OTP 有效期、错误次数阈值、限流计数策略)见 `TECH_STACK/登录管理技术方案.md` §6.3 与 §七
```
用户点击「忘记密码」
@@ -482,143 +501,127 @@ Response 200 (失败):
├─ 输入 11 位手机号,点击「获取验证码」
│ │
服务端校验手机号是否存在且状态为 active
前端文案统一为「如该手机号已注册,验证码将在 1 分钟内发送」(防止枚举)
│ │
统一响应「如该手机号已注册,验证码将在 1 分钟内发送」(防止枚举)
│ │
│ 后台:存在且 active → 生成 6 位 OTP有效期 10 分钟,存入 sms_otp_records → 发送短信
后台:手机号存在且账号可用 → 生成 OTP → 发送短信
│ 不存在或已停用 → 静默处理
步骤2输入短信验证码
├─ 输入 6 位验证码,点击「下一步」
│ │
服务端校验 OTP
正确且未过期 → 颁发一次性 sms_reset_token有效期 15 分钟)→ 进入步骤3
│ │
│ 错误(累计 < 5 次)→ 提示「验证码有误,请重新输入」
│ 错误(累计 ≥ 5 次)→ 提示「验证已失败,请重新获取验证码」,本次 OTP 作废
正确且未过期 → 颁发一次性凭证 → 进入步骤3
错误(次数未到上限)→ 提示「验证码有误,请重新输入」
错误(达到上限)→ 提示「验证已失败,请重新获取验证码」,本次 OTP 作废
│ 已过期 → 提示「验证码已过期,请重新获取」
步骤3重置密码
├─ 页面携带 sms_reset_token服务端校验有效性
├─ 服务端校验一次性凭证有效性
│ │
│ 无效/过期 → 提示「操作已超时请重新发起找回密码」跳回步骤1
├─ 用户输入新密码 + 确认新密码,实时逐条校验复杂度规则(✓/✗)
└─ 提交成功
→ 更新密码,is_initial_password = False
→ 更新密码,账号脱离「初始密码」状态
→ 清除该账号所有有效 Session强制重新登录
→ 跳转登录界面,提示「密码已重置,请使用新密码登录」
→ 跳转登录,提示「密码已重置,请使用新密码登录」
```
---
### 5.5 手机验证码登录详细说明
### 6.6 手机验证码登录(业务规则补充)
> 本节为 Story 5 的实现规范补充。短信基础设施(`sms_otp_records` 表、OTP 发送/校验逻辑)在 Story 3 找回密码中已建设完成,本节描述在**登录场景**下复用该基础设施时的关键差异点。
> 本节为 Story 5 的业务规则补充。短信能力在 Story 3 找回密码中已建设完成,本节描述在**登录场景**下复用时的关键业务差异点。
**与找回密码短信逻辑的差异对比**
**与找回密码短信逻辑的业务差异对比**
| 维度 | 找回密码Story 3 | 验证码登录Story 5 |
|------|-------------------|---------------------|
| `scene` 字段 | `password_reset` | `login` |
| OTP 有效期 | 10 分钟 | 5 分钟 |
| 每小时发送上限 | 5 次 | 10 次 |
| 验证成功后动作 | 颁发 `sms_reset_token` → 步骤三重置密码 | 直接颁发 Session Token登录成功 |
| 业务场景 | 密码重置 | 登录认证 |
| OTP 有效期 | 较长 | 较短 |
| 每小时发送上限 | 较低 | 较高 |
| 验证成功后动作 | 颁发一次性凭证 → 步骤三重置密码 | 直接登录成功 |
| 短信文案 | 「密码重置验证码」 | 「登录验证码」 |
| 账号锁定影响 | 不受密码错误锁定限制(非密码登录路径) | **受账号锁定限制**(账号维度安全策略,不区分方式)|
> 各项数值OTP 时长、每小时上限)口径见 `TECH_STACK/登录管理技术方案.md` §6.3、§6.4、§七。
**滑块验证前置规则**(验证码登录特有):
- 用户须先完成滑块拼图验证,「获取验证码」按钮方可点击
- 滑块验证通过后,拼图区域保持「验证通过」状态,不需要在点击「登录」前再次验证
- 切换 Tab 时,滑块验证状态重置(须重新完成验证后方可获取验证码)
**`sms_otp_records`复用说明**
**短信记录复用说明**
- 不新建表,复用 `DATA_MODEL_LOGIN.md` 中定义的 `sms_otp_records`
- `scene` 字段区分场景:`login` / `password_reset`,各自独立限流计数
- 同一手机号同一 scene 同一时间只有一条有效 OTP新发送时将旧记录标记为 `used`
- 不新建表,复用 `DATA_MODEL_LOGIN.md` 中定义的短信验证码记录表
- 通过场景字段区分"登录"与"密码重置",各自独立限流计数
- 同一手机号同一场景同一时间只有一条有效 OTP新发送时将旧记录置为失效
---
### 5.6 后端数据模型设计
### 6.7 后端数据模型
> **数据模型已迁移至独立文档**,请参阅:
> **`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`**
> 数据模型已迁移至独立文档,请参阅:**`DATA_MODEL/DATA_MODEL_LOGIN.md`**
该文档包含:
- `user_accounts` 账号主表完整字段定义、约束、索引、Django Model 代码)
- `login_attempts` 登录审计表
- `sms_otp_records` 短信验证码记录表(找回密码 + 验证码登录共用)
- `password_histories` 历史密码记录表
- 账号主表(完整字段定义、约束、索引)
- 登录审计表
- 短信验证码记录表(找回密码 + 验证码登录共用)
- 历史密码记录表
- Redis 缓存结构说明
- 账号状态机与创建流程
-`org.Staff` 的关联规则及跨 App 依赖设计
- Django Migrations 迁移顺序说明
- Migrations 迁移顺序说明
- 架构决策说明ADR
---
### 5.7 Electron 客户端登录相关约定
### 6.8 Electron 客户端约定
| 约定项 | 规格 |
|--------|------|
| Tenant Code 存储 | `electron-store``app.getPath('userData')` + AES 加密,不存储明文 |
| Session Token 存储 | 内存(`global` 变量)+ `session` CookieChromium 管理),不写入磁盘明文文件 |
| 登录页加载 | 客户端主进程根据 Tenant Code 构建目标 URL`https://{tenant_slug}.fonrey.com/auth/login/`),通过 `BrowserWindow.loadURL()` 加载 |
| 多标签页处理 | 同一 `BrowserWindow` 内,所有页面共享同一 Session Cookie |
| 客户端登出 | 调用服务端 `POST /api/auth/logout/` 使服务端 Session 失效 + 清除 Chromium Session Cookie |
| 窗口关闭时 | Session 保留(不自动登出),下次打开客户端时若 Session 未过期,直接进入系统 |
| 强制更新场景 | 若客户端版本低于服务端 `min_required_version`,则在登录界面前先展示「请更新客户端」提示,阻断登录流程(参见发布管理模块 PRD|
> Electron 客户端在登录链路上的实现约定Tenant Code 加密存储、Session Cookie 策略、登录页加载、多标签页、登出、窗口关闭、强制更新、安全约束)已迁移至 `TECH_STACK/登录管理技术方案.md` §十三 Electron 客户端约定。
本 PRD 仅约束业务行为:
- 客户端必须在登录前完成 Tenant 识别,且 Tenant Code 须**加密**持久化
- 客户端登出后,本地不得保留可直接复用的登录凭证
- 客户端版本低于服务端要求的最低版本时,**必须阻断登录流程**并展示更新提示(联动平台管理后台 PRD 的客户端发布章节)
- 同一客户端窗口内的多标签页应共享同一登录态
---
## 6. 技术注意事项
## 7. 技术注意事项(产品视角)
### 6.1 依赖与技术选型
### 7.1 业务依赖
| 依赖项 | 用途 | 说明 |
|--------|------|------|
| `django.contrib.auth` | 用户认证基础框架 | 扩展 `AbstractBaseUser` 而非直接使用 `User` 模型,以支持 `username` 唯一性约束在租户维度而非全局 |
| `django-tenants` | 多租户隔离 | `UserAccount` 属于租户级 SchemaTenant 验证接口属于 `shared_apps` |
| `Redis` | 滑块验证 Token 存储、登录失败计数、短信 OTP 限流计数 | 验证 Key`captcha_token:{uuid}`TTL 3min登录失败 Key`login_fail:{tenant_id}:{username}`OTP 限流 Key`sms_limit:{scene}:{phone}`TTL 1h|
| 短信服务(待选型) | 发送登录验证码 / 找回密码验证码 | 国内需选用具备短信资质的服务商(如阿里云短信、腾讯云短信);需申请短信签名和模板审核 |
| `Celery` | 异步任务处理 | 短信发送异步处理,防止接口响应超时;原邮件发送需求已废弃,短信为主要通知方式 |
| `django-ratelimit` 或自定义中间件 | 接口限流 | Tenant 验证接口、登录接口、找回密码接口均需限流 |
| `Pillow` | 滑块拼图图片处理 | 生成拼图背景图(抠出缺口区域)及对应的拼图碎片图片,输出为 Base64分别通过两个字段返回给前端 |
- 短信发送依赖具备短信资质的服务商;签名与模板审核须在功能上线前完成
- 短信发送须异步处理,避免阻塞登录链路;密码登录可作为短信故障时的保底入口
- 多租户隔离与跨 Schema 调用细节由 `TECH_STACK/登录管理技术方案.md``DATA_MODEL/DATA_MODEL_LOGIN.md` 定义
### 6.2 多租户下的 `UserAccount` 隔离
- `UserAccount` 表位于**租户 Schema 内**`django-tenants` 租户隔离范围),因此 username 唯一性约束在租户维度生效,不同租户的经纪人可以有相同用户名
- Tenant 验证接口(`/api/auth/tenant/verify/`)位于**公共 Schema**`shared_apps`),使用 `TenantModel` 查询
- 登录、找回密码等接口通过请求域名(`{tenant_slug}.fonrey.com`)切换到对应租户 Schema`django-tenants` 中间件自动处理)
### 6.3 已知风险
### 7.2 已知风险
| 风险 | 可能性 | 影响 | 缓解措施 |
|------|--------|------|---------|
| 滑块验证被机器模拟轨迹绕过 | 低 | 高 | 服务端同时校验位置偏差 + 轨迹曲线特征(非线性运动特征),拒绝匀速/程序化轨迹;后续可引入设备指纹加固 |
| Tenant Code 枚举攻击(暴力试探) | 低 | 中 | Tenant 验证接口限流每IP每分钟≤10次返回结果不区分「未找到」与「已禁用」|
| 密码重置 Token 泄露 | 低 | 高 | `sms_reset_token` 单次有效、15 分钟过期、HTTPS 传输 |
| 短信服务故障导致用户无法找回密码或验证码登录 | 中 | 高 | 短信发送失败写入告警日志;密码登录作为保底方式(非单一入口);建议配置备用短信服务商通道 |
| 多端同时登录同一账号 | 高(日常场景) | 低 | 本期允许,后续如需踢出,可在 Token 机制中引入版本号 |
| 滑块验证被机器模拟轨迹绕过 | 低 | 高 | 服务端综合位置 + 轨迹特征校验;后续可引入设备指纹加固 |
| Tenant Code 枚举攻击(暴力试探) | 低 | 中 | 接口限流 + 统一外显文案,不区分「未找到」与「已禁用」 |
| 密码重置凭证泄露 | 低 | 高 | 一次性、短时效、HTTPS 传输 |
| 短信服务故障 | 中 | 高 | 失败告警 + 密码登录保底 + 建议配置备用短信通道 |
| 多端同时登录同一账号 | 高(日常场景) | 低 | 本期允许,后续如需踢出再做版本号机制 |
### 6.4 开放问题(开发前需确认)
### 7.3 开放问题(开发前需确认)
- [ ] **短信服务商选型**使用阿里云短信 / 腾讯云短信 / 其他服务商?需运维确认并提前申请短信签名模板审核(国内审核周期 13 个工作日— 负责人:后端负责人 — 截止:开发启动前
- [ ] **Session 有效期默认值**8 小时是否满足各租户需求?是否允许租户管理员自行配置?— 负责人:产品经理 — 截止:开发启动前
- [ ] **滑块拼图实现方案**:自研Pillow 生成图片 + 前端拖拽组件)还是集成第三方行为验证服务(如极验 GeeTest / 网易易盾)?自研可控但需维护图库;第三方开箱即用但引入外部依赖,需评估数据合规要求 — 负责人:后端负责人 + 安全 — 截止:开发启动前
- [ ] **账号锁定通知**:账号被锁定后,是否自动发短信通知用户和/或通知管理员(站内消息)?— 负责人:产品经理 — 截止:开发启动前
- [ ] **短信服务商选型**:阿里云 / 腾讯云 / 其他短信签名模板审核需要 13 个工作日 — 负责人:后端负责人 — 截止:开发启动前
- [ ] **Session 默认有效期**8 小时是否满足各租户需求?是否允许 Tenant Admin 自行配置?— 负责人:产品经理 — 截止:开发启动前
- [ ] **滑块拼图实现方案**:自研还是集成第三方行为验证服务?需评估数据合规要求 — 负责人:后端负责人 + 安全 — 截止:开发启动前
- [ ] **账号锁定通知**:账号被锁定后,是否自动发短信通知用户和/或通知管理员?— 负责人:产品经理 — 截止:开发启动前
- [ ] **历史密码校验范围**:最近 3 次是否足够?是否需要额外规则(如不能与用户名相同)?— 负责人:产品经理 — 截止:开发启动前
---
## 7. 发布计划
## 8. 发布计划
| 阶段 | 时间 | 受众 | 准入门槛 |
|------|------|------|---------|
@@ -630,49 +633,50 @@ Response 200 (失败):
---
## 8. 附录
## 9. 附录
### 8.1 登录状态流转图
### 9.1 登录状态流转图
```
[未识别 Tenant]
│ 输入有效 Tenant Code
[未登录]
│ 账密登录成功
│ 账密 / 验证码登录成功
[初始密码状态](如账号为初始密码)
│ 强制改密成功
│ 强制改密成功
[已登录 - Active Session]
│ Session 过期 / 主动登出 / 管理员强制登出
[未登录](跳转登录界面
[未登录](跳转登录
[账号锁定状态]5次错误后)
30 分钟后自动解锁 或 管理员手动解锁
[账号锁定状态]密码连续错误达到阈值后)
锁定时长结束自动解锁 或 管理员手动解锁
[未登录](可重新登录)
```
### 8.2 接口清单汇总
### 9.2 业务能力清单(替代原"接口清单"
| 接口 | 方法 | Schema 位置 | 是否需要鉴权 | 说明 |
|------|------|------------|------------|------|
| `/api/auth/tenant/verify/` | POST | Publicshared | 否 | Tenant Code 验证 |
| `/api/auth/captcha/` | GET | Tenant | 否 | 获取滑块拼图验证码(返回背景图 Base64 + 碎片图 Base64 + 验证 Token |
| `/api/auth/captcha/verify/` | POST | Tenant | 否 | 提交滑动轨迹 + 位置,服务端校验并返回一次性通过凭证(供登录接口使用) |
| `/api/auth/login/` | POST | Tenant | 否 | 手机号 + 密码登录 |
| `/api/auth/login/phone/` | POST | Tenant | 否 | 手机号 + 短信验证码登录MVP 正式功能) |
| `/api/auth/logout/` | POST | Tenant | 是 | 登出,使 Session 失效 |
| `/api/auth/recover/password/request/` | POST | Tenant | 否 | 发起找回密码(发送短信验证码) |
| `/api/auth/recover/password/verify/` | POST | Tenant | 否 | 校验短信验证码,颁发一次性 `sms_reset_token` |
| `/api/auth/recover/password/reset/` | POST | Tenant | 否Token 鉴权) | 提交新密码 |
| `/api/auth/wechat/qrcode/` | GET | Tenant | 否 | **预留 v2**,获取微信二维码 |
| `/api/auth/wechat/callback/` | POST | Tenant | 否 | **预留 v2**,微信扫码回调 |
> 本文件不再列出具体接口路径与 HTTP 方法。完整 API 端点清单含路径、方法、Schema 位置、是否鉴权、预留端点)请查阅 `TECH_STACK/登录管理技术方案.md` §五。
### 8.3 相关文档参考
| 业务能力 | 提供给谁 | 说明 |
|---|---|---|
| Tenant 识别 | 客户端 | 输入 12 位 Tenant Code返回租户品牌与登录入口 |
| 获取/校验滑块验证码 | 登录页 | 行为验证码生成与校验,输出一次性通过凭证 |
| 密码登录 | 已识别租户的用户 | 手机号 + 密码 + 滑块凭证完成认证 |
| 验证码登录 | 已识别租户的用户 | 手机号 + 短信验证码完成认证(需先通过滑块再获取验证码) |
| 登出 | 已登录用户 | 销毁服务端会话与本地登录凭证 |
| 找回密码(三步) | 忘记密码的用户 | 发送 OTP → 校验 OTP → 设置新密码 |
| 首次登录强制改密 | 处于初始密码状态的用户 | 强制流程,提交后脱离初始密码状态 |
| 微信扫码登录v2 预留) | 全量用户 | MVP 仅展示禁用入口v2 实现 |
- 技术栈文档:`Project/fonrey/TECH_STACK/TECH_STACK.md`
- **登录管理数据模型**`Project/fonrey/DATA_MODEL/DATA_MODEL_LOGIN.md`
- **登录管理技术方案**`Project/fonrey/TECH_STACK/登录管理技术方案.md`
### 9.3 相关文档参考
- 技术栈总纲:`TECH_STACK/TECH_STACK.md`
- **登录管理技术方案(实现口径权威)**`TECH_STACK/登录管理技术方案.md`
- **登录管理数据模型(数据口径权威)**`DATA_MODEL/DATA_MODEL_LOGIN.md`
- 全局 API 契约:`TECH_STACK/API_CONTRACT.md`
- ADR`ADR.md`(特别是 `ADR-20260502-003` PRD/Tech 职责边界、`ADR-20260430-004` 登录接口路径同步规则)

View File

@@ -1,670 +0,0 @@
# PRD系统管理模块Admin & System Management
**状态**Draft
**作者**:产品经理
**最后更新**2026-05-01
**版本**v1.3
**利益相关方**:工程负责人、运营团队、安全合规、客户成功团队
## 变更历史
| 版本 | 日期 | 作者 | 变更说明 |
|------|------|------|---------|
| v1.0 | 2026-04-24 | 产品经理 | 初稿 |
| v1.1 | 2026-05-01 | 产品经理 | ① Story 1 补充「默认配置」内容定义(权限定义 + 系统默认角色);② Story 1 补充欢迎通知机制邮件内容规范、默认密码发放、Tenant Code、页面下载 PDF③ Story 2 补充 License 时效管理(到期自动挂起、提前 15 天预警倒计时);④ Story 3 标注为已废弃(暂不实现);⑤ 新增 Story 7平台版本总览基础数据版本 + 租户数据升级版本);⑥ §5.1.1 补充默认配置内容说明、通知机制细节、时效日期字段、到期挂起流程;⑦ 管理员角色权限矩阵补充版本总览权限 |
| v1.3 | 2026-05-01 | 产品经理 | Story 1 Tenant Admin 权限机制修正:初始 Tenant Admin 不赋予任何业务角色,由系统在租户创建时直接写入独立的「租户管理员」专属权限集合;该集合不在角色管理界面显示,不可自行修改或分配;新增/变更租户管理员须由平台运营方操作 |
---
## 1. 问题陈述
### 1.1 背景
Fonrey 是一套面向房产经纪公司的 B2B SaaS 平台,采用 `django-tenants` 实现 PostgreSQL Schema 级别的多租户隔离。随着平台商业化推进运营团队需要一套独立的管理后台Admin Console来管理租户生命周期、系统升级、备份恢复及合规审计。
**本模块解决的核心问题**:平台运营团队当前缺乏统一的工具来:
1. 管理数百家经纪公司(租户)的开通、暂停、注销流程
2. 在不中断服务的前提下对平台进行版本升级与灰度发布
3. 应对数据灾难场景(数据误删、升级失败)时快速恢复
4. 满足合规要求,对所有高危操作留存完整审计轨迹
### 1.2 核心痛点
| 痛点 | 影响方 | 当前代价 |
|------|--------|---------|
| 无统一租户管理界面,开通/暂停操作依赖人工脚本 | 运营团队 | 高错误风险,操作耗时 |
| 版本升级需停机维护,影响所有租户 | 所有用户 | SLA 违约风险 |
| 数据备份无策略,灾难恢复依赖人工 | 平台稳定性 | 数据丢失风险 |
| 高危操作无审计日志,合规风险暴露 | 管理层/合规 | 法律与客户信任风险 |
### 1.3 目标用户
| 角色 | 使用场景 | 频率 |
| --------------------------- | ----------- | ------ |
| Platform Admin平台超级管理员Platform Super Admin | 全局配置、高危操作授权 | 低频(每周) |
| 运维人员Ops Operator | 日常租户管理、监控巡检 | 高频(每日) |
| 只读审计员Read-only Auditor | 日志查询、合规报告导出 | 中频(每周) |
---
## 2. 目标与成功指标
| 目标 | 指标 | 当前基线 | 目标值 | 测量窗口 |
| -------- | ----------- | ----------- | --------------- | -------- |
| 租户管理效率提升 | 新租户开通耗时 | 人工脚本 ~30 分钟 | < 5 分钟(含自动初始化) | 上线后 30 天 |
| 平台升级零停机 | 升级期间受影响租户数 | 全量中断 | 灰度阶段受影响 ≤ 5% 租户 | 每次升级 |
| 数据恢复能力建立 | RTO恢复时间目标 | 无标准流程 | 单租户恢复 < 2 小时 | v1 上线即达标 |
| 操作合规覆盖 | 高危操作审计日志覆盖率 | 0% | 100% | 上线后 30 天 |
| 管理员安全 | MFA 启用率 | 0% | 100%(强制) | 上线即达标 |
---
## 3. 非目标Non-Goals
- **不在 v1 实现**自动化账单计费、多币种支持、Webhook 自定义集成市场
- **不在本模块**:租户内业务权限的细粒度配置(见权限管理模块 PRD
- **不在本模块**客服工单系统、SLA 自动赔付
- **不支持**:移动端浏览器(管理后台仅面向桌面,运营人员使用场景明确)
---
## 4. 用户角色与核心故事
### Persona A运营人员 Lily日常租户管理
> 负责 Fonrey 平台的日常运营,每天需要处理新客户开通、异常租户处理、客户咨询的数据导出请求,使用 PC 浏览器访问管理后台。
**Story 1**:新租户开通
> 作为运营人员,我希望通过填写表单快速完成租户开通,并由系统自动完成数据库初始化与欢迎邮件,无需手动执行脚本。
**验收标准**
- [ ] 表单提交后,系统在后台自动创建 PostgreSQL Schema 并注入默认配置(见 §5.1.1 默认配置说明),完成时间 < 60 秒
- [ ] 默认配置注入包含两部分:**1权限定义PermissionDef**:注入该平台所有权限码的定义数据(`permission_code`、描述、模块归属),作为该租户 RBAC 权限体系的基础;**2系统默认角色与权限绑定**:按「角色权限矩阵.md」定义注入 7 个系统内置业务角色——置业顾问、店管、区管、区总、副总、总经、其他职能——并完成各角色对应的权限集合绑定
- [ ] 初始 Tenant Admin 用户(以联系人手机号创建)**不通过业务角色赋权**,而是在租户创建时由系统直接写入「租户管理员」专属权限集合;该权限集合独立于 7 个业务角色之外,不在租户的角色管理界面中显示,不可由 Tenant Admin 自行修改或分配给其他用户;如需新增或变更租户管理员,须由平台运营方在管理后台操作
- [ ] 新租户创建后,平台运营管理员收到系统内通知(站内消息);租户联系人收到欢迎邮件(见下方「欢迎邮件规范」)
- [ ] 若联系人无邮箱,运营人员可在租户详情页下载「入驻信息 PDF 文档」,通过微信等渠道手动转发
- [ ] 租户访问地址采用 Fonrey 平台统一域名 + Tenant Code 参数的形式(如 `https://app.fonrey.com/?tenant=ABCD`),无需为每个租户单独创建子域名;租户详情页展示该访问链接,可一键复制
- [ ] 创建失败时回滚所有已创建资源,并显示明确的错误原因
**欢迎邮件规范**(联系人有邮箱时自动发送):
| 字段 | 内容 |
| ---- | ----------------------------------------------------------------------------------------------------------- |
| 主题 | 【房睿平台】您的账号已开通,欢迎登录 |
| 收件人 | 租户联系人邮箱 |
| 正文内容 | 公司名称、**Tenant Code**(登录时所需的租户识别码)、登录地址(`https://app.fonrey.com/?tenant={Tenant Code}`、Tenant Admin 手机号(脱敏展示后三位)、**系统初始密码**(明文,首次登录后强制修改)、平台客服联系方式 |
| 备注 | 初始密码由系统随机生成12 位,含大小写字母+数字),发送后立即标记为「首次登录强制修改」状态 |
**入驻信息 PDF 文档**(可在租户详情页下载,适用于无邮箱客户):
- 包含与欢迎邮件相同的所有关键信息
- 页面入口:租户详情 → 基本信息 Tab → 「下载入驻信息」按钮
- 文件名格式:`{公司名称}_入驻信息_{日期}.pdf`
**Story 2**:挂起问题租户
> 作为运营人员,我希望能快速冻结欠费租户的访问,同时保证数据不丢失,并在欠费解决后一键恢复。
**验收标准**
- [ ] 挂起操作执行后,该租户所有用户登录跳转至"账号已暂停"提示页,管理后台数据访问不受影响
- [ ] 支持设置到期时间,到期后系统自动恢复租户状态,并发送通知邮件
- [ ] 所有挂起/恢复操作记录于操作审计日志,包含操作人、时间、原因
**Story 2b**License 时效管理与到期自动挂起
> 作为 Platform Admin我希望每个租户能够设置 License 有效期,在到期后系统自动挂起租户,并在到期前提前预警,减少人工干预。
**验收标准**
- [ ] 每个租户的「基本信息」中包含 **License 到期日期**字段,由 Platform Admin 在创建租户或套餐续费时设定(如购买 1 年期 License
- [ ] 系统Celery Beat 每日检查)在 License 到期时自动将租户状态切换为「已挂起 Suspended」挂起原因标注为「License 到期」
- [ ] 自动挂起后租户联系人收到通知邮件Platform Admin 可手动解除挂起(续费后操作)
- [ ] **提前 15 天预警倒计时**:租户内的 Tenant Admin 在登录后的管理界面顶部看到醒目横幅提示,内容示例:「您的 License 将于 X 天后到期({到期日期}),请联系平台续费。」;提前 15 天开始出现,每天展示直至到期或续费完成
- [ ] 倒计时横幅仅对租户端 Tenant Admin 可见,不影响普通 Agent 用户体验(可配置)
- [ ] License 到期日期在管理后台租户列表和详情页均可见支持按「即将到期15 天内)」筛选
**Story 3**:响应客户数据导出请求
> **状态**:暂缓(当前 P0 计划阶段不实现,后续迭代规划)。本 Story 保留需求描述,待进入相应排期后正式细化。
---
### Persona BPlatform Admin平台超级管理员 David系统升级与回滚
> 负责平台技术运维,周期性执行版本升级,关注升级稳定性与租户影响面,有权执行所有高危操作。
**Story 4**:灰度系统升级
> 作为Platform Admin平台超级管理员我希望先对内测租户升级新版本验证稳定后再全量推送避免一次性影响所有客户。
**验收标准**
- [ ] 升级前自动执行健康检查,存在异常服务时阻断升级并提示
- [ ] 支持指定目标租户进行灰度升级,灰度租户名单可编辑
- [ ] 升级过程实时展示进度(每个租户的升级状态),支持查看升级日志
- [ ] 升级失败时系统自动告警,并提供一键回滚入口
**Story 5**:升级失败回滚
> 作为Platform Admin平台超级管理员我希望在升级出现问题时能立即回滚至上一稳定版本并生成事件报告。
**验收标准**
- [ ] 回滚操作触发前自动保存当前状态快照
- [ ] 支持全量回滚或单租户回滚
- [ ] 回滚完成后生成事件报告:失败原因、回滚耗时、影响范围
- [ ] 回滚操作需二次身份验证确认
---
### Persona C只读审计员 Carol合规审计
> 负责平台合规审查,定期导出操作日志供法务或客户审查,无任何写权限。
**Story 6**:审计日志查询与导出
> 作为审计员,我希望能按操作人、时间范围、操作类型筛选操作日志,并导出为报告格式。
**验收标准**
- [ ] 日志列表支持多维度筛选:操作人、时间范围、操作对象、操作类型(创建/修改/删除/高危操作)
- [ ] 日志条目包含:操作人、操作时间、操作对象(租户/用户ID、操作内容摘要、操作结果成功/失败)、操作来源 IP
- [ ] 支持导出筛选结果为 CSV 格式
---
### Persona DPlatform Admin平台超级管理员David平台版本总览
**Story 7**:查看平台与租户版本总览
> 作为 Platform Admin我希望能在管理界面一眼看到整个平台的版本情况包括基础数据版本和每个租户各自的数据升级版本以便掌握升级进度和版本差异。
**验收标准**
- [ ] 管理控制台「系统版本管理」页面提供「版本总览」视图,分为两个部分:
**Part 1平台基础数据版本**
- 展示当前平台基础数据(公共 Schema 中的 PermissionDef、系统配置等 seed 数据)的版本号
- 该版本对所有租户一致,每次平台升级为一次性全量升级
- 展示字段:版本号、最后升级时间、升级描述、升级执行人
**Part 2租户数据升级版本**
- 以列表形式展示每个租户当前的数据版本号(即该租户 Schema 已完成的 migration 版本)
- 由于采用灰度升级,各租户版本号可能不一致
- 列表字段:租户名称、当前数据版本号、上次升级时间、升级状态(最新 / 待升级 / 升级中 / 升级失败)
- 支持按「待升级」「升级失败」筛选,快速定位异常租户
- 点击租户行可跳转至该租户详情的「备份记录」Tab 查看详情
- [ ] 页面支持手动刷新版本状态
- [ ] 「版本总览」数据展示无需实时,允许最多 5 分钟缓存延迟
---
**Story 8**:查看与控制租户用户数
> 作为 Platform Admin我希望能在管理界面看到每个租户当前的用户数量并能设置用户数上限以便根据 License 授权进行管控(用户数是 License 计费的重要依据)。
**验收标准**
- [ ] 租户列表新增「当前用户数」列,显示该租户当前已创建的有效用户总数(含 Tenant Admin + 所有 Agent不含已删除/离职用户)
- [ ] 租户详情页「基本信息」Tab 显示:当前用户数 / License 授权用户数上限12 / 50
- [ ] Platform Admin 可在租户详情页设置「License 授权用户数上限」字段当租户实际用户数达到上限时Tenant Admin 在该租户内将无法继续创建新用户,并收到提示:「当前用户数已达 License 上限,请联系平台扩容」
- [ ] Platform Admin 可随时调整用户数上限(续费扩容 or 缩容),变更记录写入操作审计日志
- [ ] 租户列表支持按「用户数已满(≥ 上限)」筛选,便于平台运营主动识别需要续费的租户
---
### 5.1 租户管理Tenant Management
#### 5.1.1 租户生命周期
**新建租户**
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 公司名称 | Text | ✅ | 最大 100 字符 |
| 联系人 | Text | ✅ | 租户主联系人姓名 |
| 联系邮箱 | Email | ❌(可选) | 用于发送欢迎邮件及系统通知;无邮箱时改用 PDF 下载方式 |
| 所在地区 | Select | ✅ | 省市两级 |
| 订阅套餐 | Select | ✅ | Basic / Professional / Enterprise |
| 子域名 | Text | ❌(无需) | 租户访问统一使用 Fonrey 平台域名 + Tenant Code 参数,无子域名 |
| License 到期日期 | Date | ✅ | 由 Platform Admin 设定,决定租户有效期;到期后系统自动挂起 |
创建流程:
1. 表单校验通过后,后台 Celery 任务执行:
- 创建 PostgreSQL Schema`tenant_{id}`
- 执行 Migrate 初始化表结构
- 注入默认配置(见下方「默认配置内容」说明)
- 生成初始密码12 位随机,含大小写字母+数字),标记为「首次登录强制修改」
- 若联系邮箱已填写:发送欢迎邮件(含 Tenant Code、初始密码、平台访问链接 `https://app.fonrey.com/?tenant={TenantCode}`
2. 任务完成后更新租户状态为 `active`,失败则全量回滚并标记为 `failed`
3. 生成唯一 Tenant IDUUID及 Tenant Code可读短码`FR-2024-0001`),记录创建时间、创建人
**默认配置内容Schema 初始化时注入)**
| 配置类型 | 内容 | 说明 |
|---------|------|------|
| 权限定义PermissionDef | 平台全量权限码(`permission_code`、描述、模块归属) | 从 public schema 同步至租户 schema作为 RBAC 权限体系基础 |
| 系统默认角色 | Tenant Admin租户管理员、Agent经纪人 | 按权限管理模块 PRD 定义的权限集合完成角色-权限绑定 |
| 初始 Tenant Admin 用户 | 以联系人手机号创建,角色为 Tenant Admin | 首次登录后强制修改密码 |
**挂起Suspend**
- 操作触发条件:
- **手动触发**:运营人员手动触发,选择挂起原因(欠费 / 违规 / 主动申请 / 其他)
- **自动触发**Celery Beat 每日检查 License 到期日期到期后自动挂起原因标注为「License 到期」)
- 可设置挂起到期时间(留空表示永久挂起直至手动恢复)
- 挂起效果:该租户所有用户请求返回 `HTTP 403`,重定向至暂停提示页;管理后台数据仍可访问
- 到期自动恢复:`Celery Beat` 定时检查到期挂起记录,自动切换状态为 `active`仅适用于手动设置了到期时间的挂起License 到期自动挂起需 Platform Admin 手动恢复)
- 通知:挂起与恢复均向租户联系邮箱发送通知邮件(无邮箱则跳过)
- **License 到期预警**License 到期前 15 天起,租户端 Tenant Admin 管理界面顶部显示倒计时横幅,直至到期或续费后由 Platform Admin 更新 License 到期日期
**删除Delete**
| 模式 | 说明 |
|------|------|
| 软删除Soft Delete | 标记删除状态,数据保留 30 天(默认,可配置)后由 Celery 定时任务清除 |
| 硬删除Hard Delete | 立即清除所有数据、Schema、存储资源及子域名授权仅Platform Admin平台超级管理员可操作 |
删除前置条件:
1. 操作人必须确认数据导出已完成(勾选确认框)
2. 硬删除需二次身份验证MFA 确认)
3. 软删除冷静期内(默认 30 天),可在租户列表中对已删除租户执行"撤销删除"
删除完成后释放子域名、Cloudflare R2 存储桶、License 席位
#### 5.1.2 数据管理
**数据导出**
- 触发方式:管理员手动触发,选择目标租户 + 导出模块 + 格式
- 异步执行Celery 任务任务状态实时刷新Pending → In Progress → Done / Failed
- 导出包内容结构化数据CSV / JSON / SQL Dump+ 文件资产 URL 清单,**不打包文件实体**
- 导出模块选项:客户数据 / 房源数据 / 交易记录 / 系统配置 / 全量
- 导出包存储:压缩后存于 Cloudflare R2 临时目录,生成带签名下载链接,有效期 24 小时
**文件资产(图片/附件)的导出处理规则**
> **设计决策**v1 不打包文件实体,文件以 CDN 持久 URL 形式内嵌于导出数据中。
> 依据R2 Bucket 配置为 public read文件通过 Cloudflare CDN 对外提供持久访问;
> 在租户账号未被硬删除的情况下CDN URL 始终有效,满足合规/审计场景需求。
> 迁移场景(需要文件实体)走"完整备份"流程,不走"数据导出"流程。
各导出格式的文件字段表达方式:
| 导出格式 | 图片字段示例 | 附件字段示例 |
|---------|------------|------------|
| CSV | `photos` 列:多个 CDN URL 以英文分号分隔 | `attachments` 列:`文件名\|CDN URL` 以分号分隔 |
| JSON | `"photos": [{"url": "https://cdn.../xxx.jpg", "filename": "封面.jpg", "created_at": "..."}]` | `"attachments": [{"url": "...", "filename": "合同.pdf"}]` |
| SQL Dump | 文件元数据表原样导出,`file_url` 字段为 CDN URL | 同左 |
导出包内附说明文件(`README.txt`),注明:
> "图片与附件以 Cloudflare CDN 链接形式提供,链接在账号有效期内持续可访问。账号注销后链接将在 30 天冷静期结束时失效。如需迁移文件本体请联系平台支持发起完整数据备份Backup。"
**数据导出 vs 完整备份的边界**
| 维度 | 数据导出Export | 完整备份Backup |
|------|------------------|-----------------|
| 用途 | 合规审计、数据核查、业务分析 | 灾难恢复、租户迁移 |
| 文件资产 | CDN URL 清单,不含文件实体 | 含 R2 文件实体(完整同步) |
| 完成时间 | 分钟级 | 小时级(取决于文件总量) |
| 触发方式 | 运营人员手动触发 | 手动触发 / 系统自动触发(升级前) |
| 存储成本 | 极低(仅压缩包) | 较高(完整文件副本) |
**数据备份Snapshot**
- 自动触发:升级前系统自动触发该租户全量备份
- 手动触发:管理员可在租户详情页手动发起备份
- 备份内容:数据库 Schemapg_dump+ Cloudflare R2 文件存储(附件、图片)
- 备份记录展示字段:备份时间、触发方式(自动/手动)、备份大小、状态(进行中/成功/失败)
- 保留策略:默认保留最近 10 个版本,可在系统全局配置中调整
- 存储:加密存储,支持目标存储配置(本地 / S3 / Cloudflare R2 / GCS
**数据恢复Restore**
恢复流程:
```
选择目标备份版本
→ 二次确认弹窗(显示将覆盖的当前数据版本信息)
→ 自动对当前数据生成临时快照(防止恢复失误)
→ 租户切换为维护模式(用户访问显示"维护中"提示)
→ 执行数据恢复Celery 任务)
→ 恢复完成 → 自动恢复服务 → 生成恢复操作报告
```
恢复操作报告包含:操作人、操作时间、恢复前数据版本、恢复后数据版本、耗时、结果
#### 5.1.3 套餐与升级管理
**Plan 升级**
- 支持升级路径Basic → Professional → Enterprise
- 升级前展示差异对比表功能项、用户数上限、存储空间、API 调用额度)
- 生效模式:立即生效 / 按账期生效(下一个账期开始时生效)
- 升级前自动触发数据备份
- 升级失败:提供一键回滚至备份版本
- 升级历史记录:时间、操作人、升级前套餐、升级后套餐
#### 5.1.4 用户与权限管理
**Tenant Admin 管理**
- 每个租户可设置 1 至多名 Tenant Admin超级用户
- Platform Admin平台超级管理员可直接在后台创建新用户并赋予 Tenant Admin 角色,或从租户现有用户中指定
- 支持查看当前 Tenant Admin 列表,执行:新增 / 替换 / 撤销权限
**Tenant Admin 权限配置RBAC**
可配置权限项:
| 权限项 | 说明 |
|--------|------|
| 创建/删除子用户 | 是否允许 Tenant Admin 管理租户内部用户 |
| 修改系统配置 | 是否允许修改租户级系统设置(字段标签、规则等) |
| 查看账单与套餐 | 是否允许查看订阅信息和费用详情 |
| 数据导出 | 是否允许在租户端触发数据导出 |
权限基于 RBAC 模型,支持自定义角色(角色名称 + 权限集合),可在多 Tenant Admin 间复用。
**密码重置**
- Platform Admin平台超级管理员可为任意租户的任意用户发起密码重置
- 方式一:发送重置链接至注册邮箱(用户自助重置)
- 方式二:管理员直接设置临时密码(用户首次登录后强制修改)
- 所有重置操作记录于操作审计日志
#### 5.1.5 租户监控与统计
**资源监控**
实时展示指标(基于 Grafana + 自定义数据采集):
| 指标 | 展示维度 |
|------|---------|
| CPU / 内存占用 | 实时折线图 |
| 存储用量 | 当前值 vs 套餐上限 |
| API 调用次数 | 当日 / 本月累计 |
| 活跃用户数 | 当日活跃数 |
| 当日登录次数 | 累计折线图 |
| 异常请求数 | 4xx / 5xx 分类 |
| 慢查询数量 | > 500ms 查询次数 |
告警配置:支持为每个关键指标设置阈值,超限时触发邮件 / Webhook 通知。
**可用性统计Availability / SLA**
- 服务可用率Uptime统计支持日 / 周 / 月维度
- 故障事件记录:开始时间、恢复时间、持续时长、影响描述
- SLA 达标率报告:可导出供客户成功团队使用
---
### 5.2 系统管理System Management
#### 5.2.1 版本升级与回滚
**系统升级流程**
```
上传/拉取升级包(制品库 Artifact Registry
→ 系统自动健康检查(所有服务状态正常才允许继续)
→ 配置升级策略:全量 / 灰度(指定内测租户列表)
→ 升级前自动备份(对所有参与本次升级的租户)
→ 执行升级
→ 实时展示升级进度(租户维度状态列表)
→ 升级完成通知(成功/失败详情)
```
**灰度升级策略**
- 维护"内测租户组"列表由Platform Admin平台超级管理员配置
- 灰度阶段仅对内测租户执行升级,其余租户保持原版本
- 内测租户验证通过(手动确认)后,触发全量升级
**升级回滚**
- 触发条件:手动触发(管理员判断)或自动触发(监控检测到错误率超阈值)
- 回滚范围:全量回滚(所有租户)/ 单租户回滚
- 回滚前:自动保存当前状态快照
- 回滚后:生成事件报告(失败原因、回滚耗时、受影响租户列表)
- 执行回滚需二次身份验证
#### 5.2.2 定时备份策略
**全局备份计划**
| 配置项 | 选项 |
|--------|------|
| 备份频率 | 每小时 / 每日 / 每周 |
| 执行时间 | 可配置时间窗口(默认每日 02:00 |
| 保留数量 | 最近 N 个版本(默认 10 |
| 存储目标 | 本地 / AWS S3 / Cloudflare R2 / GCS |
- 支持为单个租户配置独立备份计划,覆盖全局策略
- 备份任务执行记录:开始时间、完成时间、备份大小、状态
- 备份失败:自动告警 + 支持手动重试
---
### 5.3 管理控制台Admin Console
#### 5.3.1 核心页面规格
**仪表盘Dashboard**
| 模块 | 展示内容 |
|------|---------|
| 全局概览 | 总租户数、活跃租户数、本月新增租户数 |
| 系统健康 | 各核心服务状态Django / PostgreSQL / Redis / Celery / R2 |
| 近期告警 | 最近 24 小时告警列表,按严重程度分类 |
| 资源概览 | 平台整体存储用量、API 调用量趋势图 |
| 最近操作 | 最近 10 条高危操作审计记录 |
**租户列表**
- 分页展示(默认 20 条/页)
- 搜索:按公司名称、子域名、联系邮箱关键词搜索
- 筛选按状态Active / Suspended / Deleted、套餐Basic/Pro/Enterprise、注册时间范围、**即将到期15 天内)**
- 列表字段:公司名称、子域名、套餐、状态、注册时间、活跃用户数
- 快捷操作:查看详情、挂起、发起备份、数据导出
**租户详情**
标签页结构:
| 标签 | 内容 |
|------|------|
| 基本信息 | 公司信息、联系人、子域名、套餐、状态,支持编辑部分字段 |
| 用户管理 | Tenant Admin 列表、普通用户列表、密码重置入口 |
| 套餐信息 | 当前套餐详情、用量统计、升级入口 |
| 监控数据 | 该租户资源使用图表、SLA 统计 |
| 备份记录 | 该租户备份列表、手动触发备份、恢复操作入口 |
| 操作历史 | 该租户相关的所有管理员操作日志 |
**系统版本管理**
- 当前运行版本信息
- 历史版本列表版本号、发布时间、状态Current / Previous / Archived
- 升级入口(上传/拉取升级包)
- 回滚入口(选择目标版本)
**备份管理**
- 全局备份计划配置
- 备份任务列表(支持按租户、状态、时间筛选)
- 手动触发备份(选择租户)
- 恢复操作入口
**监控与告警**
- 租户级 / 系统级监控图表(基于 Grafana iframe 嵌入或自定义实现)
- 告警规则配置(指标 + 阈值 + 通知渠道)
- 告警历史列表
**审计日志**
- 全平台操作日志,支持多维度筛选与导出
- 每条日志包含:操作人、时间、操作对象、内容摘要、结果、来源 IP
**管理员设置**
- 管理员账号管理(创建、编辑、停用)
- 角色配置Platform Admin平台超级管理员 / 运营人员 / 只读审计员)
- MFA 设置(强制启用,支持 TOTP
- IP 白名单配置
- 登录会话管理(查看活跃会话、强制登出)
#### 5.3.2 访问控制与安全
**强制要求(不可降级)**
| 安全要求 | 实现方式 |
|---------|---------|
| MFA 强制启用 | 所有管理员账号首次登录强制配置 TOTP无法跳过 |
| IP 白名单 | 仅允许指定 IP 范围访问管理控制台 URLNginx 层或应用层限制) |
| 高危操作二次验证 | 删除租户、数据恢复、系统回滚操作触发 MFA 二次确认弹窗 |
| 会话超时 | 无操作 30 分钟后自动登出Token 失效 |
| 强制登出 | Platform Admin平台超级管理员可在"管理员设置"中强制终止指定管理员的所有会话 |
**与租户应用隔离**
- 管理控制台部署在独立子域名(如 `admin.platform.com`),与租户应用域名体系分离
- 管理控制台不共享租户应用的 Session / Cookie 机制
#### 5.3.3 操作审计日志规范
所有写操作Create / Update / Delete及高危操作必须记录审计日志字段规范如下
```python
{
"id": "UUID",
"operator_id": "管理员用户 ID",
"operator_name": "管理员显示名",
"action_type": "CREATE_TENANT | SUSPEND_TENANT | DELETE_TENANT | RESTORE_DATA | SYSTEM_UPGRADE | ROLLBACK | RESET_PASSWORD | ...",
"target_type": "Tenant | User | System | Backup",
"target_id": "操作对象 ID",
"target_name": "操作对象可读名称",
"payload_summary": "操作内容摘要(非敏感字段)",
"result": "SUCCESS | FAILED",
"error_message": "失败原因(如有)",
"ip_address": "操作来源 IP",
"created_at": "ISO 8601 时间戳"
}
```
---
## 6. 技术考量
### 6.1 系统架构定位
基于 Fonrey 技术栈Django + django-tenants + PostgreSQL + Celery + Cloudflare R2管理控制台在同一 Django 项目中通过独立 App (`apps/admin_console/`) 实现,利用 Django 的 `public` Schema 作为管理控制台的数据层。
### 6.2 关键依赖
| 依赖 | 用途 | 风险等级 |
|------|------|---------|
| `django-tenants` | Schema 创建/销毁、租户切换 | 高 — 核心依赖,需确认 Schema 创建并发安全性 |
| Celery + Celery Beat | 异步备份、导出、状态同步任务 | 中 — 需监控任务队列积压 |
| PostgreSQL `pg_dump` | 数据备份与恢复 | 高 — 需测试大 Schema 备份耗时与锁表影响 |
| Cloudflare R2 | 备份文件与导出文件存储 | 中 — 需评估大文件上传/下载带宽成本 |
| Grafana | 监控图表展示 | 低 — 已在技术栈中规划 |
| TOTP`django-otp` | MFA 实现 | 低 — 成熟库,接入成本低 |
### 6.3 已知风险
| 风险 | 可能性 | 影响 | 缓解措施 |
|------|--------|------|---------|
| 大租户 Schema 备份耗时超长(>1 小时) | 中 | 高 | 异步执行 + 进度追踪;评估流式备份方案 |
| 系统升级过程中新请求涌入导致数据不一致 | 低 | 高 | 升级期间租户切换维护模式;使用数据库事务 |
| 软删除数据保留期间存储成本积累 | 中 | 低 | 合理设置默认保留期,提供平台级存储用量监控 |
| 管理控制台 IP 白名单配置错误导致运营团队被锁定 | 低 | 高 | 提供紧急访问恢复流程(通过服务器直接访问),文档化 |
### 6.4 待解决问题(开发启动前必须确认)
- [ ] **数据库备份方案**`pg_dump` 直接执行还是基于 WAL 的增量备份(如 pgBackRest— Owner: 工程负责人 — Deadline: 技术评审前
- [ ] **监控数据来源**Grafana 直接对接 PostgreSQL 指标还是通过 Prometheus Exporter— Owner: 运维团队
- [ ] **MFA 库选型**`django-otp` + TOTP 还是集成第三方认证(如 Okta— Owner: 工程负责人
- [ ] **子域名管理机制**Cloudflare DNS API 自动创建还是手动配置?— Owner: 运维团队
- [ ] **审计日志存储**:写入 `public` Schema 还是独立日志服务(如 Elasticsearch— Owner: 工程负责人
---
## 7. 发布计划
| 阶段 | 时间 | 范围 | 通过标准 |
|------|------|------|---------|
| 内部 Alpha | Week 14 | 平台内部团队使用 | 核心租户 CRUD 流程无 P0 BugMFA 可用 |
| 封闭 Beta | Week 56 | 运营团队日常使用 | 备份/恢复流程完整可用;审计日志 100% 覆盖 |
| 正式上线 | Week 7 | 全量运营团队 | 升级/回滚流程验证通过;监控告警规则配置完成 |
**回滚标准**:若正式上线后 72 小时内发现租户数据隔离漏洞或审计日志丢失,立即回滚并进入 P0 修复流程。
---
## 8. 不构建清单What We're NOT Building
| 请求/功能 | 原因 | 重新评估条件 |
|----------|------|------------|
| 自动化账单与发票生成 | 超出本模块范围,财务模块独立立项 | 财务模块 PRD 完成后接入 |
| 租户端自助迁移工具 | 当前用户规模不需要,运营团队手动处理即可 | 租户数 > 500 时重新评估 |
| 移动端管理界面 | 运营团队使用场景明确为 PC移动端收益低 | v2 规划,用户调研支持时推进 |
| Webhook 事件推送市场 | 集成复杂度高,当前无客户需求驱动 | 有 3+ 客户明确需求时评估 |
| 多语言管理界面 | 运营团队为内部人员,中文已满足需求 | 国际化扩张时规划 |
---
## 9. 附录
### 9.1 租户状态机
```
[新建中 Creating]
↓ 成功
[活跃 Active] ←──────────────────┐
↓ 手动挂起 │ 到期自动恢复 / 手动恢复
[已挂起 Suspended] ───────────────┘
↓ 删除操作(软删除)
[待清除 Pending Delete](冷静期 30 天)
↓ 冷静期到期 / 硬删除
[已删除 Deleted]
```
### 9.2 管理员角色权限矩阵
| 操作 | Platform Admin平台超级管理员 | 运营人员 | 只读审计员 |
|------|-----------|---------|-----------|
| 创建租户 | ✅ | ✅ | ❌ |
| 挂起 / 恢复租户 | ✅ | ✅ | ❌ |
| 软删除租户 | ✅ | ✅ | ❌ |
| 硬删除租户 | ✅ | ❌ | ❌ |
| 数据导出 | ✅ | ✅ | ❌ |
| 手动触发备份 | ✅ | ✅ | ❌ |
| 数据恢复 | ✅ | ❌ | ❌ |
| 系统升级 | ✅ | ❌ | ❌ |
| 系统回滚 | ✅ | ❌ | ❌ |
| 查看版本总览 | ✅ | ✅ | ❌ |
| 配置告警规则 | ✅ | ✅ | ❌ |
| 查看审计日志 | ✅ | ✅ | ✅ |
| 导出审计日志 | ✅ | ✅ | ✅ |
| 管理员账号管理 | ✅ | ❌ | ❌ |
| 强制登出管理员 | ✅ | ❌ | ❌ |
| 配置 IP 白名单 | ✅ | ❌ | ❌ |
### 9.3 页面路由规划(管理控制台)
```
/admin/ # 仪表盘
/admin/tenants/ # 租户列表
/admin/tenants/new/ # 新建租户
/admin/tenants/{id}/ # 租户详情(信息)
/admin/tenants/{id}/users/ # 租户用户管理
/admin/tenants/{id}/plan/ # 套餐信息与升级
/admin/tenants/{id}/monitoring/ # 监控数据
/admin/tenants/{id}/backups/ # 备份记录
/admin/tenants/{id}/history/ # 操作历史
/admin/system/versions/ # 版本管理
/admin/system/backups/ # 备份管理
/admin/monitoring/ # 全局监控与告警
/admin/audit-logs/ # 审计日志
/admin/settings/admins/ # 管理员设置
```

View File

@@ -12,6 +12,8 @@
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-04-30 | Atlas | 新建 README.md建立项目文档统一入口与 ADR 治理门禁 |
| 2026-05-02 | Sisyphus | 模块入口索引更新:合并『系统管理』与『客户端发布』为『平台管理后台』;同步 `ADR-20260502-001` |
| 2026-05-02 | Sisyphus | TECH_STACK 入口更新:合并为 `平台管理后台技术方案.md`,原 `客户端发布管理技术方案.md` / `系统管理技术文档.md` 已删除;同步 `ADR-20260502-002` |
## 1. 项目核心入口(必读顺序)
@@ -55,13 +57,13 @@
## 4. 模块文档入口
- 登录管理:[`PRD/登录管理/`](./PRD/登录管理/) [`TECH_STACK/登录管理技术方案.md`](./TECH_STACK/登录管理技术方案.md) [`TEST_CASES/TEST_CASES_LOGIN_MODULE.md`](./TEST_CASES/TEST_CASES_LOGIN_MODULE.md)
- 客户端发布:[`PRD/发布管理/客户端发布管理模块PRD.md`](./PRD/发布管理/客户端发布管理模块PRD.md) [`TECH_STACK/客户端发布管理技术方案.md`](./TECH_STACK/客户端发布管理技术方案.md) [`TEST_CASES/TEST_CASES_RELEASE_MODULE.md`](./TEST_CASES/TEST_CASES_RELEASE_MODULE.md)
- 平台管理后台(含原『系统管理』+ 原『客户端发布』,详见 `ADR-20260502-001` / `ADR-20260502-002`[`PRD/平台管理后台/平台管理后台PRD.md`](./PRD/平台管理后台/平台管理后台PRD.md) [`TECH_STACK/平台管理后台技术方案.md`](./TECH_STACK/平台管理后台技术方案.md) [`TEST_CASES/TEST_CASES_RELEASE_MODULE.md`](./TEST_CASES/TEST_CASES_RELEASE_MODULE.md)
- 权限管理:[`PRD/权限管理/`](./PRD/权限管理/) [`TECH_STACK/权限管理系统技术方案.md`](./TECH_STACK/权限管理系统技术方案.md)
- 房源管理:[`PRD/房源管理/`](./PRD/房源管理/) [`TECH_STACK/房源管理技术方案.md`](./TECH_STACK/房源管理技术方案.md)
- 客源管理:[`PRD/客源管理/`](./PRD/客源管理/) [`TECH_STACK/客源管理技术方案.md`](./TECH_STACK/客源管理技术方案.md)
- 楼盘管理:[`PRD/房源管理/楼盘管理模块PRD.md`](./PRD/房源管理/楼盘管理模块PRD.md) [`TECH_STACK/楼盘管理技术方案.md`](./TECH_STACK/楼盘管理技术方案.md)
- 组织人事:[`PRD/组织人事管理/`](./PRD/组织人事管理/) [`TECH_STACK/组织人事技术方案.md`](./TECH_STACK/组织人事技术方案.md)
- 系统设置:[`PRD/系统配置/`](./PRD/系统配置/) [`PRD/系统管理/`](./PRD/系统管理/) [`TECH_STACK/系统设置技术方案.md`](./TECH_STACK/系统设置技术方案.md)
- 系统设置:[`PRD/系统配置/`](./PRD/系统配置/) [`TECH_STACK/系统设置技术方案.md`](./TECH_STACK/系统设置技术方案.md)(系统管理域已并入"平台管理后台"
---

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 类组合最佳实践) |

View File

@@ -1,8 +1,8 @@
# Fonrey 登录模块测试用例文档(可自动化)
> 文档版本v1.0
> 适用范围:`PRD/登录管理/用户登录管理模块PRD.md` v2.0
> 用例编号范围:`TC-FON-000001` ~ `TC-FON-000048`(全局唯一
> 文档版本v2.0
> 适用范围:`PRD/登录管理/用户登录管理模块PRD.md` v3.0、`TECH_STACK/登录管理技术方案.md` v4.1、`DATA_MODEL/DATA_MODEL_LOGIN.md`、`TECH_STACK/API_CONTRACT.md`
> 用例编号范围:`TC-FON-000001` ~ `TC-FON-000048`、`TC-FON-000153` ~ `TC-FON-000172`(全局唯一,共 68 条
> 编号规范:`TEST_CASES/TEST_CASE_ID_SPEC.md`
---
@@ -12,6 +12,7 @@
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
| 2026-05-02 | Vulcan | 对齐 PRD v3.0 / 登录技术方案 v4.1 / DATA_MODEL_LOGIN新增 20 条登录模块补充用例TC-FON-000153~000172覆盖 API 契约统一 envelope、`/api/auth/password/change-initial/`、管理员手动解锁、短信 OTP 明文不落库、历史密码保留 3 条、验证码登录错误作废文案、微信扫码预留端点不开放、`/api/auth/wechat/*` 路由未注册、登出接口与客户端会话清理联动等关键口径;总用例数 48→68 |
## 1. 目标与原则
@@ -39,9 +40,9 @@
---
## 3. 全量测试用例清单(48条
## 3. 全量测试用例清单(68条
### 3.0 登录模块 API 端点口径(以 PRD 为准)
### 3.0 登录模块 API 端点口径(以 `TECH_STACK/登录管理技术方案.md` §五 + `TECH_STACK/API_CONTRACT.md` 为准)
- `POST /api/auth/tenant/verify/`
- `GET /api/auth/captcha/`
@@ -51,6 +52,7 @@
- `POST /api/auth/recover/password/request/`
- `POST /api/auth/recover/password/verify/`
- `POST /api/auth/recover/password/reset/`
- `POST /api/auth/password/change-initial/`
- `POST /api/auth/logout/`
> 注:测试代码生成与接口调用必须使用以上路径。
@@ -373,11 +375,146 @@
---
## F. 补充用例PRD v3.0 / 技术方案 v4.1 / DATA_MODEL / API_CONTRACT 对齐)
### TC-FON-000153 首次登录改密提交接口成功
- 级别API
- 前置:账号 `is_initial_password=true`,已完成登录并具备改密会话
- 步骤:
- `TC-FON-000153-S01` 调用 `POST /api/auth/password/change-initial/` 提交合法新密码
- `TC-FON-000153-S02` 校验返回成功,`is_initial_password=false`
- `TC-FON-000153-S03` 校验 `password_histories` 新增记录
- 预期:首次改密成功落库,账号脱离初始密码状态
### TC-FON-000154 首次登录改密弱密码拦截
- 级别API+E2E
- 前置:账号处于首次改密页
- 步骤:`S01` 输入不满足复杂度的新密码;`S02` 提交改密;`S03` 校验错误码/错误提示
- 预期:返回 `AUTH_PASSWORD_WEAK`,拒绝提交
### TC-FON-000155 首次登录改密禁止复用最近3次历史密码
- 级别API
- 前置:`password_histories` 已有最近 3 条
- 步骤:`S01` 提交与最近历史重复的新密码;`S02` 校验错误响应;`S03` 校验密码未更新
- 预期:拒绝历史重复密码,历史记录不新增
### TC-FON-000156 首次改密成功后会话保持并直达首页
- 级别E2E
- 前置:初始密码登录进入强制改密页
- 步骤:`S01` 完成改密提交;`S02` 校验未二次登录;`S03` 校验直接进入首页
- 预期:沿用当前会话进入系统(不强制重新登录)
### TC-FON-000157 找回密码成功后不再触发首次改密
- 级别API+E2E
- 前置:账号原为初始密码状态,执行找回密码三步成功
- 步骤:
- `TC-FON-000157-S01` 调用 `POST /api/auth/recover/password/reset/` 成功后校验 `is_initial_password=false`
- `TC-FON-000157-S02` 返回登录页使用新密码登录
- `TC-FON-000157-S03` 校验直接进入首页而非首次改密页
- 预期:找回密码后视为已完成身份核验,不再触发强制改密
### TC-FON-000158 主动登出销毁会话与登录凭证
- 级别API+E2E
- 前置:已登录
- 步骤:
- `TC-FON-000158-S01` 调用 `POST /api/auth/logout/`
- `TC-FON-000158-S02` 校验服务端会话失效(后续受保护请求无效)
- `TC-FON-000158-S03` Electron 客户端侧校验登录凭证清理与跳转登录页
- 预期:登出后不可复用原凭证继续访问系统
### TC-FON-000159 管理员手动解锁账号
- 级别API
- 前置:账号因连续密码错误达到阈值已处于 `locked`
- 步骤:`S01` Tenant Admin 在后台执行手动解锁;`S02` 校验 `user_accounts.status=active``locked_until` 清空;`S03` 立即发起登录请求并成功
- 预期:管理员可提前解锁,无需等待 30 分钟自动解锁
### TC-FON-000160 找回密码步骤一防枚举且无效账号不落短信记录
- 级别API
- 前置:准备“未注册手机号”“停用账号手机号”
- 步骤:`S01` 分别调用 `POST /api/auth/recover/password/request/``S02` 校验外显响应一致;`S03` 校验 `sms_otp_records` 未新增对应记录
- 预期:外显统一且不泄露账号状态,不产生无效 OTP 记录
### TC-FON-000161 短信验证码仅哈希存储(禁止明文入库)
- 级别API
- 前置:触发找回密码或验证码登录发码
- 步骤:`S01` 查询 `sms_otp_records` 最新记录;`S02` 校验存在 `otp_hash``S03` 校验无 OTP 明文字段/日志明文
- 预期:满足 DATA_MODEL 安全要求OTP 明文不落库)
### TC-FON-000162 找回密码验证码错误5次后作废并持久化状态
- 级别API
- 前置:已生成有效 `scene=password_reset` OTP
- 步骤:`S01` 连续提交错误验证码 5 次;`S02` 校验第 5 次后提示“验证码已失效,请重新获取”;`S03` 校验记录 `verify_attempts=5``is_used=true`
- 预期OTP 作废口径与数据状态一致
### TC-FON-000163 OTP 两场景有效期差异校验10 分钟 / 5 分钟)
- 级别API
- 步骤:
- `TC-FON-000163-S01` 发起 `scene=password_reset`,校验 `expires_at=created_at+10分钟`
- `TC-FON-000163-S02` 发起 `scene=login`,校验 `expires_at=created_at+5分钟`
- `TC-FON-000163-S03` 校验两场景限流计数独立
- 预期:有效期与频控均符合技术方案与数据模型
### TC-FON-000164 验证码登录错误5次后作废并提示重新获取
- 级别API+E2E
- 前置:已发送 `scene=login` 验证码
- 步骤:`S01` 连续输入错误 OTP 5 次;`S02` 校验第 5 次提示“验证码已失效,请重新获取”;`S03` 再次提交原 OTP 必须失败
- 预期:验证码登录错误上限与作废文案符合 PRD
### TC-FON-000165 登录审计 `login_attempts.failure_reason` 写入准确
- 级别API
- 步骤:
- `TC-FON-000165-S01` 触发一次密码错误,校验记录 `wrong_password`
- `TC-FON-000165-S02` 触发一次滑块失败,校验记录 `wrong_captcha`
- `TC-FON-000165-S03` 触发一次 OTP 错误,校验记录 `wrong_otp`
- 预期:失败原因枚举写入正确,便于审计与风控
### TC-FON-000166 锁定账号验证码登录返回标准错误码
- 级别API
- 前置:账号已 `locked`
- 步骤:`S01` 调用 `POST /api/auth/login/phone/``S02` 校验 HTTP 423`S03` 校验错误码 `AUTH_ACCOUNT_LOCKED`
- 预期:账号维度锁定策略对验证码登录同样生效
### TC-FON-000167 Tenant 校验限流错误契约
- 级别API
- 前置:同 IP 1 分钟内第 11 次请求
- 步骤:`S01` 触发 `POST /api/auth/tenant/verify/` 限流;`S02` 校验 HTTP 429`S03` 校验 `code=AUTH_TENANT_RATE_LIMITED` 且错误体字段完整
- 预期:限流错误符合 API_CONTRACT 与技术方案
### TC-FON-000168 API 失败响应统一 envelope 契约
- 级别API
- 前置:构造任一登录失败场景(如密码错误)
- 步骤:`S01` 调用失败接口;`S02` 校验响应包含 `ok=false``error``code``meta.request_id``meta.timestamp``S03` 校验字段类型正确
- 预期:失败响应结构符合 `TECH_STACK/API_CONTRACT.md`
### TC-FON-000169 密码登录失败错误码与状态码契约
- 级别API
- 前置:账号存在,密码错误
- 步骤:`S01` 调用 `POST /api/auth/login/``S02` 校验 HTTP 401`S03` 校验 `code=AUTH_INVALID_CREDENTIAL` 与统一错误结构
- 预期:失败状态码与错误码稳定、可机器判定
### TC-FON-000170 找回密码重置凭证无效错误契约
- 级别API
- 前置:`sms_reset_token` 无效或过期
- 步骤:`S01` 调用 `POST /api/auth/recover/password/reset/``S02` 校验 HTTP 400`S03` 校验 `code=AUTH_SMS_RESET_TOKEN_INVALID`
- 预期:错误码与文案符合技术方案
### TC-FON-000171 登录页微信扫码入口禁用态验证
- 级别E2E
- 步骤:`S01` 打开登录页;`S02` 校验“微信扫码登录 - 即将开放”为灰态不可点击;`S03` 校验无跳转行为
- 预期MVP 仅展示禁用入口,不提供可用登录能力
### TC-FON-000172 微信预留端点未开放qrcode/callback
- 级别API
- 步骤:`S01` 调用 `GET /api/auth/wechat/qrcode/`,校验 HTTP 404 或路由未注册;`S02` 调用 `POST /api/auth/wechat/callback/`,校验 HTTP 404 或路由未注册;`S03` 校验未返回可用二维码数据且未签发任何登录态
- 预期MVP 阶段微信相关预留端点均不开放
---
## 4. 工程实现指引(给测试开发工程师)
1. **目录建议**
- `tests/integration/login/test_tc_fon_000001_000048.py`
- `tests/e2e/login/test_tc_fon_000001_000048.spec.ts`
- `tests/integration/login/test_tc_fon_000001_000048_and_000153_000172.py`
- `tests/e2e/login/test_tc_fon_000001_000048_and_000153_000172.spec.ts`
2. **命名规范**
- 函数名必须带用例ID例如`def test_tc_fon_000024_account_lock_after_5_failures():`
3. **步骤日志**
@@ -385,12 +522,12 @@
4. **报告聚合**
- 生成 `reports/login_run_<run_id>.json` + `reports/login_run_<run_id>.html`
5. **CI 门禁**
- `TC-FON-000001` ~ `TC-FON-000048` 全量通过才允许合并
- `TC-FON-000001` ~ `TC-FON-000048``TC-FON-000153` ~ `TC-FON-000172` 全量通过才允许合并
---
## 5. 变更规则
- 新增登录用例:从 `TC-FON-000049` 开始递增
- 新增登录用例:从 `TC-FON-000173` 开始递增
- 后续房源/客源模块:继续用同一全局序列,不得重号
- 禁止删除历史用例ID可标记 `deprecated` 但保留编号与历史报告可追溯性

View File

@@ -0,0 +1,643 @@
# Fonrey 平台管理后台模块测试用例文档(可自动化)
> 文档版本v1.0
> 适用范围:`PRD/平台管理后台/平台管理后台PRD.md` v1.0
> 用例编号范围:`TC-FON-000073` ~ `TC-FON-000152`(全局唯一,共 80 条)
> 编号规范:`TEST_CASES/TEST_CASE_ID_SPEC.md`
> 关联 ADR`ADR-20260502-001`PRD 合并)、`ADR-20260502-002`TECH 合并)、`ADR-20260430-007`Heartbeat Upsert + 24h 活跃口径)、`ADR-20260430-008`SHA-256 强制)、`ADR-20260430-009`(客户端 API 命名空间 `/api/release/...`
---
## 变更历史
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-05-02 | Sisyphus | 初版:覆盖 4 Personas / 11 Stories / 16 页面 / §5.1§5.7 / §6 角色矩阵 / §7 状态机;登记 TC-FON-000073~000152 |
---
## 1. 目标与原则
1. 本文档用于让工程师直接生成自动化测试代码HTMX 视图集成测试 + Web E2E + 客户端 API 集成测试)。
2. 每个用例均包含**唯一 ID**与**步骤 ID**`-SYY`),可直接用于失败定位与报告聚合。
3. 用例覆盖『平台管理后台』PRD 全部 11 条 User Story 的验收标准,并叠加:
- §5.6 安全MFA / IP 白名单 / 会话超时 / 强制登出)
- §5.7 审计日志规范(字段、写入、不可变)
- §6 角色权限矩阵Platform Admin / 运营人员 / 只读审计员 三档)
- §7 租户状态机Creating → Active ↔ Suspended → Pending Delete → Deleted
4. 不覆盖租户业务模块内部(房源/客源/权限)— 那些由各自 TEST_CASES 文档承担。
---
## 2. 自动化执行与报告要求
- **执行层**
- 后台业务集成HTMX/JSON 视图):`pytest + pytest-django + TenantClient`(仅 public schema 用例可省略租户切换)
- Web E2E`playwright`PC 桌面分辨率 ≥ 1280×720
- 客户端 API 集成(`/api/release/...``pytest + requests`,使用 device-token 鉴权
- **每步必须输出**`run_id / test_case_id / step_id / status / expected_result / actual_result / error_message`
- **失败时附带**
- Web截图路径
- 后端:请求/响应快照、SQL 关键日志路径
- Celery任务 ID 与 Flower 链接
- **CI 门禁**`TC-FON-000073` ~ `TC-FON-000152` 全量通过才允许合并到主干。
---
## 3. 端点口径(以 PRD §5.5 + 技术方案 §3/§4 为准)
### 3.1 后台业务命名空间 `/admin/...`Session + CSRF + TOTP
- 认证/会话:`POST /admin/login/``POST /admin/login/mfa/``POST /admin/logout/``POST /admin/mfa/setup/``POST /admin/mfa/confirm/`
- 租户:`GET /admin/tenants/``POST /admin/tenants/``GET /admin/tenants/{id}/``PATCH /admin/tenants/{id}/``POST /admin/tenants/{id}/suspend/``POST /admin/tenants/{id}/resume/``POST /admin/tenants/{id}/soft-delete/``POST /admin/tenants/{id}/restore/``POST /admin/tenants/{id}/hard-delete/``POST /admin/tenants/{id}/license/``POST /admin/tenants/{id}/license/user-limit/``GET /admin/tenants/{id}/onboarding.pdf`
- 用户:`GET /admin/tenants/{id}/users/``POST /admin/tenants/{id}/users/{uid}/reset-password/``POST /admin/tenants/{id}/admins/`
- 备份/导出/恢复:`POST /admin/tenants/{id}/backups/``GET /admin/tenants/{id}/backups/``POST /admin/tenants/{id}/restore/``POST /admin/tenants/{id}/exports/`
- 升级:`POST /admin/system/versions/upgrade/``POST /admin/system/versions/rollback/``GET /admin/system/versions/`
- 客户端版本:`GET /admin/api/client-releases/``POST /admin/api/client-releases/``PATCH /admin/api/client-releases/{id}/``POST /admin/api/client-releases/{id}/publish/``POST /admin/api/client-releases/{id}/offline/``POST /admin/api/client-releases/{id}/rollback/``POST /admin/api/client-releases/force-update/``GET /admin/api/client-releases/distribution/`
- 审计:`GET /admin/audit-logs/``GET /admin/audit-logs/export.csv`
- 管理员:`GET /admin/settings/admins/``POST /admin/settings/admins/``POST /admin/settings/admins/{id}/deactivate/``POST /admin/settings/admins/{id}/force-logout/``POST /admin/settings/ip-whitelist/`
### 3.2 客户端运行时命名空间 `/api/release/v1/...`(设备 Token
- `GET /api/release/v1/latest?platform=win32&arch=x64`
- `POST /api/release/v1/heartbeat`
> 注:测试代码生成与请求构造必须使用以上路径;任何路径偏差视为缺陷。
---
## 4. 全量测试用例清单80 条)
> 模块前缀A. 认证与会话B. 仪表盘C. 租户列表与筛选D. 新建租户Story A1E. 挂起/恢复A2F. License 时效A3G. License 用户数A4H. 删除与撤销I. 数据导出/备份/恢复J. 套餐升级K. 租户用户管理L. 监控/告警M. 平台升级B1N. 升级回滚B2O. 版本总览B3P. 客户端发布C1Q. 客户端分布与活跃榜C2R. 强制更新S. 审计日志D1T. 安全MFA / IP / 会话 / 强制登出U. 角色矩阵交叉V. 状态机非法跃迁
---
## A. 认证与会话PRD §5.6 / §5.4.1 #1
### TC-FON-000073 平台后台登录页强制 IP 白名单
- 级别API
- 前置:来源 IP 不在 `ip_whitelist`
- 步骤:
- `TC-FON-000073-S01` `GET /admin/login/`
- `TC-FON-000073-S02` 校验 HTTP 403
- `TC-FON-000073-S03` 校验 `IpWhitelistMiddleware` 命中日志
- 预期:未在白名单的 IP 一律 403不返回登录表单。
### TC-FON-000074 首次登录强制 MFA 配置向导
- 级别E2E
- 前置:管理员 `mfa_enabled=false`
- 步骤:
- `TC-FON-000074-S01` 提交账号密码登录成功
- `TC-FON-000074-S02` 校验自动跳转 `/admin/mfa/setup/`,无法绕过
- `TC-FON-000074-S03` 完成 TOTP 绑定后跳转仪表盘
- 预期MFA 未配置则一切受保护页面均跳 setup。
### TC-FON-000075 登录失败 5 次锁定 15 分钟
- 级别API
- 步骤:
- `TC-FON-000075-S01` 同账号连续 5 次错误密码
- `TC-FON-000075-S02` 第 6 次返回 `ADMIN_ACCOUNT_LOCKED`
- `TC-FON-000075-S03` 15 分钟后自动解锁
- 预期:阈值与解锁窗口与 PRD §5.6 + 技术方案 §7.1 一致。
### TC-FON-000076 同 IP 登录尝试 20 次/小时限流
- 级别API
- 步骤:`S01` 同 IP 不同账号 20 次错误;`S02` 第 21 次 429`S03` 1 小时后恢复
- 预期IP 级限流独立于账号级锁定。
### TC-FON-000077 会话 30 分钟无操作自动登出
- 级别E2E
- 步骤:`S01` 登录后挂起 31 分钟;`S02` 任意请求;`S03` 跳转登录页且 Session 失效
- 预期:滚动续期 30 分钟超时口径生效。
### TC-FON-000078 高危操作 step-up MFA5 分钟有效期)
- 级别API
- 步骤:
- `TC-FON-000078-S01` 已登录但距上次 MFA 验证 > 5 分钟
- `TC-FON-000078-S02` `POST /admin/tenants/{id}/hard-delete/`
- `TC-FON-000078-S03` 校验 412 + `MFA_REQUIRED`,要求重新 TOTP
- 预期5 分钟以外的高危操作必须 step-up。
### TC-FON-000079 平台后台 Cookie 严格隔离
- 级别API
- 步骤:`S01` 登录获得 Session Cookie`S02` 校验 Domain=`admin.fonrey.com``SameSite=Strict``Secure``HttpOnly``S03` 校验不会发送至 `app.fonrey.com`
- 预期:与租户应用 Cookie 完全隔离。
---
## B. 仪表盘§5.4.3
### TC-FON-000080 仪表盘默认载入与 KPI 字段
- 级别E2E
- 步骤:`S01` 登录后访问 `/admin/``S02` 校验 6 模块齐全(全局概览/系统健康/近期告警/资源概览/客户端覆盖/最近操作);`S03` 校验数据非空
- 预期6 大模块全部渲染。
### TC-FON-000081 仪表盘『最近高危操作』链接到审计日志带筛选
- 级别E2E
- 步骤:`S01` 仪表盘点击某条高危记录;`S02` 跳转 `/admin/audit-logs/?action_type=...&operator_id=...``S03` 校验筛选自动应用
- 预期URL 携带筛选参数,列表只剩匹配条目。
---
## C. 租户列表与筛选§5.4.4
### TC-FON-000082 租户列表分页(默认 20 条)
- 级别API+E2E
- 步骤:`S01` `GET /admin/tenants/?page=1``S02` 校验返回 20 条;`S03` 翻第 2 页
- 预期:分页参数与默认页大小符合 §5.4.4。
### TC-FON-000083 租户列表多维度筛选
- 级别API
- 步骤:`S01` 筛选 `status=active&plan=Professional``S02` 筛选 `expiring_within_15_days=true``S03` 筛选 `users_at_limit=true`
- 预期:三组筛选与 PRD `即将到期 / 用户数已满` 行为一致。
### TC-FON-000084 租户列表关键词搜索(公司名/Tenant Code/邮箱)
- 级别API
- 步骤:`S01` 搜索公司名片段;`S02` 搜索完整 12 位 `tenant_code``S03` 搜索邮箱片段
- 预期:三种关键词均能命中。
---
## D. 新建租户Story A1§5.1.1§5.1.2
### TC-FON-000085 新建租户表单字段必填与格式校验
- 级别E2E
- 步骤:`S01` 提交缺少『公司名称』;`S02` 提交非法手机号;`S03` 提交 `license_user_limit=0`
- 预期:三类校验失败均阻断提交且显示对应错误。
### TC-FON-000086 新建租户成功路径 < 60 秒并写默认配置
- 级别API
- 步骤:
- `TC-FON-000086-S01` `POST /admin/tenants/` 成功收到 202 + `task_id`
- `TC-FON-000086-S02` 轮询任务状态在 60s 内变为 `success`
- `TC-FON-000086-S03` 校验 `tenants.status='active'`、租户 schema 内已注入 PermissionDef + 7 个内置角色 + Tenant Admin携带专属权限集合独立于 7 个业务角色)
- 预期:与 §5.1.2 默认配置内容完全一致。
### TC-FON-000087 创建失败回滚所有已创建资源
- 级别API
- 步骤:`S01` 注入 migrate 失败;`S02` 校验 `tenants.status='failed'``S03` 校验 schema 已 DROP、R2 路径未占用
- 预期:失败原子回滚,无残留。
### TC-FON-000088 联系人有邮箱时发送欢迎邮件
- 级别API
- 步骤:`S01` 创建携带邮箱;`S02` 校验邮件队列含主题『【房睿平台】您的账号已开通...』;`S03` 校验正文含 Tenant Code + 初始密码 + 脱敏手机号末三位
- 预期:邮件字段全量满足 §5.1.1 欢迎邮件规范。
### TC-FON-000089 联系人无邮箱时下载入驻信息 PDF
- 级别E2E
- 步骤:`S01` 创建不带邮箱的租户;`S02` 在详情页点击『下载入驻信息』;`S03` 校验 PDF 文件名 `{公司名称}_入驻信息_{日期}.pdf` 且包含与邮件等价信息
- 预期PDF 路径与命名符合规范。
### TC-FON-000090 Tenant Code 12 位纯数字唯一性
- 级别API
- 步骤:`S01` 连续创建 5 个租户;`S02` 校验 `tenant_code` 全部 12 位数字;`S03` 校验唯一约束
- 预期:格式与唯一性双重满足。
---
## E. 挂起/恢复Story A2§5.1.3
### TC-FON-000091 手动挂起后租户用户跳『账号已暂停』页
- 级别API+E2E
- 步骤:`S01` `POST /admin/tenants/{id}/suspend/``欠费``S02` 模拟租户内用户登录;`S03` 校验跳转暂停提示页
- 预期:挂起态对租户内用户生效。
### TC-FON-000092 挂起后平台后台仍可访问该租户数据
- 级别API
- 步骤:`S01` 挂起租户;`S02` `GET /admin/tenants/{id}/users/``S03` 返回 200
- 预期:挂起仅影响租户侧,平台侧不受影响。
### TC-FON-000093 设置到期时间自动恢复
- 级别API
- 步骤:`S01` 挂起设 `suspend_until=now+1h``S02` 时间到达后 Celery Beat 触发;`S03` 校验状态恢复 `active` + 通知邮件
- 预期:到期自动恢复 + 邮件发送。
### TC-FON-000094 挂起原因枚举校验
- 级别API
- 步骤:`S01` 提交合法 `欠费``S02` 提交合法 `license_expired``S03` 提交非法 `xxx`
- 预期DDL CHECK 与视图校验一致拒绝非法值。
---
## F. License 时效Story A3§5.1.3
### TC-FON-000095 License 到期当日自动挂起
- 级别API
- 步骤:`S01``license_expires_at=today``S02` 触发每日定时任务;`S03` 校验状态 `suspended` + `suspended_reason='license_expired'` + 审计 `AUTO_SUSPEND_LICENSE_EXPIRED`
- 预期:自动挂起 + 标准审计动作。
### TC-FON-000096 License 到期前 15 天 Tenant Admin 倒计时横幅
- 级别E2E
- 前置:到期日 = today+14
- 步骤:`S01` Tenant Admin 登录租户;`S02` 校验顶部横幅文案『您的 License 将于 X 天后到期...』;`S03` 普通 Agent 登录不显示横幅
- 预期:横幅仅 Tenant Admin 可见。
### TC-FON-000097 调整 License 到期日期写入审计
- 级别API
- 步骤:`S01` `POST /admin/tenants/{id}/license/``S02` 校验 `tenants.license_expires_at` 更新;`S03` 校验审计 `UPDATE_LICENSE`
- 预期:字段更新且有审计。
---
## G. License 用户数Story A4§5.1
### TC-FON-000098 当前用户数 / 上限实时显示
- 级别API
- 步骤:`S01` 查询租户详情;`S02` 校验返回 `current_user_count``license_user_limit``S03` 创建一个用户后再查为 +1
- 预期:实时计数。
### TC-FON-000099 达上限后租户内创建用户被拒
- 级别API
- 步骤:`S01``license_user_limit=current``S02` 租户内 Tenant Admin 创建新用户;`S03` 校验返回错误 + 提示『当前用户数已达 License 上限...』
- 预期:阻断且提示对齐 §A4。
### TC-FON-000100 调整用户数上限写入审计
- 级别API
- 步骤:`S01` `POST /admin/tenants/{id}/license/user-limit/` 50→100`S02` 校验字段更新;`S03` 校验审计 `UPDATE_LICENSE_USER_LIMIT` + before/after
- 预期:审计含变更前后值。
### TC-FON-000101 缩容到低于当前用户数允许执行但锁定新增
- 级别API
- 步骤:`S01` 当前 30 用户;`S02` 上限调至 20`S03` 校验现存用户保留但新建被拒
- 预期:缩容不删用户但阻新增。
---
## H. 删除与撤销§5.1.4
### TC-FON-000102 软删除进入 30 天冷静期
- 级别API
- 步骤:`S01` 勾选『确认数据导出已完成』后提交;`S02` 校验 `status='pending_delete'` + `pending_delete_until=now+30d``S03` 审计 `DELETE_TENANT`
- 预期:状态、时长、审计三对齐。
### TC-FON-000103 冷静期内撤销软删除
- 级别API
- 步骤:`S01` 软删除后立即 `POST /admin/tenants/{id}/restore/``S02` 校验状态恢复 `active``S03` 审计含 RESTORE 动作
- 预期:可逆。
### TC-FON-000104 硬删除强制 MFA 二次验证
- 级别API
- 步骤:`S01` 未持 step-up token 调 `hard-delete``S02` 412 `MFA_REQUIRED``S03` 携带新鲜 TOTP 后成功
- 预期:必须 MFA。
### TC-FON-000105 硬删除后释放 Tenant Code / R2 / License 席位
- 级别API
- 步骤:`S01` 硬删除完成;`S02` 校验同 Tenant Code 可被新租户复用;`S03` 校验 R2 前缀被清空、License 席位归还
- 预期:三项资源全释放。
### TC-FON-000106 硬删除前必须勾选导出确认
- 级别API
- 步骤:`S01` 不勾选确认提交;`S02` 校验 422 + 提示
- 预期:未确认则阻断。
---
## I. 数据导出 / 备份 / 恢复§5.1.5§5.1.7
### TC-FON-000107 数据导出异步任务状态流转
- 级别API
- 步骤:`S01` `POST /admin/tenants/{id}/exports/``S02` 轮询 `Pending → In Progress → Done``S03` 校验下载链接 24h 有效
- 预期:状态机与签名链接 TTL 准确。
### TC-FON-000108 CSV 导出文件资产为 CDN URL 分号分隔
- 级别API
- 步骤:`S01` 选模块『房源数据』+ CSV`S02` 下载 CSV`S03` 校验 `photos` 列以 `;` 分隔的 CDN URL
- 预期:与 §5.1.5 表格一致。
### TC-FON-000109 完整备份含 R2 文件实体
- 级别API
- 步骤:`S01` 触发完整备份;`S02` 校验产物含 `pg_dump.sql.gz` + `assets/``S03` 校验 `backup_records.size_bytes`
- 预期:含库 + 文件实体,区别于导出。
### TC-FON-000110 恢复前自动当前快照 + 维护模式
- 级别API
- 步骤:`S01` 选历史备份执行 restore`S02` 校验先生成 pre-restore 快照;`S03` 校验租户进入 `maintenance` 状态、用户访问被拒
- 预期:流程严格按 §5.1.7。
### TC-FON-000111 恢复需 MFA 二次验证
- 级别API
- 步骤:`S01` 无 step-up token 触发 restore`S02` 412 `MFA_REQUIRED`
- 预期:与硬删除一致门槛。
### TC-FON-000112 备份保留策略 N=10 自动清理
- 级别API
- 步骤:`S01` 连续触发 11 次备份;`S02` 校验最旧 1 条被自动清理;`S03` 校验 R2 对应文件已删
- 预期:保留 N 与默认 10 一致。
### TC-FON-000113 单租户备份计划覆盖全局
- 级别API
- 步骤:`S01` 全局 02:00 每日;`S02` 单租户改 06:00 每周;`S03` 校验 Beat 调度按租户配置执行
- 预期:单租户优先级高于全局。
---
## J. 套餐升级§5.1.8
### TC-FON-000114 套餐升级前展示差异对比
- 级别E2E
- 步骤:`S01` 选 Basic→Professional`S02` 校验弹窗含功能/用户数/存储/API 额度对比;`S03` 选立即生效
- 预期:差异表与生效模式齐全。
### TC-FON-000115 套餐升级前自动备份
- 级别API
- 步骤:`S01` 触发升级;`S02` 校验任务先生成备份记录;`S03` 校验 `backup_records.trigger='pre_plan_upgrade'`
- 预期:备份联动准确。
---
## K. 租户用户管理§5.1.9
### TC-FON-000116 平台直接创建 Tenant Admin
- 级别API
- 步骤:`S01` `POST /admin/tenants/{id}/admins/``S02` 校验目标 schema 写入用户 + 专属权限集合;`S03` 审计 `CREATE_ADMIN`
- 预期:写入与审计一致。
### TC-FON-000117 重置密码(链接方式)
- 级别API
- 步骤:`S01` `POST /admin/tenants/{id}/users/{uid}/reset-password/?mode=link``S02` 校验邮件含一次性链接;`S03` 链接 24h 失效
- 预期:链接 TTL 与口径对齐。
### TC-FON-000118 重置密码(临时密码方式 + 首登强制改)
- 级别API
- 步骤:`S01` `mode=temp``S02` 返回临时密码并标记 `force_change_on_first_login=true``S03` 校验审计 `RESET_PASSWORD`
- 预期:双模式齐全。
---
## L. 监控与告警§5.1.10 / §5.4.1 #13
### TC-FON-000119 阈值告警触发邮件 + Webhook
- 级别API
- 步骤:`S01` 配置 CPU>80% 邮件+Webhook`S02` 模拟指标超阈;`S03` 校验两通道均收到
- 预期:双通道并行。
### TC-FON-000120 SLA 月报可导出
- 级别API
- 步骤:`S01` 选近 30 天;`S02` `GET /admin/monitoring/sla.csv``S03` 校验 CSV 含可用率、故障事件、影响描述
- 预期:导出字段齐全。
---
## M. 平台升级Story B1§5.2.1
### TC-FON-000121 升级前健康检查阻断
- 级别API
- 步骤:`S01` 注入 Redis 异常;`S02` 触发 upgrade`S03` 校验返回 `UPGRADE_HEALTH_CHECK_FAILED`
- 预期:异常服务下不允许升级。
### TC-FON-000122 灰度升级仅作用于内测租户名单
- 级别API
- 步骤:`S01` 配置内测组 = [t1,t2]`S02` 触发灰度;`S03` 校验仅 t1/t2 数据版本前进,其余租户保持原版本
- 预期:名单边界严格。
### TC-FON-000123 升级期间实时进度按租户维度可见
- 级别E2E
- 步骤:`S01` 启动 B 类分批升级;`S02` 页面轮询 `upgrade_events.tenant_progress` JSONB`S03` 校验各租户状态实时刷新
- 预期:进度透明可观察。
### TC-FON-000124 升级失败触发告警与一键回滚入口
- 级别API
- 步骤:`S01` 模拟某租户 migrate 失败;`S02` 校验 `upgrade_events.status='halted'` 并发告警;`S03` 页面出现『一键回滚』按钮
- 预期halted 后告警 + 回滚入口出现。
---
## N. 升级回滚Story B2§5.2.2
### TC-FON-000125 回滚前自动状态快照
- 级别API
- 步骤:`S01` 触发 rollback`S02` 校验先生成 pre-rollback 快照;`S03` 审计 `ROLLBACK`
- 预期:先存档再回退。
### TC-FON-000126 单租户回滚不影响其他租户
- 级别API
- 步骤:`S01` 选 t1 回滚;`S02` 校验 t1 数据版本回退;`S03` 其他租户版本不变
- 预期scope 严格。
### TC-FON-000127 回滚需 MFA 二次验证
- 级别API
- 步骤:`S01` 无 step-up`S02` 412 `MFA_REQUIRED`
- 预期:高危操作统一门槛。
### TC-FON-000128 回滚事件报告字段齐全
- 级别API
- 步骤:`S01` 完成回滚;`S02` `GET /admin/system/versions/events/{id}/``S03` 校验失败原因/耗时/受影响租户齐全
- 预期:报告契约满足。
---
## O. 版本总览Story B3§5.4.1 #11
### TC-FON-000129 平台基础数据版本展示
- 级别E2E
- 步骤:`S01` 进入 `/admin/system/versions/``S02` 校验 Part1 卡片含版本号/最后升级时间/描述/执行人
- 预期4 字段完整。
### TC-FON-000130 各租户数据版本筛选『待升级 / 升级失败』
- 级别API+E2E
- 步骤:`S01``state=pending``S02``state=failed``S03` 点击行跳转该租户备份记录 Tab
- 预期:跳转到 `/admin/tenants/{id}/backups/`
### TC-FON-000131 版本数据 5 分钟缓存延迟可接受
- 级别API
- 步骤:`S01` 升级某租户;`S02` 立即查总览;`S03` 校验 ≤ 5 分钟内必更新
- 预期:缓存上限符合 §B3。
---
## P. 客户端发布Story C1§5.3.1
### TC-FON-000132 上传 EXE 自动计算 SHA-256
- 级别API
- 步骤:
- `TC-FON-000132-S01` `POST /admin/api/client-releases/` multipart 上传 `.exe`
- `TC-FON-000132-S02` 校验响应 `checksum_sha256` 为 64 hex 且与本地 sha256 一致
- `TC-FON-000132-S03` DB `client_releases.checksum_sha256 NOT NULL`
- 预期:强制 SHA-256`ADR-20260430-008`)。
### TC-FON-000133 SemVer 格式严格校验
- 级别API
- 步骤:`S01` 提交 `1.2``S02` 提交 `v1.2.3`(带前缀 v`S03` 提交 `1.2.3-alpha`
- 预期:仅 `X.Y.Z` 通过;其他全部 422 `RELEASE_INVALID_VERSION`
### TC-FON-000134 同 (platform, arch) 仅一个 published 版本
- 级别API
- 步骤:
- `TC-FON-000134-S01` v1.5.0 已 published
- `TC-FON-000134-S02` 直接 publish v1.6.0
- `TC-FON-000134-S03` 校验 v1.5.0 自动转 `offline`,唯一索引 `idx_client_releases_published` 不冲突
- 预期:单 published 不变量保持。
### TC-FON-000135 草稿态不对外可见
- 级别API
- 步骤:`S01` 客户端 `GET /api/release/v1/latest``S02` 校验仅返回 published`S03` draft 不在响应内
- 预期:状态隔离。
### TC-FON-000136 已发布版本回滚把当前版本置为已下线
- 级别API
- 步骤:`S01` 当前 published=v1.6.0`S02` 对 v1.5.0 调 rollback`S03` 校验 v1.5.0=published, v1.6.0=offline + 审计两条
- 预期:状态切换 + 审计成对。
### TC-FON-000137 上传体积超 500MB 拒绝
- 级别API
- 步骤:`S01` 上传 501MB`S02` 校验 413 + `RELEASE_FILE_TOO_LARGE`
- 预期:边界严格。
### TC-FON-000138 更新日志 Markdown 上限 2000 字
- 级别API
- 步骤:`S01` 提交 2001 字;`S02` 校验 422
- 预期:长度阈值。
### TC-FON-000139 便携版 ZIP 可选上传
- 级别API
- 步骤:`S01` 仅 EXE`S02` EXE+ZIP`S03` 校验两种均可成功
- 预期:可选字段。
### TC-FON-000140 客户端获取最新版本接口契约
- 级别API
- 步骤:
- `TC-FON-000140-S01` `GET /api/release/v1/latest?platform=win32&arch=x64`
- `TC-FON-000140-S02` 校验响应含 `version / download_url / checksum_sha256 / is_force_update / min_compatible_version / changelog`
- `TC-FON-000140-S03` 校验 `download_url``download.fonrey.com` CDN
- 预期:客户端契约稳定。
---
## Q. 客户端分布与活跃榜Story C2§5.3.2
### TC-FON-000141 Heartbeat Upsert 锚点 (tenant_id, device_id)
- 级别API
- 步骤:
- `TC-FON-000141-S01` 同一 device_id 在 1 小时内上报 5 次
- `TC-FON-000141-S02` 校验 `client_heartbeats` 该 (tenant_id, device_id) 唯一行被 update 而非 insert
- `TC-FON-000141-S03` 校验 `last_seen_at` 单调递增
- 预期:与 `ADR-20260430-007` 一致。
### TC-FON-000142 24h 活跃口径聚合饼图
- 级别API
- 步骤:`S01` 构造 last_seen 跨 24h 边界的 4 条;`S02` `GET /admin/api/client-releases/distribution/``S03` 校验仅最近 24h 心跳计入活跃
- 预期24h 窗口准确。
### TC-FON-000143 全平台租户活跃榜按活跃数降序
- 级别API
- 步骤:`S01` 构造 3 个 active 租户活跃数 [50,200,80]`S02` 拉取活跃榜;`S03` 校验排序为 [200,80,50]
- 预期:仅 active 租户参与,排序正确。
### TC-FON-000144 活跃榜支持历史装机总数列
- 级别API
- 步骤:`S01` 查询;`S02` 校验返回 `tenant_code / 名称 / active_24h / install_total_all_time`
- 预期:四字段齐全。
### TC-FON-000145 单租户版本分布钻取
- 级别E2E
- 步骤:`S01` 在分布页选某租户;`S02` 校验该租户内员工的客户端版本饼图;`S03` 数据来源 `client_heartbeats.tenant_id` 维度聚合
- 预期:钻取 tenant_id 准确。
---
## R. 强制更新§5.3.3
### TC-FON-000146 推送强制更新需 MFA + 二次确认
- 级别API+E2E
- 步骤:
- `TC-FON-000146-S01` 选 < v1.5.0 推送强制;`S02` 校验弹出二次确认弹窗;`S03` 校验 412 `MFA_REQUIRED`,通过 step-up 后审计 `FORCE_UPDATE_PUSH`
- 预期:双重保护。
### TC-FON-000147 客户端 latest 接口对低版本返回 is_force_update=true
- 级别API
- 步骤:`S01` 推送强制后;`S02` 客户端 v1.4.0 调 latest`S03` 校验 `is_force_update=true`
- 预期:服务端契约驱动客户端强制升级。
---
## S. 审计日志Story D1§5.7
### TC-FON-000148 审计日志多维筛选 + CSV 导出
- 级别API
- 步骤:
- `TC-FON-000148-S01``operator_id + time_range + action_type=HARD_DELETE_TENANT`
- `TC-FON-000148-S02` `GET /admin/audit-logs/export.csv`
- `TC-FON-000148-S03` 校验 CSV 列含 operator/operator_name/action_type/target_type/target_id/target_name/payload_summary/result/error_message/ip_address/created_at
- 预期:字段对齐 §5.7 + DDL。
### TC-FON-000149 审计日志不可变DB 触发器层)
- 级别API
- 步骤:
- `TC-FON-000149-S01` 直接尝试 `UPDATE platform_audit_logs SET ...`
- `TC-FON-000149-S02` 校验抛 `IntegrityError`BEFORE UPDATE OR DELETE 触发器)
- `TC-FON-000149-S03` 应用层 Manager `update()/delete()` 也抛 `AUDIT_IMMUTABLE`
- 预期:双层不可变保证。
### TC-FON-000150 审计 action_type 全集落库
- 级别API
- 步骤:`S01` 触发 16 个动作CREATE/SUSPEND/RESUME/DELETE/HARD_DELETE/RESTORE/UPGRADE/ROLLBACK/RESET_PASSWORD/RELEASE/OFFLINE/FORCE_UPDATE/UPDATE_LICENSE/UPDATE_LICENSE_USER_LIMIT/AUTO_SUSPEND_LICENSE_EXPIRED/CREATE_ADMIN`S02` 校验 16 行写入;`S03` 校验所有 `target_type` 落入 {Tenant,User,System,Backup,ClientRelease,Admin}
- 预期:枚举与 PRD §5.7 + DDL 一致。
---
## T. 安全§5.6
### TC-FON-000151 Platform Admin 强制登出指定管理员
- 级别API
- 步骤:
- `TC-FON-000151-S01` 操作员 X 已登录持有 Session
- `TC-FON-000151-S02` Platform Admin 调 `POST /admin/settings/admins/{X}/force-logout/`
- `TC-FON-000151-S03` X 下一请求被重定向 `/admin/login/`Session 失效;审计 `FORCE_LOGOUT`
- 预期:会话即刻失效。
---
## U. 角色矩阵交叉§6
### TC-FON-000152 三角色权限矩阵全覆盖回归
- 级别API
- 前置:分别准备 Platform Admin / 运营人员 / 只读审计员 三账号
- 步骤:
- `TC-FON-000152-S01` 以 Platform Admin 身份遍历 §6 全部 21 行操作,校验 ✅ 行返回 200/202、❌ 行返回 403
- `TC-FON-000152-S02` 以运营人员身份遍历同 21 行,校验:硬删除/数据恢复/系统升级/系统回滚/客户端发布/客户端回滚/强制更新/管理员账号/强制登出/IP 白名单 共 10 项 403其余 ✅ 行 200/202
- `TC-FON-000152-S03` 以只读审计员身份遍历同 21 行校验仅『查看客户端版本分布与活跃榜』『查看审计日志』『导出审计日志』3 项 200其他 18 项 403
- 预期:三角色行为与 §6 矩阵 100% 一致;任一格偏差视为 P0。
---
## 5. 状态机断言(贯穿用例)
> §7 租户状态机所有合法跃迁与非法跃迁已在用例 D/E/F/H/M/N 中以正向 + 反向覆盖:
> - Creating→ActiveD 系列
> - Active→SuspendedE 系列、F 系列
> - Suspended→ActiveE93、F95自动恢复
> - Active→Pending DeleteH102
> - Pending Delete→ActiveH103撤销
> - Pending Delete→DeletedH105硬删除立即态/ 冷静期到期清除(已并入 §6.4 定时任务,不独立列用例)
> - 非法跃迁(如 Deleted→Active由 DDL CHECK + 视图 422 双重保障,已在 D87创建失败副断言覆盖
---
## 6. 工程实现指引(给测试开发工程师)
1. **目录建议**
- `tests/integration/platform_admin/test_tc_fon_000073_000152.py`
- `tests/e2e/platform_admin/test_tc_fon_000073_000152.spec.ts`
- `tests/integration/release_api/test_tc_fon_release_endpoints.py`C1/C2/R 系列)
2. **命名规范**
- 函数名带用例 ID`def test_tc_fon_000086_create_tenant_success_within_60s():`
3. **公共夹具**
- `platform_admin_client` / `ops_operator_client` / `read_only_auditor_client`:三角色已登录、已通过 MFA 的 `Client` 实例
- `with_step_up_token` 装饰器:注入 5 分钟内 TOTP 验证状态
- `tenant_factory(status='active')` / `client_release_factory(state='draft')`
4. **步骤日志**
- 每步执行前后打印 step_id断言失败时把 step_id 写入异常消息
5. **报告聚合**
- 生成 `reports/platform_admin_run_<run_id>.json` + `reports/platform_admin_run_<run_id>.html`
- 汇总分组AV21 组)
6. **CI 门禁**
- `TC-FON-000073` ~ `TC-FON-000152` 全量通过且零 `blocked` 才允许合并到主干
---
## 7. 变更规则
- 新增本模块用例:从 `TC-FON-000153` 开始递增(同步更新 `TEST_CASE_REGISTRY.md` 水位)
- 后续模块继续用同一全局序列,不得重号
- 禁止删除历史用例 ID可标记 `deprecated` 但保留编号与历史报告可追溯性
- PRD §5 任意小节字段或验收标准变更 → 必须同步本文档对应用例并提升 `v1.x`

View File

@@ -1,221 +0,0 @@
# Fonrey 客户端发布模块测试用例文档(可自动化)
> 文档版本v1.0
> 适用范围:`PRD/发布管理/客户端发布管理模块PRD.md` v1.2
> 用例编号范围:`TC-FON-000049` ~ `TC-FON-000072`(全局唯一)
> 编号规范:`TEST_CASES/TEST_CASE_ID_SPEC.md`
---
## 变更历史
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
## 1. 目标与原则
1. 覆盖客户端发布模块 P0下载安装、自动升级、版本发布、版本分布统计。
2. 每个用例必须可追溯到 `test_case_id + step_id`
3. 优先 API 集成自动化,客户端壳层 UI 能力以 E2E/手工补充。
4. 失败报告必须定位到具体步骤,并保留请求响应或截图证据。
---
## 2. 自动化执行与报告要求(强制)
- 执行层:
- API 集成:`pytest + pytest-django`public schema
- 客户端 E2E后续 `playwright + Electron`(当前先预留)
- 每步输出:`run_id / test_case_id / step_id / status / expected_result / actual_result / error_message`
- 失败附件:
- API请求体、响应体、关键日志路径
- E2E截图与视频路径
---
## 3. 客户端发布模块 API 端点口径(以 PRD/TECH_STACK 为准)
- `GET /api/release/updates/latest/`
- `GET /api/release/updates/`
- `POST /api/release/updates/`
- `PATCH /api/release/updates/{id}/`
- `POST /api/release/updates/{id}/rollback/`
- `POST /api/release/heartbeats/`
- `GET /api/release/metrics/version-distribution/`
- `GET /api/release/metrics/tenant-installs/`
- `GET /api/release/metrics/tenant-leaderboard/`
---
## 4. 全量测试用例清单24条
## A. 下载与完整性Story 1
### TC-FON-000049 官网下载页可访问且展示最新版本
- 级别E2E
- 步骤:
- `TC-FON-000049-S01` 访问官网客户端下载页
- `TC-FON-000049-S02` 校验展示版本号/发布日期/下载按钮
- `TC-FON-000049-S03` 校验下载按钮链接可用
- 预期:页面可访问,元信息完整
### TC-FON-000050 latest 接口返回下载地址与校验值
- 级别API
- 步骤:
- `TC-FON-000050-S01` 调用 `GET /api/release/updates/latest/?platform=win32&arch=x64&current_version=1.2.0`
- `TC-FON-000050-S02` 校验返回 `download_url``checksum_sha256``release_notes`
- `TC-FON-000050-S03` 校验 `checksum_sha256` 长度为 64
- 预期:响应字段完整且格式正确
### TC-FON-000051 安装包 SHA256 校验通过
- 级别API+脚本
- 步骤:
- `TC-FON-000051-S01` 获取 `download_url``checksum_sha256`
- `TC-FON-000051-S02` 下载文件并计算本地 SHA256
- `TC-FON-000051-S03` 比对本地值与接口返回值
- 预期:一致,允许进入安装流程
### TC-FON-000052 安装包 SHA256 不一致时拒绝安装
- 级别:客户端集成
- 步骤:
- `TC-FON-000052-S01` 构造篡改包或错误 checksum
- `TC-FON-000052-S02` 触发更新下载完成后的校验
- `TC-FON-000052-S03` 校验客户端拒绝安装并保留当前版本
- 预期:提示完整性校验失败,不覆盖现版本
---
## B. 自动升级Story 3
### TC-FON-000053 无新版本时返回 has_update=false
- 级别API
- 步骤:`S01` 用当前最新版本调用 latest`S02` 校验 `has_update=false``S03` 校验 `latest_version` 存在
- 预期:不触发更新
### TC-FON-000054 有新版本时返回升级信息
- 级别API
- 步骤:`S01` 用旧版本调用 latest`S02` 校验 `has_update=true``S03` 校验 `latest_version` 高于 `current_version`
- 预期:触发可升级状态
### TC-FON-000055 普通更新允许“稍后提醒”
- 级别:客户端 E2E
- 步骤:`S01` 服务端发布 `release_type=normal``S02` 客户端检测更新;`S03` 校验出现“立即更新/稍后提醒”
- 预期:普通更新可延期
### TC-FON-000056 强制更新不允许“稍后提醒”
- 级别:客户端 E2E
- 步骤:`S01` 服务端发布 `release_type=force``S02` 客户端检测更新;`S03` 校验仅可“立即更新”
- 预期:必须升级后继续使用
### TC-FON-000057 更新下载失败时保持当前版本可用
- 级别:客户端 E2E
- 步骤:`S01` 模拟下载中断/磁盘不足;`S02` 校验错误提示;`S03` 校验当前版本仍可正常操作
- 预期:失败可恢复,不影响业务使用
### TC-FON-000058 更新成功后重启生效并保留登录态
- 级别:客户端 E2E
- 步骤:`S01` 执行更新并下载完成;`S02` 重启客户端;`S03` 校验标题版本号更新且会话恢复
- 预期:升级成功且无需重新登录
---
## C. 平台发布管理Story 4
### TC-FON-000059 平台管理员可创建草稿版本
- 级别API
- 步骤:`S01` 调用 `POST /api/release/updates/` 创建草稿;`S02` 校验 `status=draft``S03` 校验版本记录可在列表查询到
- 预期:草稿创建成功
### TC-FON-000060 版本号必须符合 SemVer
- 级别API
- 步骤:`S01` 提交非法版本号(如 `1.2``S02` 校验 400`S03` 校验错误码 `RELEASE_VERSION_INVALID`
- 预期:非法版本号被拒绝
### TC-FON-000061 发布后 latest 接口即时可见
- 级别API
- 步骤:`S01` 将草稿改为 `published`PATCH`S02` 调用 latest`S03` 校验返回该版本
- 预期:发布即时生效
### TC-FON-000062 同平台架构仅允许一个 published
- 级别API
- 步骤:`S01` 已有一个 published`S02` 尝试并发发布第二个;`S03` 校验冲突409
- 预期:唯一生效版本约束成立
### TC-FON-000063 版本下线后 latest 不再返回该版本
- 级别API
- 步骤:`S01` 将已发布版本改 `archived``S02` 调用 latest`S03` 校验不返回已下线版本
- 预期:下线立即生效
### TC-FON-000064 回滚接口将历史版本恢复为 published
- 级别API
- 步骤:`S01` 调用 `POST /api/release/updates/{id}/rollback/``S02` 校验目标版本为 `published``S03` 校验原版本自动下线
- 预期:回滚原子完成
---
## D. Heartbeat 与版本分布Story 5
### TC-FON-000065 启动心跳首次上报创建记录
- 级别API
- 步骤:`S01` 调用 `POST /api/release/heartbeats/``S02` 校验写入 `(tenant_id, device_id)``S03` 校验 `launch_count=1`
- 预期:首次上报成功
### TC-FON-000066 启动心跳重复上报走 Upsert
- 级别API
- 步骤:`S01` 对同 `(tenant_id,device_id)` 再次上报;`S02` 校验未新增新行;`S03` 校验 `launch_count+1``last_seen_at` 刷新
- 预期:符合 Upsert 语义
### TC-FON-000067 版本分布统计按 24h 活跃口径
- 级别API
- 步骤:`S01` 构造多版本心跳数据(含超 24h`S02` 调用 `GET /api/release/metrics/version-distribution/``S03` 校验仅统计最近 24h 活跃
- 预期:统计口径正确
### TC-FON-000068 指定租户安装统计返回活跃+历史总数
- 级别API
- 步骤:`S01` 调用 `GET /api/release/metrics/tenant-installs/?tenant_id=...``S02` 校验返回 `active_install_count_24h``S03` 校验返回 `historical_install_count`
- 预期:两个指标均返回且数值正确
### TC-FON-000069 租户活跃榜按活跃安装数降序
- 级别API
- 步骤:`S01` 调用 `GET /api/release/metrics/tenant-leaderboard/``S02` 校验字段包含 `tenant_code/tenant_name/active/historical``S03` 校验按活跃安装数降序
- 预期:排序与字段正确
### TC-FON-000070 心跳接口拒绝未鉴权请求
- 级别API
- 步骤:`S01` 未带会话访问 heartbeats`S02` 校验 401/403`S03` 校验无数据写入
- 预期:防伪造上报有效
### TC-FON-000071 发布管理接口拒绝非平台管理员
- 级别API
- 步骤:`S01` 租户管理员调用 `POST /api/release/updates/``S02` 校验 403`S03` 校验无新版本记录
- 预期:权限边界正确
### TC-FON-000072 下载检测接口限流生效
- 级别API
- 步骤:`S01` 同 IP 高频访问 latest`S02` 校验前 N 次成功;`S03` 校验超阈值 429
- 预期:触发限流,保护服务
---
## 5. 工程实现指引(给测试开发工程师)
1. **目录建议**
- `tests/integration/release/test_us_release.py`
- `tests/e2e/release/test_tc_fon_000049_000072.spec.ts`(预留)
2. **命名规范**
- 函数名必须带用例ID例如`def test_tc_fon_000062_single_published_constraint():`
3. **步骤日志**
- 每步打印 step_id断言失败消息包含 step_id
4. **报告产物**
- `reports/release_run_<run_id>.json`
5. **CI 门禁(阶段性)**
- P0 API 用例000050, 000053~000054, 000059~000069, 000070~000072必须全过
---
## 6. 变更规则
- 本模块新增用例从 `TC-FON-000073` 继续递增
- 全项目继续使用单一序列,不得重号
- 历史 ID 仅可 `deprecated`,不得复用

View File

@@ -11,6 +11,8 @@
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
| 2026-05-02 | Sisyphus | 登记 BATCH-PLATFORM-ADMIN-001TC-FON-000073~000152共 80 条);水位推进至 000152下一可用号 000153 |
| 2026-05-02 | Vulcan | 登录模块用例升级到 v2.0:登记 BATCH-LOGIN-002TC-FON-000153~000172共 20 条);水位推进至 000172下一可用号 000173 |
## 1) 全局规则(强制)
@@ -24,21 +26,23 @@
## 2) 当前编号水位
- **已分配到**`TC-FON-000072`
- **下一个可用编号**`TC-FON-000073`
- **最后更新人**Atlas
- **最后更新时间**2026-04-30
- **已分配到**`TC-FON-000172`
- **下一个可用编号**`TC-FON-000173`
- **最后更新人**Vulcan
- **最后更新时间**2026-05-02
> 说明:下一个新增用例(不论哪个模块)都应从 `TC-FON-000073` 开始。
> 说明:下一个新增用例(不论哪个模块)都应从 `TC-FON-000173` 开始。
---
## 3) 编号段注册总览(按批次)
| 批次ID | 模块 | 编号范围 | 数量 | 状态 | 文档 |
|---|---|---:|---:|---|---|
| BATCH-LOGIN-001 | 登录模块 | TC-FON-000001 ~ TC-FON-000048 | 48 | active | `TEST_CASES/TEST_CASES_LOGIN_MODULE.md` |
| BATCH-RELEASE-001 | 客户端发布模块 | TC-FON-000049 ~ TC-FON-000072 | 24 | active | `TEST_CASES/TEST_CASES_RELEASE_MODULE.md` |
| 批次ID | 模块 | 编号范围 | 数量 | 状态 | 文档 |
| ----------------- | ------- | ----------------------------: | --: | ------ | ----------------------------------------- |
| BATCH-LOGIN-001 | 登录模块 | TC-FON-000001 ~ TC-FON-000048 | 48 | active | `TEST_CASES/TEST_CASES_LOGIN_MODULE.md` |
| BATCH-PLATFORM-ADMIN-001 | 平台管理后台 | TC-FON-000073 ~ TC-FON-000152 | 80 | active | `TEST_CASES/TEST_CASES_PLATFORM_ADMIN_MODULE.md` |
| BATCH-LOGIN-002 | 登录模块 | TC-FON-000153 ~ TC-FON-000172 | 20 | active | `TEST_CASES/TEST_CASES_LOGIN_MODULE.md` |
**状态枚举**
- `active`:有效且执行中
@@ -51,12 +55,15 @@
> 当前先采用“编号段注册”。若后续需要逐号追踪,可在本节追加明细表。
| test_case_id | 模块 | 标题 | 状态 | 首次版本 | 备注 |
|---|---|---|---|---|---|
| TC-FON-000001 | 登录 | Tenant Code 页面首启展示 | active | v1.0 | 见登录用例文档 |
| TC-FON-000048 | 登录 | 验证码登录成功/失败/锁定限制 | active | v1.0 | 见登录用例文档 |
| TC-FON-000049 | 客户端发布 | 官网下载页可访问且展示最新版本 | active | v1.0 | 见发布模块用例文档 |
| TC-FON-000072 | 客户端发布 | 下载检测接口限流生效 | active | v1.0 | 见发布模块用例文档 |
| test_case_id | 模块 | 标题 | 状态 | 首次版本 | 备注 |
| ------------- | ----- | ------------------ | ------ | ---- | --------- |
| TC-FON-000001 | 登录 | Tenant Code 页面首启展示 | active | v1.0 | 见登录用例文档 |
| TC-FON-000048 | 登录 | 验证码登录成功/失败/锁定限制 | active | v1.0 | 见登录用例文档 |
| TC-FON-000073 | 平台管理后台 | 平台后台登录页强制 IP 白名单 | active | v1.0 | 见平台管理后台用例文档 |
| TC-FON-000152 | 平台管理后台 | 三角色权限矩阵全覆盖回归§6 全 21 行) | active | v1.0 | 见平台管理后台用例文档 |
| TC-FON-000153 | 登录 | 首次登录改密提交接口成功 | active | v2.0 | 见登录用例文档(补充批次) |
| TC-FON-000172 | 登录 | 微信预留端点未开放qrcode/callback | active | v2.0 | 见登录用例文档(补充批次) |
---
@@ -85,3 +92,5 @@
|---|---|---|
| 2026-04-30 | Atlas | 初始化注册表;登记登录模块 000001~000048下一号设为 000049 |
| 2026-04-30 | Atlas | 新增客户端发布模块测试用例文档;登记 000049~000072下一号设为 000073 |
| 2026-05-02 | Sisyphus | 新增『平台管理后台』测试用例文档80 条,覆盖 11 Stories + §5.6 安全 + §5.7 审计 + §6 角色矩阵 + §7 状态机);登记 BATCH-PLATFORM-ADMIN-001TC-FON-000073~000152水位推进至 000152下一号设为 000153 |
| 2026-05-02 | Vulcan | 登录模块测试用例升级到 v2.0:新增 20 条补充用例覆盖强制改密提交端点、管理员手动解锁、OTP 哈希存储、失败响应统一 envelope、微信预留端点不开放等登记 BATCH-LOGIN-002TC-FON-000153~000172水位推进至 000172下一号设为 000173 |

View File

@@ -91,8 +91,8 @@
</div>
</header>
<!-- ============ 主容器 ============ -->
<div class="flex">
<!-- ============ 主容器 ============ -->
<div class="flex">
<!-- 侧边栏 -->
<aside class="w-56 shrink-0 border-r border-neutral-200 bg-white min-h-[calc(100vh-3.5rem)]">
<nav class="p-3 space-y-0.5 text-sm">

View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1366" />
<title>Fonrey UI 页面主页</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
50: '#F0FDFA',
100: '#CCFBF1',
200: '#99F6E4',
500: '#14B8A6',
600: '#0F766E',
700: '#115E59',
800: '#134E4A'
},
neutral: {
50: '#F8FAFC',
100: '#F1F5F9',
200: '#E2E8F0',
300: '#CBD5E1',
400: '#94A3B8',
500: '#64748B',
600: '#475569',
700: '#334155',
800: '#1E293B',
900: '#0F172A'
}
}
}
}
};
</script>
<style>
body { background: #F8FAFC; color: #0F172A; }
</style>
</head>
<body class="antialiased text-sm">
<header class="sticky top-0 z-10 bg-primary-800 border-b border-primary-700">
<div class="max-w-[1440px] mx-auto px-6 h-14 flex items-center justify-between">
<div class="flex items-center gap-2 text-white">
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-sm font-semibold">F</div>
<span class="text-base font-semibold">Fonrey · UI 页面主页</span>
</div>
<div class="text-xs text-primary-100">集中收录所有 UI 原型页面</div>
</div>
</header>
<main class="max-w-[1440px] mx-auto px-6 py-6 space-y-6">
<section class="bg-white border border-neutral-200 rounded-lg p-4">
<h1 class="text-xl font-semibold text-neutral-900">主页 HTML导航索引</h1>
<p class="mt-1 text-xs text-neutral-500">说明:本页用于收录所有业务 UI 页面入口。<code class="bg-neutral-100 px-1 rounded">preview.html</code> 作为 UI System 参考页保留,不再改动。</p>
</section>
<section class="grid grid-cols-3 gap-4">
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<h2 class="font-semibold text-neutral-900 mb-3">登录管理3</h2>
<div class="space-y-2 text-xs">
<a href="./登录_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">登录识别页</a>
<a href="./登录_账号密码_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">账号密码登录</a>
<a href="./登录_重置密码_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">重置密码</a>
</div>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<h2 class="font-semibold text-neutral-900 mb-3">房源管理3</h2>
<div class="space-y-2 text-xs">
<a href="./房源列表_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">房源列表</a>
<a href="./房源详情_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">房源详情</a>
<a href="./新增房源_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">新增房源</a>
</div>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<h2 class="font-semibold text-neutral-900 mb-3">客源管理4</h2>
<div class="space-y-2 text-xs">
<a href="./客源列表_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">客源列表</a>
<a href="./客源详情_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">客源详情</a>
<a href="./新增客源_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">新增客源</a>
<a href="./编辑客源_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">编辑客源</a>
</div>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<h2 class="font-semibold text-neutral-900 mb-3">楼盘管理3</h2>
<div class="space-y-2 text-xs">
<a href="./区域管理_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">区域管理</a>
<a href="./楼盘列表_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">楼盘列表</a>
<a href="./楼盘详情_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">楼盘详情</a>
</div>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<h2 class="font-semibold text-neutral-900 mb-3">组织与系统4</h2>
<div class="space-y-2 text-xs">
<a href="./组织人事_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">组织人事</a>
<a href="./权限管理_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">权限管理</a>
<a href="./首页设置_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">首页设置</a>
<a href="./系统配置_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">系统配置</a>
</div>
</article>
<article class="bg-white border border-neutral-200 rounded-lg p-4">
<h2 class="font-semibold text-neutral-900 mb-3">平台管理后台2</h2>
<div class="space-y-2 text-xs">
<a href="./平台管理后台/平台管理后台_UI.html" class="block px-3 py-2 rounded border border-primary-600 bg-primary-50 text-primary-700 hover:bg-primary-100">平台管理后台(模块目录)</a>
<a href="./平台管理后台_UI.html" class="block px-3 py-2 rounded border border-neutral-200 hover:bg-neutral-50">平台管理后台(根目录副本)</a>
</div>
</article>
</section>
<section class="bg-white border border-neutral-200 rounded-lg p-4">
<h2 class="font-semibold text-neutral-900 mb-2">参考页面(不改动)</h2>
<a href="./preview.html" class="inline-flex items-center px-3 py-2 text-xs rounded border border-neutral-200 hover:bg-neutral-50">UI System Preview颜色/布局规范参考)</a>
</section>
</main>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,448 @@
# 平台管理后台 UI 设计文档
> **模块**平台管理后台Platform Admin Console
> **输出文件**`UI_DESIGN/平台管理后台/平台管理后台_UI.md`
> **设计基线**`UI_SYSTEM/UI_SYSTEM.md`、`UI_SYSTEM/组件规范设计.md`
> **需求依据**`PRD/平台管理后台/平台管理后台PRD.md`
> **数据模型依据**`DATA_MODEL/DATA_MODEL_PUBLIC.md`
> **定位声明**:本界面面向**平台管理员/运营/审计员**,非终端经纪人界面
---
## 1. 设计目标与范围
### 1.1 目标
围绕平台侧管理链路,提供一套高密度、强审计、低误操作风险的后台 UI覆盖
1. 租户全生命周期管理(开通/挂起/恢复/删除/License
2. 平台版本治理(灰度升级、回滚、版本总览)
3. 数据备份恢复与导出
4. 客户端发布治理与版本分布监控
5. 平台监控与告警
6. 审计日志与管理员安全设置MFA/IP 白名单/会话)
### 1.2 非目标UI 层)
- 不设计租户内业务页面(房源、客源等终端业务流)
- 不设计移动端适配(桌面优先,`>=1280px`
- 不设计主题切换入口(保持后台浅色高对比工作流)
---
## 2. 管理员后台设计原则Admin-first
1. **可控优先**:所有高危操作(删除、恢复、回滚、强制更新)采用统一二次确认 + MFA。
2. **信息密度优先**:列表和状态总览优先,卡片用于关键 KPI 快速判断。
3. **风险分级可视化**:高危动作使用 `danger` 语义色,关键状态统一 Badge 体系。
4. **可追溯**:写操作入口旁可快速进入审计记录,避免“做了但查不到”。
5. **可扩展 Dashboard**:仪表盘采用模块化组件插槽,便于后续新增运营看板。
---
## 3. 全局信息架构IA
## 3.1 页面路由与导航分组
### 一级导航Sidebar
- 概览看板
- 租户管理
- 系统版本
- 备份与恢复
- 客户端发布
- 监控与告警
- 审计日志
- 管理员设置
> Topbar 仅承载品牌、全局告警与管理员信息,不承担页面切换。
### 页面清单(与 PRD 对齐)
1. `/admin/login` 登录页
2. `/admin` 仪表盘
3. `/admin/tenants` 租户列表
4. `/admin/tenants/new` 新建租户
5. `/admin/tenants/{id}` 租户详情-基本信息
6. `/admin/tenants/{id}/users` 租户详情-用户管理
7. `/admin/tenants/{id}/plan` 租户详情-套餐信息
8. `/admin/tenants/{id}/monitoring` 租户详情-监控
9. `/admin/tenants/{id}/backups` 租户详情-备份记录
10. `/admin/tenants/{id}/history` 租户详情-操作历史
11. `/admin/system/versions` 系统版本管理(含版本总览)
12. `/admin/system/backups` 备份管理
13. `/admin/monitoring` 监控与告警
14. `/admin/client-releases` 客户端版本管理
15. `/admin/audit-logs` 审计日志
16. `/admin/settings/admins` 管理员设置
## 3.2 页面壳层规范(复用 UI_SYSTEM
- Topbar`h-14``bg-primary-800`,固定顶部
- Sidebar展开 `w-60`,折叠 `w-16`
- Main展开态 `ml-60`,折叠态 `ml-16`
- 主内容区:`bg-neutral-50``px-6 py-4`
- 列表工具栏:`sticky top-14 z-30`
> 仅替换导航文案与菜单结构,壳层尺寸、栅格、颜色 token 不做新发明。
---
## 4. 关键页面 UI 设计
## 4.1 登录页(平台管理员)
### 页面结构
- 左侧品牌区(平台管理后台标识)
- 右侧登录卡片(账号 + 密码)
- 登录成功后若未配置 MFA强制进入 MFA 设置向导
### 交互要点
- 登录失败提示明确(账号/密码错误、IP 不在白名单)
- 首次登录强制绑定 TOTP
- 会话超时 30 分钟后自动跳回登录页
---
## 4.2 仪表盘(可扩展)
### v1 固定模块
1. **全局概览 Stat Card**
- 总租户数
- 活跃租户数
- 本月新增
2. **系统健康面板**(服务状态)
- Django / PostgreSQL / Redis / Celery / R2
3. **近期告警列表24h**
4. **资源概览趋势**(存储/API 调用)
5. **客户端覆盖**(活跃安装数、最新版本占比)
6. **最近高危操作10条**
### Dashboard 扩展位(预留)
- `widget-slot-a`(左侧 2/3 宽)
- `widget-slot-b`(右侧 1/3 宽)
- `widget-slot-footer`(全宽)
新增 dashboard 时仅允许以「卡片组件 + 图表组件 + 列表组件」组合,不新增壳层。
---
## 4.3 租户列表
### 筛选区
- 关键词:公司名称 / Tenant Code / 联系邮箱
- 状态Active / Suspended / Pending Delete / Failed
- 套餐Basic / Professional / Enterprise
- 到期筛选即将到期15天内
- 用户数筛选:用户数已满(`current >= license_user_limit`
### 表格列
- 公司名称
- Tenant Code
- 套餐
- 状态
- License 到期日
- 活跃用户数 / License 上限
- 客户端最新版本占比
- 操作(详情 / 挂起 / 备份 / 导出)
### 批量与行操作规则
- 挂起:需要原因(欠费/违规/主动申请/其他)
- 恢复:可直接恢复或按策略恢复
- 删除:软删除默认;硬删除仅 Platform Admin 且需 MFA
---
## 4.4 租户详情(单页分区,无二级 Tab
租户详情在同一详情页内按信息分区纵向展示:基本信息 / 用户管理 / 套餐信息 / 监控 / 备份记录 / 操作历史。
页面切换统一通过 Sidebar 完成,不在详情抽屉中再设置二级 Tab。
### A. 基本信息
- 公司与联系人信息
- License 到期日期
- License 授权用户上限
- 当前用户数(含 Tenant Admin + Agent
- 租户访问链接(`https://app.fonrey.com/?tenant={TenantCode}`
- 入驻信息 PDF 下载
### B. 用户管理
- Tenant Admin 列表
- 用户列表
- 新增/替换/撤销 Tenant Admin
- 重置密码(链接 / 临时密码)
### C. 套餐信息
- 当前套餐
- 升级路径与差异对比
- 生效方式(立即 / 下一账期)
### D. 监控
- CPU/内存、存储、API 调用、慢查询、异常请求
### E. 备份记录
- 备份时间、触发方式、大小、状态
- 手动备份
- 恢复入口(仅 Platform AdminMFA
### F. 操作历史
- 该租户相关操作审计流
---
## 4.5 系统版本管理(含版本总览)
### Part 1 平台基础数据版本
- 版本号
- 最后升级时间
- 升级描述
- 执行人
### Part 2 租户数据升级版本
- 租户名称
- 当前数据版本
- 上次升级时间
- 升级状态(最新/待升级/升级中/升级失败)
- 支持筛选:待升级、升级失败
### 升级操作区
- 升级包管理
- 升级策略(全量/灰度)
- 灰度租户名单
- 实时进度(批次 + 租户维度)
- 一键回滚MFA
---
## 4.6 备份管理(全局)
### 配置区
- 频率:每小时 / 每日 / 每周
- 执行时间窗口
- 保留数量
- 存储目标local/s3/r2/gcs
### 列表区
- 备份任务状态pending/in_progress/success/failed
- 支持重试失败任务
- 支持按租户下钻
---
## 4.7 客户端版本管理
### 版本列表
- 版本号SemVer
- 版本类型(普通/强制)
- 状态(草稿/已发布/已下线)
- 发布时间
- 下载量
- 操作(发布/下线/编辑/回滚)
### 新增/编辑版本表单
- 版本号
- 最低兼容版本
- 安装包EXE
- 便携版ZIP可选
- SHA256自动生成
- 更新日志Markdown
- 内部发布说明
### 分布统计区Dashboard 风格)
- 版本分布(饼图/条形)
- 升级进度趋势
- 租户版本分布明细
- 全平台租户活跃榜tenant_code、活跃安装、历史装机
### 强制更新
- 支持按版本范围打标
- 强制更新推送需 MFA
---
## 4.8 监控与告警
### 监控维度
- 全局与租户维度切换
- 资源、错误率、SLA、慢查询
### 告警规则
- 指标 + 阈值 + 通知渠道(邮件/Webhook
- 告警历史可检索
---
## 4.9 审计日志
### 筛选项
- 操作人
- 时间范围
- 操作对象
- 操作类型(创建/修改/删除/高危)
- 结果(成功/失败)
### 列表字段
- 操作人
- 操作时间
- 对象类型/对象ID
- 内容摘要
- 结果
- 来源 IP
### 导出
- 按当前筛选导出 CSV
---
## 4.10 管理员设置
### 功能区
1. 管理员账号管理(创建/编辑/停用)
2. 角色分配super_admin / ops_operator / read_only_auditor
3. MFA 设备管理
4. IP 白名单管理CIDR
5. 活跃会话与强制登出
---
## 5. 高危操作统一交互模式(全局组件契约)
## 5.1 两段式确认
1. 首次确认弹窗(动作影响说明)
2. MFA 验证弹窗TOTP 输入)
## 5.2 高危动作清单
- 硬删除租户
- 数据恢复
- 系统回滚
- 客户端版本下线
- 强制更新推送
## 5.3 文案模板
- 标题:`确认执行高危操作?`
- 副文案:`该操作可能影响在线租户与数据可用性,请完成 MFA 验证后继续。`
- 按钮:`取消`Secondary / `继续并验证`Danger
---
## 6. 组件复用清单(来自 UI_SYSTEM/组件规范)
1. **Data Table**:租户列表、版本列表、审计日志
2. **Pagination**:统一 20/50/100 条
3. **Toolbar**:批量操作与快捷入口
4. **Section Navigation详情分区**:租户详情采用单页分区结构(无二级 Tab版本管理保持分区信息架构
5. **Modal Dialog**:确认与新建操作
6. **Drawer**:复杂编辑(规则、详情)
7. **Date Range Picker**:日志与统计筛选
8. **Stat Card**:仪表盘 KPI
> 禁止引入不在 UI_SYSTEM 定义的新视觉范式。
---
## 7. 数据模型映射DATA_MODEL_PUBLIC
| 页面能力 | 主要表 |
|---|---|
| 租户列表/详情/状态变更 | `public.tenants`, `public.tenant_status_logs` |
| 管理员登录/MFA/会话/IP 白名单 | `public.platform_admins`, `public.admin_mfa_devices`, `public.admin_sessions`, `public.ip_whitelist` |
| 审计日志 | `public.platform_audit_logs` |
| 备份与导出 | `public.backup_schedules`, `public.backup_records`, `public.export_tasks` |
| 系统版本与升级事件 | `public.system_versions`, `public.upgrade_events` |
| 客户端发布 | `public.client_releases`, `public.client_heartbeats` |
| Feature Flag后续扩展 | `public.feature_flag_definitions`, `public.feature_flag_change_log`, `public.tenants.feature_flags` |
---
## 8. 角色权限与页面可见性矩阵
| 页面/操作 | Platform Admin | 运营人员 | 只读审计员 |
|---|---:|---:|---:|
| 仪表盘查看 | ✅ | ✅ | ✅ |
| 租户创建/挂起/恢复 | ✅ | ✅ | ❌ |
| 硬删除租户 | ✅ | ❌ | ❌ |
| 调整 License 到期/用户上限 | ✅ | ✅ | ❌ |
| 备份触发 | ✅ | ✅ | ❌ |
| 数据恢复 | ✅ | ❌ | ❌ |
| 系统升级/回滚 | ✅ | ❌(只读) | ❌ |
| 客户端发布/下线/强制更新 | ✅ | ❌(只读) | ❌ |
| 审计日志查看与导出 | ✅ | ✅ | ✅ |
| 管理员设置 | ✅ | ❌ | ❌ |
---
## 9. 状态矩阵(关键页面)
| 页面 | 默认态 | 加载态 | 成功态 | 失败态 |
|---|---|---|---|---|
| 租户列表 | 最近查询结果 | 表格骨架/Spinner | Toast 成功 + 刷新列表 | 错误 Toast + 保留原列表 |
| 租户详情 | 分区信息默认展开 | 局部 Skeleton | 字段回写 + 操作日志入口 | 字段级错误提示 |
| 系统版本 | 当前版本总览 | 批次进度中 | 升级完成状态标记 | `halted` 告警 + 回滚入口 |
| 客户端发布 | 版本列表 | 上传进度 | SHA256 回填 + 发布成功 | 校验失败/上传失败提示 |
| 审计日志 | 最近记录 | 列表刷新中 | 导出任务创建成功 | 导出失败提示 + 重试 |
---
## 10. 校验与文案规范(管理员语境)
1. 所有错误文案需“可操作”:
-`版本号格式错误,应为 X.Y.Z`
-`参数非法`
2. 高危操作失败需包含最小原因:
- `MFA 验证失败,请重新输入 6 位动态码`
3. 操作成功反馈需带动作对象:
- `租户「上海某某地产」已挂起`
4. 审计可见入口:写操作成功 Toast 提供 `查看审计记录` 链接
---
## 11. 可访问性与实现约束
- 所有 Icon 按钮必须有 `aria-label`
- 表格头使用 `scope="col"`
- 弹窗支持 `Esc` 关闭(高危二次确认按流程控制)
- 焦点可见(`focus-visible:ring-*`
- 禁止仅用颜色表达状态,必须 `Badge + 文本`
---
## 12. 验收清单
- [x] 页面 IA 与路由覆盖 PRD 16 个页面
- [x] 管理员三角色权限可见性已定义
- [x] 高危操作统一二段式确认(含 MFA
- [x] Dashboard 已定义固定模块 + 扩展插槽
- [x]`DATA_MODEL_PUBLIC` 主表映射完成
- [x] 复用 `UI_SYSTEM``组件规范设计` 组件体系
- [x] 明确本后台为管理员使用,不采用终端用户视觉套路

File diff suppressed because it is too large Load Diff