核心数据模型
# 核心数据模型
版本 v1.9,2026-10-26。返回总览。此为逻辑模型和约束设计,不是逐字 DDL。
# 1. 通用约定
主键逻辑采用有符号正 int64,各数据库通过 identity/sequence/对应主键机制生成;JSON 一律输出十进制字符串,前端禁止 Number(id)。tenant_id 和所有外键也用相同数据库类型及 JSON 字符串。
持久化时间逻辑使用 UTC 毫秒精度,物理映射到各库相应时间类型,API 使用带 Z 的 RFC3339 毫秒时间;日期型业务字段单独使用 YYYY-MM-DD。金额使用最小货币单位整数或确定精度的 DECIMAL,并在合同中明确币种,禁止浮点存金额。
可维护实体统一使用 created_at、updated_at、created_by、updated_by、version、deletion_marker。租户实体增加 tenant_id。creator/updater 记录带主体类型的身份,不直接混用平台 ID 和租户用户 ID。
deletion_marker 为 0 表示有效记录,软删除时设为该行自己的非零 ID,并记录 deleted_at。业务唯一索引采用 (tenant_id, code, deletion_marker) 等组合,允许删除后重建同名记录,并保留多条历史删除记录。不能用 (tenant_id, code, deleted_bool),否则第二次删除重建可能冲突。
R1 软删除不支持恢复;关联关系先在事务中物理移除,审计保留历史。菜单/权限目录被引用时禁止直接删除,只能停用或迁移引用。对硬删除和历史清理另设保留策略,不能直接级联清空审计。
# 2. 平台与全局表
| 表 | 核心字段 | 约束和用途 |
|---|---|---|
| platform_users | id, username, password_hash, status, password_version, password_changed_at, failed_login_count, locked_until | 有效 username 唯一;锁定与口令时间用于等保策略(见 二级等保) |
| platform_security_policies | id, min_password_length, complexity_mode, max_password_age_days, lockout_threshold, lockout_duration_minutes, session_idle_minutes, version | 平台默认安全策略单行或版本化配置;变更写 platform_audit_logs |
| platform_roles / platform_user_roles | id/code;user_id/role_id | R1 保留平台管理员,非租户角色 |
| platform_role_permissions | role_id, permission_code | 只引用平台权限目录 |
| platform_sessions / platform_tokens | user_id, revoked_at;session_id, kind, digest, expires_at, consumed_at | 平台会话独立表,不与租户 token 混用 |
| tenants | id, code, name, status, expires_at, package_id, authz_version, permission_mode, user_limit, user_count | 有效 code 唯一;状态和配额权威来源 |
| tenant_packages | id, code, name, status, version | 发布后通过版本/变更记录审计套餐调整 |
| tenant_permission_grants | tenant_id, permission_code | custom 模式的租户允许权限集合,组合唯一 |
| capabilities | code, module, name, status | 稳定能力标识,控制套餐开通范围 |
| package_capabilities | package_id, capability_code | 组合唯一 |
| permission_catalog | code, audience, capability_code, name, status | 由代码种子发布;API 使用已知码 |
| menu_catalog | id, code, parent_id, kind, route_name, path, component_key, permission_code, capability_code, meta, sort, status | 全局页面目录;页面组件 key 必须在前端注册表 |
| platform_audit_logs | actor_id, action, target_type/id, result, request_id, occurred_at | 平台操作;按时间索引和保留 |
表中受维护的全局实体同样使用通用审计字段。令牌、关系和日志采用各自生命周期字段,不机械附加软删除。
套餐是能力集合,不直接复制菜单。有效菜单由能力、权限和启用状态投影产生,避免套餐菜单与角色菜单两套定义互相漂移。平台 UI 对目录只开放发布范围内的显示配置,不允许运营输入任意可执行组件。
# M3 已落地的表名与一致性
上述表格同时包含目标模型;schema v11 的实际名字为 tenants、tenant_packages、auth_capability_catalog、package_capabilities、auth_permission_catalog、auth_menu_catalog、auth_tenant_permissions 和 platform_audit_events。现有会话/令牌物理表为 auth_sessions/auth_tokens,以 realm 与固定 audience 隔离,不宣称已拆出目标中的独立平台表。
tenant_packages.user_quota 是配额来源;userCount 是在租户锁内统计 users(tenant_id,deletion_marker) 的未删除行,不新增双写 user_count/user_limit。tenants 增加 package_id、session_version、initialized_at、initialized_user_id,保留 permissions_mode 与 authz_version。初始化主体 ID 只作历史定位,不作为授权关系;真正授权仍依赖同租户复合外键保护的角色关系。
权限目录通过 capability_code 关联已发布能力,套餐能力关系及租户自定义权限都有外键,custom 关系的 realm CHECK 固定 admin。平台操作审计不进入租户业务日志。v10 从历史租户回填兼容套餐与初始化标记、保留账号和会话;v11 只发布审计导航。恢复/续期使用会话代次 fencing,套餐能力收窄则推进授权版本。事务与验证见 M3 实施记录。
# 3. 租户系统表
| 表 | 核心字段 | 约束和用途 |
|---|---|---|
| system_users | id, tenant_id, username, password_hash, nickname, dept_id, status, password_version, must_change_password, password_changed_at, failed_login_count, locked_until | 有效用户名在租户内唯一;password_changed_at 支撑口令有效期;锁定字段在成功登录时清零失败计数 |
| tenant_security_policies | tenant_id, mode, min_password_length, complexity_mode, max_password_age_days, lockout_threshold, lockout_duration_minutes, session_idle_minutes, version | inherit/custom 完整模式,版本化继承或完整覆盖;变更写 system_audit_events |
| system_departments | id, tenant_id, parent_id, code, name, leader_user_id, sort, status | 树不能成环;有效 code 唯一 |
| system_posts | id, tenant_id, code, name, sort, status | 岗位,租户内唯一 code |
| system_user_posts | tenant_id, user_id, post_id | 组合唯一,关联双方同租户 |
| system_roles | id, tenant_id, code, name, data_scope, is_reserved, status | 保留角色不可普通编辑 |
| system_user_roles | tenant_id, user_id, role_id | 组合唯一,外键约束同租户 |
| system_role_permissions | tenant_id, role_id, permission_code | 权限引用目录,service 校验能力范围 |
| system_role_menus | tenant_id, role_id, menu_id | 全局目录授权;不赋予目录之外的 API 权限 |
| system_sessions | id, tenant_id, user_id, password_version, created_at, last_seen_at, idle_expires_at, absolute_expires_at, revoked_at, revoke_reason | 用户设备会话;撤销影响全部关联 token |
| system_tokens | id, tenant_id, session_id, kind, digest, issued_at, expires_at, consumed_at | access/refresh 摘要,digest 唯一;轮换旧 refresh 保留到会话绝对到期 |
| system_login_logs | tenant_id, user_id 可空, account_fingerprint, result, reason, ip, request_id, occurred_at | 失败时不记录输入密码;未知租户失败写平台安全日志 |
| system_audit_logs | tenant_id, actor_id, action, resource_type/id, diff_redacted, result, request_id, occurred_at | 追加写,普通管理员不可删改 |
R1 允许用户没有部门,此时 DEPT 范围不返回任何记录,SELF 仍按本人计算。树根 parent_id 为 NULL;禁止用指向不存在行的 0 破坏外键。用户的部门关系与部门 leader 的循环引用通过分步插入/更新完成,leader 可空。
# 4. 数据完整性与索引
租户主表和引用表设置 (tenant_id, id) 唯一键,关联表使用复合外键 (tenant_id, user_id)、(tenant_id, role_id) 等指向它们;数据库外键不理解软删除,因此 service 还需验证被引用记录有效。
常见索引建议:
| 访问模式 | 索引或约束 |
|---|---|
| 登录定位用户 | users(tenant_id, username, deletion_marker) UNIQUE |
| 用户分页 | users(tenant_id, deletion_marker, status, id);按实际筛选再评估 dept_id 索引 |
| 部门树 | departments(tenant_id, parent_id, deletion_marker, sort) |
| 用户有效角色 | user_roles(tenant_id, user_id, role_id) UNIQUE;增加角色反查索引 |
| 角色权限 | role_permissions(tenant_id, role_id, permission_code) UNIQUE |
| token 验证 | tokens(digest) UNIQUE;tokens(tenant_id, session_id, kind) |
| 用户会话撤销 | sessions(tenant_id, user_id, revoked_at) |
| 锁定账户查询 | users(tenant_id, locked_until) 或平台 users(locked_until) 部分索引(仅未软删) |
| 审计查询 | audit_logs(tenant_id, occurred_at, id) |
用户名和业务 code 先规范化大小写规则,默认 ASCII 小写用户名;展示名称保留 Unicode。选择字符集和 collation 时明确定义比较规则,不依赖数据库默认值。模糊搜索不承诺任意大表 %keyword% 高性能。
角色关联、权限关系和菜单分配同事务写入,同时更新 tenant.authz_version。角色权限码的 audience 必须为 admin,平台角色只接受 platform 码;此规则由发布的目录及 service 校验,不能只靠字符串前缀。
更新携带 version,SQL WHERE version = expected,更新成功递增;未命中返回 409,避免两位管理员覆盖彼此编辑。批量修改授权先锁定租户/目标角色并检查版本,再替换关系。
complexity_mode 枚举建议:off、three_of_four(大写/小写/数字/特殊字符任选其三)。max_password_age_days 为 0 表示不强制过期。策略读取在登录、改密与管理端更新时生效;M1 起使用平台默认种子,M2 起支持租户覆盖与安全管理员维护。
fago/fago-migrations/{mysql,postgres}/0003_mlps_identity.sql 已把锁定字段、password_changed_at、会话 last_seen_at 和 platform_security_policies 种子加到 MySQL/PostgreSQL 迁移。schema v7 已创建 tenant_security_policies 并为既有租户回填继承行;新租户 bootstrap 同事务创建。运行时统一解析覆盖,坏行/缺行拒绝为 503;schema v8 发布安全策略导航。
schema v6 已发布租户侧权限目录(管理/查询接口仍分工作包实施):system:security-policy:read/update、system:user:unlock、system:audit-log:read、system:login-log:read;对应 platform:* audience 码仍待平台工作包发布,以支撑 二级等保 三员矩阵。
# 5. R2 扩展表
| 能力 | 表和要点 |
|---|---|
| 字典(v15 已实施) | infra_dict_types / infra_dict_items;类型 tenant/code、条目 tenant/type/code 唯一;复合同租户父级外键;软删除标记参与唯一键 |
| 参数(v15 已实施) | infra_configs,tenant/code 唯一;is_sensitive 创建后不可变;敏感值所有 HTTP DTO 均为 null |
| 文件(v16 已实施) | infra_files,tenant_id、storage_key、mime、size_bytes、sha256、state 和清理 lease;键/token 不复用、deleted tombstone 重复清理 |
| 通知(v18 已实施) | infra_templates / infra_send_tasks / infra_receipts;租户内模板/任务复合外键,发送人幂等唯一键,站内回执按 tenant/task 去重 |
| 执行历史(v18 已实施) | infra_message_attempts;引用 inbox,唯一(inbox_id,claim_version),重放不覆盖旧结果;租户/事件/消费端显式过滤 |
| 任务(v19 已实施) | infra_jobs / infra_job_runs / infra_job_attempts;配置/游标版本分离、准入代次、唯一触发身份、租约与递增认领版本;历史只 FK run |
| 事件(v17 已实施) | infra_outbox / infra_inbox;不可变 envelope/digest、唯一(consumer,event_id)、lease/token、退避/dead/版本重放;v18 正式 Event worker 已装配站内通知 |
| 代码生成 | codegen_projects / codegen_tables / codegen_columns,记录生成器版本和产物摘要 |
v15 另有 infra_dict_templates、infra_dict_template_items 和 infra_config_templates 只读全局模板表。租户显式复制编码/版本的快照,不自动合并/回退;来源字段不做跨来源外键,防止模板生命周期级联破坏租户副本。业务表的版本、删除与审计在租户锁内原子处理;value_text 不进入审计。文件 v16 已通过本地验证;事件 v17 原语与 Event/Redis 专项已通过,v18 站内通知与正式 Event worker 已接入;v19 定时任务已装配计划/运行/历史,业务受理与运行完成同事务。执行历史仅 FK inbox,不反向 FK 业务表或租户,避免失败事务已经持有 inbox 时形成反向锁序;tenant_id 由核对过的本库事件复制,公开投影始终显式过滤。
schema v14 已新增 system_role_departments,双复合外键约束角色、部门与租户一致;system_roles.data_scope 扩为 24 字符并加入两种范围的 CHECK。历史迁移字节保持不变。
DEPT_AND_CHILDREN 在现有一万节点/64 层上限内采用同事务的有界完整树展开;比原计划的递归 CTE/closure 表更适合当前三方言共用实现,取舍见 ADR-017。部门移动仍在租户行锁内更新并推进授权版本,自选范围只保留有效节点,引用节点删除须先解除角色关系。
# 6. 迁移与初始化
建议顺序:全局目录与平台身份 → 租户和套餐 → 部门/岗位 → 用户 → 角色与关联 → 会话/token → 审计 → R2 基础设施。循环关系在建表后添加外键。
权限、菜单和能力种子按稳定 code 幂等同步,不覆盖租户自定义角色授权。种子变更记录版本,停用目录项需更新授权版本并刷新导航。平台账号及初始密码由独立 bootstrap 交互/Secret 提供,迁移 SQL 不内置通用密码。
必须验证:空库升级、上一发布版本升级、同一迁移重复执行的拒绝/已执行判断、并发迁移互斥、失败后的脏状态处理和备份恢复。存量 Java 数据迁移属于独立项目,需先确定源版本和数据规模,不在 R1 默认范围内。
# 7. 多用户、多终端和 SSO 扩展
R1的物理会话原先保存realm/client_id;schema v20实际增加subject_type、subject_id、audience、client_version及设备标签/散列,并按原有两种固定client回填。后台用户可保留 user_id 复合外键,但不把未来会员 ID 混进同一个无类型字段。多用户类型采用各自用户表/明确主键关系,服务账号独立存储。
| 表 | 用途与约束 |
|---|---|
| identity_realms / identity_clients | v20已实现固定realm元数据、client/受众/终端/grant、期限/会话上限及版本;重定向白名单仍待OIDC增量 |
| identity_providers | v23已实现 realm/tenant 作用域、不可变 issuer/client/code、部署档案/秘密引用、启停与CAS版本;不存秘密明文 |
| identity_external_bindings | v23已实现精确 issuer/sub 范围唯一、明确三类主体外键及跨租户CHECK;停用保留键,不按邮箱合并 |
| member_users / member_sessions / member_tokens | v21已实现独立会员账号、会话及摘要凭据;租户/主体复合外键、固定member受众/type、密码与租户/client版本fencing |
| member_management_events | v21已实现管理操作者admin与目标member的独立定位、状态/版本审计,不保存名称或秘密 |
| service_accounts / service_credentials / service_tokens | v22独立服务主体、scope、凭证摘要、轮换与当前版本撤销 |
| identity_sso_management_events | v23配置/绑定同事务审计;平台写还关联平台审计,失败全回滚 |
| identity_sso_states(待实施) | 一次性 state 摘要、nonce、PKCE verifier 的受控存储、client/tenant、expires_at、consumed_at |
| identity_idp_sessions(待实施) | issuer + sid/sub 与本地 session 的关联,用于统一登出 |
| inbox_events | consumer + event_id 唯一,与消费业务写入同事务 |
SSO state 数据短期保留,verifier 等敏感材料加密或由可信短期存储管理,不出现在日志。provider 配置增加版本,并审计绑定和解绑。
# 8. 方言与通用工作流模型
表清单是逻辑模型:布尔、毫秒时间、大对象、JSON、大小写及主键生成方式在每种方言 migration 定义;禁止直接沿用 MySQL 类型作为统一 DDL。详见 适配方案。
纯 Go 引擎与默认业务模块共享 UnitOfWork:审批状态、资源状态、命令结果、事件及 outbox 可同事务提交。外部存储字节和跨库服务仍通过任务/事件/补偿协调。
| 表组 | 主要状态和约束 |
|---|---|
| wf_definitions / wf_definition_versions | 租户、code、不可变 graph/form/compiler version、required_engine_version、发布摘要 |
| wf_definition_admissions | tenant_id、definition_version_id、start_enabled、version;发布内容不变,可变发起准入独立保存;启用/停用须锁定对应运行时准入 |
| wf_runtime_versions | 显式全局运维表:engine_version 唯一、admission_state(enabled/draining/retired)、admission_epoch、version;仅受控部署仓库可跨租户检查引用 |
| wf_form_instances / wf_form_instance_versions | 内置 form-instance 资源;归属用户/租户、formVersion、修订号、冻结提交字段与附件引用、状态;提交版本不可覆盖 |
| wf_instances / wf_resource_bindings | 定义/表单/编译器版本、engine_version、ResourceRef、状态、version;绑定不可变资源版本和运行语义 |
| wf_resource_claims | tenant + resourceType/id/version + purposeKey 唯一活动占位,结束时释放 |
| wf_tokens / wf_node_activations / wf_join_states | instance、scope、activation、branch、token 状态,防止跨轮次误 join |
| wf_tasks / wf_task_candidates / wf_decisions | 唯一任务与参与人决策,AND/OR/串行计数与任务 version |
| wf_timers / wf_jobs | instance、engine_version、dueAt、leaseOwner/until、fencingToken、attempt、状态;按精确兼容运行时领取 |
| wf_commands / wf_events | tenant + commandId 唯一、调用主体、payloadHash、engine_version、admission_epoch、命令状态与结果;instance + sequence 唯一事件 |
| document_versions / file_versions | 不可变内容/对象版本、hash、大小与 tenant,不能覆盖正在审批的版本 |
| resource_acl / temporary_grants | 资源类型/id/version、实例/任务、主体、权限、expiresAt、revokedAt |
定时器及任务抢占、resource claim、command 去重与实例并行状态均通过七数据库的相同合同测试。json graph 保持跨库可序列化,业务查询需要的字段显式建列,不依赖特定库 JSON 查询。
独立表单提交在同一个 UnitOfWork 中冻结表单修订并创建工作流/活动占位,活动唯一约束同样覆盖 form-instance。节点可编辑数据单独追加版本与审计,不覆盖提交快照;业务状态变更与内容版本分开维护。
engine_version 不随应用发布自动更新;新实例精确采用所选定义的 required_engine_version,默认版本切换只影响后续编译/发布的新定义。发布、退役和回滚除检查活动实例、命令、job/timer/在途租约外,还检查所有仍开放发起的定义是否有匹配处理器。
StartInstance 首次受理、发布/启用定义和运行时退役按“运行时准入行 → 定义准入行 → 资源/实例”顺序在事务中锁定,禁止先在事务外计数再移除处理器。关闭版本准入后新发起被拒绝,已经提交的 accepted 命令或实例计入排空;重投同一受理命令不创建第二个准入。只有所有定义准入关闭、所有运行引用为零时才将版本置 retired。版本状态变更递增 admission_epoch,并要求提交核验,避免检查与新发起竞争。
实例跨语义迁移只能通过显式迁移记录与检查点,SQL schema 升级不能隐式改绑,详见 运行时版本与滚动升级。
工作流表只保留受控业务快照和引用,文件字节在对象存储;物理清理检查活动引用与留存约束。不能把“删实例”级联成“删审计或删原文件”。详见 工作流方案。
# 当前物理实现补充(M2 schema v9)
schema v5 追加 system_departments、system_posts、system_user_posts、system_audit_events 和 users.department_id,保留 v1–v4 历史字节。部门根为 NULL,用户/父部门/岗位关联均使用 (tenant_id,id) 复合外键;岗位编码只接受 ASCII 小写,避免 MySQL 与 PostgreSQL 的大小写唯一性差异。数据库结构、锁序、删除保护和容量边界见 M2 实施记录。继续追加 0006_users_roles.sql,新增 system_roles、system_user_roles、system_role_permissions、system_role_menus,用户增加软删除与维护字段。原用户名唯一约束扩展为 (tenant_id,username,deletion_marker),允许软删除后以新 ID 重建。所有角色关联使用同租户复合外键;菜单/权限复合引用固定 admin audience。旧账号保留 hash、密码版本、会话及既有组织归属,回填真实角色关系;运行时不再从旧 users.role_code 回退授权。SELF/DEPT/ALL 的实际读写边界已通过双库测试;租户安全策略覆盖与口令有效期已接入 schema v7/v8;schema v9 增加身份日志 UA/账号指纹、租户/时间和 requestId 索引及日志导航,旧事件字段不改写;审计查询、二次投影与查询留痕已实现。
# 服务身份持久化(v22)
service_accounts独立记录租户、固定service client、状态与账号/凭据版本;不引用自然人角色或部门。service_credentials只存随机Key ID对应的secret摘要及期限/client版本,service_tokens固定realm/audience/subjectType与各版本、scope快照及期限。token到credential的复合外键同时包含tenant/account/keyId,不能借同租户其他账号凭据拼接主体。service_account_scopes关联发布的service_scope_catalog,scope再关联租户能力;运行时仍限制已实现消费者。service_management_events把admin操作者与service目标分别通过复合外键定位,只记录动作和版本。轮换/启停/授权修改、token撤销与审计在同一显式事务提交,失败回滚;无隐式DDL、默认生产凭据或自动删除历史。操作与限制见服务手册。
# OIDC 管理持久化(v23)
配置自然作用域为 platform/0、admin/正租户、member/正租户,使用可空 tenant_ref_id 外键与CHECK同时表达平台例外。绑定记录的 platform_user_id、admin_user_id、member_user_id 必须恰好一个与 realm/tenant/subject_type/subject_id 相符,不能用普通无类型ID模拟主体。来源 provider 使用 id/realm/tenant/issuer_digest 组合外键。
issuer/sub 域分离 SHA256 摘要作为索引定位,原值用于精确一致性校验;唯一键是 realm/tenant/issuer_digest/subject_digest,不含 source_provider_id,因此相同issuer更换应用不能重新认领外部身份。列表通过独立安全规则生成范围指纹,原始sub不出DTO。解绑是版本推进后的停用,不释放历史键;同名本地重建不承接墓碑身份。
平台配置写固定锁配置控制行→当前操作者/会话→client→provider,使用ReadCommitted与显式CAS以避免等待控制锁后读取旧RR快照;没有自动重试一次性敏感命令。租户写沿用保留管理员/租户敏感事务边界。配置写、绑定写、领域审计及平台第二份审计原子提交;关闭不依赖外部网络/秘密可用性。登录state、IdP会话映射及logout jti去重尚未增加,管理表不能替代这些认证状态。
# v27–v29平台基础设施增量
| 表/字段 | 当前用途与边界 |
|---|---|
| infra_jobs的cron_expression/time_zone/misfire_policy/needs_configuration | 唯一公开调度为cron;旧非cron原字段保留归档,停用并清零游标/推进准入代次 |
| infra_dict_templates / infra_dict_template_items | 公共标准/复制模板,父版本覆盖所有条目发布;is_builtin由迁移维护,在线不可修改 |
| infra_config_templates | 公开默认值、稳定编码和发布版本;租户显式复制,不能携带敏感值 |
| infra_platform_control | 固定控制行串行化平台目录容量/发布与父任务受理,不反向锁租户 |
| infra_platform_configs | 独立平台私有参数,无tenant_id;is_sensitive创建后固定,管理出口隐藏原值 |
| infra_platform_jobs / infra_platform_job_runs / infra_platform_job_attempts | 无虚假租户外键的父计划、运行和认领历史;配置版本、游标版本、准入代次分别维护 |
| infra_platform_tenant_job_runs / infra_platform_tenant_job_attempts | 真tenant_id与parent_run_id,目标独立租约/限流/恢复;只来源于持久父运行,不另建Cron日历 |
| infra_platform_maintenance_results | 固定90天/每表100条统计结果清理的截止点与数量,和运行同事务;自身与任务/审计历史均不参与该清理 |
| infra_platform_snapshots / infra_platform_tenant_snapshots | 固定统计处理器按运行键幂等保存目录数量,无参数原值 |
子运行引用父计划/父运行,故父运行受理保持完整事务;刻意不为待办队列建立tenant外键,避免父插入反向取得所有租户共享锁,与子业务的租户→计划锁形成环。目标存在性在在线准备时通过tenancy公共facade检查,实际业务始终通过当前真实租户守卫;目标消失或能力撤销记录独立拒绝历史,不能借无外键绕过准入。
已有租户字典/参数的来源版本和原值在升级时保持不变。敏感值隐藏与存储原值分离,在线审计不包含公开或私有参数值。非空升级、重跑及双库当前读交错的验证见M5增量记录。