const express = require('express'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const app = express(); const PORT = process.env.PORT || 3003; // ── 中间件 ──────────────────────────────────────────── app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // 统一设置 JSON 响应的 charset app.use((_req, res, next) => { res.setHeader('Content-Type', 'application/json; charset=utf-8'); next(); }); // ── 数据层 ──────────────────────────────────────────── const DATA_DIR = path.join(__dirname, 'data'); const DATA_FILE = path.join(DATA_DIR, 'todos.json'); function ensureDataDir() { if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } } function readTodos() { ensureDataDir(); try { if (!fs.existsSync(DATA_FILE)) return []; const raw = fs.readFileSync(DATA_FILE, 'utf-8'); return JSON.parse(raw); } catch { // 文件损坏时备份并重建 if (fs.existsSync(DATA_FILE)) { const backup = DATA_FILE + '.bak.' + Date.now(); fs.copyFileSync(DATA_FILE, backup); console.warn(`数据文件损坏,已备份至 ${backup}`); } return []; } } let todos = readTodos(); let writeTimer = null; function writeTodos() { clearTimeout(writeTimer); writeTimer = setTimeout(() => { ensureDataDir(); fs.writeFileSync(DATA_FILE, JSON.stringify(todos, null, 2), 'utf-8'); }, 300); // 300ms 防抖 } function generateId() { return crypto.randomUUID(); } // ── 辅助函数 ────────────────────────────────────────── /** 递归收集所有子孙节点 ID */ function collectDescendantIds(parentId) { const ids = []; const children = todos.filter(t => t.parentId === parentId); for (const child of children) { ids.push(child.id); ids.push(...collectDescendantIds(child.id)); } return ids; } /** 获取某个任务的最大层级深度(用于限制三级) */ function getDepth(itemId) { let depth = 0; let current = todos.find(t => t.id === itemId); while (current && current.parentId) { depth++; current = todos.find(t => t.id === current.parentId); } return depth; } // ── API 路由 ────────────────────────────────────────── // 获取所有任务(排除已删除的) app.get('/api/todos', (_req, res) => { res.json(todos.filter(t => t.status !== 'deleted')); }); // 创建任务 app.post('/api/todos', (req, res) => { const { title, parentId } = req.body; if (!title || !title.trim()) { return res.status(400).json({ error: '标题不能为空' }); } // 检查层级限制:最多三级(主/子/孙) if (parentId) { const parent = todos.find(t => t.id === parentId); if (!parent) { return res.status(404).json({ error: '父任务不存在' }); } const parentDepth = getDepth(parentId); if (parentDepth >= 2) { return res.status(400).json({ error: '最多支持三级任务(主/子/孙)' }); } } const now = new Date().toISOString(); const todo = { id: generateId(), title: title.trim(), status: 'active', parentId: parentId || null, createdAt: now, updatedAt: now, }; todos.push(todo); writeTodos(); res.status(201).json(todo); }); // 更新任务 app.patch('/api/todos/:id', (req, res) => { const { id } = req.params; const { title, status } = req.body; const index = todos.findIndex(t => t.id === id); if (index === -1) { return res.status(404).json({ error: '任务不存在' }); } const todo = todos[index]; const now = new Date().toISOString(); if (title !== undefined) { if (!title.trim()) { return res.status(400).json({ error: '标题不能为空' }); } todo.title = title.trim(); } if (status !== undefined) { if (!['active', 'completed', 'abandoned', 'deleted'].includes(status)) { return res.status(400).json({ error: '无效的状态值' }); } todo.status = status; todo.updatedAt = now; // 级联完成:当标记为 completed 时,所有子孙也变为 completed if (status === 'completed') { const descendantIds = collectDescendantIds(id); for (const descId of descendantIds) { const desc = todos.find(t => t.id === descId); if (desc) { desc.status = 'completed'; desc.updatedAt = now; } } } } todo.updatedAt = now; writeTodos(); res.json(todo); }); // 删除任务(软删除:标记为 deleted,级联子孙) app.delete('/api/todos/:id', (req, res) => { const { id } = req.params; const todo = todos.find(t => t.id === id); if (!todo) { return res.status(404).json({ error: '任务不存在' }); } const idsToDelete = new Set([id, ...collectDescendantIds(id)]); const now = new Date().toISOString(); let count = 0; for (const t of todos) { if (idsToDelete.has(t.id)) { t.status = 'deleted'; t.updatedAt = now; count++; } } writeTodos(); res.json({ deleted: count }); }); // ── 设置接口 ────────────────────────────────────────── const SETTINGS_FILE = path.join(DATA_DIR, 'settings.json'); function readSettings() { ensureDataDir(); try { if (!fs.existsSync(SETTINGS_FILE)) return { autoSaveEnabled: false, autoSaveSec: 10 }; return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf-8')); } catch { return { autoSaveEnabled: false, autoSaveSec: 10 }; } } function writeSettings(s) { ensureDataDir(); fs.writeFileSync(SETTINGS_FILE, JSON.stringify(s, null, 2), 'utf-8'); } app.get('/api/settings', (_req, res) => { res.json(readSettings()); }); app.put('/api/settings', (req, res) => { const s = readSettings(); if (req.body.autoSaveEnabled !== undefined) s.autoSaveEnabled = req.body.autoSaveEnabled; if (req.body.autoSaveSec !== undefined) s.autoSaveSec = req.body.autoSaveSec; writeSettings(s); res.json(s); }); // 回退到 index.html(SPA) app.get('*', (_req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // ── 启动服务 ────────────────────────────────────────── function startServer(port) { const server = app.listen(port, () => { console.log(`✅ Todo List 服务已启动: http://localhost:${port}`); console.log(`📁 数据文件: ${DATA_FILE}`); }); server.on('error', (err) => { // 生产环境(PM2 管理端口)不自动跳,直接抛错 if (err.code === 'EADDRINUSE' && process.env.NODE_ENV !== 'production') { console.warn(`⚠️ 端口 ${port} 被占用,尝试 ${port + 1}…`); startServer(port + 1); } else { throw err; } }); } startServer(PORT);