laoyang
9a67fce296
fix(同步): 修复删除 404、远端覆盖本地修改、404 残留无法清理 #6
采纳社区反馈(小褚的 fork)中的 3 处高危修复。均为纯逻辑修复,
不改 UI 架构、不动状态管理范式。
## 1. remotePath 用服务器真实文件名(删除/推送全链路)
### 缺陷
上传/删除 URL 里的资源名来自 `remotePath`,而它一直被硬编码成
`encodeURIComponent(uid) + '.ics'` —— **猜的**。服务器不保证文件名等于 UID:
家书 CalDav.php 用 `preg_replace('/[^A-Za-z0-9._-]/', '_', $uid)` 生成文件名,
UID `movie-188@familybook` 的真实资源叫 `movie-188_familybook.ics`。
猜错 ⇒ DELETE 打到不存在的路径 ⇒ 404 ⇒ 服务器上删不掉(用户看到"删了又回来");
PUT 同理会写到另一个文件名下,造成同一事件两个资源。
### 修法(三条链路都要覆盖)
1. `RemoteEvent` 新增 `remotePath` 字段
2. **GET 补拉**:`r.remotePath = fileName`(从 REPORT 的 href 末段取)
3. **etag 未变复用**(常态路径,之前漏了):`rowToRemote` 增加第三个参数,
传当前 href 的真实文件名。不补这里,前一条等于白做 ——
因为大部分资源 etag 不变、根本不走 GET。
4. `applyRemote` 落库:优先用 `r.remotePath`,为空才退回按 uid 猜
(本地新建等服务器上还没有资源的场景)。
5. 更新分支同步 `found.remotePath`:服务器上资源改名后要跟着改,
否则后续 DELETE/PUT 仍打旧路径。
## 2. applyRemote 加 dirty/deleted 白名单(防复活)
### 缺陷
原来只有 `if (e.dirty) continue`(dirty 行不入 keyed 索引)。
**只排除出索引不够**:远端数据 `keyed.get(key)` 拿不到 → 走"新增"分支
→ 又插一行。结果同一事件在本地有两行(用户改的 + 远端旧版),
用户看到"改完又变回去"或"出现两条"。
### 修法(补两层)
- `dirtyKeyed`:dirty 行单独索引,远端同 key 出现时直接 `continue`,
保留本地版本,不新增不覆盖。
- `dirtyDeletedUids`:本地已标记删除的 uid 集合,远端该 uid 的任何版本
一律忽略。**按 uid 而非 key** —— 删重复系列时本地只留主事件标 deleted,
但远端还有一串 RECURRENCE-ID 实例行,key 各不相同,只有 uid 能整族挡住。
(`markDeleted` 同时置 `deleted=1` 和 `dirty=1`,故 `dirty && deleted` 可同时覆盖。)
## 3. DELETE 404/410 物理清库
### 缺陷(真正的坑在 pushSeries)
`DavClient.deleteRemote` 已把 404 视作成功,但**系列删除路径**仍踩坑:
```ts
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,不清本地**。
用户看到日程还在、怎么删都删不掉、每次同步都重复失败。
### 修法
两条路径(单条 + 系列)都区分状态码:
- 401 → 抛出(凭据失效,后续每条都会失败)
- **404 / 410 → 服务器上本就没有,视为删除成功,继续清理本地**
- 其它 → 真失败,保留待推送
## 验证状态
- 编译:`BUILD SUCCESSFUL`
- 静态交叉核查:
- `rowToRemote` 唯一调用处已补第三参数
- `getDirty` 只查 `dirty=1`,deleted 行(双置)仍会被推送,无漏推
- `purge` / `purgeByUid` 均为 `store.delete`,确认是物理删除
- 剩余 3 处 `encodeURIComponent(uid)+'.ics'`(MirrorInbound 247、
SystemCalendarImport 99、EventEditPage 1088)属**本地新建**场景,
此时服务器上尚无资源,用 uid 作初始文件名是正确行为;
PUT 一旦成功,该 URL 即成为资源真名,故无需再改。
- ⚠️ **真机未验证**:设备当前未连接(hdc list targets 返回空),
需连上后按下列步骤验证。
## 待真机验证项
1. 新建日程 → 同步 → 在应用内删除 → 服务器端确认已消失
(重点验服务器实际文件名 ≠ uid 的账号,如家书)
2. 修改日程标题 → 不手动同步 → 等自动同步 → 标题应保持修改后的值
3. 删除重复日程整个系列 → 服务器端确认整族消失,本地无残留
4. 若服务器上资源已被外部删除,应用内删除该日程应能正常清掉本地行
2026-11-03 07:36:06 +08:00
..
2026-11-03 07:36:06 +08:00
2026-09-13 15:50:37 +08:00
2026-09-13 15:50:37 +08:00
2026-09-13 15:50:37 +08:00
2026-09-13 15:50:37 +08:00