Files
nexus/Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md

489 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fonrey — Public Schema 数据模型
> **作者**: Backend Architect
> **版本**: v1.0
> **日期**: 2026-04-24
> **权威源**: 本文件是 `public` schema 所有表的唯一权威定义
> **设计依据**: 系统管理模块 PRD`PRD/系统管理/系统管理模块PRD.md`
> **索引文档**: [`DATA_MODEL.md §三`](./DATA_MODEL.md)(仅保留摘要索引,开发以本文件为准)
---
## 一、概览
`public` schema 存储**平台运营层**数据,与各租户的业务 schema 完全隔离。
### 架构定位
```
PostgreSQL Instance
├── public schema平台运营层← 本文件覆盖
│ ├── tenants 租户注册与生命周期
│ ├── domains 域名路由
│ ├── tenant_status_logs 状态变更审计
│ ├── platform_admins 管理员账号
│ ├── admin_mfa_devices TOTP 设备
│ ├── admin_sessions 登录会话
│ ├── ip_whitelist 访问控制
│ ├── platform_audit_logs 操作审计
│ ├── backup_schedules 备份计划
│ ├── backup_records 备份记录
│ ├── export_tasks 数据导出
│ ├── system_versions 版本历史
│ └── upgrade_events 升级记录
├── tenant_abc schema租户业务层见各子文档
└── tenant_xyz schema
```
### 表清单
| 表名 | 说明 | 节 |
|------|------|----|
| `public.tenants` | 租户主表(每家房产公司一条记录) | §2.1 |
| `public.domains` | 域名↔租户映射(多域名支持) | §2.1 |
| `public.tenant_status_logs` | 租户状态变更不可变审计日志 | §2.1 |
| `public.platform_admins` | 平台管理员账号3 种角色) | §2.2 |
| `public.admin_mfa_devices` | 管理员 TOTP MFA 设备(强制启用) | §2.2 |
| `public.admin_sessions` | 管理员登录会话30 min 超时,支持强制登出) | §2.2 |
| `public.ip_whitelist` | 管理控制台 CIDR 白名单 | §2.2 |
| `public.platform_audit_logs` | 所有写操作+高危操作审计append-only建议月度分区 | §2.3 |
| `public.backup_schedules` | 全局/租户级定时备份计划 | §2.4 |
| `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 |
---
## 二、DDL 定义
### 2.1 租户管理
```sql
-- ============================================================
-- 文件: shared_schema.sql
-- 用途: django-tenants 公共 Schema存放平台运营层数据
-- 设计依据: 系统管理模块 PRD v1.0
-- ============================================================
-- ────────────────────────────────────────────────────────────
-- 1. 租户管理
-- ────────────────────────────────────────────────────────────
-- 租户状态枚举(生命周期状态机,见 PRD §9.1
-- creating → active ←→ suspended → pending_delete → deleted
-- ↑ 硬删除直接到 deleted
-- 租户主表(每家房产经纪公司一条记录)
CREATE TABLE public.tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
schema_name VARCHAR(63) UNIQUE NOT NULL, -- PG schema 名,最长 63 字符,创建后不可修改
name VARCHAR(255) NOT NULL, -- 公司名称
short_name VARCHAR(100), -- 简称/品牌名
contact_name VARCHAR(100) NOT NULL, -- 主联系人姓名
contact_email VARCHAR(254) NOT NULL, -- 联系邮箱(接收通知/欢迎邮件)
region VARCHAR(100), -- 所在地区(省市,如「上海市」)
plan VARCHAR(20) NOT NULL DEFAULT 'basic'
CHECK (plan IN ('basic','professional','enterprise')),
-- 状态机
status VARCHAR(20) NOT NULL DEFAULT 'creating'
CHECK (status IN ('creating','active','suspended','pending_delete','deleted','failed')),
suspended_until TIMESTAMPTZ, -- NULL = 永久挂起,非 NULL = Celery Beat 定时恢复
suspended_reason VARCHAR(50)
CHECK (suspended_reason IN ('overdue','violation','requested','other')),
deleted_at TIMESTAMPTZ, -- 软删除时间戳;硬删除直接物理删除行
-- 订阅
paid_until DATE, -- 订阅到期日
on_trial BOOLEAN NOT NULL DEFAULT TRUE,
-- 灰度升级
is_canary BOOLEAN NOT NULL DEFAULT FALSE, -- TRUE = 内测租户,参与灰度升级
-- 元数据
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID, -- 创建该租户的管理员 ID可 NULL初始化时
extra JSONB NOT NULL DEFAULT '{}' -- 预留扩展字段
);
CREATE INDEX idx_tenants_status ON public.tenants(status);
CREATE INDEX idx_tenants_suspended_until ON public.tenants(suspended_until)
WHERE status = 'suspended' AND suspended_until IS NOT NULL;
CREATE INDEX idx_tenants_canary ON public.tenants(is_canary) WHERE is_canary = TRUE;
CREATE INDEX idx_tenants_pending_delete ON public.tenants(deleted_at)
WHERE status = 'pending_delete';
-- 域名映射表(支持多域名绑定一个租户,子域名创建后不可修改)
CREATE TABLE public.domains (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
domain VARCHAR(253) UNIQUE NOT NULL, -- 含子域名的完整域名(如 abc.platform.com
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_domains_tenant ON public.domains(tenant_id);
CREATE UNIQUE INDEX idx_domains_primary ON public.domains(tenant_id) WHERE is_primary = TRUE;
-- 租户状态变更日志append-only不可删除
-- 记录所有 status 变更creating→active / active→suspended / suspended→active / →pending_delete / →deleted
CREATE TABLE public.tenant_status_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
from_status VARCHAR(20), -- NULL 表示初始创建
to_status VARCHAR(20) NOT NULL,
reason TEXT,
operator_id UUID, -- 操作管理员 IDNULL = 系统自动Celery
operator_name VARCHAR(100), -- 快照,防止管理员被删后失去可追溯性
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-- 无 deleted_at无 UPDATEappend-only
);
CREATE INDEX idx_tenant_status_logs_tenant ON public.tenant_status_logs(tenant_id, created_at DESC);
```
### 2.2 平台管理员
```sql
-- ────────────────────────────────────────────────────────────
-- 2. 平台管理员
-- ────────────────────────────────────────────────────────────
-- 管理员账号(与租户 staff 完全独立,存于 public schema
CREATE TABLE public.platform_admins (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(150) UNIQUE NOT NULL,
email VARCHAR(254) UNIQUE NOT NULL,
display_name VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL, -- Django PBKDF2 / Argon2 哈希
role VARCHAR(20) NOT NULL
CHECK (role IN ('super_admin','ops_operator','read_only_auditor')),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE, -- 首次登录前为 FALSE配置 TOTP 后变 TRUE
last_login_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES public.platform_admins(id) ON DELETE SET NULL
);
CREATE INDEX idx_platform_admins_role ON public.platform_admins(role) WHERE is_active = TRUE;
-- MFA 设备TOTP每管理员可注册多个设备但通常一个
CREATE TABLE public.admin_mfa_devices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
admin_id UUID NOT NULL REFERENCES public.platform_admins(id) ON DELETE CASCADE,
device_name VARCHAR(100) NOT NULL DEFAULT 'Authenticator App',
totp_secret VARCHAR(255) NOT NULL, -- Base32 加密存储
is_confirmed BOOLEAN NOT NULL DEFAULT FALSE, -- 首次验证通过后置 TRUE
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMPTZ
);
CREATE INDEX idx_admin_mfa_devices_admin ON public.admin_mfa_devices(admin_id)
WHERE is_confirmed = TRUE;
-- 管理员登录会话(支持强制登出)
CREATE TABLE public.admin_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
admin_id UUID NOT NULL REFERENCES public.platform_admins(id) ON DELETE CASCADE,
session_token VARCHAR(255) UNIQUE NOT NULL, -- 随机安全令牌
ip_address INET NOT NULL,
user_agent TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL, -- 默认 NOW() + 30 分钟,活动时滚动续期
revoked_at TIMESTAMPTZ, -- 强制登出时记录
revoked_by UUID REFERENCES public.platform_admins(id) ON DELETE SET NULL
);
CREATE INDEX idx_admin_sessions_admin ON public.admin_sessions(admin_id) WHERE is_active = TRUE;
CREATE INDEX idx_admin_sessions_expires ON public.admin_sessions(expires_at) WHERE is_active = TRUE;
-- IP 白名单(管理控制台访问限制)
CREATE TABLE public.ip_whitelist (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cidr CIDR NOT NULL, -- 如 203.0.113.0/24 或 203.0.113.5/32
label VARCHAR(100), -- 备注,如「上海办公室」
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES public.platform_admins(id) ON DELETE SET NULL
);
CREATE INDEX idx_ip_whitelist_active ON public.ip_whitelist(cidr) WHERE is_active = TRUE;
```
### 2.3 审计日志append-only
```sql
-- ────────────────────────────────────────────────────────────
-- 3. 审计日志append-only
-- ────────────────────────────────────────────────────────────
-- 平台操作审计日志(所有写操作 + 高危操作,无 deleted_at无 UPDATE
CREATE TABLE public.platform_audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
operator_id UUID, -- 管理员 IDNULL 表示系统自动操作
operator_name VARCHAR(100), -- 快照(防止账号删除后失去溯源)
action_type VARCHAR(50) NOT NULL,
-- CREATE_TENANT | SUSPEND_TENANT | RESUME_TENANT | DELETE_TENANT | HARD_DELETE_TENANT
-- RESTORE_DATA | TRIGGER_BACKUP | SYSTEM_UPGRADE | ROLLBACK
-- RESET_PASSWORD | CREATE_ADMIN | DEACTIVATE_ADMIN | FORCE_LOGOUT
-- UPDATE_IP_WHITELIST | UPDATE_BACKUP_SCHEDULE | EXPORT_DATA | ...
target_type VARCHAR(30) NOT NULL, -- Tenant | User | System | Backup | Admin
target_id VARCHAR(255), -- 操作对象 IDUUID 或其他)
target_name VARCHAR(255), -- 操作对象可读名称(快照)
payload_summary TEXT, -- 操作内容摘要(非敏感字段)
result VARCHAR(10) NOT NULL DEFAULT 'SUCCESS'
CHECK (result IN ('SUCCESS','FAILED')),
error_message TEXT,
ip_address INET,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-- 无 deleted_at无 UPDATE建议按月 RANGE 分区
);
CREATE INDEX idx_audit_logs_operator ON public.platform_audit_logs(operator_id, created_at DESC);
CREATE INDEX idx_audit_logs_action ON public.platform_audit_logs(action_type, created_at DESC);
CREATE INDEX idx_audit_logs_target ON public.platform_audit_logs(target_type, target_id, created_at DESC);
CREATE INDEX idx_audit_logs_created ON public.platform_audit_logs(created_at DESC);
```
### 2.4 备份与导出
```sql
-- ────────────────────────────────────────────────────────────
-- 4. 备份与导出
-- ────────────────────────────────────────────────────────────
-- 定时备份计划(全局策略 + 租户覆盖策略)
CREATE TABLE public.backup_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES public.tenants(id) ON DELETE CASCADE,
-- NULL = 全局默认计划;非 NULL = 该租户的独立计划(覆盖全局)
frequency VARCHAR(10) NOT NULL DEFAULT 'daily'
CHECK (frequency IN ('hourly','daily','weekly')),
scheduled_time TIME NOT NULL DEFAULT '02:00', -- 执行时间窗口UTC
retention_count INTEGER NOT NULL DEFAULT 10, -- 最多保留 N 个备份版本
storage_target VARCHAR(20) NOT NULL DEFAULT 'r2'
CHECK (storage_target IN ('local','s3','r2','gcs')),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES public.platform_admins(id) ON DELETE SET NULL,
UNIQUE (tenant_id) -- 每个租户最多一条独立计划NULL tenant_id 用应用层保证全局唯一
);
-- 备份任务执行记录
CREATE TABLE public.backup_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
trigger_type VARCHAR(10) NOT NULL
CHECK (trigger_type IN ('auto','manual','pre_upgrade','pre_restore')),
status VARCHAR(15) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','in_progress','success','failed')),
storage_target VARCHAR(20) NOT NULL,
storage_path TEXT, -- R2/S3 存储路径
size_bytes BIGINT, -- 备份包大小
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
error_message TEXT,
triggered_by UUID REFERENCES public.platform_admins(id) ON DELETE SET NULL,
upgrade_event_id UUID, -- 关联升级事件pre_upgrade 类型)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_backup_records_tenant ON public.backup_records(tenant_id, created_at DESC);
CREATE INDEX idx_backup_records_status ON public.backup_records(status)
WHERE status IN ('pending','in_progress');
-- 数据导出任务(异步 Celery 执行)
CREATE TABLE public.export_tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
requested_by UUID REFERENCES public.platform_admins(id) ON DELETE SET NULL,
modules TEXT[] NOT NULL,
-- 'clients' | 'properties' | 'transactions' | 'system_config' | 'all'
format VARCHAR(10) NOT NULL
CHECK (format IN ('csv','json','sql_dump')),
status VARCHAR(15) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','in_progress','done','failed')),
storage_path TEXT, -- R2 临时目录路径
download_url TEXT, -- 带签名下载链接
expires_at TIMESTAMPTZ, -- 下载链接有效期(默认 24 小时)
size_bytes BIGINT,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_export_tasks_tenant ON public.export_tasks(tenant_id, created_at DESC);
CREATE INDEX idx_export_tasks_status ON public.export_tasks(status)
WHERE status IN ('pending','in_progress');
```
### 2.5 版本升级管理
```sql
-- ────────────────────────────────────────────────────────────
-- 5. 版本升级管理
-- ────────────────────────────────────────────────────────────
-- 平台版本历史
CREATE TABLE public.system_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
version_number VARCHAR(50) UNIQUE NOT NULL, -- 如 v2.3.1
release_notes TEXT,
artifact_url TEXT, -- 制品库地址
status VARCHAR(15) NOT NULL DEFAULT 'previous'
CHECK (status IN ('current','previous','archived')),
released_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES public.platform_admins(id) ON DELETE SET NULL
);
CREATE UNIQUE INDEX idx_system_versions_current ON public.system_versions(status)
WHERE status = 'current'; -- 全局只允许一个 current 版本
-- 升级事件(每次执行升级或回滚对应一条记录)
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')),
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}]
tenant_progress JSONB NOT NULL DEFAULT '[]',
-- 回滚触发条件
rollback_reason TEXT,
incident_report TEXT, -- 回滚后生成的事件报告
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()
);
CREATE INDEX idx_upgrade_events_status ON public.upgrade_events(status, created_at DESC);
```
---
## 三、关键约束与禁止操作
| 规则 | 说明 |
|------|------|
| `tenant_status_logs` append-only | 禁止 UPDATE / DELETE状态机变更只追加新行 |
| `platform_audit_logs` append-only | 禁止 UPDATE / DELETE建议按月 RANGE 分区 |
| `public.tenants.schema_name` 不可修改 | 创建后禁止 UPDATEPG schema 绑定 |
| `public.domains.domain` 不可修改 | 子域名创建后禁止 UPDATE |
| `system_versions` 唯一 current | `idx_system_versions_current` 部分唯一索引保证全局只有一个 `status='current'` |
| `backup_schedules.tenant_id` UNIQUE | 每个租户最多一条独立计划;`NULL` 全局计划由应用层保证唯一 |
| `platform_admins``staff` 完全独立 | 不共享表、不共享 auth 系统 |
| MFA 强制 | `platform_admins.mfa_enabled` 在首次 TOTP 确认后才变 TRUE登录流必须检查 |
| `admin_sessions` 30 分钟滚动超时 | 应用层每次活跃请求更新 `expires_at = NOW() + 30min` |
---
## 四、状态机
### 4.1 租户生命周期
```
creating ──(初始化完成)──► active
active ──(逾期/违规/申请)──► suspended ──(恢复条件满足)──► active
active ──(申请注销)──► pending_delete ──(30天后/管理员确认)──► deleted
suspended ──(申请注销)──► pending_delete
creating ──(初始化失败)──► failed
```
**字段映射**
- `status` 枚举:`creating | active | suspended | pending_delete | deleted | failed`
- `suspended_until = NULL`:永久挂起;`suspended_until IS NOT NULL`Celery Beat 定时自动恢复
- `deleted_at`:软删除时间戳;硬删除时物理删除整行
### 4.2 升级事件状态
```
pending → health_check → in_progress → success
└─► failed → rolled_back
```
---
## 五、查询模式
### 5.1 常用查询
```sql
-- 查询所有活跃租户
SELECT id, name, plan, paid_until
FROM public.tenants
WHERE status = 'active'
ORDER BY created_at DESC;
-- 查询即将到期的租户7 天内)
SELECT id, name, contact_email, paid_until
FROM public.tenants
WHERE status = 'active'
AND paid_until BETWEEN CURRENT_DATE AND CURRENT_DATE + 7;
-- 查询灰度租户canary 升级目标)
SELECT id, schema_name, name
FROM public.tenants
WHERE is_canary = TRUE AND status = 'active';
-- 查询某租户所有状态变更历史
SELECT from_status, to_status, reason, operator_name, created_at
FROM public.tenant_status_logs
WHERE tenant_id = $1
ORDER BY created_at DESC;
-- 查询待自动恢复的挂起租户Celery Beat 使用)
SELECT id, schema_name, name
FROM public.tenants
WHERE status = 'suspended'
AND suspended_until IS NOT NULL
AND suspended_until <= NOW();
-- 查询某管理员近 30 天的审计记录
SELECT action_type, target_type, target_name, result, created_at
FROM public.platform_audit_logs
WHERE operator_id = $1
AND created_at >= NOW() - INTERVAL '30 days'
ORDER BY created_at DESC;
-- 查询进行中的备份任务
SELECT br.id, t.name AS tenant_name, br.trigger_type, br.started_at
FROM public.backup_records br
JOIN public.tenants t ON t.id = br.tenant_id
WHERE br.status IN ('pending', 'in_progress')
ORDER BY br.created_at DESC;
```
### 5.2 禁止查询
| 禁止操作 | 原因 |
|----------|------|
| `UPDATE public.tenant_status_logs` | append-only 审计表 |
| `DELETE FROM public.platform_audit_logs` | append-only 审计表 |
| `UPDATE public.tenants SET schema_name = ...` | schema 名绑定 PG 物理 schema |
| `UPDATE public.domains SET domain = ...` | 域名路由不可变 |
---
## 六、版本历史
| 版本 | 日期 | 变更 |
|------|------|------|
| v1.0 | 2026-04-24 | 从 `DATA_MODEL.md §三` 独立拆分;内容等价于 v1.2 DATA_MODEL.md §三 |