TaskBridge 使用「HTTP 同步 + WebSocket 通知」的架构。
- HTTP API 负责真实数据同步。
- WebSocket 只负责通知任务发生变化。
- WebSocket 消息不携带完整任务数据。
- 客户端收到通知后,调用
GET /api/v1/sync/pull拉取增量。 - Android 后台不长期保活 WebSocket,后台同步使用 WorkManager。
- Windows 桌面端可以常驻 WebSocket。
任务核心字段:
iduser_idtitlecontentstatusprioritytagprojectplanned_datedue_timeremind_timerepeat_ruleversionis_deletedcreated_atupdated_atdeleted_at
同步日志记录每次任务变更,主要用于审计、增量同步和排查问题。
客户端任务表额外维护:
local_idserver_idsync_statuslocal_updated_atlast_sync_at
sync_status 可选值:
syncedpending_createpending_updatepending_deleteconflict
待上传变更必须写入持久化同步队列,不能只保存在内存中。
POST /api/v1/sync/push 接收客户端本地变更数组。
支持动作:
createupdatedeletecompleterestore
处理规则:
create创建服务端任务,生成server_id,version从 1 开始。update先校验任务归属和version,成功后递增version。delete使用软删除,不物理删除。complete将任务状态改为completed。restore恢复软删除任务。- 服务端统一使用自己的时间写入
updated_at。 - 每条成功变更都会写入
sync_logs。 - 处理完成后通知同一用户下除当前设备外的在线设备。
同步上传必须支持客户端安全重试。服务端以 user_id + device_id + local_id + action + version 识别一次客户端变更:
- 同一幂等键、同一 payload 重放时,服务端返回第一次成功应用后的任务结果,不重复创建任务,也不重复写成功日志。
- 同一幂等键但 payload 被篡改或队列项被错误复用时,
create会返回failed,消息为idempotency key reused with different payload。 - 同一幂等键的
update已经落后于当前服务端版本时,服务端返回正常conflict和服务端任务快照,客户端按冲突流程处理。 - 客户端不应把
local_id当成“可反复覆盖的草稿 ID”;如果要表达一条新的本地变更,必须使用新的队列项版本或在本地合并后再上传。
这条规则的回归测试在 backend/tests/test_sync.py::test_sync_push_rejects_reused_idempotency_key_with_different_payload,用于确保非法重放不会退化成数据库唯一约束错误。
GET /api/v1/sync/pull?last_sync_time=... 返回服务端在该时间之后变化的任务。
响应字段:
changed_tasks:新增或更新的任务。deleted_tasks:软删除任务。server_time:服务端当前时间,客户端保存为下一次拉取的last_sync_time。has_more:是否还有下一页。next_cursor_updated_at/next_cursor_id:下一页游标。
客户端应分页拉取直到 has_more=false,每页都在本地事务中合并数据,并且只在最后一页合并成功后更新 last_sync_time。
当前版本使用 version 做乐观锁:
- 客户端
version等于服务端version时,可以提交修改。 - 客户端
version小于服务端version时,服务端返回冲突。 - 客户端将本地任务标记为
conflict,对比这台设备版本和同步来的版本,由用户选择保留哪一版。
后续可以扩展字段级合并,例如标题采用最新值、子清单按 item ID 合并。
- App 前台可连接 WebSocket。
- App 后台不要求长期保持 WebSocket。
- WorkManager 在网络恢复后上传本地同步队列。
- App 启动后主动执行一次增量拉取。
- 小组件只读取本地 Room,不直接请求网络。
- 同步完成后刷新任务列表、提醒和小组件。
- 应用启动后建立 WebSocket 长连接。
- 断线后自动重连。
- 本地 SQLite 操作立即更新主窗口和悬浮窗。
- 网络可用时调用
POST /api/v1/sync/push上传同步队列。 - 收到 WebSocket 通知后调用
GET /api/v1/sync/pull。 - 同步完成后通知主窗口和悬浮窗刷新。
- 网络失败: 保留本地同步队列,稍后重试。
- Token 过期: 先刷新 Token,再重试同步。
- 设备未注册: 先调用设备注册接口。
- 版本冲突: 标记为
conflict,等待用户处理。 - 服务端失败: 增加尝试次数,避免无限快速重试。
POST /api/v1/sync/push会先校验device_id是否属于当前用户。- 更新、删除、完成和恢复任务时,服务端只接受当前用户拥有的
server_id。 - 设置
parent_task_id时,父任务必须属于当前用户且未删除。 - WebSocket 通知不携带完整任务内容,客户端必须通过鉴权后的 HTTP 拉取真实数据。