From 4ce701e99aa5b045c4da924a0d4827e25b45792f Mon Sep 17 00:00:00 2001 From: "jif2.zhang" Date: Mon, 26 Oct 2026 01:37:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(0.2.1):=20M5=20=E9=97=AD=E7=8E=AF=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E9=80=9A=E8=BF=87=20=E2=80=94=E2=80=94=20verify=20?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=207=20=E6=AD=A5=E5=85=A8=E7=BB=BF=20(?= =?UTF-8?q?=E4=B8=8B=E8=A1=8C/=E7=A6=BB=E7=BA=BF=E5=86=99/=E4=B8=8A?= =?UTF-8?q?=E8=A1=8C/=E5=AE=9A=E5=8F=B7/=E5=9B=9E=E5=A1=AB/REST=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4/=E5=88=A0=E9=99=A4=E5=90=8C=E6=AD=A5);=20=E6=96=B9?= =?UTF-8?q?=E6=A1=88=E6=96=87=E6=A1=A3=E8=A1=A5=20Sequential=20ID=20Mappin?= =?UTF-8?q?g?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/V0.2/离线编辑模块/0.2.1实施方案.md | 9 +++++++++ scripts/verify-powersync-client.mjs | 23 +++++++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/V0.2/离线编辑模块/0.2.1实施方案.md b/docs/V0.2/离线编辑模块/0.2.1实施方案.md index 346c525..7229d34 100644 --- a/docs/V0.2/离线编辑模块/0.2.1实施方案.md +++ b/docs/V0.2/离线编辑模块/0.2.1实施方案.md @@ -51,6 +51,15 @@ - 主键仍为自增 Int,业务外键不动;客户端上行一律以 uuid 引用父子行,服务端解析为内部 id; - Prisma datasource: `mysql` → `postgresql`;本分支重建基线迁移(旧 MySQL 迁移文件移除,历史保留在 git)。 +### 3.1 Sequential ID Mapping(落地时按官方模式补充) + +按 PowerSync 官方 [Sequential ID Mapping](https://docs.powersync.com/client-sdks/advanced/sequential-id-mapping) 模式实施,解决"离线新建行同步回填后本地 id 变化"问题: + +- 任务域 6 表再加**外键 uuid 镜像列**(`Task.projectUuid/columnUuid/parentUuid`、`BoardColumn.projectUuid`、`ProjectMember.projectUuid`、`TaskMember/TaskTag.taskUuid`),由 **PG 触发器**在写入时按 int 外键自动回填(迁移 `20261026010000_uuid_fk_mapping`),库级 NOT NULL; +- Sync Streams 以 **`uuid` 作为客户端主键**(`SELECT "uuid" AS "id"`),镜像列别名输出回 `projectId/columnId/parentId/taskId` —— 客户端 schema 与页面代码**零改动**; +- 服务端 upload 处理器按 uuid 解析父行(`findUnique({ where: { uuid } })`),解析后再写 int 外键 → 触发器回填镜像,两侧闭环一致; +- 镜像列在 Prisma 模型中声明为可空(触发器保证库级非空),避免 create 校验拦截。 + ## 4. 同步与冲突 - Sync Rules 仅下发任务域 6 表 + `User`(展示名称/头像用),按 `ProjectMember.userId = auth.parameters.userId` 过滤项目可见性;时序/监控/米家等表不纳入; diff --git a/scripts/verify-powersync-client.mjs b/scripts/verify-powersync-client.mjs index 836ab8f..f86c2fb 100644 --- a/scripts/verify-powersync-client.mjs +++ b/scripts/verify-powersync-client.mjs @@ -147,7 +147,7 @@ async function main() { }) console.log('[verify] 3. 首次同步完成') - const projects = await db.getAll('SELECT id, code, name FROM Project ORDER BY id') + const projects = await db.getAll('SELECT id, uuid, code, name FROM Project ORDER BY id') console.log(`[verify] 项目 ${projects.length} 个:`, projects.map((p) => `${p.code || '?'}(${p.name})`).join(', ')) const tasks = await db.getAll('SELECT COUNT(*) AS n FROM Task') const numbered = await db.getAll('SELECT COUNT(*) AS n FROM Task WHERE number IS NOT NULL') @@ -183,9 +183,14 @@ async function main() { if (!syncedBack) throw new Error('超时: 未观测到服务端编号回填') console.log(`[verify] 5. 闭环 OK! 服务端分配编号: ${target.code}-${syncedBack.number} (项目 ${target.code})`) - // 直接从服务端核对 (REST 视角) - const serverCheck = await api(`/tasks/${newId}`, { token: login.token }) - console.log(`[verify] 6. 服务端确认: 任务存在, number=${serverCheck.item.number}, uuid=${serverCheck.item.uuid.slice(0, 13)}...`) + // 直接从服务端核对 (REST 视角: REST 仍按 int id 寻址, 通过列表接口按 uuid 匹配) + const plist = await api('/projects', { token: login.token }) + const pj = plist.items.find((p) => p.uuid === target.uuid) + if (!pj) throw new Error('服务端确认失败: 项目列表中找不到目标项目') + const tlist = await api(`/projects/${pj.id}/tasks`, { token: login.token }) + const hit = tlist.items.find((t) => t.uuid === newId) + if (!hit || hit.number == null) throw new Error('服务端确认失败: 任务不存在或未定号') + console.log(`[verify] 6. 服务端确认: number=${hit.number}, uuid=${hit.uuid.slice(0, 13)}...`) // 清理验证数据 (本地删除 -> 上行删除) await db.execute('DELETE FROM Task WHERE id = ?', [newId]) @@ -194,13 +199,15 @@ async function main() { const left = await db.getOptional('SELECT 1 AS x FROM ps_crud LIMIT 1') if (!left) break } - const gone = await api(`/tasks/${newId}`, { token: login.token }).catch(() => null) - if (!gone) console.log('[verify] 7. 验证任务已从服务端清除') + const gone = await api(`/projects/${pj.id}/tasks`, { token: login.token }) + if (!gone.items.some((t) => t.uuid === newId)) console.log('[verify] 7. 验证任务已从服务端清除') else throw new Error('验证任务删除未同步到服务端') db.close() - fs.rmSync(dbFile, { force: true }) - fs.rmSync(dbFile + '-journal', { force: true }) + try { + fs.rmSync(dbFile, { force: true }) + fs.rmSync(dbFile + '-journal', { force: true }) + } catch { /* Windows 下可能被占用, 留在临时目录无害 */ } console.log('[verify] 全部通过: 下行同步 / 离线写入 / 上行幂等 / 服务端编号 / 回填 / 删除同步') }