本篇要回答的问题:在改造 QPaint 时,引入多租户(多用户 uid)会不会改变 DOM 树结构?底层业务逻辑实现层的**数据结构(mongodb 表设计)与算法(用户故事的实现)**该怎么落地?
四部曲第一部解决了 RESTful API 层的脚手架(restrpc + httptest)。本讲转向底层业务逻辑实现层:从"使用界面(接口)→ 数据结构 → 算法"三段式展开,并完成多租户的初步改造(先用 mock 授权,真正的帐号授权留给第三、四部)。
使用界面:多租户不该改变 DOM 层级
DOM 树原本是三层:Document ⇒ Drawing ⇒ Shape。引入多租户后,要不要变成四层 Document ⇒ User ⇒ Drawing ⇒ Shape?
关键判断:多租户不应影响 DOM 树结构。正确做法是让 Drawing"隶属于某个 uid"——多租户只是给 DOM 树多加一层安全约定(避免访问到无权限资源),而非改变层级。
它不改层级,却会改变接口方法——Document 类各方法都要带上 uid 做归属校验:
| 方法 | 改造前 | 改造后 |
|---|---|---|
| Add | Add() (drawing, err) |
Add(uid) (drawing, err) |
| Get | Get(dgid) |
Get(uid, dgid),校验 drawing 是否属于该 uid |
| Delete | Delete(dgid) |
Delete(uid, dgid),同样校验归属 |
关键洞见:QPaint 里 Drawing/Shape 接口没受多租户影响,只是因为业务简单;应极力避免接口因多租户而变化,但有时这种影响不可避免。此外,描述使用界面时不能只讲语言层约定——如 Shape 的
json.Marshal结果必须符合 API 层预期,这也是它的隐性约束。
数据结构:为什么选 mongodb
程序 = 数据结构 + 算法
服务端的数据结构不完全自己做主——存储即数据结构,所以第一要务是选合适的存储中间件,再在其上组织数据。
| 候选 | 是否适合 QPaint | 原因 |
|---|---|---|
| 关系型数据库 | 不太适合 | 需提前固定 Schema |
| mongodb(文档型) | 选它 | 图形(Shape)种类多、Schema 开放、今天无法提前预期 |
表(mongodb 叫 Collection)与索引设计:
| 表 | 内容 | 索引设计 | 理由 |
|---|---|---|---|
| drawing | 所有 drawing | 为 uid 建索引 | 迟早要 List 某用户的全部 drawing |
| shape | 所有 shape | 为 (dgid, spid) 建联合唯一索引 | spid 仅在 drawing 内唯一,需联合 dgid |
算法:用户故事背后的实现机制
关键判断:到了详细设计阶段,需求分析里的角色与用户故事,就变成了子系统/模块/类/函数的使用界面;而算法就是用户故事背后的实现机制。
典型用户故事的实现(伪代码,但每条 mongo 操作都真实有效):
| 用户故事 | 关键实现要点 |
|---|---|
| 创建 drawing(uid) | db.drawing.insert({_id, uid, shapes:[]}) |
| 取 drawing 内容(uid, dgid) | 先 findOne({_id, uid}),再遍历 shapes 逐个取 shape |
| 删除 drawing(uid, dgid) | drawing.remove({_id, uid}) 成功后 shape.remove({dgid}) |
| 创建 shape | 先确认 drawing 归属,再 shape.insert + drawing.update $push |
| 删除 shape | 确认归属后 $pull 成功再 shape.remove |
关键洞见:凡涉及多次修改的操作都应以事务进行。否则如"删 drawing 成功、删 shape 前宕机",业务上看似正常,系统却残留永远清不掉的孤立 shape 对象。
网络协议:先上 mock 授权
底层已支持多租户,协议层也要相应调整。本讲只做最简单的:引入 mock 授权头 Authorization QPaintStub <uid>。由于有了授权,不能再直接用 restrpc.Env,于是自定义 Env 在 OpenEnv 里解析出 UID:
| 步骤 | 动作 |
|---|---|
| 自定义 Env | 内嵌 restrpc.Env 并加 UID 字段 |
| OpenEnv | 解析 Authorization 头、校验前缀 QPaintStub、取出 uid |
| 全局替换 | 把所有 restrpc.Env 换成自定义 Env,Document 调用补 env.UID |
总结
本讲完成了底层业务逻辑实现层的多租户改造:结构上坚持"多租户不改 DOM 层级,只加安全约定 + 给 Document 接口加 uid";数据结构上因 Shape Schema 开放而选 mongodb,并精心设计 uid、(dgid,spid) 两处索引;算法上用伪代码描述用户故事,并强调多写操作必须事务化。协议层先用 mock 授权过渡,真正的帐号与 OAuth 留给后两讲。
先把"是什么"回答清楚
| 概念 | 一句话说明 |
|---|---|
| 多租户约定 | 不改 DOM 层级,仅作安全约定 + 接口加 uid 校验归属 |
| 存储即数据结构 | 服务端先选存储中间件,再在其上组织数据 |
| mongodb / Collection | 文档型数据库,适合 Schema 开放的 Shape |
| 索引 | drawing 按 uid、shape 按 (dgid,spid) 联合唯一 |
| 算法 | 用户故事背后的实现机制;多写操作须事务化 |
一句话速记
多租户只给 DOM 树加"归属约定"和接口 uid,不改层级;因 Shape Schema 开放选 mongodb,靠 uid 与 (dgid,spid) 索引支撑算法,多写操作必须走事务。
几条值得记住的判断
- 多租户应避免改接口、更不该改 DOM 层级——能避则避,避不掉再改。
- 服务端"数据结构"谈的其实是存储中间件选型 + 表/索引设计。
- 多步修改不上事务,迟早留下永远清不掉的孤立对象。
思考题
本讲坚持"多租户只加约定、不改结构"。回到你的系统:当初引入多租户时,你是否粗暴地在数据模型里硬塞了一层 User,导致结构和接口被污染?哪些多写操作至今没有用事务包裹、正悄悄制造孤立数据?

