从服务器职责出发,梳理权限表的定位、结构、使用方式。服务器是"权威账本 + 数据仓库 + 路由",纯响应式;读写与同步由客户端主导;版本号由时间式机制天然承载。
一、服务器职责
1.1 三件事
服务器只做三件事: 1. 维护权限表(grants):谁对什么有权限 2. 存储数据(values):资源的字节 + 版本号 3. 响应请求:按权限返回 / 按权限接收 / 移交仲裁不解释数据、不执行业务、不做权限计算、不主动推送。
1.2 不做什么
| 不做 | 原因 |
|---|---|
| 不解释 R(字节内容) | 只存 bytes |
| 不执行业务逻辑 | 客户端负责 |
| 不计算权限(继承、组、角色) | 中间环节暂不考虑,最终落到 grants |
| 不主动下传 | 纯响应式,客户端请求才下发 |
| 不做同步决策 | 同步由客户端主导 |
服务器是"账本 + 仓库 + 路由",不是"业务处理器",也不是"主动推送者"。
1.3 与客户端的分工
服务器:关心设备(pc_id),按表响应请求 客户端:关心登录人(user),按权限表过滤展示,主导读写与同步 服务器以"设备"为单位,纯响应 客户端以"用户"为单位,主动驱动二、权限表:grants
2.1 结构
CREATETABLEgrants(resourceINTEGERNOTNULL,-- 资源编号pc_idINTEGERNOTNULL,-- 设备编号levelTEXTNOTNULL,-- 'read' | 'own'PRIMARYKEY(resource,pc_id));-- 一个资源只有一个写者CREATEUNIQUEINDEXidx_one_own_per_resourceONgrants(resource)WHERElevel='own';-- 按设备查CREATEINDEXidx_grants_pcONgrants(pc_id);2.2 语义
有记录 → 能读(客户端可请求数据) level='own' → 还能写(是数据源,隐含读权限) 读:有记录 = 有读权限(客户端可请求) 写:level='own' = 写权限(同时隐含读权限)注意:有读权限 ≠ 服务器主动下传。有读权限只是"客户端可以请求"。
2.3 唯一性
一个资源: - 多行(多个读者,level='read') - 一行 own(唯一写者,level='own') 由部分唯一索引保证。三、数据表:values
3.1 结构
CREATETABLEvalues(resourceINTEGERPRIMARYKEY,kindINTEGERNOTNULL,-- 资源类型bytesBLOBNOTNULL,-- 数据字节versionINTEGERNOTNULL-- 版本号(TimeId));3.2 语义
kind:资源类型(用于客户端反序列化) bytes:数据(不透明,服务器不解释) version:版本号(时间式机制,单调递增)3.3 版本号的来源
version 直接用 TimeId: - 由时间式同步机制天然承载 - 每次写产生新 TimeId - 单调递增、全局唯一、带物理时间语义 不需要额外的 u64 版本字段。四、客户端主导的读写与同步
4.1 客户端做什么
客户端: - 决定何时读(拉取数据) - 决定何时写(编辑、上传) - 决定何时同步(上传本地变更、拉取更新) - 处理冲突(用 version 比对) 服务器: - 存数据(values) - 记权限(grants) - 响应请求(pull / push / transfer) - 不做决策客户端是"驱动者",服务器是"服务者"。
4.2 写权限设备是数据源
写权限设备 D 拥有 R: - D 是数据源 - R 的数据在 D 本地产生 - D 上传给服务器(备份 + 分发) - 平时不需要从服务器下传 - 如果服务器下传给 D,反而可能覆盖更新的数据4.3 下传的场景
正常情况下传(写权限设备): - 无 异常下传(写权限设备): 1. 设备移交后首次登录 → 新 owner 需要 R 的当前数据 2. 本地数据损坏 / 丢失 → 从服务器恢复 正常下传(读权限设备): - 客户端请求时下发 - 按需拉取五、下发:pull(客户端请求触发)
5.1 流程
设备 D 请求数据: 1. 客户端发起请求(不是服务器主动) 2. 查 grants WHERE pc_id = D 3. 得到 D 有权限的资源 4. 查 values 拿 bytes + version 5. 返回关键:客户端请求才下发,服务器不主动推。
5.2 SQL
-- D 有权限的资源(有记录 = 有读权限)SELECTresourceFROMgrantsWHEREpc_id=?;-- 拿数据SELECTresource,kind,bytes,versionFROMvaluesWHEREresourceIN(SELECTresourceFROMgrantsWHEREpc_id=?);5.3 增量拉取
客户端请求: "给我 last_sync_version 之后的变化" 带 last_sync_version(上次同步到的 TimeId) 服务器返回: version > last_sync_version 的所有变更按时间式版本增量拉取。
六、接收:push
6.1 流程
设备 D 上传资源 R: 1. 查 grants WHERE resource = R AND pc_id = D AND level = 'own' 2. 存在 → 允许 3. 不存在 → 拒绝 4. 校验 base_version == values[R].version ? - 匹配 → 接受,version = 新 TimeId - 不匹配 → 拒绝,返回服务器最新 5. 更新 values6.2 SQL
-- 校验 D 是否是 R 的写者SELECT1FROMgrantsWHEREresource=?ANDpc_id=?ANDlevel='own';-- 校验版本 + 更新UPDATEvaluesSETbytes=?,version=?WHEREresource=?ANDversion=?;-- 影响行数 0 → 版本冲突写必须 level = ‘own’,且版本匹配。
6.3 冲突处理
版本冲突时: 客户端收到: - 服务器最新 version - 服务器最新 bytes 客户端决定: - 合并(重新编辑,基于新版本) - 覆盖(放弃服务器,强制上传) - 放弃(放弃本地)冲突处理在客户端。
七、移交
7.1 流程
移交时需要服务器: 1. A 请求移交 R 给 B 2. 服务器查 grants:R 的 own 是不是 A? 3. 是 → 更新 grants:R 的 own 改为 B 4. 返回成功 5. A 本地清除,B 本地获得7.2 移交后的首次下传
B 获得 R 的写权限后: - B 的设备上还没有 R 的数据 - B 首次登录时,请求 R 的数据 - 服务器下发 R 的最新 bytes + version - B 本地存储,开始作为新的数据源移交后首次登录,下传一次。
7.3 对比
| 操作 | 权限来源 | 服务器参与 | 下传? |
|---|---|---|---|
| 读(read 设备) | 本地缓存 | 请求时 | ✅ 请求触发 |
| 写(平时) | 本地缓存 | 上传时 | ❌ 不下传 |
| 写(移交后首次) | 服务器 | 确认 + 下发 | ✅ 一次性 |
| 写(本地损坏) | 服务器 | 请求恢复 | ✅ 请求触发 |
| 移交 | 服务器 | ✅ | ❌ |
八、时间式版本号
8.1 版本号的本质
版本号 = TimeId(时间式同步机制) - 单调递增 - 全局唯一 - 带物理时间语义 每次写: values[R].version = 新 TimeId 不需要额外的 u64 版本字段。8.2 用途
1. 冲突检测: 客户端 base_version vs 服务器 version 2. 增量拉取: version > last_sync_version 3. 审计: version 带时间,可追溯8.3 统一
一套 TimeId,覆盖: - 棒 ID - 版本号 - 冲突检测 - 增量同步九、完整的服务器代码骨架
pubstructServer{grants:GrantsTable,values:ValuesTable,}implServer{/// 拉取权限:客户端问"我能访问什么"fnpull_grants(&self,pc:PcId)->Result<Vec<Grant>>{self.grants.find_by_pc(pc)}/// 拉取数据:客户端请求它有权访问的资源/// 客户端请求触发,服务器不主动推fnpull_values(&self,pc:PcId,resources:&[ResourceId],since:Option<TimeId>,)->Result<Vec<ServerResource>>{// 校验 pc 对这些资源有权限letallowed:Vec<_>=resources.iter().filter(|r|self.grants.has_access(**r,pc).unwrap_or(false)).collect();// 按 since 过滤增量allowed.iter().filter_map(|r|self.values.get(*r).ok()).filter(|v|since.map_or(true,|s|v.version>s)).collect()}/// 接收:pc 上传资源 Rfnpush(&self,pc:PcId,resource:ResourceId,base_version:TimeId,bytes:Vec<u8>,)->Result<TimeId>{// 1. 校验写权限letlevel=self.grants.level_of(resource,pc)?;iflevel!=Level::Own{returnErr(Error::NotOwner);}// 2. 校验版本letcurrent=self.values.get(resource)?;ifcurrent.version!=base_version{returnErr(Error::VersionConflict{latest:current.version,latest_bytes:current.bytes,});}// 3. 生成新版本 + 存储letnew_version=TimeId::next();self.values.update(resource,bytes,new_version)?;Ok(new_version)}/// 移交:把 R 的写权限从 A 转给 Bfntransfer(&self,actor:PcId,resource:ResourceId,new_pc:PcId)->Result<()>{letlevel=self.grants.level_of(resource,actor)?;iflevel!=Level::Own{returnErr(Error::NotOwner);}self.grants.update_owner(resource,new_pc)?;Ok(())}}四个方法:pull_grants / pull_values / push / transfer,全部响应式。
十、中间环节暂不考虑
10.1 暂不考虑
暂不考虑: - 权限继承(子资源继承父资源) - 组授权(组可读 → 组成员可读) - 角色授权(角色可读 → 角色成员可读) - 动态权限(按条件计算)10.2 grants 是"最终形态"
不管权限怎么计算(继承、组、角色), 最终都落到 grants 表: (resource, pc_id, level) 服务器只认 grants。10.3 先跑通
第一步: - grants 表手动维护 - pull_grants / pull_values / push / transfer - 时间式版本号 - 跑通 第二步: - 加继承、组、角色 - 自动生成 grants十一、一句话
服务器是"权威账本 + 数据仓库 + 路由":维护 grants(权限)和 values(数据 + 版本),纯响应式。读写与同步由客户端主导;写权限设备是数据源,平时不下传;移交后首次登录、本地损坏时才下传。
版本号由时间式同步机制(TimeId)天然承载,一套机制覆盖棒 ID、版本、冲突检测、增量同步。grants 是权限的最终形态,中间环节暂不考虑。