feat(0.2.1): M5 闭环验证通过 —— verify 脚本 7 步全绿 (下行/离线写/上行/定号/回填/REST确认/删除同步); 方案文档补 Sequential ID Mapping
This commit is contained in:
@@ -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` 过滤项目可见性;时序/监控/米家等表不纳入;
|
||||
|
||||
@@ -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()
|
||||
try {
|
||||
fs.rmSync(dbFile, { force: true })
|
||||
fs.rmSync(dbFile + '-journal', { force: true })
|
||||
} catch { /* Windows 下可能被占用, 留在临时目录无害 */ }
|
||||
console.log('[verify] 全部通过: 下行同步 / 离线写入 / 上行幂等 / 服务端编号 / 回填 / 删除同步')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user