采纳社区反馈(小褚的 fork)中的 3 处高危修复。均为纯逻辑修复,不改 UI 架构。
对应提交:9a67fce(2 文件,+99 / -11)
9a67fce
上传/删除 URL 里的资源名来自 remotePath,而它一直被硬编码成 encodeURIComponent(uid) + '.ics' —— 猜的。
remotePath
encodeURIComponent(uid) + '.ics'
服务器不保证「文件名 = ICS 里的 UID」:
preg_replace('/[^A-Za-z0-9._-]/', '_', $uid)
movie-188@familybook
movie-188_familybook.ics
猜错 ⇒ DELETE 打到不存在的路径 ⇒ 404 ⇒ 服务器上删不掉(用户看到"删了又回来"); PUT 同理会写到另一个文件名下,造成同一事件两个资源。
RemoteEvent
r.remotePath = fileName
rowToRemote
applyRemote
found.remotePath
第 3、4 条是关键:REPORT 只返回 etag,未变就复用本地行、不 GET。 实测 99% 的行走这条 ⇒ 只改 GET 分支对存量数据等于无效。
原来只有 if (e.dirty) continue(dirty 行不入 keyed 索引)。 只排除出索引不够:远端数据 keyed.get(key) 查不到 → 走"新增"分支 → 又插一行。结果同一事件本地有两行(用户改的 + 远端旧版)。
if (e.dirty) continue
keyed.get(key)
dirtyKeyed
dirtyDeletedUids
DavClient.deleteRemote 已把 404 视作成功,但系列删除路径仍踩坑:
DavClient.deleteRemote
if (master.deleted) { try { await DavClient.deleteRemote(url, auth); } catch (err) { ...401 抛出... LogUtil.write(`推送删除重复系列失败:...`); return; // ⭐ 404/410 也走到这里 → 直接 return } await EventDb.purgeByUid(context, calKey, uid); // ← 永远执行不到 }
URL 拼错(正是修复 1 的 bug 造成)或服务器上资源本就不在 ⇒ DELETE 返回 404 ⇒ 进 catch ⇒ return,不清本地 ⇒ 用户看到"删不掉的日程",每次同步重复失败。
单条 + 系列两条路径都区分状态码:
BUILD SUCCESSFUL(hvigor,hAP 调试包)
BUILD SUCCESSFUL
设备:5NC0225B27004718(HarmonyOS) 服务器:DAViCal 1.1.9 @ nas.yangyq.net/caldav.php/synccalendartest
5NC0225B27004718
nas.yangyq.net/caldav.php/synccalendartest
DAViCal 的命名规则是「把 uid 做 URL 编码后直接当文件名」:
uid : 20261102T153543-10c5d3cb@169.254.186.221 文件名 : 20261102T153543-10c5d3cb%40169.254.186.221.ics
猜的名字 == 真名,同步 746 条日程后 remotePath 全部等于 uid 猜测值, 无论修复是否生效都看不出差异。
所以必须造一个「文件名 ≠ UID」的资源才能验出来。
直接在服务器上 PUT 一个刻意不匹配的资源:
文件名 : trap-file.ics UID : trap-uid-with-special-chars@example.com
服务器上两个路径的实际状态(这是核心证据):
tfxnz/trap-file.ics
tfxnz/trap-uid-with-special-chars%40example.com.ics
⇒ 旧代码删除时必然打到 404。
应用同步后,本地库该行为:
uid
trap-uid-with-special-chars@example.com
trap-file.ics
trap-uid-with-special-chars%40example.com.ics
在应用内对该日程执行「编辑 → 删除日程 → 确认删除」,实测日志:
DELETE https://nas.yangyq.net/caldav.php/synccalendartest/tfxnz/trap-file.ics 响应码: 204 SyncLog HTTP DELETE https://nas.yangyq.net/caldav.php/synccalendartest/tfxnz/trap-file.ics → 204 SyncLog 推送删除(标题已脱敏)uid=trap-uid-with-special-chars@example.com → https://nas.yangyq.net/caldav.php/synccalendartest/tfxnz/trap-file.ics SyncLog 本地删除日程(标题已脱敏)uid=trap-uid-with-special-chars@example.com 范围=all
用的是 trap-file.ics,HTTP 204 成功。
trap-file.ics → 404(已删除) REPORT tfxnz 资源数 → 0
本地库 trap 行数 → 0
服务器删了、本地也清了,无残影。
同步日志:
日历本[5]「家庭计划」日程落库完成:新增0 更新0 删除0 不变1
同 uid 在库里始终 1 行 — 没有因为「查不到就新增」而插出重复行。
第一次拉数据库读到 735 行、remotePath 全是 uid 猜测值,差点误判修复失效。
真实原因:WAL 未合并。
sync_calendar.db 344 KB sync_calendar.db-wal 428 KB ← 最新数据全在这里
只拉主库会看到过期快照。必须连 -wal、-shm 一起拉再查询。
-wal
-shm
3 条修复全部验证通过。陷阱数据已彻底清理(服务器 404、本地 0 行),测试日历无残留。
No dependencies set.
The note is not visible to the blocked user.
背景
采纳社区反馈(小褚的 fork)中的 3 处高危修复。均为纯逻辑修复,不改 UI 架构。
对应提交:
9a67fce(2 文件,+99 / -11)修复 1:remotePath 用服务器真实文件名
缺陷
上传/删除 URL 里的资源名来自
remotePath,而它一直被硬编码成encodeURIComponent(uid) + '.ics'—— 猜的。服务器不保证「文件名 = ICS 里的 UID」:
preg_replace('/[^A-Za-z0-9._-]/', '_', $uid)生成文件名movie-188@familybook的真实资源是movie-188_familybook.ics猜错 ⇒ DELETE 打到不存在的路径 ⇒ 404 ⇒ 服务器上删不掉(用户看到"删了又回来");
PUT 同理会写到另一个文件名下,造成同一事件两个资源。
修法(三条链路都覆盖)
RemoteEvent新增remotePath字段r.remotePath = fileName(从 REPORT 的 href 末段取)rowToRemote增加第三参数applyRemote落库优先用真名;更新分支同步found.remotePath修复 2:applyRemote 加 dirty/deleted 白名单(防复活)
缺陷
原来只有
if (e.dirty) continue(dirty 行不入 keyed 索引)。只排除出索引不够:远端数据
keyed.get(key)查不到 → 走"新增"分支→ 又插一行。结果同一事件本地有两行(用户改的 + 远端旧版)。
修法
dirtyKeyed:dirty 行单独索引,远端同 key 出现时直接跳过dirtyDeletedUids:本地已删待推送的 uid 集合,远端该 uid 任何版本一律忽略—— 按 uid 而非 key:删重复系列时本地只留主事件标 deleted,
但远端还有一串 RECURRENCE-ID 实例行,key 各不相同,只有 uid 能整族挡住
修复 3:DELETE 404/410 物理清库
缺陷(真正的坑在 pushSeries)
DavClient.deleteRemote已把 404 视作成功,但系列删除路径仍踩坑:URL 拼错(正是修复 1 的 bug 造成)或服务器上资源本就不在 ⇒ DELETE 返回 404
⇒ 进 catch ⇒ return,不清本地 ⇒ 用户看到"删不掉的日程",每次同步重复失败。
修法
单条 + 系列两条路径都区分状态码:
编译状态
BUILD SUCCESSFUL(hvigor,hAP 调试包)✅ 真机验证通过
设备:
5NC0225B27004718(HarmonyOS)服务器:DAViCal 1.1.9 @
nas.yangyq.net/caldav.php/synccalendartest关键发现:这台服务器天然测不出该 bug
DAViCal 的命名规则是「把 uid 做 URL 编码后直接当文件名」:
猜的名字 == 真名,同步 746 条日程后
remotePath全部等于 uid 猜测值,无论修复是否生效都看不出差异。
所以必须造一个「文件名 ≠ UID」的资源才能验出来。
造陷阱资源
直接在服务器上 PUT 一个刻意不匹配的资源:
服务器上两个路径的实际状态(这是核心证据):
tfxnz/trap-file.ics(真实文件名)tfxnz/trap-uid-with-special-chars%40example.com.ics(旧代码会拼的地址)⇒ 旧代码删除时必然打到 404。
验证 1:remotePath 存的是真名 ✅
应用同步后,本地库该行为:
uidtrap-uid-with-special-chars@example.comremotePath(实际存储)trap-file.ics✅trap-uid-with-special-chars%40example.com.ics验证 2:删除打到正确地址并成功 ✅
在应用内对该日程执行「编辑 → 删除日程 → 确认删除」,实测日志:
用的是
trap-file.ics,HTTP 204 成功。验证 3:服务器端确实删掉了 ✅
验证 4:本地物理清库 ✅
服务器删了、本地也清了,无残影。
验证 5:防复活白名单未误伤 ✅
同步日志:
同 uid 在库里始终 1 行 — 没有因为「查不到就新增」而插出重复行。
完整同步结果
⚠️ 踩坑记录:拉库必须连 WAL
第一次拉数据库读到 735 行、
remotePath全是 uid 猜测值,差点误判修复失效。真实原因:WAL 未合并。
只拉主库会看到过期快照。必须连
-wal、-shm一起拉再查询。结论
3 条修复全部验证通过。陷阱数据已彻底清理(服务器 404、本地 0 行),测试日历无残留。