> **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, -- 操作管理员 ID;NULL = 系统自动(Celery) operator_name VARCHAR(100), -- 快照,防止管理员被删后失去可追溯性 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- 无 deleted_at,无 UPDATE,append-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, -- 管理员 ID;NULL 表示系统自动操作 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), -- 操作对象 ID(UUID 或其他) 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.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 = 灰度 -- 升级编排状态机(与技术文档 §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_apps(public 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.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); ``` --- ## 三、关键约束与禁止操作 | 规则 | 说明 | |------|------| | `tenant_status_logs` append-only | 禁止 UPDATE / DELETE;状态机变更只追加新行 | | `platform_audit_logs` append-only | 禁止 UPDATE / DELETE;建议按月 RANGE 分区 | | `public.tenants.schema_name` 不可修改 | 创建后禁止 UPDATE,PG 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 禁止操作 |