Files
nexus/Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md
2026-04-26 20:33:29 +08:00

870 lines
46 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.
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
# Fonrey — Public Schema 数据模型
> **作者**: Backend Architect
> **版本**: v1.2
> **日期**: 2026-04-26
> **权威源**: 本文件是 `public` schema 所有表的唯一权威定义
> **设计依据**: 系统管理模块 PRD`PRD/系统管理/系统管理模块PRD.md`);客户端发布管理模块 PRD`PRD/发布管理/客户端发布管理模块PRD.md`
> **索引文档**: [`DATA_MODEL.md §三`](./DATA_MODEL.md)(仅保留摘要索引,开发以本文件为准)
---
## 一、概览
`public` schema 存储**平台运营层**数据,与各租户的业务 schema 完全隔离。
### 架构定位
```
PostgreSQL Instance
├── public schema平台运营层← 本文件覆盖
│ ├── tenants 租户注册与生命周期(含 feature_flags JSONB
│ ├── 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 升级事件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
```
### 表清单
| 表名 | 说明 | 节 |
|------|------|----|
| `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` | 升级/回滚事件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 |
---
## 二、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 = 内测租户,参与灰度升级
-- 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(),
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';
-- 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 (
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 版本
-- 升级事件(每次执行升级或回滚对应一条记录)
-- 设计依据: 系统管理技术文档 §8.5§8.6A/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 = 灰度
-- 升级编排状态机(与技术文档 §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, -- 回滚后生成的事件报告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(),
-- 数据完整性约束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 客户端发布管理
```sql
-- ────────────────────────────────────────────────────────────
-- 6. 客户端发布管理
-- ────────────────────────────────────────────────────────────
-- 设计依据: 客户端发布管理模块 PRD §5.3
-- 说明: 本表属于 shared_appspublic schema所有租户共享同一套客户端版本
-- 不做多租户隔离。
-- 客户端版本发布表
CREATE TABLE public.client_releases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- 版本标识
version VARCHAR(20) UNIQUE NOT NULL,
-- SemVer 格式,如 '1.2.3';由应用层校验格式,数据库层仅保证唯一性
platform VARCHAR(20) NOT NULL DEFAULT 'win32'
CHECK (platform IN ('win32')),
-- 当前仅支持 Windows后续支持 darwin / linux 时扩展 CHECK
arch VARCHAR(10) NOT NULL DEFAULT 'x64'
CHECK (arch IN ('x64', 'arm64')),
-- 版本类型与状态
release_type VARCHAR(10) NOT NULL DEFAULT 'normal'
CHECK (release_type IN ('normal', 'force')),
-- normal = 普通更新提示但可延后force = 强制升级,不可跳过
status VARCHAR(10) NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'published', 'archived')),
-- draft = 草稿不对外生效published = 已发布客户端可感知archived = 已下线
-- 兼容性约束
min_required_version VARCHAR(20),
-- 低于该版本的客户端将被强制要求升级NULL = 无最低版本限制
-- 由应用层在查询时比较 SemVer数据库层不做运算
-- 安装包EXE
download_url TEXT NOT NULL,
-- Cloudflare R2 公开 URL格式
-- https://download.fonrey.com/releases/v{version}/fonrey-setup-{version}-win.exe
checksum_sha256 VARCHAR(64) NOT NULL,
-- 安装包 SHA256 十六进制字符串64 位),客户端下载完成后校验
file_size_bytes BIGINT,
-- 安装包字节大小,用于前端展示下载大小
-- 便携版ZIP可选
portable_url TEXT,
-- 无需安装的 ZIP 版本,供无管理员权限的企业环境使用
portable_checksum_sha256 VARCHAR(64),
-- 更新内容
release_notes TEXT NOT NULL,
-- 对外展示的更新日志Markdown 格式,最多 2000 字
internal_notes TEXT,
-- 内部技术说明,不对外展示
-- 统计
download_count INTEGER NOT NULL DEFAULT 0,
-- 该版本安装包被下载次数,由应用层在每次下载时原子递增
-- 时间与操作人
published_at TIMESTAMPTZ,
-- 首次设为 published 时记录,后续状态变更不更新此字段
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,
published_by UUID REFERENCES public.platform_admins(id) ON DELETE SET NULL
);
-- 只允许一条 published 记录(同平台+架构下的当前生效版本)
CREATE UNIQUE INDEX idx_client_releases_published
ON public.client_releases(platform, arch)
WHERE status = 'published';
-- 快速查找草稿/已发布版本(管理后台列表查询)
CREATE INDEX idx_client_releases_status ON public.client_releases(status, created_at DESC);
-- 按版本号快速定位(客户端更新检测时传入 current_version 查询)
CREATE INDEX idx_client_releases_version ON public.client_releases(version);
```
### 2.7 Feature Flag 灰度体系
```sql
-- ────────────────────────────────────────────────────────────
-- 7. Feature Flag 灰度体系
-- ────────────────────────────────────────────────────────────
-- 设计依据: 系统管理技术文档 §8.7C 类升级 / 运行时功能灰度)
-- 说明: 与 §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无 UPDATEappend-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);
```
---
## 三、关键约束与禁止操作
| 规则 | 说明 |
|------|------|
| `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` |
| `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 |
---
## 四、状态机
### 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 升级事件状态
```
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`(双重审计)。
---
## 五、查询模式
### 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;
-- 客户端更新检测查询当前生效版本platform=win32, arch=x64
SELECT version, release_type, download_url, portable_url,
checksum_sha256, file_size_bytes, release_notes, published_at
FROM public.client_releases
WHERE platform = 'win32'
AND arch = 'x64'
AND status = 'published'
LIMIT 1;
-- 客户端版本管理列表(管理后台,含各状态版本)
SELECT version, platform, arch, release_type, status,
download_count, published_at, created_at
FROM public.client_releases
ORDER BY created_at DESC;
-- 统计各版本活跃客户端数(需结合客户端上报心跳表,当前仅记录下载量)
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 禁止查询
| 禁止操作 | 原因 |
|----------|------|
| `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 = ...` | 域名路由不可变 |
| `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 强制保护 |
---
## 六、版本历史
| 版本 | 日期 | 变更 |
|------|------|------|
| 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 禁止操作 |