Skip to content

CodeNote App 离线功能方案

版本:v1.1 范围:Android 客户端全功能离线能力设计(离线可用、离线缓存、联网同步、上传下载) 核心要求:全链路管控、全功能覆盖、扩展性强、适配全部


1. 目标与设计原则

原则含义
全链路管控从「数据产生 → 本地落库 → 离线可用 → 联网同步 → 服务端确认」每一条链路都有明确状态与兜底,不允许数据产生后丢失
全功能覆盖每个功能模块都明确「离线可用 / 降级可用 / 必须联网」三档定义,不留模糊地带
扩展性强同步协议按 entityType 注册表驱动,新增实体 = 注册 + 实现 handler,不改协议骨架
适配全部适配匿名用户(设备级)、登录用户(账号级)、多账号/多设备、弱网/断网/恢复联网全场景

统一心智模型

用户操作 → 本地优先写入(Room,带 syncStatus)→ 界面即时反馈(乐观 UI)
         → 联网时后台同步(push → pull)→ 冲突按策略解决 → 最终一致

离线 ≠ 功能不可用;离线 = 本地可用 + 延迟一致。只有强校验/强实时类功能才必须联网。


2. 现状盘点

2.1 客户端已实现

能力位置说明
本地数据库core/data/.../CodeNoteDatabase.ktRoom,11 实体:categories / qrcodes / scan_records / assets / checkin_activities / checkin_records / passes / album_photos / orgs / org_members / notifications
同步状态机各实体 syncStatusPendingCreate / PendingUpdate / PendingDelete / Synced,DAO 提供 getUnsynced()
同步管理器core/data/sync/SyncManager.ktpushLocalChanges() / pullChanges() / fullSync()
同步调度SyncScheduler.kt + SyncWorker.ktWorkManager 30 分钟周期(联网约束)+ 手动触发;先 push 后 pull
手动同步页feature/sync/SyncScreen.kt上次同步时间展示、手动触发
本地登录态TokenStore / UserProfileStore / DeviceUserManagertoken、用户资料、设备标识本地持久化
文件上传core/data/.../FileManager.ktOSS STS 直传 + 压缩 + 重试 + 服务端兜底 + 远程删除
配额缓存QuotaConfig本地缓存配额/额度

2.2 服务端已实现

接口说明
GET /api/sync/pull?since=增量拉取:qrCodes / categories / scanRecords + serverTime
POST /api/sync/push推送本地变更,支持 QR_CODE / CATEGORY / SCAN_RECORD,返回冲突列表
GET /api/sync/conflicts / POST /api/sync/resolve冲突查询与解决(acceptServer)
冲突策略LWW(Last Writer Wins),默认服务端优先
表结构sync_mapping(本地 id ↔ 服务端 id)、unsynced_recordstoken_blacklist

2.3 现状缺陷(必须修复)

#缺陷影响
1契约不一致:客户端 push 发送 entityType="qr_code"(小写),服务端匹配 "QR_CODE"(大写)除分类外推送全部落 else → Unknown entity type数据同步不上去
2pull 契约不一致:服务端返回 Map(qrCodes/categories/scanRecords),客户端按 List<SyncConflictResponse> 解析增量拉取不可用
3实体覆盖不全:assets / checkin_records / album_photos 客户端已带 syncStatus 且 push 已发送,服务端不处理;org / notification / pass 未接入数据仅本地存在,多设备/换机丢失
4无上传队列持久化弱网上传成功率低,无断点续传
5无同步结果可观测用户无法感知数据是否已同步
6同步时机粗糙仅 30 分钟周期 + 手动

3. 安装时预置数据(本地数据基本要求)

3.1 原则

  • 业务数据不预置(账号隔离);预置的是「运行时骨架 + 静态资源」

3.2 预置清单

类别内容
数据库 SchemaRoom 全部表结构 + 版本迁移脚本
默认分类若产品需要,预置并标记 isPreset(或首次登录 pull 下发,二选一)
静态资源隐私政策/用户协议/关于页、引导图、WebView 本地页、主题资源
配置模板默认主题、设置默认值、上传限制默认值(可被服务端覆盖)
同步锚点lastSyncTime = 0(首次联网自动全量拉取)

3.3 本地数据基本要求(运行时)

