Docker Build and Push / build-image (push) Successful in 1m27s
- 数据: Project.autoArchive + Task.archived/archivedAt/archiveSource, 迁移 20261026000000_task_archive (两索引) - 领域: server/task-domain/archive.js —— hiddenAtOf 与保留口径精确同源, autoArchiveDue 到期判定, runAutoArchiveForProject (读时惰性) + runAutoArchiveSweep (每小时 cron 兜底) - 接口: POST /tasks/:id/archive|unarchive (连带子任务, 幂等); GET /projects/:id/tasks/archived (keyword/kind/columnId/from/to 过滤, archivedAt+id 游标分页); 项目 PUT 收 autoArchive (枚举校验); 主列表/子任务/项目 taskCount 全部排除归档 - 前端: ⋯ 菜单「归档」(确认含子任务条数); 项目栏「已归档」入口 → ArchivedTasks 弹层 (关键词/类型/列/归档时段过滤 + 加载更多 + 恢复/删除); 项目管理「归档设置」四档 Picker
113 lines
4.2 KiB
JavaScript
113 lines
4.2 KiB
JavaScript
// 任务归档领域逻辑 (T-ARC)
|
|
// 自动归档判定: 倒计时起点 = 任务"被隐藏的时刻"(与 date.js isVisibleByRetention
|
|
// 精确同口径推导): 完成日 + 保留窗口后的上海零点; 加上项目 autoArchive 档位即到期时刻。
|
|
// 执行双保险: 项目任务加载时惰性执行 (runAutoArchiveForProject) + 每小时 cron 兜底。
|
|
import { prisma } from '../db.js'
|
|
import { toDateStr, addDays, startOfWeek, startOfMonth } from './date.js'
|
|
|
|
export const AUTO_ARCHIVE_OPTIONS = ['3d', '1w', '1m', 'never']
|
|
const DAY_MS = 24 * 3600 * 1000
|
|
const SHANGHAI_OFFSET_MS = 8 * 3600 * 1000
|
|
|
|
/** 上海日历日 "YYYY-MM-DD" 的零点时刻 (UTC 表示) */
|
|
function shanghaiMidnight(dayStr) {
|
|
const [y, m, d] = dayStr.split('-').map(Number)
|
|
return new Date(Date.UTC(y, m - 1, d) - SHANGHAI_OFFSET_MS)
|
|
}
|
|
|
|
/** 上海日历日 + n 个月后的零点 (月末溢出由 Date 归一化) */
|
|
function addMonths(dayStr, n) {
|
|
const [y, m, d] = dayStr.split('-').map(Number)
|
|
return new Date(Date.UTC(y, m - 1 + n, d) - SHANGHAI_OFFSET_MS)
|
|
}
|
|
|
|
/**
|
|
* 任务被隐藏的时刻: doneAt 所在完成日之后, 保留窗口结束的上海零点。
|
|
* 与 date.js isVisibleByRetention 的可见判定精确同口径:
|
|
* never → doneAt 即刻隐藏; today → 完成日次日零点; 3d → 完成日+3 零点;
|
|
* week → 完成周次下一周一零点; month → 次月 1 日零点; forever → 永不隐藏 (null)
|
|
*/
|
|
export function hiddenAtOf(doneAt, option) {
|
|
if (!doneAt) return null
|
|
const day = toDateStr(doneAt)
|
|
switch (option) {
|
|
case 'never': return new Date(doneAt)
|
|
case 'today': return shanghaiMidnight(addDays(day, 1))
|
|
case '3d': return shanghaiMidnight(addDays(day, 3))
|
|
case 'week': return shanghaiMidnight(addDays(startOfWeek(day), 7))
|
|
case 'month': return shanghaiMidnight(String(addDays(day, 32)).slice(0, 8) + '01')
|
|
default: return null // forever: 永不隐藏
|
|
}
|
|
}
|
|
|
|
/** 自动归档到期时刻: 隐藏时刻 + 档位; 'never' 或无法隐藏 → null */
|
|
export function autoArchiveDue(autoArchive, doneAt, columnRetention) {
|
|
if (!autoArchive || autoArchive === 'never') return null
|
|
const hidden = hiddenAtOf(doneAt, columnRetention)
|
|
if (!hidden) return null
|
|
switch (autoArchive) {
|
|
case '3d': return new Date(hidden.getTime() + 3 * DAY_MS)
|
|
case '1w': return new Date(hidden.getTime() + 7 * DAY_MS)
|
|
case '1m': {
|
|
const d = new Date(hidden)
|
|
d.setUTCMonth(d.getUTCMonth() + 1)
|
|
return d
|
|
}
|
|
default: return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 对单个项目执行一轮到期自动归档 (T-ARC-02)。
|
|
* 只处理 status=done / doneAt 非空 / 未归档 的任务; 幂等。
|
|
* @returns {number} 本次归档条数
|
|
*/
|
|
export async function runAutoArchiveForProject(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 columns = await prisma.boardColumn.findMany({
|
|
where: { projectId },
|
|
select: { id: true, retention: true },
|
|
})
|
|
const retentionById = new Map(columns.map((c) => [c.id, c.retention || 'forever']))
|
|
|
|
const tasks = await prisma.task.findMany({
|
|
where: { projectId, archived: false, status: 'done', doneAt: { not: null } },
|
|
select: { id: true, doneAt: true, columnId: true },
|
|
})
|
|
const now = new Date()
|
|
const dueIds = []
|
|
for (const t of tasks) {
|
|
const due = autoArchiveDue(project.autoArchive, t.doneAt, retentionById.get(t.columnId))
|
|
if (due && due <= now) dueIds.push(t.id)
|
|
}
|
|
if (!dueIds.length) return 0
|
|
|
|
const r = await prisma.task.updateMany({
|
|
where: { id: { in: dueIds } },
|
|
data: { archived: true, archivedAt: now, archiveSource: 'auto' },
|
|
})
|
|
return r.count
|
|
}
|
|
|
|
/** 每小时 cron 兜底: 扫描所有启用了自动归档的项目 */
|
|
export async function runAutoArchiveSweep() {
|
|
const projects = await prisma.project.findMany({
|
|
where: { autoArchive: { not: 'never' } },
|
|
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
|
|
}
|