fix(任务): 归档以整棵树为单位并移除隐藏中间态
Docker Build and Push / build-image (push) Successful in 1m42s

- 自动归档改为 doneAt + autoArchive 到期,只判定顶层任务,到期整树归档
- 手动归档/恢复提升到根父任务,子任务完全跟随父任务
- 新增 normalizeArchiveTree 修复历史父子归档不一致
- 移除按列保留窗口的显示过滤(前后端实现删除,T-FL-01~15 废止)
- 列 retention 字段仅兼容旧数据保留,不再参与判定
- 已归档查询返回归档子任务,删除提示可正确显示连带条数
This commit is contained in:
jif2.zhang
2026-11-10 09:20:40 +08:00
parent 7b52220aad
commit e05ed6b491
15 changed files with 255 additions and 489 deletions
+21 -11
View File
@@ -6,7 +6,6 @@ import {
loadTaskForUser, loadProjectForUser, loadColumns, loadMemberIds,
} from '../task-domain/access.js'
import { resolveTaskWrite } from '../task-domain/resolveStatus.js'
import { filterVisible } from '../task-domain/retention.js'
import { copyTask } from '../task-domain/copyTask.js'
import { runAutoArchiveForProject } from '../task-domain/archive.js'
import { genUuid, withUuid, withTaskNumber } from '../task-domain/identity.js'
@@ -65,24 +64,31 @@ router.get('/projects/:id/tasks', async (req, res, next) => {
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
})
const shaped = rows.map(shape)
// 保留选项过滤 (T-FL-*): 已完成任务按所属列配置隐藏
const visible = filterVisible(shaped, columns)
res.json({ items: visible, columns, total: shaped.length })
res.json({ items: shaped, columns, total: shaped.length })
} catch (e) { next(e) }
})
// ---------- 归档 (T-ARC) ----------
/**
* 归档/恢复以整棵树为单位: 子任务不单独变化。
* 即使调用方传的是子任务 id, 也提升到根父任务处理。
*/
async function loadTreeIds(task) {
const rootId = task.parentId ? task.parentId : task.id
const children = await prisma.task.findMany({ where: { parentId: rootId }, select: { id: true } })
return [rootId, ...children.map((c) => c.id)]
}
// POST /api/tasks/:id/archive 手动归档: 连带全部子任务, 幂等
router.post('/tasks/:id/archive', async (req, res, next) => {
try {
const userId = req.user.sub
const found = await loadTaskForUser(req.params.id, userId)
if (!found) return res.status(404).json({ error: '任务不存在或无权限' })
const { task } = found
const children = await prisma.task.findMany({ where: { parentId: task.id }, select: { id: true } })
const ids = await loadTreeIds(found.task)
const r = await prisma.task.updateMany({
where: { id: { in: [task.id, ...children.map((c) => c.id)] } },
where: { id: { in: ids } },
data: { archived: true, archivedAt: new Date(), archiveSource: 'manual' },
})
res.json({ ok: true, count: r.count })
@@ -95,10 +101,9 @@ router.post('/tasks/:id/unarchive', async (req, res, next) => {
const userId = req.user.sub
const found = await loadTaskForUser(req.params.id, userId)
if (!found) return res.status(404).json({ error: '任务不存在或无权限' })
const { task } = found
const children = await prisma.task.findMany({ where: { parentId: task.id }, select: { id: true } })
const ids = await loadTreeIds(found.task)
const r = await prisma.task.updateMany({
where: { id: { in: [task.id, ...children.map((c) => c.id)] } },
where: { id: { in: ids } },
data: { archived: false, archivedAt: null, archiveSource: null },
})
res.json({ ok: true, count: r.count })
@@ -135,7 +140,12 @@ router.get('/projects/:id/tasks/archived', async (req, res, next) => {
const rows = await prisma.task.findMany({
where,
include: TASK_INCLUDE,
// 已归档任务整树归档: 子任务也是 archived=true, 这里要一并取出,
// 否则「彻底删除」弹窗无法提示连带条数
include: {
...TASK_INCLUDE,
children: { ...TASK_INCLUDE.children, where: { archived: true } },
},
orderBy: [{ archivedAt: 'desc' }, { id: 'desc' }],
take: limit + 1,
})