Files
nexus/Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md
2026-04-29 15:43:49 +08:00

46 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 — Public Schema 数据模型

作者: Backend Architect
版本: v1.2
日期: 2026-04-26
权威源: 本文件是 public schema 所有表的唯一权威定义
设计依据: 系统管理模块 PRDPRD/系统管理/系统管理模块PRD.md);客户端发布管理模块 PRDPRD/发布管理/客户端发布管理模块PRD.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 租户管理

-- ============================================================
-- 文件: 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 平台管理员

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

-- ────────────────────────────────────────────────────────────
-- 3. 审计日志append-only
-- ────────────────────────────────────────────────────────────

-- 平台操作审计日志(所有写操作 + 高危操作,无 deleted_at无 UPDATE
CREATE TABLE public.platform_audit_logs (
    id              UUID NOT NULL DEFAULT gen_random_uuid(),
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),   -- 分区键
    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,
    -- 无 deleted_at无 UPDATE按月 RANGE 分区

    PRIMARY KEY (id, created_at)                   -- 分区表主键必须包含分区键
) PARTITION BY RANGE (created_at);

CREATE TABLE public.platform_audit_logs_2026_04 PARTITION OF public.platform_audit_logs
    FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE public.platform_audit_logs_2026_05 PARTITION OF public.platform_audit_logs
    FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE public.platform_audit_logs_default PARTITION OF public.platform_audit_logs DEFAULT;

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 备份与导出

-- ────────────────────────────────────────────────────────────
-- 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 版本升级管理

-- ────────────────────────────────────────────────────────────
-- 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 客户端发布管理

-- ────────────────────────────────────────────────────────────
-- 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 灰度体系

-- ────────────────────────────────────────────────────────────
-- 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_adminsstaff 完全独立 不共享表、不共享 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_idsupgrade_type 一致性 A_app 类型必须为空数组CHECK 约束 chk_app_upgrade_no_gray 强制);只有 B_schema 类型才使用灰度名单与批次字段
upgrade_events 状态流转单向 状态机详见 §4.2succeeded / 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 NULLCelery 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_reasonhalted 状态时记录失败指标快照(健康门控不通过原因)
  • 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 常用查询

-- 查询所有活跃租户
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_typegray_tenant_idsbatch_sizebatch_concurrencybatch_interval_secondsfailure_policycurrent_batch_nototal_batch_counthealth_gate_configpre_backup_record_idhalted_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 禁止操作