diff --git a/Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md b/Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md index 2c6dd7ba..035c31cc 100644 --- a/Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md +++ b/Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md @@ -2,8 +2,8 @@ # Fonrey — Public Schema 数据模型 > **作者**: Backend Architect -> **版本**: v1.0 -> **日期**: 2026-04-24 +> **版本**: v1.2 +> **日期**: 2026-04-26 > **权威源**: 本文件是 `public` schema 所有表的唯一权威定义 > **设计依据**: 系统管理模块 PRD(`PRD/系统管理/系统管理模块PRD.md`);客户端发布管理模块 PRD(`PRD/发布管理/客户端发布管理模块PRD.md`) > **索引文档**: [`DATA_MODEL.md §三`](./DATA_MODEL.md)(仅保留摘要索引,开发以本文件为准) @@ -20,7 +20,7 @@ PostgreSQL Instance │ ├── public schema(平台运营层)← 本文件覆盖 -│ ├── tenants 租户注册与生命周期 +│ ├── tenants 租户注册与生命周期(含 feature_flags JSONB) │ ├── domains 域名路由 │ ├── tenant_status_logs 状态变更审计 │ ├── platform_admins 管理员账号 @@ -32,8 +32,10 @@ PostgreSQL Instance │ ├── backup_records 备份记录 │ ├── export_tasks 数据导出 │ ├── system_versions 版本历史 -│ ├── upgrade_events 升级记录 -│ └── client_releases 客户端版本发布 +│ ├── upgrade_events 升级事件(A/B/C 分级 + B 类分批编排) +│ ├── client_releases 客户端版本发布 +│ ├── feature_flag_definitions Feature Flag 定义(C 类灰度控制平面) +│ └── feature_flag_change_log Feature Flag 变更历史(append-only) │ ├── tenant_abc schema(租户业务层,见各子文档) └── tenant_xyz schema @@ -55,8 +57,11 @@ PostgreSQL Instance | `public.backup_records` | 备份任务执行记录(自动/手动/升级前/恢复前) | §2.4 | | `public.export_tasks` | 数据导出异步任务(CSV/JSON/SQL Dump) | §2.4 | | `public.system_versions` | 平台版本历史,唯一 current 约束 | §2.5 | -| `public.upgrade_events` | 升级/回滚事件,含灰度租户维度进度快照 | §2.5 | +| `public.upgrade_events` | 升级/回滚事件,A/B/C 升级类型分级,B 类按租户分批编排 + 健康门控 | §2.5 | | `public.client_releases` | Windows 客户端发布版本,含安装包 URL、SHA256、强制更新标记 | §2.6 | +| `public.feature_flag_definitions` | Feature Flag 全局定义(C 类运行时灰度控制平面) | §2.7 | +| `public.tenants.feature_flags` | 租户级 Flag 显式覆盖(`tenants` 表新增 JSONB 列) | §2.7 | +| `public.feature_flag_change_log` | Feature Flag 变更历史(append-only) | §2.7 | --- @@ -106,6 +111,11 @@ CREATE TABLE public.tenants ( -- 灰度升级 is_canary BOOLEAN NOT NULL DEFAULT FALSE, -- TRUE = 内测租户,参与灰度升级 + -- Feature Flag 租户级覆盖(详见 §2.7) + -- 格式: {"flag_key": true|false, ...} + -- 业务代码只读;写入由 apps.admin_console 经 feature_flag_change_log 审计 + feature_flags JSONB NOT NULL DEFAULT '{}'::jsonb, + -- 元数据 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), @@ -119,6 +129,9 @@ 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'; +-- 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); -- 域名映射表(支持多域名绑定一个租户,子域名创建后不可修改) CREATE TABLE public.domains ( @@ -351,32 +364,89 @@ CREATE UNIQUE INDEX idx_system_versions_current ON public.system_versions(status WHERE status = 'current'; -- 全局只允许一个 current 版本 -- 升级事件(每次执行升级或回滚对应一条记录) +-- 设计依据: 系统管理技术文档 §8.5–§8.6(A/B/C 升级类型分级 + B 类分批编排) CREATE TABLE public.upgrade_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), from_version_id UUID REFERENCES public.system_versions(id) ON DELETE SET NULL, to_version_id UUID NOT NULL REFERENCES public.system_versions(id) ON DELETE RESTRICT, event_type VARCHAR(10) NOT NULL CHECK (event_type IN ('upgrade','rollback')), + + -- 升级类型(决定本表大部分字段是否生效;详见技术文档 §8.5) + -- A_app = 应用代码升级(蓝绿全量切换,灰度名单/批次字段无效) + -- B_schema = 租户 Schema 迁移(按租户分批,批次字段全部生效) + -- C_feature = Feature Flag 灰度(不在本表编排,详见 §2.7;本字段保留以便统一记录) + upgrade_type VARCHAR(16) NOT NULL DEFAULT 'B_schema' + CHECK (upgrade_type IN ('A_app','B_schema','C_feature')), + strategy VARCHAR(10) NOT NULL DEFAULT 'full' CHECK (strategy IN ('full','canary')), -- full = 全量,canary = 灰度 - status VARCHAR(15) NOT NULL DEFAULT 'pending' - CHECK (status IN ('pending','health_check','in_progress','success','failed','rolled_back')), - -- 升级进度:每个租户的状态存为 JSONB 数组 - -- [{tenant_id, tenant_name, status, started_at, completed_at, error}] + -- 升级编排状态机(与技术文档 §8.6.1 一致) + -- draft → pre_check → pre_backup → batch_running ⇄ batch_done → succeeded + -- ↓ 任一批失败/门控不通过 + -- halted → rollback_running → rolled_back + -- ↓ 整体失败 + -- failed + status VARCHAR(20) NOT NULL DEFAULT 'draft' + CHECK (status IN ( + 'draft','pre_check','pre_backup','batch_running','batch_done', + 'halted','succeeded','failed','rollback_running','rolled_back' + )), + halted_reason TEXT, -- halted 状态时记录失败指标快照(健康门控不通过原因) + + -- 灰度名单(B_schema 类型生效;A_app 类型必须为空数组) + -- [tenant_id, ...] + gray_tenant_ids JSONB NOT NULL DEFAULT '[]', + + -- 批次编排参数(B_schema 类型生效) + batch_size INTEGER NOT NULL DEFAULT 5 + CHECK (batch_size BETWEEN 1 AND 100), -- 每批包含的租户数 + batch_concurrency INTEGER NOT NULL DEFAULT 2 + CHECK (batch_concurrency BETWEEN 1 AND 20), -- 批内并发执行的租户数 + batch_interval_seconds INTEGER NOT NULL DEFAULT 300 + CHECK (batch_interval_seconds BETWEEN 0 AND 86400), -- 批间观察窗口(秒) + failure_policy VARCHAR(16) NOT NULL DEFAULT 'halt_batch' + CHECK (failure_policy IN ('halt_batch','continue')), + -- halt_batch = 任一租户失败立即中断当前批,进入 halted 等待人工 + -- continue = 其他租户继续,仅标记失败(适用低风险变更) + current_batch_no INTEGER NOT NULL DEFAULT 0, -- 已完成批次序号 + total_batch_count INTEGER, -- 总批次数 = ceil(len(gray_tenant_ids)/batch_size) + + -- 健康门控阈值(覆盖默认值;默认值见技术文档 §8.6.5) + -- {"error_rate_5xx_5m": 0.005, "p95_latency_5m": 2000, + -- "celery_queue_pending": 1000, "sentry_new_issues_5m": 5} + health_gate_config JSONB NOT NULL DEFAULT '{}', + + -- 升级前全局备份引用(B_schema 必须;恢复兜底用) + pre_backup_record_id UUID REFERENCES public.backup_records(id) ON DELETE SET NULL, + + -- 单租户进度快照 + -- 数组格式(保留向后兼容): + -- [{tenant_id, tenant_name, status, started_at, completed_at, + -- snapshot_id, batch_no, error}] + -- status 取值:pending | running | success | failed | rolled_back tenant_progress JSONB NOT NULL DEFAULT '[]', - -- 回滚触发条件 + -- 回滚相关 rollback_reason TEXT, - incident_report TEXT, -- 回滚后生成的事件报告 + incident_report TEXT, -- 回滚后生成的事件报告(halted 转 rolled_back 时写) started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, initiated_by UUID REFERENCES public.platform_admins(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- 数据完整性约束:A_app 类型不允许带灰度名单(蓝绿全量切换) + CONSTRAINT chk_app_upgrade_no_gray + CHECK (upgrade_type <> 'A_app' OR gray_tenant_ids = '[]'::jsonb) ); CREATE INDEX idx_upgrade_events_status ON public.upgrade_events(status, created_at DESC); +CREATE INDEX idx_upgrade_events_type ON public.upgrade_events(upgrade_type, created_at DESC); +-- 用于「halted 状态需要超管处理」的实时告警查询 +CREATE INDEX idx_upgrade_events_halted ON public.upgrade_events(created_at DESC) + WHERE status = 'halted'; ``` ### 2.6 客户端发布管理 @@ -460,6 +530,92 @@ CREATE INDEX idx_client_releases_status ON public.client_releases(status, create CREATE INDEX idx_client_releases_version ON public.client_releases(version); ``` +### 2.7 Feature Flag 灰度体系 + +```sql +-- ──────────────────────────────────────────────────────────── +-- 7. Feature Flag 灰度体系 +-- ──────────────────────────────────────────────────────────── +-- 设计依据: 系统管理技术文档 §8.7(C 类升级 / 运行时功能灰度) +-- 说明: 与 §2.5 upgrade_events 的 B 类(schema 迁移)配合使用, +-- 负责「行为切换」的运行时灰度。详见技术文档 §8.7.6 四步发布流程。 + +-- 全局 Flag 注册表(控制平面) +CREATE TABLE public.feature_flag_definitions ( + key VARCHAR(64) PRIMARY KEY, + -- 命名规范:snake_case,业务前缀 + 功能名 + 版本号 + -- 例:'property_ai_description_v2' / 'search_ranking_algo_2026q2' + description TEXT NOT NULL, -- 业务说明(必填,便于审计回溯) + default_value BOOLEAN NOT NULL DEFAULT FALSE, + -- 未在 tenants.feature_flags 显式覆盖、且不命中 rollout_strategy 时返回此值 + + -- 灰度策略 + rollout_strategy VARCHAR(16) NOT NULL DEFAULT 'tenant' + CHECK (rollout_strategy IN ('tenant','percentage','user')), + -- tenant = 仅按 tenants.feature_flags 显式开关,不做百分比 + -- percentage = 按租户 ID 稳定哈希进百分比桶(rollout_config.percentage) + -- user = 按用户 ID 稳定哈希(rollout_config.percentage),需调用方传 user + rollout_config JSONB NOT NULL DEFAULT '{}', + -- percentage / user 策略下:{"percentage": 30} + -- tenant 策略下:通常为空 {} + + owner_admin_id UUID REFERENCES public.platform_admins(id) ON DELETE SET NULL, + -- 责任人(用于到期清理提醒、问题归因) + + -- 生命周期 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + archived_at TIMESTAMPTZ, + -- 归档后所有查询永久返回 FALSE;不删除以保留审计链路 + archived_reason TEXT +); + +CREATE INDEX idx_ff_def_active ON public.feature_flag_definitions(rollout_strategy) + WHERE archived_at IS NULL; +CREATE INDEX idx_ff_def_owner ON public.feature_flag_definitions(owner_admin_id) + WHERE archived_at IS NULL; + +-- 租户级 Flag 覆盖(存于 tenants 表的 JSONB 列) +-- 设计上不单建表,避免每次 is_enabled() 多查一次 JOIN; +-- public.tenants.feature_flags 列与 idx_tenants_feature_flags_gin 索引已在 §2.1 声明, +-- 此处用 IF NOT EXISTS 兜底,便于本节单独执行(如分阶段迁移): +ALTER TABLE public.tenants + ADD COLUMN IF NOT EXISTS feature_flags JSONB NOT NULL DEFAULT '{}'::jsonb; + -- 格式:{"flag_key": true|false, ...} + -- 仅 platform_admin 通过本模块写入;业务代码只读 + +CREATE INDEX IF NOT EXISTS idx_tenants_feature_flags_gin + ON public.tenants USING gin (feature_flags); + +-- 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, + -- 不加外键引用 feature_flag_definitions(key):归档/重命名时不应连带删除审计 + tenant_id UUID REFERENCES public.tenants(id) ON DELETE SET NULL, + -- NULL 表示全局变更(如调整 rollout_strategy / percentage / archive) + -- 非 NULL 表示该租户的覆盖值变更 + change_scope VARCHAR(20) NOT NULL + CHECK (change_scope IN ( + 'definition_create','definition_update','definition_archive', + 'tenant_override_set','tenant_override_clear' + )), + old_value JSONB, -- 变更前的完整值(NULL 表示首次创建) + new_value JSONB NOT NULL, -- 变更后的完整值 + operator_id UUID NOT NULL REFERENCES public.platform_admins(id) ON DELETE RESTRICT, + -- 强制保留操作人;账号停用不可级联删除审计 + operator_name VARCHAR(100) NOT NULL, -- 快照 + reason TEXT NOT NULL, -- 强制填写变更原因(业务流程要求) + ip_address INET, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + -- 无 deleted_at,无 UPDATE,append-only +); + +CREATE INDEX idx_ff_log_flag ON public.feature_flag_change_log(flag_key, created_at DESC); +CREATE INDEX idx_ff_log_tenant ON public.feature_flag_change_log(tenant_id, created_at DESC) + WHERE tenant_id IS NOT NULL; +CREATE INDEX idx_ff_log_operator ON public.feature_flag_change_log(operator_id, created_at DESC); +``` + --- ## 三、关键约束与禁止操作 @@ -478,6 +634,11 @@ CREATE INDEX idx_client_releases_version ON public.client_releases(version); | `client_releases` 唯一 published | `idx_client_releases_published` 部分唯一索引保证同平台+架构下只有一条 `status='published'` | | `client_releases` 不可跨租户隔离 | 本表属于 public schema,所有租户共享;禁止在租户 schema 中创建副本 | | `client_releases.download_count` 原子递增 | 必须使用 `UPDATE ... SET download_count = download_count + 1`,禁止先读后写 | +| `upgrade_events.gray_tenant_ids` 与 `upgrade_type` 一致性 | A_app 类型必须为空数组(CHECK 约束 `chk_app_upgrade_no_gray` 强制);只有 B_schema 类型才使用灰度名单与批次字段 | +| `upgrade_events` 状态流转单向 | 状态机详见 §4.2;`succeeded` / `rolled_back` / `failed` 为终态,禁止再写回中间态 | +| `feature_flag_change_log` append-only | 禁止 UPDATE / DELETE;任何 Flag 变更都必须经此表追溯,含 `reason` 强制非空 | +| `feature_flag_definitions.key` 不可修改 | 创建后禁止 UPDATE 主键;废弃 Flag 走 `archived_at` 软归档 | +| `tenants.feature_flags` 写入路径强约束 | 仅允许 `apps.admin_console.services.feature_flags` 写入;业务代码只读,且必须经 `is_enabled()` 服务接口(不得直读 JSONB) | --- @@ -501,10 +662,50 @@ creating ──(初始化失败)──► failed ### 4.2 升级事件状态 ``` -pending → health_check → in_progress → success - └─► failed → rolled_back +draft ──提交──► pre_check ──健康检查通过──► pre_backup ──全局备份完成──► batch_running + │ + ┌───── 当前批全部租户成功 ───────┘ + ▼ + batch_done + │ + 是否还有下一批 ◄┤ + ↓ 否 │ 是 + succeeded 下一批 → batch_running + │ + │ 任一批失败 / 健康门控不通过 + ▼ + halted + │ + 超管选择 ◄────┤ + ↓ 回滚 │ 继续下一批 + rollback_running batch_running + ↓ + rolled_back ``` +**字段映射**(详见技术文档 §8.6): +- `upgrade_type ∈ {A_app, B_schema, C_feature}`:A_app 类型不参与本状态机(蓝绿全量切换由运维侧执行),仅记录元数据 +- `current_batch_no` / `total_batch_count`:批次进度,UI §4.6 据此渲染进度表 +- `halted_reason`:halted 状态时记录失败指标快照(健康门控不通过原因) +- `tenant_progress[].status ∈ {pending, running, success, failed, rolled_back}`:单租户细粒度状态 +- 终态 `succeeded` / `rolled_back` / `failed` 一旦写入禁止再变更(应用层 + 审计日志双重保证) + +### 4.3 Feature Flag 变更流程 + +``` +[admin 创建 Flag definition] ──► reason=必填 ──► 写 feature_flag_change_log(scope=definition_create) + │ + ├─► 调整 rollout_strategy / percentage ──► scope=definition_update + │ + ├─► 租户级覆盖开/关 ──► scope=tenant_override_set / tenant_override_clear + │ 同时 UPDATE tenants.feature_flags + │ + └─► 归档 ──► archived_at = NOW(), scope=definition_archive + 归档后 is_enabled() 永久返回 FALSE,但记录保留 +``` + +**强制约束**:所有变更必须由 `apps.admin_console` 服务层写入;任何写操作都必须同时落 `feature_flag_change_log`(含 `reason`)+ `platform_audit_logs`(双重审计)。 + --- ## 五、查询模式 @@ -576,6 +777,69 @@ SELECT version, download_count FROM public.client_releases WHERE status IN ('published', 'archived') ORDER BY published_at DESC; + +-- ============================================================ +-- 升级编排相关查询(B 类 Schema 迁移分批升级) +-- ============================================================ + +-- 查询当前处于 halted 状态、需要超管处理的升级事件(实时告警查询) +SELECT id, to_version_id, current_batch_no, total_batch_count, + halted_reason, started_at, initiated_by +FROM public.upgrade_events +WHERE status = 'halted' +ORDER BY created_at DESC; + +-- 查询某租户的升级历史(含每次升级的最终状态) +SELECT ue.id, ue.to_version_id, ue.upgrade_type, ue.status, + progress->>'status' AS tenant_status, + progress->>'completed_at' AS tenant_completed_at, + progress->>'error' AS tenant_error +FROM public.upgrade_events ue, + jsonb_array_elements(ue.tenant_progress) AS progress +WHERE progress->>'tenant_id' = $1::text +ORDER BY ue.created_at DESC; + +-- 查询某次升级事件中失败的租户列表(用于人工排查) +SELECT progress->>'tenant_id' AS tenant_id, + progress->>'tenant_name' AS tenant_name, + progress->>'error' AS error, + progress->>'snapshot_id' AS snapshot_id +FROM public.upgrade_events ue, + jsonb_array_elements(ue.tenant_progress) AS progress +WHERE ue.id = $1 + AND progress->>'status' = 'failed'; + +-- ============================================================ +-- Feature Flag 相关查询(C 类运行时灰度) +-- ============================================================ + +-- 查询所有未归档的 Flag 定义(管理后台列表) +SELECT key, description, default_value, rollout_strategy, rollout_config, + owner_admin_id, created_at +FROM public.feature_flag_definitions +WHERE archived_at IS NULL +ORDER BY created_at DESC; + +-- 查询某 Flag 当前已显式开启的租户列表(GIN 索引加速) +SELECT id, name, schema_name, feature_flags->$1 AS flag_value +FROM public.tenants +WHERE feature_flags ? $1 + AND status = 'active'; + +-- 查询某 Flag 近 90 天的所有变更(审计回溯) +SELECT change_scope, tenant_id, old_value, new_value, + operator_name, reason, created_at +FROM public.feature_flag_change_log +WHERE flag_key = $1 + AND created_at >= NOW() - INTERVAL '90 days' +ORDER BY created_at DESC; + +-- 查询某管理员近 30 天的 Flag 变更操作(个人操作审计) +SELECT flag_key, change_scope, tenant_id, reason, created_at +FROM public.feature_flag_change_log +WHERE operator_id = $1 + AND created_at >= NOW() - INTERVAL '30 days' +ORDER BY created_at DESC; ``` ### 5.2 禁止查询 @@ -588,6 +852,11 @@ ORDER BY published_at DESC; | `UPDATE public.domains SET domain = ...` | 域名路由不可变 | | `UPDATE public.client_releases SET version = ...` | 版本号创建后不可修改,变更须新建记录 | | 在租户 schema 中创建 `client_releases` 副本 | 本表属于 public schema,多租户共享,禁止下沉到租户层 | +| `UPDATE public.feature_flag_change_log` / `DELETE FROM ...` | append-only 审计表 | +| `UPDATE public.feature_flag_definitions SET key = ...` | Flag key 是主键且为业务标识,禁止修改;废弃用 `archived_at` | +| 在租户 schema 内重复定义 `feature_flag_definitions` | Flag 体系是平台级控制平面,必须存于 public schema | +| 业务代码直接读 `tenants.feature_flags` JSONB | 必须经 `apps.admin_console.services.feature_flags.is_enabled()` 服务接口,绕过策略层会导致灰度策略失效 | +| `UPDATE public.upgrade_events` 修改终态行 | `succeeded` / `rolled_back` / `failed` 状态的事件禁止再次修改,应用层 ORM Manager 强制保护 | --- @@ -597,3 +866,4 @@ ORDER BY published_at DESC; |------|------|------| | v1.0 | 2026-04-24 | 从 `DATA_MODEL.md §三` 独立拆分;内容等价于 v1.2 DATA_MODEL.md §三 | | v1.1 | 2026-04-24 | 新增 §2.6 `client_releases` 表(客户端发布管理);同步更新表清单、约束规则、查询模式 | +| v1.2 | 2026-04-26 | 配合系统管理技术文档 v1.2 升级分批专节:§2.5 `upgrade_events` 增加 `upgrade_type`、`gray_tenant_ids`、`batch_size`、`batch_concurrency`、`batch_interval_seconds`、`failure_policy`、`current_batch_no`、`total_batch_count`、`health_gate_config`、`pre_backup_record_id`、`halted_reason` 字段,状态机扩展为 10 态;新增 §2.7 Feature Flag 体系(`feature_flag_definitions` / `tenants.feature_flags` JSONB / `feature_flag_change_log`);同步更新表清单、§4.2 升级状态机、§4.3 Flag 流程、§5.1 查询模式、§5.2 禁止操作 | diff --git a/Project/fonrey/REVIEW/REVIEW_全局_2026-04-26.md b/Project/fonrey/REVIEW/REVIEW_全局_2026-04-26.md new file mode 100644 index 00000000..9ee0c148 --- /dev/null +++ b/Project/fonrey/REVIEW/REVIEW_全局_2026-04-26.md @@ -0,0 +1,521 @@ +# Fonrey 全局系统设计 Review 报告 + +> **Review 类型**:全量 Review(PRD + DATA_MODEL + TECH_STACK + UI/UX + TASK 交叉验证) +> **Review 模式**:聚焦 Top 风险 + 与 `REVIEW_全局_2026-04-25.md` 增量对比 +> **Review 日期**:2026-04-26 +> **Reviewer**:首席系统设计 Reviewer(AI 辅助) +> **当前阶段**:需求 ~85% / 数据模型 ~70% / UI 设计 ~30%(HTML 原型已启动)/ TECH 横切规范 ~50% +> **覆盖文档**:8 份 PRD(含 7 份模块 PRD + TASK.md)、8 份 DATA_MODEL、4 份 TECH_STACK、3 份 UI_SYSTEM、UI_DESIGN(10 份原型/设计稿) +> **问题分级**:🔴 Blocker(阻塞开发) / 🟠 Major(必须修复但不阻塞) / 🟡 Minor(建议优化) + +--- + +## 〇、执行摘要(Executive Summary) + +### 整体评价 + +相较 2026-04-25 版本,文档体系出现 **3 项实质性进展**: +1. **UI_DESIGN 原型已启动**(昨日 U-06 已修复):客源列表/详情/新增/编辑、房源列表 5 类核心页面均已产出 HTML + Markdown 双版本,规范深度足够。 +2. **租户注销 SOP 已落地**(昨日 X-02 已修复):`PRD/系统管理` 已含导出范围、30 天宽限期、清除时序的完整生命周期。 +3. **楼盘 4 类锁字段已实现**(昨日 D-02 部分修复):`complexes.lock_building/lock_room/lock_info/lock_standard_room` DDL 已写入。 + +但本次审查也暴露出**昨日 Review 未触及的新一类系统性风险——枚举一致性塌方**:客源状态、客源等级、房源状态、操作日志类型等核心枚举在 PRD ↔ DATA_MODEL ↔ TASK AC 三方均无权威源,且互相矛盾。这是一类比"Keyset 分页缺失"更隐蔽、修复成本更高的债务。 + +同时,**昨日列出的 2 个 Blocker(P-01 权限档位 / D-01 Keyset 分页)均未修复**,新增 1 个 Blocker(系统配置 PRD 仍为空骨架但被 9 条 P0/P1 任务引用为权威),共 **3 个 Blocker**,必须在编码启动前清零。 + +### 核心问题摘录(Top 10) + +| # | 等级 | 编号 | 问题 | 维度 | 状态 | +|---|------|------|------|------|------| +| 1 | 🔴 | **B-01** | 系统配置 PRD(`PRD/系统配置/系统配置.md`)仍为 128 行空骨架,但 TASK.md 排期 9 条 P0/P1 任务(US-SETTING-001/010~012/020~023)以其为权威;`lookup_items` 入口、字段必填、活跃度阈值(30 天)等无配置出口 | PRD↔TASK | 🆕 新增 | +| 2 | 🔴 | **B-02** | **核心枚举三方不一致**:客源状态(PRD"求购/求租/租购"vs DDL `buying/renting/buy_or_rent`)、等级(PRD A~E vs DDL A_urgent/A/B/C/D)、房源 status(DDL 未含 `sold/unlisted/paused`,但 TASK AC 与查询语句均使用) | PRD↔Data↔TASK | 🆕 新增 | +| 3 | 🔴 | **B-03** | 权限 PRD §3 非目标声明"三档"与 §5.6 / Story 3 验收"五档" + DATA_MODEL_PERMISSION 的"DataScope 跨层级叠加"实质性冲突 | PRD↔Data | ⚠️ 昨日 P-01 部分修复,仍 Blocker | +| 4 | 🔴 | **B-04** | Keyset 分页规范在 TECH_STACK / DATA_MODEL / 测试规范中**完全缺位**,89k 房源 + 200 万跟进日志的列表查询设计错误 | TECH/Data | ⚠️ 昨日 D-01 未修复 | +| 5 | 🟠 | **M-01** | 测试规范 v1.0 与 django-tenants 多租户工作模式冲突:`LocMemCache`/`FileSystemStorage`/`CELERY_TASK_ALWAYS_EAGER=True` 使权限 Redis 快照失效、跨租户隔离、Celery 异步导出三类场景无法验证;`TenantClient`/`tenant fixture` 仅在 §11.2 Prompt 模板中被引用但无实现 | TECH/测试 | 🆕 新增 | +| 6 | 🟠 | **M-02** | 主表乐观锁字段缺失:`properties` / `clients` 仍无 `version`;`complexes` 4 类锁已建字段但无解锁权限/过期规则 PRD | Data↔PRD | ⚠️ 昨日 D-02 部分修复 | +| 7 | 🟠 | **M-03** | 高写入表分区 DDL 仍未落地:`follow_logs` / `property_photos` / `permission_change_logs` / `login_attempts` / `platform_audit_logs` 均仅文字"建议月度分区",无 `PARTITION BY` 子句 | Data | ⚠️ 昨日 D-04 未修复 | +| 8 | 🟠 | **M-04** | TECH_STACK 三大横切规范仍未补:Celery 多租户 schema 切换(昨 T-01)、R2 文件路径前缀(昨 T-02)、查询索引矩阵(昨 T-03) | TECH/多租户 | ⚠️ 昨日均未修复 | +| 9 | 🟠 | **M-05** | 89k 数据 < 2 秒列表查询是 TASK AC 中的硬性 NFR(US-PROPERTY-002 / US-CLIENT-002),但 TECH_STACK + 测试规范无 p95/EXPLAIN/性能基准对应任务,AC 不可测 | NFR↔TECH↔测试 | 🆕 新增 | +| 10 | 🟠 | **M-06** | 客户端发布无签名校验/防降级机制:`/api/client/updates/latest/` 与 `download_url` 公开 URL,仅 SHA256 完整性校验,可被中间人投递降级版本 | 安全 | 🆕 新增 | + +### 风险等级分布 + +| 等级 | 本次(2026-04-26) | 昨日(2026-04-25) | 净变化 | +|------|---|---|---| +| 🔴 Blocker | **3** | 2 | +1(系统配置空骨架升级) | +| 🟠 Major | **12** | 19 | -7(含 6 项已修复 + 1 项降级) | +| 🟡 Minor | **6** | 17 | -11(聚焦 Top 风险,未列入本期) | +| 合计 | **21** | 38 | — | + +### 增量对比一览(昨日 P0/P1 → 今日状态) + +| 昨日编号 | 简述 | 状态 | 今日处置 | +|----------|------|------|----------| +| P-01 | 权限范围档位冲突 | 🟡 部分修复 | 升 B-03 仍 Blocker | +| D-01 | Keyset 分页缺失 | 🔴 未修复 | 转 B-04 | +| P-02 | 系统配置 PRD 缺失 | 🔴 仍空骨架 | 升 B-01 | +| D-02 | 主表乐观锁/楼盘锁 | 🟡 部分修复 | 转 M-02 | +| D-04 | 高写入表分区 | 🔴 未修复 | 转 M-03 | +| D-07 | 楼盘价格走势/市场报盘 | 🟡 部分修复 | M-08(降级) | +| T-01/T-02/T-03 | Celery/R2/索引规范 | 🔴 均未修复 | 合并 M-04 | +| U-01/U-02/U-03/U-06 | UI 组件 + Wireframe | 🟡 U-06 已修复,复杂组件仍缺 | 转 M-09 | +| X-01 | Redis Key tenant 前缀 | 🟡 部分修复 | M-10(降级) | +| X-02 | 租户注销 SOP | ✅ 已修复 | 关闭 | +| S-01 | 加密密钥管理 | 🔴 未修复 | M-11 | + +--- + +## 一、PRD 一致性审查(PRD ↔ PRD ↔ TASK) + +### 🔴 B-01 系统配置 PRD 空骨架阻塞 9 条 P0/P1 任务 + +**文档**:`PRD/系统配置/系统配置.md:1-128`(仅章节标题,无内容);`PRD/TASK.md:59, 86-88, 107-110` + +**事实**: +- 系统配置 PRD 仅 128 行骨架,无任何字段定义、规则说明、UI 描述。 +- TASK.md 在 Phase 1/2/3 共排期 9 条 Story,全部以"参考 PRD:系统配置.md"为权威: + - P0:US-SETTING-001(房源相关设置-字段必填/自定义字段/标签) + - P1:US-SETTING-010/011/012(首页/相关方/客源参数) + - P2:US-SETTING-020~023(人事 OA/交易/财务/合同模板) +- 客源 PRD `DATA_MODEL_CLIENT.md:31` 提到"活跃度新配偶 30 天"阈值由 lookup_items 配置,但配置入口 PRD 不存在。 +- 房源 PRD 多处提到"字段标签可配置"、"自定义字段"、"标签管理",全部指向系统配置 PRD。 + +**影响**: +- US-SETTING-001 是 Phase 1 P0,无 PRD 即无法启动开发。 +- 所有"可配置"行为(活跃度阈值、自动转公阈值、字段标签)的运营管理入口断链。 +- DATA_MODEL_PUBLIC `lookup_items` 表已建好,但其管理 UI 与权限设计无规格。 + +**责任**:PM 必须在编码 Phase 1 启动前完成 `PRD/系统配置/系统配置.md` 至少 P0 范围(US-SETTING-001 内容),否则 TASK.md 应将 US-SETTING-001 暂时移出 Phase 1。 + +--- + +### 🔴 B-02 核心枚举三方不一致(PRD ↔ DDL ↔ TASK AC) + +**文档**:多处(见下表) + +| 字段 | PRD 表述 | DATA_MODEL CHECK | TASK AC | 冲突类型 | +|------|----------|------------------|---------|----------| +| 客源 status | `PRD/客源管理/客源管理模块PRD.md:465,504,1757,1786`:求购/求租/租购/我购/我租/成交/暂缓/无效/公 | `DATA_MODEL_CLIENT.md:471,511`:`buying / renting / buy_or_rent`(仅 3 值,无暂缓/无效/我购/我租/成交/公) | `TASK.md` US-CLIENT-002/010/011/012/013 均按 PRD 中文 | 三方均不同 | +| 客源 grade | PRD `1634`:A~E 五级 | `DATA_MODEL_PROPERTY.md:266`(注:客源 grade 字段实际位置待确认):`A_urgent/A/B/C/D` 5 项无 E | `TASK.md:308` US-CLIENT-009:A 急迫/B 较强/C 一般/D 较弱/E 暂不关注 五级 | DDL 缺 E,混用 `A_urgent` | +| 房源 status | PRD 8 态:出售/出租/租售/暂缓/他售/他租/成交/未挂牌 | `DATA_MODEL_PROPERTY.md:197`:`for_sale/for_rent/for_sale_rent/...` | `TASK.md:232` US-PROPERTY-008:在售→暂缓/成交/下架;`TASK.md:1121` 隐式查询 `status NOT IN ('sold','unlisted')` | DDL CHECK 全集未冻结,"sold/unlisted/paused" 出现在查询但未确认在 CHECK 中 | +| 操作日志 type | 客源 PRD `668`:明文"系统实际枚举值更多" | DATA_MODEL_CLIENT 未给 CHECK 完整列表 | `TASK.md:578-581` US-CLIENT-024 AC:"按操作类型筛选" | 全集未冻结 | +| 二级 Tab "暂缓" | 客源 PRD `713-717` Tab:求购/求租/暂缓/全部私客 | `DATA_MODEL_CLIENT.md:471,511` 状态枚举无 `paused/on_hold` | `US-CLIENT-002` AC 含暂缓 Tab | DDL 缺值 | +| `tenants.status` | `DATA_MODEL_PUBLIC.md:96`:6 值含 `failed` | 同文件 `488-494` 状态机图:仅 5 态,未含 `failed` | — | 同一文档内自相矛盾 | + +**影响**: +- 迁移脚本将凭主观翻译,开发期会出现"经纪人在 PRD 看到的中文"与"DDL CHECK 拒绝的英文"不一致的运行时报错。 +- 状态机断链导致 transition 校验逻辑(如"暂缓→在售")无权威实现。 +- 测试 factory 不知如何取样;form choices 与 query filter 各自定义。 + +**责任**:PM + 架构师共同冻结一份《Fonrey 枚举字典 v1.0》(建议放入 `DATA_MODEL/ENUMS.md`),所有 PRD/DDL/TASK 引用统一锚点。 + +--- + +### 🔴 B-03 权限数据范围档位冲突仍为 Blocker(昨日 P-01 部分修复) + +**文档**:`PRD/权限管理/权限管理模块PRD.md:46`(§3 非目标)vs `:492-499`(§5.6 五档)vs `DATA_MODEL/DATA_MODEL_PERMISSION.md:24,31`(DataScope 跨层级叠加) + +**事实**: +- §3 非目标仍写"数据范围控制以**本人 / 本部门 / 全公司**三档为主,本期不含行级权限"。 +- §5.6 已扩为五档(本人/本组/本门店/本区域/全公司)。 +- DATA_MODEL_PERMISSION `staff_data_scopes` 已实现"跨层级叠加"("本组 ∪ 门店B"),实质上为对象级权限的弱化形态,已超出 §3 非目标边界。 + +**影响**: +- 同一 PRD 内 §3 与 §5.6 自相矛盾,开发无权威依据。 +- `staff_data_scopes` 是否本期落地不明,权限模块(US-PERMISSION-001~005)无法启动。 + +**责任**:PM 在 PRD v1.2 中锁定五档 + 删除 §3 三档表述 + 明确 DataScope 跨层级叠加是否本期范围。 + +--- + +### 🟠 M-07 楼盘合并/申请流程仅占位 + +**文档**:`PRD/房源管理/楼盘管理模块PRD.md:117` + +**事实**:PRD 仅写"小区名称/地址只读 — 合并/申请流程"占位,未定义触发条件、审批人、合并规则、对历史房源/客源的影响。`DATA_MODEL_COMPLEX` 无对应表/工作流。 + +**影响**:US-COMPLEX-001 录入与维护流程不闭环;楼盘合并将在生产环境出现"合并后历史房源 complex_id 漂移"问题。 + +--- + +### 🟠 M-08 客源新房/租房 Tab 字段未冻结 + +**文档**:`PRD/客源管理/客源管理模块PRD.md:1663`(明文"字段待截图补充确认") + +**事实**:US-CLIENT-014(编辑客源 Tab 三:二手/新房/租房)AC 不可测,DATA_MODEL_CLIENT 需求字段 schema 不可冻结。 + +--- + +## 二、DATA_MODEL 完整性审查 + +### 🔴 B-04 Keyset 分页规范全局缺位(昨日 D-01 未修复) + +**文档**:`TECH_STACK/TECH_STACK.md`(全文未现 keyset/分页规范);`DATA_MODEL_PROPERTY.md:1083` §6 / `DATA_MODEL_CLIENT.md:453` §五 查询模式参考章节均未给 Keyset SQL 模板 + +**事实**: +- 89k 房源 + 200 万/年跟进日志使用 OFFSET 分页,第 100 页响应将达秒级。 +- TASK.md US-PROPERTY-002 / US-CLIENT-002 的 AC 均要求"<2 秒"。 +- 测试规范无性能基准任务覆盖。 + +**责任**:架构师在 `TECH_STACK.md` 新增 §「分页规范」,并在 `DATA_MODEL_PROPERTY.md` §6 / `DATA_MODEL_CLIENT.md` §五 给出 Keyset SQL 模板(`WHERE (created_at, id) < (?, ?) ORDER BY created_at DESC, id DESC LIMIT 21`)。 + +--- + +### 🟠 M-02 主表乐观锁/楼盘锁规则缺失 + +**文档**:`DATA_MODEL_COMPLEX.md:194-197`(楼盘 4 类锁字段已建);`DATA_MODEL_PROPERTY.md` / `DATA_MODEL_CLIENT.md` / `DATA_MODEL_COMPLEX.md` 主表均无 `version` 字段;`PRD/房源管理/楼盘管理模块PRD.md:82` 列出 4 类锁但无解锁/过期规则 + +**事实**: +- 楼盘 `lock_building/lock_room/lock_info/lock_standard_room` 字段已落库 ✅ +- 但 PRD 未规定每类锁的解锁权限、自动/手动解锁规则、锁过期机制。 +- US-COMPLEX-001/002 AC 不含锁状态变更流程。 +- `properties.version` / `clients.version` 仍未补,多人协作场景的写冲突无防护。 + +**责任**:架构师补 `version` 字段;PM 补 4 类锁的业务规则。 + +--- + +### 🟠 M-03 高写入表分区 DDL 仍未落地(昨日 D-04 未修复) + +**文档**:`DATA_MODEL_PROPERTY.md:523`(follow_logs)/ `:801`(property_photos);`DATA_MODEL.md:596-604`(仅文字描述) + +**事实**: +- `follow_logs`(200 万+/年)、`property_photos`(500 万+)、`permission_change_logs`、`login_attempts`、`platform_audit_logs` 的 DDL 中均无 `PARTITION BY` 子句,仅在文字描述/分区策略表中"建议按月 RANGE 分区"。 +- 开发期不分区,未来再分区将带来全表数据迁移成本。 + +**责任**:架构师在每张高写入表的 DDL 中给出 `PARTITION BY RANGE (created_at)` + `pg_partman` 自动维护脚本。 + +--- + +### 🟠 M-08 楼盘价格走势已建,市场报盘仍缺(昨日 D-07 部分修复) + +**文档**:`DATA_MODEL_COMPLEX.md:376` `complex_price_trends` ✅;`DATA_MODEL_CLIENT.md:505` §5.4 私客自动转公(仅 SQL 查询示例,非完整 job);`market_quotes` 表全表缺失 + +**事实**: +- 楼盘价格走势 ✅ 已建。 +- "市场报盘"(房源 PRD §)仍无 `market_quotes` 表。 +- 私客→公客自动转换仅给查询 SQL 示例,无完整 Celery beat 调度任务定义、无幂等保证、无并发控制。 + +**影响**:US-CLIENT-016(系统自动将超时无跟进的私客转为公客)AC 无法实现。 + +--- + +## 三、TECH_STACK 完整性审查 + +### 🟠 M-04 三大横切规范全部仍未补(昨日 T-01/T-02/T-03 未修复) + +**文档**:`TECH_STACK/TECH_STACK.md`(全文) + +| 子项 | 现状 | 影响 | +|------|------|------| +| Celery 多租户 schema 切换 | TECH_STACK.md L30/44/80/90/165 仅零散提及 Celery,无统一规范 | Worker 在错误 schema 下查询、跨租户数据污染 | +| R2 文件路径前缀 | TECH_STACK.md L31/105/119/166 仅提及 bucket 名 | 共享 bucket 下 `file_key` 无租户标识,存在隐患 | +| 查询索引矩阵 | TECH_STACK/ 目录无"索引规范.md";DATA_MODEL.md §六 实为 Redis 缓存策略 | 89k 房源多条件筛选无覆盖索引验证依据 | + +**责任**:架构师补 §「异步任务规范」、§「R2 路径规范」、`TECH_STACK/索引规范.md` 三份内容。 + +--- + +### 🟠 M-01 测试规范与 django-tenants 多租户工作模式冲突 + +**文档**:`TECH_STACK/测试规范.md:393-413`(settings_test)/`570-575`(引用未实现的 TenantClient)/`594`(禁用真实外部服务) + +**事实**: +- `LocMemCache`(402-406):django-tenants 需要 `tenant-cache-key` 前缀化的 Redis 行为;权限快照走 Redis(TASK L617 AC "Redis 权限快照失效重载"),切到 LocMem 后跨进程/跨租户隔离测试失效。 +- `FileSystemStorage`(411-413):替换 R2 用于测试本身合理,但与 §594 "禁止测试直接调用真实外部服务"叠加后,`tenants/{schema_name}/...` 路径前缀策略(M-04 R2)无 mock 验证手段。 +- `CELERY_TASK_ALWAYS_EAGER=True`(398-399):US-CLIENT-003 等要求"Celery 异步处理"的 AC 无法在 EAGER 模式下验证异步语义。 +- `DATABASES "由环境变量注入"`(393-395):未约定 `ENGINE=django_tenants.postgresql_backend`,可能落到错误的 backend 上。 +- `TenantClient` / 多租户 fixture:仅在 §11.2 Prompt 模板中被引用,无实现,§12 又"禁止使用 Django 原生 Client()",造成所有 integration 测试无统一基类。 + +**影响**: +- 权限模块 Redis 快照失效场景、跨租户隔离场景的自动化测试不可信。 +- US-PERMISSION-* / US-CLIENT-003 / US-CLIENT-016 等核心场景的覆盖率指标存在水分。 + +**责任**:架构师与测试负责人协同:(a) `tests/conftest.py` 提供 `TenantClient` 与 `tenant_factory` 实现;(b) Redis 改用 fakeredis 而非 LocMemCache;(c) 异步场景使用 `pytest-celery` 的 `celery_app` fixture,区分 EAGER 单元测试与真实 broker 集成测试。 + +--- + +### 🟠 M-05 性能 NFR 无 SLI/SLO 落地(昨日 P-04 未修复) + +**文档**:`TASK.md:189, 252` US-PROPERTY-002 / US-CLIENT-002 AC 写"89000 条数据下查询 < 2 秒";`PRD/客源管理/客源管理模块PRD.md:1958-1960` 仅泛述"分页加载";TECH_STACK 与测试规范无 p95/索引/EXPLAIN 验收 + +**事实**: +- 性能 AC 散布于 PRD/TASK 多处(录入耗时 ≤ 30s、配房 < 3s、登录成功率、自动更新 ≥ 98%、RTO < 2h),无统一的 NFR 矩阵。 +- TECH_STACK 未承接为 SLI/SLO;测试规范无性能基准任务(如 pytest-benchmark + EXPLAIN ANALYZE 校验)。 + +**影响**:所有性能 AC 不可测,验收阶段将沦为主观判断。 + +--- + +### 🟠 M-11 加密密钥管理方案缺失(昨日 S-01 未修复) + +**文档**:`TECH_STACK/TECH_STACK.md:96` 仅一句"禁止硬编码密钥" + +**事实**: +- 全文未给出 KMS / Vault / 环境变量加密管理说明。 +- `core.encryption` 仅作为模块名出现,密钥轮换流程未定义。 +- 手机号、身份证、银行卡号、产证号等多处使用 AES-256-GCM 加密,密钥泄露后批量解密风险高。 + +**责任**:架构师补 KMS/Vault 选型 + 轮换 SOP + `core.encryption` 接口契约。 + +--- + +## 四、UI/UX 完整性审查 + +### ✅ U-06 已修复:UI_DESIGN 原型已启动 + +**证据**:`UI_DESIGN/preview.html`、`UI_DESIGN/客源列表_UI.html`(81KB)、`客源详情_UI.html`、`房源列表_UI.html`、`新增客源_UI.html`、`编辑客源_UI.html`;`UI_DESIGN/客源管理/`(4 份 md,60KB+/40KB+);`UI_DESIGN/房源管理/房源列表_UI.md`(69KB) + +**事实**:HTML 原型与对应 Markdown 设计稿均已启动,规范深度足够(含功能范围/页面规范/Data Table/HTMX/权限矩阵)。 + +--- + +### 🟠 M-09 复杂组件原型仍缺 + 楼盘/客源原型不完整(昨日 U-01/U-02/U-03 未修复) + +**文档**:`UI_SYSTEM/UI_SYSTEM.md`(1742 行);`UI_SYSTEM/组件清单.md:186`(仅在评论中提到 TreeSelect "建议手写 80~120 行 JS");`UI_DESIGN/房源管理/` 仅 1 个文件;`UI_DESIGN/客源管理/` 缺关键浮层 + +**事实**: +- 组件清单仍缺 4 类关键复杂组件: + 1. **Stepper / Wizard**(多步表单):录入私客、转成交浮层 + 2. **PermissionTree**(14 个一级模块树 + 多分组 + Toggle/Select/Number):US-PERMISSION-001/004 最复杂页面 + 3. **智能配房比对器**(4 分组卡片):US-CLIENT-020 录客配房 + 4. **审批前置弹层**:US-CLIENT-015 跨团队需店长权限、号码方审批 +- UI_DESIGN/房源管理/ 仅 `房源列表_UI.md`,TASK.md L152/160 引用 `楼盘列表_UI.md` / `楼盘详情_UI.md` 不存在。 +- UI_DESIGN/客源管理/ 缺 US-CLIENT-007(带看)/011(转公客)/012(转成交)/015(相关员工)/020(智能配房)独立原型,全部压缩进 `客源详情_UI.md` 单文件,颗粒度不足。 + +**责任**:UI/UX 设计师补 4 类复杂组件 + 楼盘 2 份原型 + 客源 5 份关键浮层独立原型。 + +--- + +## 五、多租户隔离审查(横切) + +### 🟠 M-10 Redis Key 命名跨模块未统一(昨日 X-01 部分修复) + +**文档**:`TECH_STACK/登录管理技术方案.md:553-562`(§九 Redis Key 规范,使用 `{tenant_id}:` 前缀);`TECH_STACK.md` / `权限管理系统技术方案.md` 未见统一规范引用 + +**事实**: +- 登录方案已落地 Redis Key 规范表 ✅,但前缀使用 `{tenant_id}:` 而非昨日 Review 建议的 `{tenant_schema_name}:`。 +- TECH_STACK.md 总纲未引用此规范;权限技术方案未明确 Redis Key 是否带 tenant 前缀。 +- 跨模块(登录/权限/缓存)若不统一,多租户 Redis 共享时存在键冲突风险。 + +**责任**:架构师在 `TECH_STACK.md` 新增 §「Redis Key 规范」,统一 `{tenant_schema_name}:` 前缀(schema_name 比 tenant_id 在日志/调试时更直观),并在登录/权限技术方案中显式引用。 + +--- + +## 六、合规与安全审查 + +### 🟠 M-06 客户端发布无防降级/重放校验 + +**文档**:`PRD/发布管理/客户端发布管理模块PRD.md:58`(明文非目标"客户端代码混淆/反逆向");`DATA_MODEL_PUBLIC.md:413-416, 419-421` + +**事实**: +- `/api/client/updates/latest/` 响应未要求服务端签名。 +- `client_releases.download_url` 为 R2 公开 URL,无签名/有效期,旧客户端可继续拉取旧版本绕过强制升级。 +- `min_required_version` 由"应用层比较 SemVer",未定义服务端签名/客户端公钥校验。 +- US-RELEASE-011 SHA256 仅校验完整性,不防替换(中间人可投递降级版本)。 + +**影响**:中间人攻击可投递已撤销的旧版本(含已修复漏洞的版本),合规与安全双重风险。 + +**责任**:架构师与发布团队补:(a) 更新元数据签名(Ed25519/RSA-PSS);(b) `download_url` 改用签名 URL(短有效期);(c) 客户端内置公钥校验。 + +--- + +### 🟠 M-12 RPO 数值缺失,备份计划与 SLA 断链 + +**文档**:`PRD/系统管理/系统管理模块PRD.md:58`(仅 RTO < 2h);`DATA_MODEL_PUBLIC.md:265-280` `backup_schedules` 默认 daily 02:00、retention 10 + +**事实**: +- daily 频率隐含 RPO ≈ 24h 但 PRD 未声明。 +- 与"全局/租户级覆盖"组合下,SLA 不可承诺。 +- `tenants.status` 含 `failed`(DDL)但状态机图(同文 488-494)仅 5 态未含 `failed`,故障租户无 SLA 出口。 + +**责任**:PM 补 RPO 数值;架构师补 `failed` 状态的退出路径。 + +--- + +## 七、性能与容量审查 + +### 🟠 M-05 89k 数据 < 2 秒 AC 不可测(已在 §三列出) + +### 🟠 M-13 智能配房算法路径仍未文档化 + +**文档**:客源 PRD §5 智能配房;TECH_STACK 与 DATA_MODEL 未有"实时 vs 异步""匹配算法 SQL"说明 + +**事实**: +- 客源 PRD 要求智能配房响应 < 3s。 +- 匹配字段(户型/面积/价格/区域)跨 properties/clients 两表的 4-6 维筛选,是否使用物化视图/PostgreSQL 多列 GiST 未定。 +- `client_property_matches` 表是否本期建(TASK Phase 3 才有 US-CLIENT-020)?若 Phase 1 不需要,PRD 提及的 < 3s 应明确不在 Phase 1 范围。 + +--- + +## 八、可维护性与扩展性 + +### 🟠 M-14 TASK.md PRD 锚点链接断链 + +**文档**:`TASK.md:124, 132, 139, 150, 158, 167` 等 + +**事实**:TASK.md 大量"参考 PRD:xxx#锚点"形式的链接,但所引锚点(如 `登录管理/用户登录管理模块PRD.md - 账号密码登录` / `楼盘管理模块PRD.md - 楼盘信息管理`)在对应 PRD 中作为标题不存在,开发无法直接跳转。 + +**影响**:TASK.md 作为开发入口的"可执行性"打折扣。 + +--- + +## 九、行动清单(按责任人 + 优先级) + +### PM(产品经理) + +| ID | 等级 | 任务 | 关联编号 | +|----|------|------|----------| +| PM-1 | 🔴 | **补齐 `PRD/系统配置/系统配置.md` P0 范围**(US-SETTING-001:字段必填/自定义字段/标签管理;lookup_items 配置入口;活跃度/转公阈值配置) | B-01 | +| PM-2 | 🔴 | 与架构师协同冻结《Fonrey 枚举字典 v1.0》(建议 `DATA_MODEL/ENUMS.md`),覆盖客源 status/grade、房源 status、操作日志 type、`tenants.status` 等核心枚举 | B-02 | +| PM-3 | 🔴 | 修订权限 PRD v1.2:删除 §3 三档表述,锁定五档 + DataScope 跨层级叠加是否本期范围 | B-03 | +| PM-4 | 🟠 | 楼盘合并/申请流程详述(触发/审批/对历史房源影响) | M-07 | +| PM-5 | 🟠 | 客源新房/租房 Tab 字段冻结 | M-08 | +| PM-6 | 🟠 | 在 `PRD_MVP.md` 中汇总 NFR 矩阵(性能/可用性/安全/合规),含 RPO 数值 | M-05, M-12 | +| PM-7 | 🟠 | 修复 TASK.md 内所有 PRD 锚点断链 | M-14 | + +### 架构师 + +| ID | 等级 | 任务 | 关联编号 | +|----|------|------|----------| +| ARCH-1 | 🔴 | TECH_STACK 新增 §「分页规范」+ Keyset SQL 模板;DATA_MODEL_PROPERTY/CLIENT §查询模式补 Keyset 示例 | B-04 | +| ARCH-2 | 🟠 | 修复测试规范多租户冲突:补 `TenantClient` 实现、`tenant_factory` fixture;Redis 改用 fakeredis;区分 EAGER/真实 broker | M-01 | +| ARCH-3 | 🟠 | `properties` / `clients` 主表补 `version`;楼盘 4 类锁补解锁/过期机制(与 PM 协同) | M-02 | +| ARCH-4 | 🟠 | 高写入表(follow_logs/property_photos/permission_change_logs/login_attempts/platform_audit_logs)补 `PARTITION BY RANGE` DDL + pg_partman 脚本 | M-03 | +| ARCH-5 | 🟠 | TECH_STACK 三大横切规范补:异步任务(Celery tenant_schema 必传 + `@with_tenant_context`)/R2 路径前缀(`tenants/{schema_name}/...`)/查询索引矩阵 | M-04 | +| ARCH-6 | 🟠 | TECH_STACK 性能 NFR → SLI/SLO 映射 + 测试规范补 pytest-benchmark/EXPLAIN ANALYZE | M-05 | +| ARCH-7 | 🟠 | 客户端发布签名机制:服务端签发 Ed25519/RSA-PSS、客户端内置公钥、`download_url` 改签名 URL | M-06 | +| ARCH-8 | 🟠 | `market_quotes` 表 + 私客自动转公完整 Celery beat 任务(含幂等/并发控制) | M-08 | +| ARCH-9 | 🟠 | TECH_STACK 新增 §「Redis Key 规范」,统一 `{tenant_schema_name}:` 前缀 | M-10 | +| ARCH-10 | 🟠 | 加密密钥管理方案:KMS/Vault 选型、轮换 SOP、`core.encryption` 接口契约 | M-11 | +| ARCH-11 | 🟠 | `tenants.status='failed'` 状态机出口补全 | M-12 | +| ARCH-12 | 🟠 | 智能配房算法路径文档(实时 vs 异步、索引/物化视图选型) | M-13 | + +### UI/UX 设计师 + +| ID | 等级 | 任务 | 关联编号 | +|----|------|------|----------| +| UI-1 | 🟠 | 补 4 类复杂组件原型:Stepper/Wizard、PermissionTree、智能配房比对器、审批前置弹层 | M-09 | +| UI-2 | 🟠 | 补 UI_DESIGN/房源管理/楼盘列表_UI.md / 楼盘详情_UI.md(修复 TASK 引用) | M-09 | +| UI-3 | 🟠 | 客源关键浮层独立原型:US-CLIENT-007/011/012/015/020 | M-09 | + +--- + +## 十、结论 + +### 是否可进入开发阶段 + +**否,必须先解决 4 个 Blocker**: + +| 编号 | Blocker | 影响范围 | +|------|---------|----------| +| B-01 | 系统配置 PRD 空骨架 | 阻塞 9 条 P0/P1 任务(US-SETTING-001/010~012/020~023) | +| B-02 | 核心枚举三方不一致 | 阻塞客源/房源全部 CRUD 模块(US-CLIENT-* / US-PROPERTY-*) | +| B-03 | 权限档位冲突 | 阻塞权限模块(US-PERMISSION-001~005) | +| B-04 | Keyset 分页缺失 | 阻塞高基数列表查询(US-PROPERTY-002 / US-CLIENT-002) | + +**可立即并行启动的部分**: +- 公共 Schema(`tenants`、`platform_admins`、`audit_logs`)开发——文档完整,无 Blocker。 +- 楼盘/区域/学校 CRUD——DATA_MODEL_COMPLEX 完整、4 类锁字段已建,主流程清晰(解锁规则可后置补充)。 +- Electron 客户端框架搭建——TECH_STACK §8 决策已封闭(但发布签名机制 M-06 需在 RC 前补齐)。 +- 登录模块(accounts App)——文档完整(含 Redis Key 规范),仅需在多租户场景下补 fakeredis 测试基线。 +- UI 静态原型实施——核心 5 页已有 HTML 原型可直接转 HTMX 模板。 + +**必须延后的部分**: +- 系统配置模块(B-01 解除前)。 +- 权限模块(B-03 解除前)。 +- 房源/客源高基数列表查询(B-04 解除前;可先实施 OFFSET 分页,但需排期重构)。 +- 89k 性能 AC 验收(M-05 解除前,AC 不可测)。 + +### 文档体系成熟度评分(与昨日对比) + +| 维度 | 今日 | 昨日 | 变化 | 说明 | +|------|------|------|------|------| +| PRD 业务清晰度 | 7.5/10 | 8.5/10 | ↓ | 新发现枚举塌方 + 系统配置空骨架 | +| DATA_MODEL 落地深度 | 8.5/10 | 8/10 | ↑ | 楼盘锁字段、价格走势、租户注销已落地 | +| TECH_STACK 完整性 | 6/10 | 6/10 | — | 三大横切规范仍未补 | +| UI/UX 系统化 | 7/10 | 5/10 | ↑↑ | UI_DESIGN 原型显著启动 | +| 跨文档一致性 | 5.5/10 | 6.5/10 | ↓ | 枚举塌方拉低评分 | +| 多租户隔离严谨度 | 7.5/10 | 7.5/10 | — | Redis Key 部分修复但未统一 | +| 测试可执行性 | 5/10 | (未评估) | 🆕 | 测试规范与 django-tenants 工作模式冲突 | +| **整体** | **6.8/10** | **7/10** | ↓ 0.2 | 新发现风险大于已修复风险,但绝对水平仍属"中等偏上" | + +**结论**:本次 Review 的实质性进展(UI 启动、租户注销 SOP、楼盘锁字段)值得肯定,但**枚举塌方**和**测试规范多租户冲突**是新发现的系统性风险,必须在编码启动前优先治理。在 4 个 Blocker 解除 + 6 项核心 Major 修复后,整体成熟度可重回 8.0+。 + +--- + +## 附录 A:本次 Review 涵盖文档 + +### PRD(8 份) +- `PRD/PRD_MVP.md` v1.0 +- `PRD/TASK.md`(834 行) +- `PRD/房源管理/房源管理模块PRD.md` v2.1(1881 行) +- `PRD/房源管理/楼盘管理模块PRD.md` +- `PRD/客源管理/客源管理模块PRD.md` v1.4(2050 行) +- `PRD/权限管理/权限管理模块PRD.md` v1.1 +- `PRD/组织人事管理/组织人事管理模块PRD.md` v1.2 +- `PRD/系统管理/系统管理模块PRD.md` v1.0 +- `PRD/登录管理/用户登录管理模块PRD.md` v1.4 +- `PRD/发布管理/客户端发布管理模块PRD.md` v1.0 +- `PRD/系统配置/系统配置.md`(128 行骨架) + +### DATA_MODEL(8 份) +- `DATA_MODEL/DATA_MODEL.md` v1.3、`DATA_MODEL_PUBLIC.md`、`DATA_MODEL_ORG.md`、`DATA_MODEL_COMPLEX.md`、`DATA_MODEL_PROPERTY.md`、`DATA_MODEL_CLIENT.md`、`DATA_MODEL_PERMISSION.md`、`DATA_MODEL_LOGIN.md` + +### TECH_STACK(4 份) +- `TECH_STACK/TECH_STACK.md` v2.0 +- `TECH_STACK/登录管理技术方案.md` v2.0 +- `TECH_STACK/权限管理系统技术方案.md` v1.0 +- `TECH_STACK/测试规范.md` v1.0 + +### UI_SYSTEM(3 份) +- `UI_SYSTEM/UI_SYSTEM.md` v1.2(1742 行) +- `UI_SYSTEM/组件清单.md` +- `UI_SYSTEM/组件规范设计.md` + +### UI_DESIGN(10 份原型/设计稿) +- `UI_DESIGN/preview.html`、`客源列表_UI.html`、`客源详情_UI.html`、`房源列表_UI.html`、`新增客源_UI.html`、`编辑客源_UI.html` +- `UI_DESIGN/客源管理/客源列表_UI.md`、`客源详情_UI.md`、`新增客源_UI.md`、`编辑客源_UI.md` +- `UI_DESIGN/房源管理/房源列表_UI.md` + +--- + +## 附录 B:问题汇总速查表 + +| ID | 等级 | 维度 | 责任人 | 简述 | vs 昨日 | +|----|------|------|--------|------|---------| +| B-01 | 🔴 | PRD↔TASK | PM | 系统配置 PRD 空骨架 | ⚠️ P-02 升级 | +| B-02 | 🔴 | PRD↔Data↔TASK | PM+架构师 | 核心枚举三方不一致 | 🆕 新增 | +| B-03 | 🔴 | PRD↔Data | PM | 权限档位三档/五档冲突 | ⚠️ P-01 部分修复 | +| B-04 | 🔴 | TECH/Data | 架构师 | Keyset 分页规范缺失 | ⚠️ D-01 未修复 | +| M-01 | 🟠 | TECH/测试 | 架构师 | 测试规范与 django-tenants 冲突 | 🆕 新增 | +| M-02 | 🟠 | Data↔PRD | 架构师+PM | 主表乐观锁/楼盘锁规则缺失 | ⚠️ D-02 部分修复 | +| M-03 | 🟠 | Data | 架构师 | 高写入表分区 DDL 未落地 | ⚠️ D-04 未修复 | +| M-04 | 🟠 | TECH/多租户 | 架构师 | Celery/R2/索引矩阵三大横切规范 | ⚠️ T-01/T-02/T-03 未修复 | +| M-05 | 🟠 | NFR↔TECH↔测试 | PM+架构师 | 89k <2 秒 AC 不可测 | 🆕 新增(含昨 P-04) | +| M-06 | 🟠 | 安全 | 架构师 | 客户端发布无防降级/签名 | 🆕 新增 | +| M-07 | 🟠 | PRD | PM | 楼盘合并/申请流程占位 | 🆕 新增 | +| M-08 | 🟠 | PRD/Data | PM+架构师 | 客源新房/租房字段未冻结 + market_quotes 缺 + 转公 job 不完整 | 部分新增 | +| M-09 | 🟠 | UI | UI/UX | 复杂组件 + 楼盘/客源浮层原型缺 | ⚠️ U-01/U-02/U-03 未修复 | +| M-10 | 🟠 | 多租户 | 架构师 | Redis Key 跨模块未统一前缀 | ⚠️ X-01 部分修复 | +| M-11 | 🟠 | 安全 | 架构师 | 加密密钥管理方案缺失 | ⚠️ S-01 未修复 | +| M-12 | 🟠 | 合规 | PM+架构师 | RPO 缺 + tenants.status=failed 状态机断链 | 🆕 新增 | +| M-13 | 🟠 | 性能 | 架构师 | 智能配房算法路径未文档化 | ⚠️ P-11 未修复(升级) | +| M-14 | 🟠 | 维护 | PM | TASK.md PRD 锚点断链 | 🆕 新增 | + +**合计**:🔴 4 / 🟠 14 = **18 项 Top 风险**(聚焦 Top 模式,未列入 Minor) + +### 已关闭项(昨日 → 今日) + +| 昨日编号 | 简述 | 关闭依据 | +| --------------- | ----------------------------------------- | ------------------------------------------------- | +| X-02 | 租户注销→导出→清除链路 | `PRD/系统管理` §189-227, §552-553 已落地完整生命周期 | +| U-06 | Wireframe 未启动 | UI_DESIGN/ 已产出 5 页 HTML + 5 份 md 设计稿 | +| 楼盘 4 类锁字段 | DATA_MODEL_COMPLEX 缺 lock_* 字段 | `DATA_MODEL_COMPLEX.md:194-197` 已建(业务规则待补,转 M-02) | +| 楼盘价格走势表 | DATA_MODEL_COMPLEX 缺 complex_price_trends | `DATA_MODEL_COMPLEX.md:376` 已建 | +| 登录 Redis Key 规范 | 跨模块未统一 | 登录方案 §九已建 ✅;统一 schema 前缀仍待办(转 M-10) | + +--- + +*Report Generated: 2026-04-26 by AI Reviewer* +*基线版本:REVIEW_全局_2026-04-25.md* diff --git a/Project/fonrey/TECH_STACK/系统管理技术文档.md b/Project/fonrey/TECH_STACK/系统管理技术文档.md new file mode 100644 index 00000000..972de8a2 --- /dev/null +++ b/Project/fonrey/TECH_STACK/系统管理技术文档.md @@ -0,0 +1,889 @@ +> **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 命名空间完全隔离。 + +--- + +## 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 二次确认 Modal(UI_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 # PlatformAuditLog(append-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_export(CSV/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) `` | 已登录 | +| `/admin/tenants/new/` | GET | `TenantCreateView` | 新建表单 | HTML(Page) | 运营+ | +| `/admin/tenants/new/` | POST | `TenantCreateView` | 提交开通 | HTML(Partial) 表单/422 + `HX-Trigger: showToast` | 运营+ | +| `/admin/tenants//` | GET | `TenantDetailView` | 进入详情 | HTML(Page) | 已登录 | +| `/admin/tenants//edit/` | POST | `TenantUpdateView` | 编辑可变字段 | HTML(Partial) | 运营+ | +| `/admin/tenants//suspend/` | POST | `TenantSuspendView` | 挂起 | HTML(Partial) 状态徽章 + Toast | 运营+ | +| `/admin/tenants//resume/` | POST | `TenantResumeView` | 恢复 | HTML(Partial) | 运营+ | +| `/admin/tenants//soft-delete/` | POST | `TenantSoftDeleteView` | 软删除 | HTML(Partial) | 运营+ | +| `/admin/tenants//hard-delete/` | POST | `TenantHardDeleteView` | 硬删除(需 MFA 二次) | HTML(Partial) | **超级管理员** | +| `/admin/tenants//restore-deletion/` | POST | `TenantRestoreDeletionView` | 撤销软删除(冷静期内) | HTML(Partial) | 运营+ | +| `/admin/tenants//users/` | GET | `TenantUserListPartialView` | 详情页 Tab:用户 | HTML(Partial) | 已登录 | +| `/admin/tenants//users//reset-password/` | POST | `TenantUserResetPasswordView` | 重置租户用户密码 | HTML(Partial) | 运营+ | +| `/admin/tenants//admins/` | GET | `TenantAdminListPartialView` | Tenant Admin 列表 | HTML(Partial) | 运营+ | +| `/admin/tenants//admins/grant/` | POST | `TenantAdminGrantView` | 赋予管理员角色 | HTML(Partial) | 运营+ | +| `/admin/tenants//admins/revoke/` | POST | `TenantAdminRevokeView` | 撤销管理员 | HTML(Partial) | 运营+ | +| `/admin/tenants//plan/upgrade/` | POST | `TenantPlanUpgradeView` | 套餐升级 | HTML(Partial) | 运营+ | +| `/admin/tenants//monitoring/` | GET | `TenantMonitoringPartialView` | 监控 Tab(Grafana iframe) | HTML(Partial) | 已登录 | +| `/admin/tenants//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//backups/` | GET | `TenantBackupListPartialView` | 详情页 Tab:备份 | HTML(Partial) | 已登录 | +| `/admin/tenants//backups/trigger/` | POST | `TenantBackupTriggerView` | 手动触发备份 | HTML(Partial) + `HX-Trigger: showToast` | 运营+ | +| `/admin/system/backups//status/` | GET | `BackupStatusPartialView` | HTMX 每 5s 轮询任务进度 | HTML(Partial) 行 | 已登录 | +| `/admin/system/backups//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//status/` | GET | `ExportStatusPartialView` | 轮询任务进度 | HTML(Partial) | 已登录 | +| `/admin/system/exports//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//progress/` | GET | `UpgradeProgressPartialView` | HTMX 每 3s 轮询进度 | HTML(Partial) 表格 | 已登录 | +| `/admin/system/versions//rollback/` | POST | `RollbackView` | 回滚(需 MFA 二次) | HTML(Partial) | 超级管理员 | +| `/admin/system/versions//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//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//deactivate/` | POST | `AdminDeactivateView` | 停用 | HTML(Partial) | 超级管理员 | +| `/admin/settings/admins//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//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:`
无权限
` | `HX-Trigger: {"fonrey:toast":{"type":"error","message":"权限不足"}}` | +| 需要 MFA 二次确认 | 401 | 触发前端打开 MFA Modal | `HX-Trigger: {"fonrey:mfa-required":{"action":"hard_delete_tenant","target":""}}` | +| 未登录 / 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":"导出任务已提交"}} + + {{ task.modules }}排队中…— + +``` + +**轮询规约**: +- 轮询间隔:备份/导出 = 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 < SUPER;SUPER 可访问全部 + +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//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 URL,TTL = **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 创建 + 迁移 + 默认数据注入 | 30–60s | 不重试(失败必须人工介入) | 标记 `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 扫描冷静期到期租户 | 取决于租户大小,1–10 min | 不重试 | 标记 `failed_to_purge`,告警 | +| `hard_delete_tenant` | 视图触发 | 1–10 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` | 视图触发(高危) | 5–30 min | **不重试** | 失败 → 自动回滚到恢复前快照;事件报告写 `upgrade_events.incident_report` | +| `run_export` | 视图触发 | 1–15 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` | 升级失败 / 手动 | 5–30 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}` 快照到独立文件,约 1–10s | 用于该租户失败时秒级回滚(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` | DB(progress 字段) | = 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//rollout/` | POST | `FeatureFlagRolloutView`(调整百分比 / 策略) | 超级管理员 | +| `/admin/feature-flags//archive/` | POST | `FeatureFlagArchiveView`(归档) | 超级管理员 | +| `/admin/tenants//feature-flags/` | GET | `TenantFlagsPartialView`(详情页 Tab:租户级覆盖) | 已登录 | +| `/admin/tenants//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.5):CPU/内存来自 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()` 字段完整、状态机非法迁移抛错 | +| View(HTTP) | `django-tenants` 公共 schema TestCase + `Client(HTTP_HOST='admin.fonrey.com')` | 三角色 × 关键端点的 200/403/401(未登录)三场景;MFA step-up 拦截;CSRF | +| View(HTMX) | 同上 + `HTTP_HX_REQUEST='true'` | 验证返回为 partial(不含 `` 根标签),且响应头包含约定的 `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`,不与租户加密密钥共用 | +| 密码哈希 | Argon2id(Django `ARGON2_PASSWORD_HASHER`),不允许降级 PBKDF2 | +| 暴力破解防护 | 登录失败 5 次锁定账号 15 min(Redis 计数器,Key `pub:login:fail:{username}`);同 IP 失败 20 次锁定 IP 1h | +| Session 安全 | Cookie `Secure`、`HttpOnly`、`SameSite=Strict`;Cookie domain 限定 `admin.fonrey.com`(不允许跨子域) | +| CSRF | 所有写操作启用 CSRF;HTMX 通过 `hx-headers='{"X-CSRFToken": "..."}'` 在 base 模板注入 | +| CSP | `default-src 'self'`;Grafana iframe 域加入 `frame-src` 白名单;禁止 `unsafe-inline` 脚本(HTMX/Alpine 的内联事件已用 attribute 模式,符合) | +| 高危操作 | 硬删除/恢复/升级/回滚必须 MFA step-up(5 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 类组合最佳实践) | diff --git a/Project/fonrey/prompt/TECH_STACK 系统管理技术方案提示词.md b/Project/fonrey/prompt/TECH_STACK 系统管理技术方案提示词.md new file mode 100644 index 00000000..b753e21b --- /dev/null +++ b/Project/fonrey/prompt/TECH_STACK 系统管理技术方案提示词.md @@ -0,0 +1,155 @@ +## 角色与背景 + +你是一名资深系统架构师,具备 Django 全栈开发、PostgreSQL 数据库设计、云基础设施和 API 设计的专业能力。 +你的核心方法是:基于产品需求推导技术方案,每个设计决策都附带选型理由,确保方案在当前技术栈约束内可落地。 + +**工作目录**:`/mnt/d/Workspace/nexus` + +**你的职责边界**: +- ✅ 负责:数据库模型(DDL)、API 端点设计、系统架构、安全方案、部署规范、目录结构 +- ❌ 不负责:用户故事、验收标准、页面交互描述——这些见配套 PRD 文档 + +--- + +## 项目背景 + +**项目**:**Fonrey(房睿)**——面向房地产经纪公司的 B2B SaaS 平台 +**多租户模式**:django-tenants(PostgreSQL Schema 隔离),所有查询必须基于当前租户 Schema +**数据量级**:89,000+ 条房源/客源记录,需要关注查询性能 + +请读取以下文档作为设计输入: +- 架构师方法论:`Project/fonrey/prompt/engineering-backend-architect.md` +- 现有技术栈草案:`Project/fonrey/TECH_STACK/TECH_STACK.md` +- 现有数据模型草案:`Project/fonrey/DATA_MODEL/DATA_MODEL.md` +- 现有UI总体设计方案:`Project/fonrey/UI_SYSTEM/UI_SYSTEM.md` + +--- + +## 需求输入(本次设计的 PRD 依据) + +请读取以下 PRD 文档,从中提取:功能边界、字段规范、权限要求、性能指标,作为本次技术设计的需求基准。 + +- `Project/fonrey/PRD/系统管理/系统管理模块PRD` + +请读取以下 DATA_MODEL 文档,从中提取该模块得数据模型设计,作为本次技术设计得需求基准: +- `Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC` + + +--- + +## 技术栈约束(不得变更,不得建议替代方案) + +| 层级 | 技术选型 | 关键约束 | +|------|---------|---------| +| 前端 | HTMX + Alpine.js + Tailwind CSS | ❌ 禁止 React / Vue / Angular | +| 后端 | Django 4.x(ASGI)| 使用 Class-Based Views,遵循 Django 约定 | +| 数据库 | PostgreSQL | 多租户 Schema 隔离(django-tenants) | +| 缓存 | Redis | 会话、计数器、热点数据缓存 | +| 异步 | Celery + Redis Broker | 耗时 > 500ms 的任务必须走 Celery | +| 文件存储 | Cloudflare R2 | 图片 / 文件上传,不得存本地磁盘 | +| 当前阶段 | Web 端 | 移动端为 v2,当前不设计 App API | + +详细约束见:`Project/fonrey/TECH_STACK/TECH_STACK.md` + + +## 任务:TECH_STACK 技术文档补全 + +**输出路径**:`Project/fonrey/TECH_STACK/系统管理技术文档.md` +(在现有草案基础上补全 / 新增本次模块相关章节,不覆盖已有内容) + +### 2.1 Django App 目录结构 + +针对本次 PRD 涉及的 App,输出标准目录结构: + +``` +apps/【app_name】/ +├── models.py # 数据模型(对应 DATA_MODEL.md) +├── views.py # 视图(HTMX 局部刷新端点 + 页面视图) +├── urls.py # 路由 +├── forms.py # Django Form / ModelForm +├── serializers.py # 仅 JSON API 场景使用 +├── tasks.py # Celery 异步任务 +├── signals.py # Django Signals(如有) +├── admin.py # Django Admin 注册 +├── tests/ +│ ├── test_models.py +│ ├── test_views.py +│ └── test_tasks.py +└── templates/【app_name】/ + ├── 【feature】_list.html # 完整页面 + ├── 【feature】_detail.html + ├── partials/ + │ ├── 【feature】_row.html # HTMX 局部模板 + │ ├── 【feature】_form.html + │ └── 【feature】_pagination.html + └── components/ + └── 【reusable_component】.html +``` + +### 2.2 API 端点设计 + +> 基于 PRD 第 4 章(用户故事)和第 5 章(交互流程)推导所需端点。 + +| URL Pattern | HTTP 方法 | 视图名称 | 触发场景 | 响应类型 | 权限要求 | +|------------|----------|---------|---------|---------|---------| +| `/complex//` | GET | `ComplexDetailView` | 进入楼盘详情页 | HTML(完整页面) | 已登录 | +| `/complex//buildings/` | GET | `BuildingListPartialView` | HTMX 刷新楼栋列表 | HTML Partial | 已登录 | +| `/complex//buildings/add/` | POST | `BuildingCreateView` | 提交新增楼栋表单 | HTML Partial / JSON | 店长及以上 | +| `/complex/photos/upload/` | POST | `ComplexPhotoUploadView` | 上传图片至 R2 | JSON | 已登录 | + +**HTMX 响应规范**: +- 成功操作 → 返回更新后的 HTML Partial + `HX-Trigger: showToast` 响应头 +- 表单校验失败 → 返回含错误信息的表单 Partial,HTTP 422 +- 权限不足 → 返回 HTTP 403,前端 HTMX 触发全局错误 Toast + +**Celery 异步任务端点**: +- 异步任务提交后立即返回 `{"task_id": "xxx", "status": "pending"}` +- 前端通过轮询或 SSE 获取任务状态 + +### 2.3 权限与认证实现 + +> 基于 PRD 第 5.4 节(权限控制表)实现。 + +- **角色体系**:超级管理员 / 经纪公司管理员(店长) / 经纪人 / 运营行政 +- **数据范围控制**:经纪人默认只查询自己名下数据,使用 Django Manager 层面过滤 +- **视图层权限装饰器**:统一使用 `@permission_required` 或自定义 Mixin + +```python +# 权限 Mixin 示例(不需要完整代码,给出接口约定) +class BranchManagerRequiredMixin(LoginRequiredMixin): + """要求店长及以上角色""" + ... +``` + +### 2.4 缓存策略 + +| 缓存对象 | Key 格式 | TTL | 失效条件 | +|---------|---------|-----|---------| +| 楼盘基础信息 | `complex:{tenant}:{id}` | 10min | 编辑后主动清除 | +| 区域下拉选项 | `region:{tenant}:list` | 60min | 区域数据变更 | +| 经纪人列表 | `agent:{tenant}:list` | 5min | 人员变动 | + +### 2.5 文件上传规范(Cloudflare R2) + +- **上传流程**:前端直传 R2(Presigned URL)或后端中转,说明选型理由 +- **文件命名**:`{tenant}/{app}/{model_id}/{uuid}.{ext}` +- **类型校验**:后端二次校验 MIME Type,不信任前端传入的文件类型 +- **大小限制**:在 Django 视图层和 Nginx 层双重限制 + +### 2.6 Celery 异步任务规范 + +> 列出本次模块中需要异步处理的任务。 + +| 任务名称 | 触发场景 | 预估耗时 | 重试策略 | 失败处理 | +|---------|---------|---------|---------|---------| +| `process_complex_photo` | 图片上传后压缩/生成缩略图 | ~2s | 最多 3 次,指数退避 | 记录错误日志,通知用户 | +| `export_complex_list` | 导出楼盘列表 Excel | ~5-30s | 最多 2 次 | 标记任务失败,提示重试 | + +### 2.7 测试规范 + +每个 App 须包含以下测试覆盖: + +- **Model 测试**:字段约束、软删除、多租户隔离 +- **View 测试**:正常响应、权限拒绝(403)、表单校验失败(422) +- **HTMX 端点测试**:验证响应为 HTML Partial 而非完整页面 +- **Celery 任务测试**:使用 `CELERY_TASK_ALWAYS_EAGER=True` 同步测试 \ No newline at end of file