[{"data":1,"prerenderedAt":742},["ShallowReactive",2],{"article:device-cloud-api-sync":3},{"meta":4,"raw":18,"references":19,"prev":37,"next":48,"body":58,"toc":732},{"slug":5,"title":6,"date":7,"updated":8,"tags":9,"summary":14,"draft":15,"lang":16,"minutes":17},"device-cloud-api-sync","端云协同 API 设计：离线同步、弱网重试与版本兼容","2026-08-18","2026-10-11",[10,11,12,13],"架构","API 设计","离线优先","端云协同","用本地事务与 outbox 保住离线记录，以业务操作 ID 去重；设计逐项确认、游标失效、删除传播、冲突策略和内容包原子切换，并验证断电与重连。",false,"zh-CN",5,"客户端离线记录了十次操作，重连时上报成功，但确认响应丢了。客户端重发后，云端应仍然只有十条记录。端云同步的目标是：**本地操作不丢，重传不重复，冲突有规则，用户知道数据同步到了哪里。** 这些约束同时适用于移动 App、桌面客户端与边缘设备。\n\n## 先分数据的权威来源\n\n离线优先让 UI 读取本地数据，并不意味着所有数据都由设备决定。操作事件可在本地生成；内容由云端版本管理；配额权益、支付结果由服务端权威确认，离线不能随意合并出更多余额。\n\n```text\n用户操作\n  → 同一本地事务：更新可见状态 + 写入 outbox\n  → 后台提交带 op_id 的操作\n  → 服务端去重并确认\n  → 客户端只移除已确认条目\n```\n\nOutbox 是持久出站队列。操作与入队必须同事务，否则断电会留下“界面已改但从未同步”的状态。[Android 离线优先指南](https:\u002F\u002Fdeveloper.android.com\u002Ftopic\u002Farchitecture\u002Fdata-layer\u002Foffline-first)\n\n![端云同步：本地事务、持久队列、幂等确认、增量下行与恢复](\u002Fdiagrams\u002Fdevice-cloud-sync.svg)\n\n*图 1 · 传输允许重复，业务应用负责去重；下行数据与游标同事务提交。*\n\n## 上行：逐项确认和原子去重\n\n操作 ID 在创建时生成，所有重试复用；服务端按用户或设备范围加 `op_id` 唯一约束，并绑定请求摘要。同 ID 不同内容是错误，不能静默返回成功。\n\n```json\n{\n  \"acked\": [\"op-1\", \"op-2\"],\n  \"retryable\": [{ \"id\": \"op-3\", \"reason\": \"temporary_unavailable\" }],\n  \"rejected\": [{ \"id\": \"op-4\", \"reason\": \"invalid_content_revision\" }]\n}\n```\n\n永久拒绝进入可恢复的错误队列，用户可查看或修正；临时错误退避，不删除。去重记录与业务写入原子提交，不能先查询再写入。跨服务写入需 outbox、唯一业务键或补偿与对账。\n\n幂等窗口要覆盖预期离线周期。若只保留一天，设备一月后重传可能再次执行；长期业务 ID 或快照确认机制更重要。不要假定自定义幂等头让所有上游自动去重。[Stripe 幂等实现范例](https:\u002F\u002Fdocs.stripe.com\u002Fapi\u002Fidempotent_requests)\n\n## 冲突：不能把两个累计值直接相加\n\n设备 A 和 B 都看到累计处理条数 100，A 增至 110，B 增至 120，相加成 230 显然不对。可以上报有唯一 ID 的操作事件或增量，去重后聚合；也可在适合的数据类型上使用明确设计的 CRDT。\n\n| 数据 | 可选策略 | 必须防范 |\n| --- | --- | --- |\n| 操作、审计事件 | 唯一事件追加并去重 | 重传、多设备重复记录 |\n| 昵称与设置 | 服务端版本号加条件更新 | 并发覆盖用户意图 |\n| 协作文档 | 领域合并或 CRDT | 实现与数据模型不匹配 |\n| 权益、金额 | 服务端权威事务 | 离线合并产生重复权益 |\n\nLast-write-wins 适合可容忍覆盖的数据，但可能丢更新。设备物理时钟不可信；逻辑或混合逻辑时钟帮助排序，并不自动解决冲突或真实性。重要更新使用 revision、`If-Match` 或业务规则检测冲突。\n\n## 下行：游标、快照与删除一起设计\n\n增量返回 `changes` 和不透明的 `next_cursor`。客户端在同一事务中应用变化并推进游标；崩溃后重复拉取，也应按对象版本幂等应用。\n\n```text\npage = pull(local_cursor)\ntransaction:\n    apply_upserts_and_deletes(page.changes)\n    save_cursor(page.next_cursor)\n```\n\n服务端为游标定义有效期与保留边界。游标过期返回明确状态，客户端获取完整快照，再接续增量。快照与后续增量需要共同的稳定版本或水位，不能在下载中漏掉并发更新。\n\n删除用可传播的墓碑或删除事件；保留期短于离线周期时，需要快照重建来识别消失数据。客户端待上报旧记录不得让已删除对象复活。授权改变也应刷新本地可见数据，并定义离线访问政策。\n\n## API 兼容：新增字段也可能破坏客户端\n\n旧客户端未必忽略未知字段或枚举值，字段加法也需契约测试。固定旧版请求、响应和真实客户端解析器，验证新增字段、未知枚举、空值与错误体。\n\n服务端按设备型号与版本下发能力，网关做协议适配，减少业务里散落版本判断。版本弃用依据存量、支持承诺和安全风险制定，不承诺永远支持所有旧版；确需停用时给升级与离线数据导出路径。\n\n## 内容分发与多端权限\n\n内容包使用版本 manifest、分块下载、校验和与签名。**哈希检测损坏，签名验证来源**，二者不等价。下载到暂存区，全部验证后原子切换版本；校验失败或断电保留旧版。差分失败可回退完整包，回滚也要避免加载已撤销的不安全版本。\n\n统计报告可由云端预聚合，展示统计截止时间。管理端与普通客户端的写入能力按角色授权，变更同样经过版本检查和同步协议。聚合结果也要应用租户和对象权限，不能因报告来自缓存就跳过授权。\n\n## 重试、状态与验证\n\n指数退避加抖动、请求优先级、分块续传和电量约束一起考虑。后台同步可跨多次联网继续，单次联网尝试仍有截止时间；“持续最终同步”不等于一个 HTTP 请求无限等待。\n\n界面区分本地已保存、待同步、已同步、需处理冲突。用户主动刷新不清空队列或生成新操作 ID。\n\n至少演练：本地提交后断电、服务端成功但 ack 丢失、批次部分拒绝、下行应用中崩溃、游标过期、多设备并发、离线后删除、包校验失败、最老客户端重连。检查数据不丢不重、权限正确和状态可见，而非只看接口返回 200。\n\n## 落地检查\n\n1. 本地写入与 outbox 同事务，业务 ID 随重试保持稳定。\n2. 服务端原子去重，逐项 ack，明确永久失败恢复路径。\n3. 增量、删除、快照和游标水位成套设计。\n4. 按数据类型处理冲突，累计值不直接相加。\n5. 老客户端契约测试，内容包验签后原子切换。\n6. 用断电、丢 ack 和长离线演练验证不丢不重。\n\n## 参考资料\n\n- [Android — Build an offline-first app](https:\u002F\u002Fdeveloper.android.com\u002Ftopic\u002Farchitecture\u002Fdata-layer\u002Foffline-first)\n- [Stripe — Idempotent requests](https:\u002F\u002Fdocs.stripe.com\u002Fapi\u002Fidempotent_requests)\n- [Firebase — Offline data](https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Ffirestore\u002Fmanage-data\u002Fenable-offline)\n- [AWS — Retry with backoff](https:\u002F\u002Fdocs.aws.amazon.com\u002Fprescriptive-guidance\u002Flatest\u002Fcloud-design-patterns\u002Fretry-backoff.html)",[20,25,29,33],{"title":21,"url":22,"type":23,"note":24,"checked":8},"Android — Build an offline-first app","https:\u002F\u002Fdeveloper.android.com\u002Ftopic\u002Farchitecture\u002Fdata-layer\u002Foffline-first","官方文档","本地优先的数据层、同步与持久队列设计；本文跨端方案为工程归纳。",{"title":26,"url":27,"type":23,"note":28,"checked":8},"Stripe — Idempotent requests","https:\u002F\u002Fdocs.stripe.com\u002Fapi\u002Fidempotent_requests","Stripe 幂等键的具体契约；不能假设所有外部服务具有相同行为。",{"title":30,"url":31,"type":23,"note":32,"checked":8},"Firebase — Offline data","https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Ffirestore\u002Fmanage-data\u002Fenable-offline","Firestore 离线缓存与同步语义；不是所有数据库的通用冲突规则。",{"title":34,"url":35,"type":23,"note":36,"checked":8},"AWS — Retry with backoff","https:\u002F\u002Fdocs.aws.amazon.com\u002Fprescriptive-guidance\u002Flatest\u002Fcloud-design-patterns\u002Fretry-backoff.html","重试与退避模式的用途和限制；不能替代业务幂等。",{"slug":38,"title":39,"date":40,"updated":8,"tags":41,"summary":46,"draft":15,"lang":16,"minutes":47},"minors-ai-safety-engineering","面向未成年人的 AI 产品：合规要求与安全工程","2026-09-15",[42,43,44,45],"安全合规","未成年人保护","内容安全","LLM","核对法规适用范围与年龄边界，补齐生成内容标识、授权和删除链路；把数据保护、流式审核、风险分流、防依赖与人工处置落实到工程。",6,{"slug":49,"title":50,"date":51,"updated":8,"tags":52,"summary":56,"draft":15,"lang":16,"minutes":57},"llm-eval-loop","评测闭环：让每次 Prompt 变更都可回归","2026-07-14",[45,53,54,55],"评测","Evals","质量","从小规模回归集起步，区分确定性校验、模型裁判与人工评审；给出技术问答 rubric、统计边界、版本记录和上线门槛，避免用平均分掩盖退化。",7,{"type":59,"children":60},"root",[61,77,83,88,101,114,123,132,138,151,437,442,453,459,464,569,582,588,609,618,623,628,634,639,644,649,661,666,672,677,682,687,692,727],{"type":62,"tag":63,"props":64,"children":65},"element","p",{},[66,69,75],{"type":67,"value":68},"text","客户端离线记录了十次操作，重连时上报成功，但确认响应丢了。客户端重发后，云端应仍然只有十条记录。端云同步的目标是：",{"type":62,"tag":70,"props":71,"children":72},"strong",{},[73],{"type":67,"value":74},"本地操作不丢，重传不重复，冲突有规则，用户知道数据同步到了哪里。",{"type":67,"value":76}," 这些约束同时适用于移动 App、桌面客户端与边缘设备。",{"type":62,"tag":78,"props":79,"children":81},"h2",{"id":80},"先分数据的权威来源",[82],{"type":67,"value":80},{"type":62,"tag":63,"props":84,"children":85},{},[86],{"type":67,"value":87},"离线优先让 UI 读取本地数据，并不意味着所有数据都由设备决定。操作事件可在本地生成；内容由云端版本管理；配额权益、支付结果由服务端权威确认，离线不能随意合并出更多余额。",{"type":62,"tag":89,"props":90,"children":95},"pre",{"className":91,"code":93,"language":67,"meta":94},[92],"language-text","用户操作\n  → 同一本地事务：更新可见状态 + 写入 outbox\n  → 后台提交带 op_id 的操作\n  → 服务端去重并确认\n  → 客户端只移除已确认条目\n","",[96],{"type":62,"tag":97,"props":98,"children":99},"code",{"__ignoreMap":94},[100],{"type":67,"value":93},{"type":62,"tag":63,"props":102,"children":103},{},[104,106],{"type":67,"value":105},"Outbox 是持久出站队列。操作与入队必须同事务，否则断电会留下“界面已改但从未同步”的状态。",{"type":62,"tag":107,"props":108,"children":111},"a",{"href":22,"rel":109},[110],"nofollow",[112],{"type":67,"value":113},"Android 离线优先指南",{"type":62,"tag":63,"props":115,"children":116},{},[117],{"type":62,"tag":118,"props":119,"children":122},"img",{"alt":120,"src":121},"端云同步：本地事务、持久队列、幂等确认、增量下行与恢复","\u002Fdiagrams\u002Fdevice-cloud-sync.svg",[],{"type":62,"tag":63,"props":124,"children":125},{},[126],{"type":62,"tag":127,"props":128,"children":129},"em",{},[130],{"type":67,"value":131},"图 1 · 传输允许重复，业务应用负责去重；下行数据与游标同事务提交。",{"type":62,"tag":78,"props":133,"children":135},{"id":134},"上行逐项确认和原子去重",[136],{"type":67,"value":137},"上行：逐项确认和原子去重",{"type":62,"tag":63,"props":139,"children":140},{},[141,143,149],{"type":67,"value":142},"操作 ID 在创建时生成，所有重试复用；服务端按用户或设备范围加 ",{"type":62,"tag":97,"props":144,"children":146},{"className":145},[],[147],{"type":67,"value":148},"op_id",{"type":67,"value":150}," 唯一约束，并绑定请求摘要。同 ID 不同内容是错误，不能静默返回成功。",{"type":62,"tag":89,"props":152,"children":156},{"className":153,"code":154,"language":155,"meta":94,"style":94},"language-json shiki shiki-themes vitesse-light vitesse-light","{\n  \"acked\": [\"op-1\", \"op-2\"],\n  \"retryable\": [{ \"id\": \"op-3\", \"reason\": \"temporary_unavailable\" }],\n  \"rejected\": [{ \"id\": \"op-4\", \"reason\": \"invalid_content_revision\" }]\n}\n","json",[157],{"type":62,"tag":97,"props":158,"children":159},{"__ignoreMap":94},[160,172,242,337,429],{"type":62,"tag":161,"props":162,"children":165},"span",{"class":163,"line":164},"line",1,[166],{"type":62,"tag":161,"props":167,"children":169},{"style":168},"--shiki-default:#999999;--shiki-dark:#999999",[170],{"type":67,"value":171},"{\n",{"type":62,"tag":161,"props":173,"children":175},{"class":163,"line":174},2,[176,182,188,193,198,203,208,214,218,223,228,233,237],{"type":62,"tag":161,"props":177,"children":179},{"style":178},"--shiki-default:#99841877;--shiki-dark:#99841877",[180],{"type":67,"value":181},"  \"",{"type":62,"tag":161,"props":183,"children":185},{"style":184},"--shiki-default:#998418;--shiki-dark:#998418",[186],{"type":67,"value":187},"acked",{"type":62,"tag":161,"props":189,"children":190},{"style":178},[191],{"type":67,"value":192},"\"",{"type":62,"tag":161,"props":194,"children":195},{"style":168},[196],{"type":67,"value":197},":",{"type":62,"tag":161,"props":199,"children":200},{"style":168},[201],{"type":67,"value":202}," [",{"type":62,"tag":161,"props":204,"children":206},{"style":205},"--shiki-default:#B5695977;--shiki-dark:#B5695977",[207],{"type":67,"value":192},{"type":62,"tag":161,"props":209,"children":211},{"style":210},"--shiki-default:#B56959;--shiki-dark:#B56959",[212],{"type":67,"value":213},"op-1",{"type":62,"tag":161,"props":215,"children":216},{"style":205},[217],{"type":67,"value":192},{"type":62,"tag":161,"props":219,"children":220},{"style":168},[221],{"type":67,"value":222},",",{"type":62,"tag":161,"props":224,"children":225},{"style":205},[226],{"type":67,"value":227}," \"",{"type":62,"tag":161,"props":229,"children":230},{"style":210},[231],{"type":67,"value":232},"op-2",{"type":62,"tag":161,"props":234,"children":235},{"style":205},[236],{"type":67,"value":192},{"type":62,"tag":161,"props":238,"children":239},{"style":168},[240],{"type":67,"value":241},"],\n",{"type":62,"tag":161,"props":243,"children":245},{"class":163,"line":244},3,[246,250,255,259,263,268,272,277,281,285,289,294,298,302,306,311,315,319,323,328,332],{"type":62,"tag":161,"props":247,"children":248},{"style":178},[249],{"type":67,"value":181},{"type":62,"tag":161,"props":251,"children":252},{"style":184},[253],{"type":67,"value":254},"retryable",{"type":62,"tag":161,"props":256,"children":257},{"style":178},[258],{"type":67,"value":192},{"type":62,"tag":161,"props":260,"children":261},{"style":168},[262],{"type":67,"value":197},{"type":62,"tag":161,"props":264,"children":265},{"style":168},[266],{"type":67,"value":267}," [{",{"type":62,"tag":161,"props":269,"children":270},{"style":178},[271],{"type":67,"value":227},{"type":62,"tag":161,"props":273,"children":274},{"style":184},[275],{"type":67,"value":276},"id",{"type":62,"tag":161,"props":278,"children":279},{"style":178},[280],{"type":67,"value":192},{"type":62,"tag":161,"props":282,"children":283},{"style":168},[284],{"type":67,"value":197},{"type":62,"tag":161,"props":286,"children":287},{"style":205},[288],{"type":67,"value":227},{"type":62,"tag":161,"props":290,"children":291},{"style":210},[292],{"type":67,"value":293},"op-3",{"type":62,"tag":161,"props":295,"children":296},{"style":205},[297],{"type":67,"value":192},{"type":62,"tag":161,"props":299,"children":300},{"style":168},[301],{"type":67,"value":222},{"type":62,"tag":161,"props":303,"children":304},{"style":178},[305],{"type":67,"value":227},{"type":62,"tag":161,"props":307,"children":308},{"style":184},[309],{"type":67,"value":310},"reason",{"type":62,"tag":161,"props":312,"children":313},{"style":178},[314],{"type":67,"value":192},{"type":62,"tag":161,"props":316,"children":317},{"style":168},[318],{"type":67,"value":197},{"type":62,"tag":161,"props":320,"children":321},{"style":205},[322],{"type":67,"value":227},{"type":62,"tag":161,"props":324,"children":325},{"style":210},[326],{"type":67,"value":327},"temporary_unavailable",{"type":62,"tag":161,"props":329,"children":330},{"style":205},[331],{"type":67,"value":192},{"type":62,"tag":161,"props":333,"children":334},{"style":168},[335],{"type":67,"value":336}," }],\n",{"type":62,"tag":161,"props":338,"children":340},{"class":163,"line":339},4,[341,345,350,354,358,362,366,370,374,378,382,387,391,395,399,403,407,411,415,420,424],{"type":62,"tag":161,"props":342,"children":343},{"style":178},[344],{"type":67,"value":181},{"type":62,"tag":161,"props":346,"children":347},{"style":184},[348],{"type":67,"value":349},"rejected",{"type":62,"tag":161,"props":351,"children":352},{"style":178},[353],{"type":67,"value":192},{"type":62,"tag":161,"props":355,"children":356},{"style":168},[357],{"type":67,"value":197},{"type":62,"tag":161,"props":359,"children":360},{"style":168},[361],{"type":67,"value":267},{"type":62,"tag":161,"props":363,"children":364},{"style":178},[365],{"type":67,"value":227},{"type":62,"tag":161,"props":367,"children":368},{"style":184},[369],{"type":67,"value":276},{"type":62,"tag":161,"props":371,"children":372},{"style":178},[373],{"type":67,"value":192},{"type":62,"tag":161,"props":375,"children":376},{"style":168},[377],{"type":67,"value":197},{"type":62,"tag":161,"props":379,"children":380},{"style":205},[381],{"type":67,"value":227},{"type":62,"tag":161,"props":383,"children":384},{"style":210},[385],{"type":67,"value":386},"op-4",{"type":62,"tag":161,"props":388,"children":389},{"style":205},[390],{"type":67,"value":192},{"type":62,"tag":161,"props":392,"children":393},{"style":168},[394],{"type":67,"value":222},{"type":62,"tag":161,"props":396,"children":397},{"style":178},[398],{"type":67,"value":227},{"type":62,"tag":161,"props":400,"children":401},{"style":184},[402],{"type":67,"value":310},{"type":62,"tag":161,"props":404,"children":405},{"style":178},[406],{"type":67,"value":192},{"type":62,"tag":161,"props":408,"children":409},{"style":168},[410],{"type":67,"value":197},{"type":62,"tag":161,"props":412,"children":413},{"style":205},[414],{"type":67,"value":227},{"type":62,"tag":161,"props":416,"children":417},{"style":210},[418],{"type":67,"value":419},"invalid_content_revision",{"type":62,"tag":161,"props":421,"children":422},{"style":205},[423],{"type":67,"value":192},{"type":62,"tag":161,"props":425,"children":426},{"style":168},[427],{"type":67,"value":428}," }]\n",{"type":62,"tag":161,"props":430,"children":431},{"class":163,"line":17},[432],{"type":62,"tag":161,"props":433,"children":434},{"style":168},[435],{"type":67,"value":436},"}\n",{"type":62,"tag":63,"props":438,"children":439},{},[440],{"type":67,"value":441},"永久拒绝进入可恢复的错误队列，用户可查看或修正；临时错误退避，不删除。去重记录与业务写入原子提交，不能先查询再写入。跨服务写入需 outbox、唯一业务键或补偿与对账。",{"type":62,"tag":63,"props":443,"children":444},{},[445,447],{"type":67,"value":446},"幂等窗口要覆盖预期离线周期。若只保留一天，设备一月后重传可能再次执行；长期业务 ID 或快照确认机制更重要。不要假定自定义幂等头让所有上游自动去重。",{"type":62,"tag":107,"props":448,"children":450},{"href":27,"rel":449},[110],[451],{"type":67,"value":452},"Stripe 幂等实现范例",{"type":62,"tag":78,"props":454,"children":456},{"id":455},"冲突不能把两个累计值直接相加",[457],{"type":67,"value":458},"冲突：不能把两个累计值直接相加",{"type":62,"tag":63,"props":460,"children":461},{},[462],{"type":67,"value":463},"设备 A 和 B 都看到累计处理条数 100，A 增至 110，B 增至 120，相加成 230 显然不对。可以上报有唯一 ID 的操作事件或增量，去重后聚合；也可在适合的数据类型上使用明确设计的 CRDT。",{"type":62,"tag":465,"props":466,"children":467},"table",{},[468,492],{"type":62,"tag":469,"props":470,"children":471},"thead",{},[472],{"type":62,"tag":473,"props":474,"children":475},"tr",{},[476,482,487],{"type":62,"tag":477,"props":478,"children":479},"th",{},[480],{"type":67,"value":481},"数据",{"type":62,"tag":477,"props":483,"children":484},{},[485],{"type":67,"value":486},"可选策略",{"type":62,"tag":477,"props":488,"children":489},{},[490],{"type":67,"value":491},"必须防范",{"type":62,"tag":493,"props":494,"children":495},"tbody",{},[496,515,533,551],{"type":62,"tag":473,"props":497,"children":498},{},[499,505,510],{"type":62,"tag":500,"props":501,"children":502},"td",{},[503],{"type":67,"value":504},"操作、审计事件",{"type":62,"tag":500,"props":506,"children":507},{},[508],{"type":67,"value":509},"唯一事件追加并去重",{"type":62,"tag":500,"props":511,"children":512},{},[513],{"type":67,"value":514},"重传、多设备重复记录",{"type":62,"tag":473,"props":516,"children":517},{},[518,523,528],{"type":62,"tag":500,"props":519,"children":520},{},[521],{"type":67,"value":522},"昵称与设置",{"type":62,"tag":500,"props":524,"children":525},{},[526],{"type":67,"value":527},"服务端版本号加条件更新",{"type":62,"tag":500,"props":529,"children":530},{},[531],{"type":67,"value":532},"并发覆盖用户意图",{"type":62,"tag":473,"props":534,"children":535},{},[536,541,546],{"type":62,"tag":500,"props":537,"children":538},{},[539],{"type":67,"value":540},"协作文档",{"type":62,"tag":500,"props":542,"children":543},{},[544],{"type":67,"value":545},"领域合并或 CRDT",{"type":62,"tag":500,"props":547,"children":548},{},[549],{"type":67,"value":550},"实现与数据模型不匹配",{"type":62,"tag":473,"props":552,"children":553},{},[554,559,564],{"type":62,"tag":500,"props":555,"children":556},{},[557],{"type":67,"value":558},"权益、金额",{"type":62,"tag":500,"props":560,"children":561},{},[562],{"type":67,"value":563},"服务端权威事务",{"type":62,"tag":500,"props":565,"children":566},{},[567],{"type":67,"value":568},"离线合并产生重复权益",{"type":62,"tag":63,"props":570,"children":571},{},[572,574,580],{"type":67,"value":573},"Last-write-wins 适合可容忍覆盖的数据，但可能丢更新。设备物理时钟不可信；逻辑或混合逻辑时钟帮助排序，并不自动解决冲突或真实性。重要更新使用 revision、",{"type":62,"tag":97,"props":575,"children":577},{"className":576},[],[578],{"type":67,"value":579},"If-Match",{"type":67,"value":581}," 或业务规则检测冲突。",{"type":62,"tag":78,"props":583,"children":585},{"id":584},"下行游标快照与删除一起设计",[586],{"type":67,"value":587},"下行：游标、快照与删除一起设计",{"type":62,"tag":63,"props":589,"children":590},{},[591,593,599,601,607],{"type":67,"value":592},"增量返回 ",{"type":62,"tag":97,"props":594,"children":596},{"className":595},[],[597],{"type":67,"value":598},"changes",{"type":67,"value":600}," 和不透明的 ",{"type":62,"tag":97,"props":602,"children":604},{"className":603},[],[605],{"type":67,"value":606},"next_cursor",{"type":67,"value":608},"。客户端在同一事务中应用变化并推进游标；崩溃后重复拉取，也应按对象版本幂等应用。",{"type":62,"tag":89,"props":610,"children":613},{"className":611,"code":612,"language":67,"meta":94},[92],"page = pull(local_cursor)\ntransaction:\n    apply_upserts_and_deletes(page.changes)\n    save_cursor(page.next_cursor)\n",[614],{"type":62,"tag":97,"props":615,"children":616},{"__ignoreMap":94},[617],{"type":67,"value":612},{"type":62,"tag":63,"props":619,"children":620},{},[621],{"type":67,"value":622},"服务端为游标定义有效期与保留边界。游标过期返回明确状态，客户端获取完整快照，再接续增量。快照与后续增量需要共同的稳定版本或水位，不能在下载中漏掉并发更新。",{"type":62,"tag":63,"props":624,"children":625},{},[626],{"type":67,"value":627},"删除用可传播的墓碑或删除事件；保留期短于离线周期时，需要快照重建来识别消失数据。客户端待上报旧记录不得让已删除对象复活。授权改变也应刷新本地可见数据，并定义离线访问政策。",{"type":62,"tag":78,"props":629,"children":631},{"id":630},"api-兼容新增字段也可能破坏客户端",[632],{"type":67,"value":633},"API 兼容：新增字段也可能破坏客户端",{"type":62,"tag":63,"props":635,"children":636},{},[637],{"type":67,"value":638},"旧客户端未必忽略未知字段或枚举值，字段加法也需契约测试。固定旧版请求、响应和真实客户端解析器，验证新增字段、未知枚举、空值与错误体。",{"type":62,"tag":63,"props":640,"children":641},{},[642],{"type":67,"value":643},"服务端按设备型号与版本下发能力，网关做协议适配，减少业务里散落版本判断。版本弃用依据存量、支持承诺和安全风险制定，不承诺永远支持所有旧版；确需停用时给升级与离线数据导出路径。",{"type":62,"tag":78,"props":645,"children":647},{"id":646},"内容分发与多端权限",[648],{"type":67,"value":646},{"type":62,"tag":63,"props":650,"children":651},{},[652,654,659],{"type":67,"value":653},"内容包使用版本 manifest、分块下载、校验和与签名。",{"type":62,"tag":70,"props":655,"children":656},{},[657],{"type":67,"value":658},"哈希检测损坏，签名验证来源",{"type":67,"value":660},"，二者不等价。下载到暂存区，全部验证后原子切换版本；校验失败或断电保留旧版。差分失败可回退完整包，回滚也要避免加载已撤销的不安全版本。",{"type":62,"tag":63,"props":662,"children":663},{},[664],{"type":67,"value":665},"统计报告可由云端预聚合，展示统计截止时间。管理端与普通客户端的写入能力按角色授权，变更同样经过版本检查和同步协议。聚合结果也要应用租户和对象权限，不能因报告来自缓存就跳过授权。",{"type":62,"tag":78,"props":667,"children":669},{"id":668},"重试状态与验证",[670],{"type":67,"value":671},"重试、状态与验证",{"type":62,"tag":63,"props":673,"children":674},{},[675],{"type":67,"value":676},"指数退避加抖动、请求优先级、分块续传和电量约束一起考虑。后台同步可跨多次联网继续，单次联网尝试仍有截止时间；“持续最终同步”不等于一个 HTTP 请求无限等待。",{"type":62,"tag":63,"props":678,"children":679},{},[680],{"type":67,"value":681},"界面区分本地已保存、待同步、已同步、需处理冲突。用户主动刷新不清空队列或生成新操作 ID。",{"type":62,"tag":63,"props":683,"children":684},{},[685],{"type":67,"value":686},"至少演练：本地提交后断电、服务端成功但 ack 丢失、批次部分拒绝、下行应用中崩溃、游标过期、多设备并发、离线后删除、包校验失败、最老客户端重连。检查数据不丢不重、权限正确和状态可见，而非只看接口返回 200。",{"type":62,"tag":78,"props":688,"children":690},{"id":689},"落地检查",[691],{"type":67,"value":689},{"type":62,"tag":693,"props":694,"children":695},"ol",{},[696,702,707,712,717,722],{"type":62,"tag":697,"props":698,"children":699},"li",{},[700],{"type":67,"value":701},"本地写入与 outbox 同事务，业务 ID 随重试保持稳定。",{"type":62,"tag":697,"props":703,"children":704},{},[705],{"type":67,"value":706},"服务端原子去重，逐项 ack，明确永久失败恢复路径。",{"type":62,"tag":697,"props":708,"children":709},{},[710],{"type":67,"value":711},"增量、删除、快照和游标水位成套设计。",{"type":62,"tag":697,"props":713,"children":714},{},[715],{"type":67,"value":716},"按数据类型处理冲突，累计值不直接相加。",{"type":62,"tag":697,"props":718,"children":719},{},[720],{"type":67,"value":721},"老客户端契约测试，内容包验签后原子切换。",{"type":62,"tag":697,"props":723,"children":724},{},[725],{"type":67,"value":726},"用断电、丢 ack 和长离线演练验证不丢不重。",{"type":62,"tag":78,"props":728,"children":730},{"id":729},"参考资料",[731],{"type":67,"value":729},[733,734,735,736,737,738,739,740,741],{"id":80,"depth":174,"text":80},{"id":134,"depth":174,"text":137},{"id":455,"depth":174,"text":458},{"id":584,"depth":174,"text":587},{"id":630,"depth":174,"text":633},{"id":646,"depth":174,"text":646},{"id":668,"depth":174,"text":671},{"id":689,"depth":174,"text":689},{"id":729,"depth":174,"text":729},1792400815607]