fix(0.2.1): upload 契约对齐客户端 —— 外键列即父行 uuid (projectId/columnId/parentId/taskId); 附 Node 闭环验证脚本

This commit is contained in:
jif2.zhang
2026-10-26 01:14:34 +08:00
parent a714013e3e
commit 9caaa303e5
4 changed files with 292 additions and 15 deletions
+16 -15
View File
@@ -31,7 +31,8 @@ router.post('/token', authRequired, async (req, res, next) => {
// ---------- 上传队列 ----------
// 契约: POST { ops: [{ id, table, op: 'PUT'|'DELETE', uuid, data }] }
// table ∈ Project | BoardColumn | ProjectMember | Task | TaskMember | TaskTag
// data 为客户端本地全量行; 父引用走 *Uuid 字段 (projectUuid / columnUuid / parentUuid / taskUuid)
// data 为客户端本地全量行 (列名与客户端 schema 一致); 外键列 (projectId/columnId/parentId/taskId)
// 的值即父行的本地 id = uuid, 服务端据此解析为内部自增 id
// 返回 { results: [{ id, ok, skipped?, error? }] }, 单条失败不影响同批其它 op
const TABLES = ['Project', 'BoardColumn', 'ProjectMember', 'Task', 'TaskMember', 'TaskTag']
const TABLE_RANK = Object.fromEntries(TABLES.map((t, i) => [t, i]))
@@ -120,7 +121,7 @@ async function applyPut(op, ctx) {
if (Object.keys(data).length) await prisma.boardColumn.update({ where: { uuid }, data })
return {}
}
const project = await prisma.project.findUnique({ where: { uuid: d.projectUuid } })
const project = await prisma.project.findUnique({ where: { uuid: d.projectId } })
if (!project || !(await assertMember(project.id))) throw new Error('项目不存在或无权限')
const name = String(d.name || '').trim()
if (!name || name.length > 20) throw new Error('列名非法')
@@ -143,7 +144,7 @@ async function applyPut(op, ctx) {
// 成员关系只增不改: 已存在即视为完成 (幂等)
return { skipped: true }
}
const project = await prisma.project.findUnique({ where: { uuid: d.projectUuid } })
const project = await prisma.project.findUnique({ where: { uuid: d.projectId } })
if (!project || !(await assertMember(project.id))) throw new Error('项目不存在或无权限')
const target = Number(d.userId)
if (!target) throw new Error('userId 非法')
@@ -179,16 +180,16 @@ async function applyPut(op, ctx) {
if (d.sortOrder !== undefined) data.sortOrder = Number(d.sortOrder) || 0
if (d.assigneeId !== undefined) data.assigneeId = d.assigneeId ? Number(d.assigneeId) : null
if (d.doneAt !== undefined) data.doneAt = isoTime(d.doneAt)
if (d.columnUuid !== undefined) {
const col = await prisma.boardColumn.findUnique({ where: { uuid: d.columnUuid } })
if (d.columnId !== undefined) {
const col = await prisma.boardColumn.findUnique({ where: { uuid: d.columnId } })
if (!col || col.projectId !== existing.projectId) throw new Error('看板列不存在')
data.columnId = col.id
}
if (d.parentUuid !== undefined) {
if (!d.parentUuid) {
if (d.parentId !== undefined) {
if (!d.parentId) {
data.parentId = null
} else {
const parent = await prisma.task.findUnique({ where: { uuid: d.parentUuid } })
const parent = await prisma.task.findUnique({ where: { uuid: d.parentId } })
if (!parent || parent.projectId !== existing.projectId) throw new Error('父任务不存在')
if (parent.parentId) throw new Error('仅支持一级子任务')
data.parentId = parent.id
@@ -198,13 +199,13 @@ async function applyPut(op, ctx) {
return {}
}
// 离线新建: 服务端解析父引用并分配项目内编号
const project = await prisma.project.findUnique({ where: { uuid: d.projectUuid } })
const project = await prisma.project.findUnique({ where: { uuid: d.projectId } })
if (!project || !(await assertMember(project.id))) throw new Error('项目不存在或无权限')
const title = String(d.title || '').trim()
if (!title || title.length > 30) throw new Error('任务标题非法')
let columnId = null
if (d.columnUuid) {
const col = await prisma.boardColumn.findUnique({ where: { uuid: d.columnUuid } })
if (d.columnId) {
const col = await prisma.boardColumn.findUnique({ where: { uuid: d.columnId } })
if (!col || col.projectId !== project.id) throw new Error('看板列不存在')
columnId = col.id
} else {
@@ -216,8 +217,8 @@ async function applyPut(op, ctx) {
columnId = first.id
}
let parentId = null
if (d.parentUuid) {
const parent = await prisma.task.findUnique({ where: { uuid: d.parentUuid } })
if (d.parentId) {
const parent = await prisma.task.findUnique({ where: { uuid: d.parentId } })
if (!parent || parent.projectId !== project.id) throw new Error('父任务不存在')
if (parent.parentId) throw new Error('仅支持一级子任务')
parentId = parent.id
@@ -248,7 +249,7 @@ async function applyPut(op, ctx) {
if (table === 'TaskMember') {
const existing = await prisma.taskMember.findUnique({ where: { uuid } })
if (existing) return { skipped: true }
const task = await prisma.task.findUnique({ where: { uuid: d.taskUuid } })
const task = await prisma.task.findUnique({ where: { uuid: d.taskId } })
if (!task || !(await assertMember(task.projectId))) throw new Error('任务不存在或无权限')
const uid = Number(d.userId)
if (!uid) throw new Error('userId 非法')
@@ -263,7 +264,7 @@ async function applyPut(op, ctx) {
if (table === 'TaskTag') {
const existing = await prisma.taskTag.findUnique({ where: { uuid } })
if (existing) return { skipped: true }
const task = await prisma.task.findUnique({ where: { uuid: d.taskUuid } })
const task = await prisma.task.findUnique({ where: { uuid: d.taskId } })
if (!task || !(await assertMember(task.projectId))) throw new Error('任务不存在或无权限')
const name = String(d.name || '').trim().slice(0, 20)
if (!name) throw new Error('标签名非法')