Files
nexus/Project/fonrey/TECH_STACK/系统管理技术文档.md
2026-04-26 20:33:29 +08:00

54 KiB
Raw Blame History

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 范围依据:

关键定位:本模块是 平台运营后台admin.fonrey.com),数据全部存于 PostgreSQL public schema归属 django-tenantsSHARED_APPS不参与租户 schema 切换。所有功能仅限平台管理员(platform_admins)访问,与租户应用(*.fonrey.com的认证体系、Session、URL 命名空间完全隔离。


1. 模块边界与定位

维度 说明
部署域名 admin.fonrey.com独立子域Nginx 层 IP 白名单)
Schema 归属 publicSHARED_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.admindjango.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 目录结构

本模块对应单个 Appapps/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 ViewsListView / DetailView / FormView),禁止函数视图
  • tasks/services/ 分离:tasks/ 是 Celery 入口(薄壳),业务逻辑落在 services/,便于单测
  • templates/admin_console/partials/ 命名以 _partial/partials 区分完整页 vs HTMX 局部模板

3. 路由命名空间与设置

3.1 settings 关键配置

# 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 命名空间

# 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 局部刷新(仅返回片段,前端 swapJSON 仅用于 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 异步任务前端协议

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

# 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 BackendPlatformAdminBackend,校验 platform_admins.password_hashDjango PBKDF2/Argon2登录成功后写 admin_sessions,并把 request.session['admin_id']session_token 关联。

不复用 django.contrib.auth.User

  • 租户业务用 django.contrib.auth + apps.account.User(在租户 schema
  • 平台管理后台完全独立,避免角色/Session 跨域污染

5.3 中间件链(顺序敏感)

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_whitelistRedis 缓存),未命中返回 403 静态页(不暴露后台存在)
  • AdminSessionMiddleware:每次请求把 admin_sessions.expires_at = NOW() + 30min,过期则清 cookie 并 302 到 /admin/login/

5.4 视图层 Mixin

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"""

典型组合

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_contextcall_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

-- 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

# 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_logplatform_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.GrafanaWebhookViewHMAC 签名校验)
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 adminCI 守门)

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 SecureHttpOnlySameSite=StrictCookie 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 层 triggerBEFORE UPDATE OR DELETE ON platform_audit_logs ... RAISE EXCEPTIONORM 层 Manager 重写 update() delete() 抛错
跨域名严禁串台 租户 host 上访问 /admin/... 必须 404管理 host 上访问租户 URL 必须 404IpWhitelistMiddleware + URLConf 双重保证
Django Admin 全环境弃用 INSTALLED_APPS 不包含 django.contrib.adminurls_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 类组合最佳实践)