组织、工作区、成员:三层模型与一次数据迁移

给 344 个账号补 workspace_id 的那天,才发现 8 张业务表都挂在 user_id 上。本文用四张核心表、8 个归属键与 12.4 万行数据,复算 62 分钟回填、2 倍写放大与 7 天回滚窗口,并给出四步迁移顺序。

加一个字段是下午的活,补十二万行归属是两个月。

一、加一个字段的那个下午

周岚以为这是一个下午的活:给用户表加一个 workspace_id,默认值指向一个自动创建的“我的工作区”。

写到第三张表时她停住了。文档、分享链接、API 密钥、Webhook、账单记录、审计日志、模板、导出任务,八张业务表都直接挂在 user_id 上,没有任何一张知道“归属”是什么。

更麻烦的是老数据。三百四十四个账号里,一百个个人档持有者、一百个团队席位、一百四十四个免费与试用账号;谁是哪个工作区的成员,历史记录里没有答案。

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

加字段是分钟级,补归属是周级。 数据库不会拒绝一个带默认值的列,但默认值会把所有历史数据塞进同一个错误的位置。

归属不是外键,是语义。 user_id 回答“谁创建的”,workspace_id 回答“它属于谁”;两者在个人产品里恰好重合,在团队产品里必须分开。

二、四张核心表:谁连人,谁连组织

先把人和组织之间的路径画出来,再谈字段。

表一行代表什么主键必须有的约束
organizations一个付费主体,可能被收购或更名org_id软删除;名称不要求全平台唯一
workspaces一个隔离空间,成员与资源在此共享workspace_id必须属于一个组织;slug 在组织内唯一
memberships一个人在一个工作区里的身份(workspace_id, user_id)角色只存这里;删除即撤销访问
invitations一次尚未接受的邀请invite_id邮箱加工作区唯一;必须有状态与过期时间

membership 是唯一同时连人和组织的表。 它一边指向 user,一边指向 workspace,workspace 再指向 organization;去掉它,人和组织之间就没有合法路径。

角色挂在 membership 上,不挂在 user 上。 同一个人在两个工作区可以是管理员和只读;角色写在 user 上,这个人在第二个工作区就只能是同一种身份。

邀请是成员关系的前置状态,不是一张日志表。 它需要过期时间、接受次数与撤销状态;否则一个九十天前的链接仍然能把陌生人放进工作区。

外键只是底线,约束才是模型。 组织软删除后工作区不能继续写入、成员关系删除后邀请必须失效,这些规则要写在数据库或统一的写入口里。

不要给 memberships 加自增主键就完事。 复合主键能天然阻止同一个人在同一空间出现两条角色记录;用自增主键就必须再加唯一索引。

三、八张业务表:哪些属于组织,哪些属于工作区

归属键不是按表名猜的,是按“谁共享、谁付钱”判的。

业务表归属键属于谁判据
documentsworkspace_id工作区内容在团队内共享,跨区不可见
share_linksworkspace_id工作区链接的可见范围由工作区决定
api_keysworkspace_id工作区密钥代表一个空间的调用身份
webhooksworkspace_id工作区回调只关心本空间的资源
audit_logsorg_id组织跨工作区的合规证据要能一次导出
billing_recordsorg_id组织账单合并到付费主体
templatesorg_id组织品牌模板被多个工作区复用
export_jobsworkspace_id工作区导出范围不越过一个空间

判据一:谁付钱,谁拥有账单与审计。 账单、发票、审计日志属于组织,因为它们回答的是“这家公司花了多少、谁动过”。

判据二:谁共享,谁拥有内容。 文档、链接、密钥、Webhook 属于工作区,因为它们的可见范围就是一个空间的边界。

判据三:跨工作区复用的配置放组织层。 模板、品牌与默认策略放组织;一旦放错,第二个工作区就要复制一份。

归属键要一次定完,不要边做边加。 八张表分三次改,就会有三种默认值口径;迁移脚本的复杂度不是线性增长。

八张表里五张归工作区、三张归组织。 这个比例不是巧合:内容与调用身份天然按空间隔离,钱与合规证据天然按付费主体合并。

四、四步迁移:先补默认值,再切换读路径

不停机的迁移不是一次大爆炸,是四次可回滚的推进。