要求说明
本地为主用户可产生的数据先落 Room(含 syncStatus),界面只读本地
写队列未同步数据 = 隐式持久化队列(syncStatus != Synced
容量控制本地缓存设上限(缩略图 LRU、扫描记录归档),防存储膨胀
账号隔离数据按 ownerId(用户)/deviceId(匿名)分区,切换账号不串数据
加密token 等敏感数据加密存储(EncryptedSharedPreferences,必要时 SQLCipher)

4. 核心处理模型(全链路)

一句话模型:「本地产生 → 打标入队 → 联网分发 → 服务端/OSS 处理 → 结果回写 → 归档」

4.1 统一处理链路(同步 / 上传 / 下载)

4.1.1 同步链路(业务数据)

来源:本地 Room 未同步数据(syncStatus != Synced,按 entityType 分组)

  ▼ 请求:POST /api/sync/push(SyncPushItem[]:entityType/entityLocalId/operation/data/updatedAt)
发往:服务端 SyncService(按 entityType 分派 handler)

  ▼ 处理:upsert/delete 落正式业务表 + sync_mapping 维护(本地id↔remoteId)
结果:成功 / 冲突列表(SyncConflictResponse)

  ▼ 回写:本地标记 Synced + 写入 remoteId;冲突进冲突表待用户决策
归档:服务端业务表 + sync_mapping;本地 syncStatus=Synced + lastSyncTime 更新

反向(拉取):

来源:服务端增量(updatedAt >= lastSyncTime)

  ▼ 请求:GET /api/sync/pull?since=lastSyncTime
发往:SyncService.pull()

  ▼ 处理:按实体查询增量 → 组装响应
结果:增量数据列表 + serverTime

  ▼ 回写:本地 upsert(LWW:按 updatedAt 比较)→ 覆盖/保留
归档:本地 lastSyncTime = serverTime(断点续拉锚点)

4.1.2 上传链路(文件 → OSS)

来源:本地图片/文件(相机拍摄、相册选择、头像裁剪)

  ▼ 请求:POST /api/files/sts-token → 获取 OSS STS 临时凭证
发往:阿里云 OSS(STS 直传,分片/put,含压缩与指数重试)

  ▼ 处理:OSS 落对象(bucket: codenote-server / 前缀 codenote/)
结果:OSS URL(https://codenote-server.oss-cn-guangzhou.aliyuncs.com/...)

  ▼ 回写:POST /api/files/callback 通知服务端登记文件;业务接口(相册/资产/头像)更新 url 字段
归档:本地 upload_tasks 标记完成 + url 写入实体 + syncStatus 流转;服务端 files 表登记

4.1.3 下载链路(OSS → 本地缓存)

来源:OSS URL / 服务端返回的 url 字段

  ▼ 请求:缩略图/原图 GET(OSS 直读或 /api/files/signed-url 签名 URL)
发往:OSS(CDN/直连)

  ▼ 处理:Coil/Glide 磁盘缓存(LRU)
结果:本地文件

  ▼ 回写:缓存命中(离线可读)
归档:磁盘缓存 LRU 淘汰;本地数据库存 url 引用,不存文件本体

4.2 三大数据端

载体内容一致性角色
本地数据Room 11 实体 + DataStore(token/资料/偏好)+ 本地文件(图片缓存、上传队列)全量可离线数据 + 待同步队列 + 登录态主写端(用户操作先落本地)
服务器数据MySQL ~40 表(users/qr_codes/categories/scan_records/assets/checkin/pass/album/org/rbac/notifications/sync_mapping/unsynced_records…)账号权威数据、跨设备共享、RBAC 权限、通知权威端(最终一致目标、冲突仲裁)
OSS 数据阿里云 OSS bucket codenote-server,前缀 codenote/图片/头像/文件二进制文件权威端(URL 为引用,业务表存 url)

端间关系

本地(写) ──push──▶ 服务器(权威) ──pull──▶ 本地(读)
本地(文件) ──STS直传──▶ OSS(权威) ──url 登记──▶ 服务器(files 表/业务表)
OSS(读) ──缓存──▶ 本地(缩略图/原图)

4.3 请求功能全景(请求什么 → 核心处理)

按功能域归类(以 ApiService + RepositoryImpls 现状为准)。标 ⚡ = 必须联网,🟡 = 可离线入队,✅ = 本地闭环。

认证域(⚡ 全部联网)

请求核心处理
login / phone-login / phone-password-login / email-login / one-click-login / device-login服务端验凭据 → 签发 token/refreshToken → 本地 TokenStore 持久化 + 拉取用户资料 + 首次 fullSync
register创建账号 → 同上
refresh刷新 token(401 时自动续期)
logout服务端注销会话 + 本地清 token/资料/会话
短信/邮件验证码(send/bind/link/forgot/change)服务端下发验证码(运营商/邮件通道)
绑定/解绑(phone/email)服务端校验验证码 → 更新 user_identities
密码(set/reset/change)服务端校验并更新凭据
会话管理(sessions CRUD/switch/remove)多端会话生命周期;切换 = 本地换 token + 拉资料
账号链接/切换(link/switch)多账号关联;切换后本地数据按 userId 隔离
设备管理(devices/remove)服务端登记/解绑设备(配合 device-login)
quotas配额下发 → 本地 QuotaConfig 缓存(🟡 离线可用缓存值)

用户域

请求核心处理
userinfo GET/PUT拉取/更新资料 → 本地 UserProfileStore 同步(🟡 本地已缓存可离线看)
current-org / username更新用户偏好/标识 → 本地同步

二维码/分类域(✅ 离线为主 + 同步)

请求核心处理
categories CRUD/排序/置顶/树本地写(Pending*)→ push 同步;列表本地读
qr_codes CRUD/收藏/置顶/排序/相邻本地写(Pending*)→ push 同步;列表/搜索本地读

扫描域

请求核心处理
扫码记录 create(本地)本地落 scan_records(PendingCreate)→ push 同步(🟡)
记录列表本地读

资产域(🟡 已缓存可读,写本地待同步)

请求核心处理
assets CRUD/转移/报废/导出/地图服务端处理;客户端本地缓存 + 变更打标(部分已带 syncStatus)
资产分类/字段定义/盘点服务端处理 + 本地缓存

活动/签到/通行证域

请求核心处理
activities CRUD/状态服务端(⚡ 活动实时性);已缓存可读
check-in 提交本地落 checkin_records(PendingCreate)→ push(🟡),重复提交防抖(活动+用户唯一)
pass 签发/核验签发服务端(⚡);核验必须联网(防伪时效校验);本地通行证可展示(🟡)

相册域(🟡 上传入队)

请求核心处理
照片列表本地读 + 服务端拉取合并
上传savePhotoLocally(本地文件)→ upload_tasks 队列 → OSS → callback → 更新 url(🟡)
删除本地 PendingDelete → 同步删 OSS + 服务端(🟡)
syncPhoto单张照片补同步

组织域(🟡 缓存可读,操作需联网)

请求核心处理
orgs CRUD/加入/退出/转让/统计/二维码服务端权威(⚡ 撮合/权限实时性);本地 orgs/org_members 缓存
成员/审批/邀请服务端(⚡);缓存可读
组织 RBAC 角色服务端权限表(⚡ 权限判定在服务端)
位置/分享服务端 + 本地缓存

通知域(❌ 实时推送需联网,缓存可读)

请求核心处理
列表/未读数/总数服务端 + 本地 notifications 表缓存(🟡 已缓存离线可读)
WS 实时推送联网在线收;离线错过 → 恢复联网 pull 补齐

同步域

请求核心处理
sync pull/push/conflicts/resolve见 4.1.1

文件域

请求核心处理
sts-token签发 OSS 临时凭证(上传前必调,⚡)
upload(服务端兜底)直传失败时走服务端中转上传(⚡)
callback上传后通知服务端登记 files 表
signed-url / files DELETE签名下载 / 删除远程对象

系统域

请求核心处理
app/update/check版本检查(⚡,离线跳过)
login-records登录记录上报

4.4 状态机全集

S1 业务数据同步状态机(客户端 Room)

            ┌──────────────┐
  新建 ───▶ │ PendingCreate│──┐
  修改 ───▶ │ PendingUpdate│──┤ push 成功 ──▶ Synced
  删除 ───▶ │ PendingDelete│──┘      │
            └──────────────┘         │ 失败/断网
                 ▲                   ▼
                 └──── 保持 Pending,指数退避重试(幂等)
  • 删除 = 软删除标记(deleted=1 + PendingDelete),服务端确认后物理清理
  • 每行带 updatedAt + remoteId(sync_mapping 映射)

S2 文件上传状态机(客户端 upload_tasks)

Pending(待传) ──▶ Uploading(上传中) ──▶ Uploaded(已传 OSS) ──▶ Notified(已通知服务端登记) ──▶ Done(业务字段更新,归档)
     │                  │                     │
     └────── Failed ────┴── retry < N ────────┘
     (持久化,恢复联网续传;超过 N 次转人工/降级服务端兜底上传)

S3 登录态状态机(客户端 IdentityManager)

LoggedOut ──登录/注册──▶ LoggedIn(账号)
     │                     │
     └──设备登录──▶ Anonymous(设备用户) ──绑定/登录──▶ LoggedIn
     ▲                     │
     └────────── logout / 会话失效 ◀───────────────┘
切换账号/链接账号:LoggedIn(uidA) ⇄ LoggedIn(uidB),本地数据按 ownerId 隔离

S4 冲突处理状态机(同步)

无冲突 ──检测──▶ 冲突
  │                ├─ 自动可解(LWW:updatedAt 新者胜 / 本地删优先)──▶ 归档
  │                └─ 需用户决策(本地改 vs 服务端删等)──▶ conflicts 列表 ──resolve(acceptServer)──▶ 归档

S5 组织邀请/审批状态机(服务端)

PENDING(待处理) ──接受/批准──▶ APPROVED(成员生效:绑 MEMBER 角色)

      └──拒绝/取消──▶ REJECTED / CANCELLED

S6 通知状态机

Unread ──已读──▶ Read;离线收到(pull 补齐)默认 Unread,联网实时推送即时更新未读数

S7 服务端离线记录状态机(unsynced_records / sync_mapping)

客户端 push 到达 → 落正式表 + sync_mapping(本地id↔remoteId)
未匹配(重复/脏数据)→ unsynced_records 登记 → 人工/自动修复

4.5 归档机制

归档内容归档方式
本地已同步数据、上传任务、同步锚点syncStatus=Synced、upload_tasks 标记 Done、lastSyncTime 更新;LRU 缓存淘汰;软删除物理清理
服务端业务数据 + 映射正式业务表 + sync_mappingunsynced_records 登记异常;admin_logs 审计
OSS文件对象对象 + 生命周期规则(可选:按保留期清理未引用对象)

5. 离线功能矩阵

✅ 完全离线可用 | 🟡 降级可用 | ❌ 必须联网

功能模块档位说明
本地登录态维持token 本地,断网不登出;首次登录需联网
二维码生成/扫描本地闭环;记录待同步
分类管理本地闭环
水印相机拍照+水印本地合成
相册浏览本地照片/缩略图
相册上传/原图下载🟡上传入队;原图下载需联网
签到🟡动作本地记录;活动实时状态需联网
通行证🟡已下发可展示;核验必须联网
资产🟡缓存可看;写操作待同步
组织🟡缓存可浏览;加入/邀请/变更需联网
通知❌→🟡实时推送需联网;缓存列表可读,恢复联网 pull 补齐
活动🟡缓存可看;报名/更新需联网
登录/注册/验证码强联网
账号切换🟡本地多账号可切;未缓存账号需联网
分享链接/文件下载需联网

判定标准(新功能照此归类)

  • 数据只在本机产生、服务端仅备份 → ✅ 离线可用 + 待同步
  • 需要服务端校验(核验/防伪/权限实时性)→ ❌(或 🟡 先记后验)
  • 需要他人数据(成员/通知/组织状态)→ ❌ 需联网,但缓存上次结果降级 🟡
  • 强实时(推送/实时状态)→ ❌

6. 离线数据处理(写入、冲突、恢复)

6.1 统一状态机(见 4.4 S1)

6.2 冲突策略(见 4.4 S4,LWW 增强)

场景策略
同字段不同值updatedAt 较新者;相等服务端优先
本地删 vs 服务端改本地删优先(显式删除意图)
本地改 vs 服务端删冲突列表,用户决策
新增撞唯一键按 clientId 映射复用服务端 id 后转 UPDATE

6.3 匿名用户数据

  • 匿名产生:ownerId=0 + deviceId 标识,本地正常使用
  • 登录/绑定后:归属迁移(ownerId 0→userId),并入账号同步链

6.4 数据完整性兜底

  • 同步失败不丢数据(Pending 保持 + 退避重试)
  • Room 完整性校验失败:备份损坏文件 → 重建库 → 全量 pull
  • pull 成功写 lastSyncTime,支持断点续拉

7. 联网后同步与上传下载

7.1 触发时机

时机方式
网络恢复ConnectivityManager 监听 → 立即触发
App 进入前台触发一次(节流 ≥5 分钟)
登录成功/切换账号fullSync(先 push 匿名数据再 pull 全量)
周期任务WorkManager 30 分钟(保留)
关键写操作后轻量单类型 push(扫码/签到)
手动同步页「立即同步」

7.2 同步顺序(固定管道)

上传待传文件 → push 本地变更(按 entityType 分组)→ pull 增量(since)→ 冲突处理 → 更新 lastSyncTime

7.3 上传下载(见 4.1.2 / 4.1.3)

  • 上传队列 upload_tasks 表 + 后台消费 + 持久化失败 + 断点续传(OSS 分片)
  • 缩略图 LRU 缓存;原图按需下载;配额本地缓存预检

7.4 同步结果可观测

  • 同步页:上次同步时间、各 entityType 待同步数、失败原因、冲突列表(含解决入口)

8. 绕不过去的功能(必须联网)的处理

功能为什么绕不过处理策略
登录/注册/验证码身份认证必须服务端离线引导登录页 + 提示联网;已登录用户离线不受影响
短信/一键登录运营商通道服务端接入同上
通行证核验防伪/时效校验服务端离线展示本地通行证;核验提示联网
组织加入/邀请服务端撮合 + 权限校验缓存可看,操作提示联网
通知实时推送WS/推送服务离线收不到实时;恢复联网 pull 补齐
分享到组织组织存在性与权限本地生成分享图 ✅;同步到组织需联网
原图下载文件在 OSS缩略图离线可看,原图提示联网
会员/支付(后续)资金订单服务端闭环下单必联网;离线仅展示缓存

通用降级原则:❌ 功能页面必须给「网络不可用」提示 + 重试入口;已缓存的查看类能力不因离线而不可用。


9. 扩展性设计

9.1 同步协议注册表

服务端 SyncService.push / Android SyncManager.pushLocalChanges
  = when(entityType) 分派
新增实体只需:
  1. 两端注册 entityType 常量(统一大写,如 "QR_CODE",消除大小写不一致)
  2. 服务端 handler(userId, item):upsert/delete + sync_mapping 维护
  3. 客户端 DAO getUnsynced + SyncPushItem 构建
  4. pull 增量查询 + 响应解析(两端 DTO 对齐)

9.2 协议版本

  • /api/sync/pull?since=&version=1,服务端按 version 决定响应字段
  • 客户端最低兼容版本校验,不兼容提示升级

9.3 接入 Checklist(新实体)

  • [ ] Room 实体带 syncStatus/updatedAt/remoteId/ownerId
  • [ ] 两端 entityType 常量一致(大写全链路唯一)
  • [ ] 服务端 pull 增量 + push handler + 冲突处理
  • [ ] 客户端 push 构建 + pull 解析 + 待同步数展示
  • [ ] 离线三档归类写入矩阵(§5)

10. 实施路线

阶段内容验收标准
P0 打通现状修复缺陷 1/2(entityType 大小写、pull 契约对齐);二维码/分类/扫码记录全链路通断网产生 → 联网后服务端可见
P1 全实体覆盖assets/checkin/album_photos 服务端 handler;org/notification/pass 接入同步全部带 syncStatus 实体可同步
P2 上传队列upload_tasks + 后台消费 + 断点续传 + 网络恢复触发弱网上传成功率 ≥ 95%
P3 体验增强网络恢复自动同步、前台触发、同步结果页/通知、匿名数据归属迁移用户无感同步,数据不丢
P4 扩展治理协议版本化、冲突 UI 完善、容量/加密治理、矩阵文档维护新实体按 Checklist 半小时接入

11. 风险与决策点

说明待决策
同步契约重构push/pull 契约两端不一致,需协同改直接对齐新契约(推荐,无历史负担)
软删除服务端业务表需支持 deleted 标记统一软删除语义
通知离线补齐实时推送走 WS;离线靠 pull 增量pull 扩展 notifications
容量策略本地缓存上限与清理策略图片/记录保留策略需产品确认
OSS 生命周期未引用对象清理是否启用生命周期规则