
客户端是业务处理器读、写、本地加工。服务器读取侧只查since之后的内容、返回所存客户端读取侧必须判断响应形态全量替换 / 增量合并因为处理方式不同。写以增量为核心断层时直接全量。0 全量标记资源的第一个包就是全量。格式归资源层ResourceK业务换算归本地处理系统界面为可选结构体同步由客户端主导。一、客户端定位1.1 客户端做什么客户端 - 读给 since服务器返回所存客户端判断形态并处理 - 写向服务器推数据增量 / 全量 - 本地加工读 → 判断 → 写条件流、机器加工 - 界面人看 / 人改可选客户端是“驱动者”服务器是“服务者”。1.2 客户端不做什么不做原因不存储全部数据只存需要的最小暴露不判断权限来源服务器算好给权限表不主动接收推送自己请求才拿数据不解释格式格式归资源层K不合并冲突断层时直接全量上传1.3 与服务器的分工服务器关心设备pc_id按表响应请求 客户端关心登录人user主导读写与同步 服务器以“设备”为单位 客户端以“用户”为单位二、两个核心操作2.1 增量读统一请求客户端判断形态客户端请求 PullRequest { since: TimeId } // 本地 last_version 没有 Full / Delta 之分只给 since 服务器 只查 since 之后的内容返回所存 PullResponse { end: TimeId, data: PullData::Full | Delta | None } 不判断模式since 0 自然得全量since 0 自然得增量 客户端必须判断形态处理方式不同 1. 比较 last_version vs end last_version end → 已同步跳过 last_version end → 进入第 2 步 2. 按 data 形态处理 Full → replace_all丢弃旧数据整体替换 Delta → apply保留旧数据应用变化 None → 无变化服务器只查询不判断模式。客户端必须判断形态因为处理方式不同。2.2 增量写连续性检查write_delta(changes) 每个 change 带 base_version base_version 永远 0增量基于全量之后的 end 服务器检查 base_version current_end 相等 → 追加end TimeId::next() 不等 → 拒绝断层返回 current_end2.3 全量写资源的第一个包 / 大修改 / 断层恢复write_full(full) resource 0全量标记TimeId 时间原点 服务器 旧数据 → 历史记录 收全量 → 当前状态 end TimeId::next()不重置继续递增2.4 全量与增量的选择场景方式资源的第一个包全量resource 0日常小改增量大修改换场地、重构全量重要节点移交、归档全量增量断层全量本地损坏全量首次登录读取since 0自然得全量先全量后增量。0 全量标记。三、时间式版本号3.1 TimeId 天然版本号TimeId 2000-03-01 00:00 起算的毫秒数 - 单调递增 - 全局唯一 - 物理时间语义 - 永不重置全量也不重置继续递增 TimeId(0) 2000-03-01 时间原点 全量标记3.20的语义单一位置值语义pull(since 0)0从头读取自然得全量push(resource 0)0全量上传base_version永远 0增量基于全量之后的 end0 全量标记。增量与0无关。3.3 资源生命周期资源创建 end 0初始无数据 第一个上传包 resource 0 → 全量 → 服务器接收end TimeId::next() 0 后续上传包 resource R 0 → 增量 base_version 上一个 end永远 0先全量后增量。增量永远基于end 0。3.4 接收侧连续性检查增量上传 base_version current_end 相等 → 连续追加 不等 → 断层拒绝 全量上传 resource 0 不看 base_version end TimeId::next()继续递增比旧 end 大3.5 断层处理增量上传被拒base_version ! current_end 客户端不知道丢了什么、服务器有什么 → 直接全量上传resource 0 → 服务器旧数据入历史收全量end 递增 → 不合并一步到位四、资源层ResourceK泛型 K管格式换算4.1 两个 trait/// 资源接口实例方法pubtraitK{fnkind(self)-i64;fnencode(self)-ResultVecu8;// 写标准数据 → bytes}/// 解码接口静态方法SizedpubtraitDecodable:KSized{fndecode(bytes:[u8])-ResultSelf;// 读bytes → 标准数据}4.2 泛型资源结构体pubstructResourceK:K{pubid:ResourceId,pubvalue:K,// 具体类型pubversion:TimeId,}泛型版优势编译期类型安全、无装箱、K::decode直接调。4.3 编解码方法implK:DecodableResourceK{pubfnfrom_bytes(id:ResourceId,bytes:[u8],version:TimeId)-ResultSelf{Ok(Self{id,value:K::decode(bytes)?,version})}pubfnto_bytes(self)-ResultVecu8{self.value.encode()}}4.4 格式换算 ≠ 业务换算换算归属内容格式换算Kdecode / encodebytes ↔ 标准数据业务换算LocalProcessor标准数据 → 新标准数据K是翻译官Processor是加工者。五、权限两个集合无记录即隐藏5.1 两个集合pubstructPermissionTable{readable:HashSetResourceId,// 可读writable:HashSetResourceId,// 可写}不假设writable ⊆ readable读写独立。5.2 无记录即隐藏Hidden 不在 readable、也不在 writable 默认无权限 不需要显式存储5.3 权限表来源服务器下发登录时全量 运行中增量 本地缓存离线可用 定期同步客户端权限表是“设备拥有者权限表”的阉割版按登录人取子集。5.4 安全边界客户端权限表 UI 提示用 服务端校验 真正安全 客户端每次写操作服务端独立校验不信任本地权限。六、数据ResourceK与ResourceStore6.1 单个资源pubstructResourceK:K{pubid:ResourceId,pubvalue:K,pubversion:TimeId,}6.2 资源集合按类型分容器推荐pubstructResourceStore{leaves:HashMapResourceId,ResourceLeaveRequest,contacts:HashMapResourceId,ResourceContactBook,drawings:HashMapResourceId,ResourceDrawingMeta,}每类资源一个 map类型安全最推荐。6.3 或统一 擦除类型多时pubstructResourceStore{resources:HashMapResourceId,BoxdynAny,versions:HashMapResourceId,TimeId,}6.4 替换 vs 应用客户端必须区分implResourceStore{/// 全量丢弃旧数据整体替换pubfnreplace_all(mutself,bytes:[u8])-Result(){self.resources.clear();letalldecode_all(bytes)?;forrinall{self.resources.insert(r.id,r);}Ok(())}/// 增量保留旧数据应用变化pubfnapply(mutself,changes:[RawChange])-Result(){forcinchanges{matchc.op{Insertself.resources.insert(c.id,c.data),Updateself.resources.insert(c.id,c.data),Deleteself.resources.remove(c.id),}}Ok(())}}两种处理不能互替客户端必须判断形态。6.5 缺失检查implResourceStore{pubfnensureK:Decodablestatic(mutself,id:ResourceId)-ResultResourceK{if!self.contains::K(id){letr:ResourceKself.pull::K(id)?;self.insert(r);}Ok(self.get::K(id).unwrap())}}缺失检查不是坏事强制显式声明需求、暴露真实使用、清晰缓存策略。七、同步客户端主导7.1 客户端决定何时读客户端决定 何时写客户端决定 何时同步客户端决定 全量 or 增量客户端决定请求时7.2 人工发布人工编辑 ≠ 实时同步 - 编辑期间本地草稿 - 点击发布才提交 发布 批量提交。7.3 服务器不合并服务器 - 存 bytes - 校验连续性 - 不合并 断层时 客户端直接全量上传不合并7.4 同步状态pubstructPendingChange{pubresource:ResourceId,pubbase_version:TimeId,pubop:Op,pubpayload:Vecu8,// K::encode 产生}pubstructSyncState{publast_version:TimeId,pubpending:VecPendingChange,pubin_progress:bool,publast_error:OptionString,}7.5 同步主流程implClient{pubfnsync(mutself)-Result(){// 1. 上传 pending增量if!self.pending.is_empty(){matchself.push_delta(self.pending){Ok(new_end){self.last_versionnew_end;self.pending.clear();}Err(Error::NotContinuous{..}){// 断层不知道丢了什么 → 直接全量上传self.push_full()?;returnOk(());}Err(e)returnErr(e),}}// 2. 拉取更新只给 since服务器返回所存letrespself.transport.pull(PullRequest{since:self.last_version})?;// 3. 比较本地是否已最新ifself.last_versionresp.end{returnOk(());}// 4. 必须判断形态处理方式不同matchresp.data{PullData::Full(bytes)self.store.replace_all(bytes)?,// 全量替换PullData::Delta(changes)self.store.apply(changes)?,// 增量合并PullData::None{}}self.last_versionresp.end;Ok(())}}比较在客户端防御形态判断在客户端处理必需查询在服务器。八、本地处理系统业务换算8.1 LocalProcessor大脑pubstructLocalProcessor{rules:VecRule,conditions:ConditionSet,outputs:VecOutputSink,}implLocalProcessor{pubfnprocessK:K(mutself,resources:mut[ResourceK])-Result(){forrinresources{letkindr.value.kind();// 按 kind 加工...}Ok(())}}处理系统只看ResourceK不关心 bytes。8.2 数据流服务器 bytes ↓ K::decode格式换算 标准数据 → ResourceK ↓ LocalProcessor业务换算 新标准数据 → ResourceK ↓ K::encode格式换算 服务器 bytesK管格式Processor管业务。8.3 界面独立结构体pubstructReadViewK:K{resource:ResourceK,fields:VecFieldDisplay,}pubstructEditViewK:K{resource:ResourceK,form:FormState,}界面是“资源的视图”与读写解耦。无界面客户端不实例化。九、结构总览9.1 六层1. 身份层Sessionuser pc token 2. 权限层PermissionTablereadable / writable 3. 数据层ResourceK ResourceStore 4. 同步层PendingChange SyncState SyncStrategy 5. 本地处理LocalProcessor业务换算 6. 界面ReadViewK / EditViewK可选9.2 主结构pubstructClient{session:Session,permissions:PermissionTable,store:ResourceStore,sync:SyncState,strategy:SyncStrategy,transport:Transport,processor:OptionLocalProcessor,}9.3 核心方法implClient{pubfnlogin(mutself,cred:Credential)-Result();pubfnpull(mutself,since:TimeId)-ResultPullResponse;pubfnpush_delta(mutself,changes:[RawChange])-ResultTimeId;pubfnpush_full(mutself)-ResultTimeId;pubfnsync(mutself)-Result();}十、完整流程10.1 首次登录1. 认证 2. 拉取权限表readable / writable 3. pull(since 0) → 服务器返回 Full自然得全量 4. 客户端判断形态Full → replace_all 5. 按 kind 用 K::decode 解码 6. last_version end 0 7. 就绪10.2 日常读1. ensure::K(id)本地有 → 用无 → 拉 2. 按 readable 判断是否展示 3. 展示 ResourceK10.3 日常写1. ensure::K(id) 2. 按 writable 判断能否编辑 3. 编辑 4. K::encode → bytes 5. 记录 PendingChange带 base_version 0 6. 按策略同步10.4 增量同步连续1. push_delta(pending, base_version last_version 0) 2. 服务器base_version current_end → 追加end TimeId::next() 3. 客户端last_version 新 endpending 清除10.5 增量同步断层1. push_delta(pending, base_version V) 2. 服务器V ! current_end → 拒绝 3. 客户端不知道丢了什么 → push_full() 4. 服务器旧数据入历史收全量end TimeId::next()不重置 5. 客户端last_version 新 endpending 清除10.6 拉取更新1. pull(since last_version) 2. 服务器只查 since 之后的内容返回所存 3. 客户端 a. 比较 last_version vs end → 跳过 → 判断形态 b. 形态处理 Full → replace_all丢弃旧数据 Delta → apply合并 None → 跳过 4. last_version end10.7 资源的第一个包1. 资源创建end 0 2. 客户端首次上传push_full(resource 0) 3. 服务器收全量end TimeId::next() 0 4. 后续改用增量base_version 0十一、一句话客户端是业务处理器读、写、本地加工。读请求统一只给since服务器只查since之后的内容、返回所存不判断模式客户端必须判断响应形态——全量replace_all丢弃旧数据增量apply合并处理方式不同。写以增量为核心base_version永远 0断层时直接全量上传resource 0不合并。0 全量标记资源的第一个包就是全量。end永不重置全量也TimeId::next()递增。资源层用ResourceK泛型K管格式换算LocalProcessor管业务换算。权限两个集合无记录即隐藏。同步由客户端主导人工发布为界。界面是可选结构体与读写解耦。服务器查询不判断模式。客户端必须判断形态因为处理方式不同。先全量后增量断层即全量K管格式Processor管业务六层分离有界面无界面共用读写。