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