Docker Build and Push / build-image (push) Failing after 3m51s
需求 142 条 (T-*), 设计见 docs/V0.2/任务模块/ 数据层: - 新增 6 张表: Project / ProjectMember / BoardColumn / Task / TaskMember / TaskTag - 看板列是实体(可增删改排序), Task.columnId 外键 RESTRICT 防误删列丢任务 - Task.status(todo/doing/done) 与 columnId 双落库, 二者正交 领域层 server/task-domain/ (唯一口径, 前后端不重复实现): - resolveStatus: 列决定状态(只看目标列, 无拖出回退); 状态与列不匹配时按列序取第一个绑定该状态的列自动归位, 无匹配列则不移动; 子任务完全豁免 - retention: 逐列 6 档保留窗口(永不显示/当天/3天/本周/本月/永久显示), 以 doneAt 判定 - copyTask: 跨项目深拷贝, 人员按目标项目成员过滤并回报丢弃名单 接口: /api/projects, /api/columns, /api/tasks (均校验项目成员) 前端: - 底部 Tab 插入「任务」, 位于「打卡」之后; 默认首页仍为打卡 - 列表视图(子任务缩进+折叠) / 看板视图(长按拖拽换列、拖到卡片降为子任务) - 列设置(列管理+状态绑定+保留选项) / 任务详情(扩展信息卡片默认收起) / 成员管理 - 视图记忆与上次项目存设备端 已验证: 95 文件语法 0 错误、相对导入 0 缺失、领域逻辑用例全部通过
90 lines
3.1 KiB
JavaScript
90 lines
3.1 KiB
JavaScript
// 任务模块: 跨项目拷贝 (T-CP-*)
|
|
// 拷贝而非移动: 新建记录 + 深拷贝子任务, 源任务零改动.
|
|
import { prisma } from '../db.js'
|
|
|
|
/**
|
|
* 拷贝一条任务(含子任务)到目标项目。
|
|
*
|
|
* 规则:
|
|
* - 全字段复制: 标题/描述/状态/列/扩展卡片/标签 (T-CP-02)
|
|
* - 子任务一并复制, 且归属目标项目 (T-CP-03, T-CP-08)
|
|
* - creatorId = 操作者 (T-CP-04)
|
|
* - 负责人 / 任务成员只保留"属于目标项目成员"的部分, 其余丢弃并回报 (T-CP-07, T-EX-08)
|
|
* - 状态与列沿用源值, 不强行纠正 (T-EX-14)
|
|
*
|
|
* @returns {{ task, dropped: { assignee: null|{id,name}, members: {id,name}[] } }}
|
|
*/
|
|
export async function copyTask({ source, targetProject, targetColumns, operatorId, targetMemberIds }) {
|
|
const memberSet = new Set(targetMemberIds)
|
|
|
|
// 列映射: 目标项目里找同名列, 找不到就用第一列 (保证 columnId 合法)
|
|
const pickColumn = (sourceColumnId) => {
|
|
const srcCol = source.__columns?.find((c) => c.id === sourceColumnId)
|
|
if (srcCol) {
|
|
const same = targetColumns.find((c) => c.name === srcCol.name)
|
|
if (same) return same.id
|
|
}
|
|
return targetColumns[0]?.id
|
|
}
|
|
|
|
// 人员过滤
|
|
const keepAssignee = source.assigneeId && memberSet.has(source.assigneeId) ? source.assigneeId : null
|
|
const droppedMembers = []
|
|
const keptMemberIds = []
|
|
for (const m of source.members || []) {
|
|
if (memberSet.has(m.userId)) keptMemberIds.push(m.userId)
|
|
else droppedMembers.push(m.user)
|
|
}
|
|
const droppedAssignee = source.assigneeId && !memberSet.has(source.assigneeId) ? source.assignee : null
|
|
|
|
const baseData = (extra) => ({
|
|
projectId: targetProject.id,
|
|
title: source.title,
|
|
description: source.description || '',
|
|
status: source.status,
|
|
columnId: extra.columnId,
|
|
parentId: extra.parentId ?? null,
|
|
assigneeId: extra.parentId ? (source.assigneeId && memberSet.has(source.assigneeId) ? source.assigneeId : null) : keepAssignee,
|
|
creatorId: operatorId,
|
|
extras: source.extras ?? [],
|
|
doneAt: source.status === 'done' ? (source.doneAt || new Date()) : null,
|
|
sortOrder: source.sortOrder ?? 0,
|
|
tags: {
|
|
create: (source.tags || []).map((t) => ({ name: t.name })),
|
|
},
|
|
})
|
|
|
|
const newTask = await prisma.task.create({
|
|
data: {
|
|
...baseData({ columnId: pickColumn(source.columnId) }),
|
|
members: {
|
|
create: keptMemberIds.map((uid) => ({ userId: uid })),
|
|
},
|
|
},
|
|
})
|
|
|
|
// 子任务: 同法复制, parentId 指向新父任务
|
|
for (const child of source.children || []) {
|
|
const childMembers = (child.members || [])
|
|
.filter((m) => memberSet.has(m.userId))
|
|
.map((m) => m.userId)
|
|
await prisma.task.create({
|
|
data: {
|
|
...baseData({
|
|
columnId: pickColumn(child.columnId),
|
|
parentId: newTask.id,
|
|
}),
|
|
members: { create: childMembers.map((uid) => ({ userId: uid })) },
|
|
},
|
|
})
|
|
}
|
|
|
|
return {
|
|
task: newTask,
|
|
dropped: {
|
|
assignee: droppedAssignee ? { id: droppedAssignee.id, name: droppedAssignee.name } : null,
|
|
members: droppedMembers.map((u) => ({ id: u.id, name: u.name })),
|
|
},
|
|
}
|
|
}
|