步骤动作观察指标回滚方式
一、回填给个人档建默认工作区,八张表写入归属空归属行数降到零字段可空,停止写入
二、双写新旧字段同时写,读仍走 user_id两列不一致率关闭双写开关
三、切读按 workspace_id 查询,user_id 降为创建者错误率与慢查询按工作区灰度切回
四、清理删除归属语义,保留创建者字段一周无回滚需求从备份恢复
现有工作区 24 = 20 个团队各 1 个 + 4 个团队多开 1 个
个人档持有者 100 → 各补 1 个默认工作区
迁移后工作区 = 24 + 100 = 124
免费与试用账号 144 → 首次创建内容时惰性创建,不预填

8 张业务表合计 = 124000 行
回填速率 = 2000 行/分钟
回填耗时 = 124000 ÷ 2000 = 62 分钟

双写 7 天,写放大 2 倍
按日均写入 4800 行估算,7 天 = 33600 行
额外写入 = 33600 行,行数不变、写次数翻倍

回滚窗口 = 7 天
核对 = 20 + 4 = 24,其中团队多开 4 个

62 分钟是回填时间,不是迁移时间。 真正的成本在验证:八张表逐张比对归属,任何一张漏掉都会在切换后变成越权访问。

双写的七天是观察期,不是保险期。 它的价值在于让你看到不一致率降到零,而不是让你多一个后悔的借口。

读路径切换要能按工作区灰度。 先切四个多开工作区的团队,再切单工作区团队,最后切个人;出问题时影响面可控。

旧字段不要立刻删。 user_id 保留为 created_by,回答“谁创建的”;归属语义交给 workspace_id,两个问题不要挤在一个列里。

二十四个工作区里有四个是团队多开的,这四个最容易出错。 多开的空间往往有交叉成员,迁移时要逐个确认成员关系,而不是批量套默认值。

五、行动清单与复算验收

在写迁移脚本之前,先用下面八项把模型和回滚条件钉住。

  • 列出所有直接挂在 user_id 上的业务表,标出哪些其实是归属
  • 给 organizations、workspaces、memberships、invitations 画出主键与外键
  • 确认角色字段只在 memberships 上,user 表里没有残留
  • 给 invitations 加过期时间、状态与邮箱加工作区的唯一约束
  • 用十二万四千行、每分钟两千行算出回填窗口,并预留两倍验证时间
  • 为四个多开工作区的团队单独列出成员核对表
  • 准备读路径开关,能按工作区灰度切换
  • 写下回滚条件:不一致率超过多少、错误率超过多少就停

验收标准:你能在不停机的情况下给旧数据补上归属,并说清每一张表属于组织还是工作区;说不清,说明迁移还停在加字段的阶段。

使用边界与实操验收

本文讨论的是数据模型与迁移顺序,不涉及具体数据库选型或权限框架。

三层不是三张表。 organizations、workspaces、memberships 是三个概念,落到 schema 可能是四张表,也可能多一张组织成员表;关键是归属路径唯一。

不要把组织层当成权限层。 组织回答“属于哪家公司”,权限回答“这个人能做什么”;两者混在一起,第二个工作区就会继承错误的角色。

行级安全不是迁移的替代品。 它能在读的时候兜底,但不能替你补历史数据的归属;先有正确的列,再谈策略。

本文不覆盖邀请流程。 邀请的过期、重发、接受与撤销在下一篇展开;这里只保证表结构能支撑它。

迁移顺序比工具重要。 同一套四步,用脚本还是用平台都能做;顺序错了,工具只会让错误更快发生。

关键参考与核验范围

核验日期:2026-10-25。以下链接用于核对上下文边界、行级隔离与约束设计的一般做法,本文未逐条引用其内容。

引用口径。 本文只借用“归属先于授权”这一一般原则;三百四十四个账号、十二万四千行、62 分钟与七天全部是教学假设,与上述材料无关。

核验范围。 核验的是“四张核心表 + 八个归属键 + 四步迁移”这套结构是否仍可操作,不是比对某个数据库或身份服务的功能。

如何自行复核。 在测试库复制一份生产数据,按四步走一遍,记录每一步的空归属行数与不一致率;如果第三步切读后出现任何一条越权记录,就回到第二步重新核对归属键。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元