Docker Build and Push / build-image (push) Successful in 1m42s
- 自动归档改为 doneAt + autoArchive 到期,只判定顶层任务,到期整树归档 - 手动归档/恢复提升到根父任务,子任务完全跟随父任务 - 新增 normalizeArchiveTree 修复历史父子归档不一致 - 移除按列保留窗口的显示过滤(前后端实现删除,T-FL-01~15 废止) - 列 retention 字段仅兼容旧数据保留,不再参与判定 - 已归档查询返回归档子任务,删除提示可正确显示连带条数
118 lines
4.3 KiB
JavaScript
118 lines
4.3 KiB
JavaScript
// 任务归档领域逻辑 (T-ARC)
|
|
// 自动归档: 顶层任务完成后, 经过项目 autoArchive 档位即到期; 到期时把该任务
|
|
// 及其全部子任务作为一个整体归档。子任务不单独判定, 完全跟随父任务。
|
|
// 执行双保险: 项目任务加载时惰性执行 (runAutoArchiveForProject) + 每小时 cron 兜底。
|
|
import { prisma } from '../db.js'
|
|
|
|
export const AUTO_ARCHIVE_OPTIONS = ['3d', '1w', '1m', 'never']
|
|
const DAY_MS = 24 * 3600 * 1000
|
|
const SHANGHAI_OFFSET_MS = 8 * 3600 * 1000
|
|
|
|
/** 按上海日历月加 n 个月 (保留时刻), 用于 1m 档 */
|
|
function addMonthsShanghai(date, n) {
|
|
const shifted = new Date(date.getTime() + SHANGHAI_OFFSET_MS)
|
|
shifted.setUTCMonth(shifted.getUTCMonth() + n)
|
|
return new Date(shifted.getTime() - SHANGHAI_OFFSET_MS)
|
|
}
|
|
|
|
/** 自动归档到期时刻: 完成时刻 + 档位; 'never' 或未完成 -> null */
|
|
export function autoArchiveDue(autoArchive, doneAt) {
|
|
if (!autoArchive || autoArchive === 'never' || !doneAt) return null
|
|
const at = doneAt instanceof Date ? doneAt : new Date(doneAt)
|
|
if (Number.isNaN(at.getTime())) return null
|
|
switch (autoArchive) {
|
|
case '3d': return new Date(at.getTime() + 3 * DAY_MS)
|
|
case '1w': return new Date(at.getTime() + 7 * DAY_MS)
|
|
case '1m': return addMonthsShanghai(at, 1)
|
|
default: return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 修复历史数据里父子归档状态不一致: 子任务完全跟随父任务。
|
|
* 只支持一级子任务; 返回被修正的条数。
|
|
*/
|
|
export async function normalizeArchiveTree(projectId) {
|
|
const tasks = await prisma.task.findMany({
|
|
where: { projectId },
|
|
select: { id: true, parentId: true, archived: true, archivedAt: true, archiveSource: true },
|
|
})
|
|
const byId = new Map(tasks.map((t) => [t.id, t]))
|
|
const fixes = []
|
|
for (const t of tasks) {
|
|
if (!t.parentId) continue
|
|
const parent = byId.get(t.parentId)
|
|
if (!parent || parent.archived === t.archived) continue
|
|
fixes.push({
|
|
id: t.id,
|
|
archived: parent.archived,
|
|
archivedAt: parent.archived ? (parent.archivedAt || new Date()) : null,
|
|
archiveSource: parent.archived ? (parent.archiveSource || 'manual') : null,
|
|
})
|
|
}
|
|
if (!fixes.length) return 0
|
|
await prisma.$transaction(fixes.map((f) => prisma.task.update({
|
|
where: { id: f.id },
|
|
data: {
|
|
archived: f.archived,
|
|
archivedAt: f.archivedAt,
|
|
archiveSource: f.archiveSource,
|
|
},
|
|
})))
|
|
return fixes.length
|
|
}
|
|
|
|
/**
|
|
* 对单个项目执行一轮到期自动归档 (T-ARC-02)。
|
|
* 只看顶层任务: 完成 + 档位到期 -> 该任务与全部子任务一起归档。幂等。
|
|
* @returns {number} 本次归档条数
|
|
*/
|
|
export async function runAutoArchiveForProject(projectId) {
|
|
// 先修复历史父子不一致, 保证要么都显示、要么都归档
|
|
await normalizeArchiveTree(projectId)
|
|
|
|
const project = await prisma.project.findUnique({
|
|
where: { id: projectId },
|
|
select: { autoArchive: true },
|
|
})
|
|
if (!project || !AUTO_ARCHIVE_OPTIONS.includes(project.autoArchive) || project.autoArchive === 'never') return 0
|
|
|
|
const roots = await prisma.task.findMany({
|
|
where: { projectId, parentId: null, archived: false, status: 'done', doneAt: { not: null } },
|
|
select: { id: true, doneAt: true },
|
|
})
|
|
const now = new Date()
|
|
const dueRootIds = []
|
|
for (const t of roots) {
|
|
const due = autoArchiveDue(project.autoArchive, t.doneAt)
|
|
if (due && due <= now) dueRootIds.push(t.id)
|
|
}
|
|
if (!dueRootIds.length) return 0
|
|
|
|
// 整棵树一起归档: 子任务不单独判定, 避免父任务归档而子任务仍留在看板
|
|
const children = await prisma.task.findMany({
|
|
where: { parentId: { in: dueRootIds } },
|
|
select: { id: true },
|
|
})
|
|
const ids = [...dueRootIds, ...children.map((c) => c.id)]
|
|
const r = await prisma.task.updateMany({
|
|
where: { id: { in: ids } },
|
|
data: { archived: true, archivedAt: now, archiveSource: 'auto' },
|
|
})
|
|
return r.count
|
|
}
|
|
|
|
/** 每小时 cron 兜底: 修复所有项目的父子归档一致性, 并执行到期自动归档 */
|
|
export async function runAutoArchiveSweep() {
|
|
const projects = await prisma.project.findMany({ select: { id: true } })
|
|
let total = 0
|
|
for (const p of projects) {
|
|
try {
|
|
total += await runAutoArchiveForProject(p.id)
|
|
} catch (e) {
|
|
console.error('[archive] 项目自动归档失败:', p.id, e.message)
|
|
}
|
|
}
|
|
return total
|
|
}
|