const express = require('express'); const path = require('path'); const multer = require('multer'); const initSqlJs = require('sql.js'); const { v4: uuidv4 } = require('uuid'); const fs = require('fs'); const app = express(); const PORT = 3000; let db; async function initDB() { const SQL = await initSqlJs(); const dbPath = path.join(__dirname, 'data', 'sqlite.db'); if (fs.existsSync(dbPath)) { const fileBuffer = fs.readFileSync(dbPath); db = new SQL.Database(fileBuffer); } else { db = new SQL.Database(); } const schema = ` CREATE TABLE IF NOT EXISTS groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, display_seconds INTEGER NOT NULL DEFAULT 30, enabled INTEGER NOT NULL DEFAULT 1, update_time INTEGER NOT NULL DEFAULT 0, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, group_id INTEGER NOT NULL, filename TEXT NOT NULL, original_name TEXT, size INTEGER DEFAULT 0, width INTEGER DEFAULT 280, height INTEGER DEFAULT 280, enabled INTEGER NOT NULL DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY, value TEXT ); `; db.run(schema); // 确保有 config 行 const configCheck = db.exec("SELECT COUNT(*) as cnt FROM config WHERE key='feature_enabled'"); if (configCheck[0].values[0][0] === 0) { db.run("INSERT INTO config (key, value) VALUES ('feature_enabled', 'false')"); } saveDB(); } function saveDB() { const data = db.export(); const buffer = Buffer.from(data); fs.writeFileSync(path.join(__dirname, 'data', 'sqlite.db'), buffer); } function runQuery(sql, params) { const stmt = db.prepare(sql); if (params && params.length > 0) { stmt.bind(params); } const results = []; while (stmt.step()) { results.push(stmt.getAsObject()); } stmt.free(); return results; } function runExec(sql, params) { db.run(sql, params || []); saveDB(); } // Multer配置 const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, path.join(__dirname, 'uploads')); }, filename: function (req, file, cb) { const ext = path.extname(file.originalname); cb(null, uuidv4() + ext); } }); const upload = multer({ storage: storage }); // 中间件 app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, 'public'))); app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // 认证中间件 const authMiddleware = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token || token !== 'admin-token') { return res.status(401).json({ error: '未授权' }); } next(); }; // 认证API app.post('/api/login', (req, res) => { const { username, password } = req.body; if (username === 'admin' && password === 'admin123') { res.json({ success: true, token: 'admin-token' }); } else { res.status(401).json({ success: false, error: '用户名或密码错误' }); } }); // 分组管理API app.get('/api/groups', (req, res) => { const groups = runQuery('SELECT * FROM groups ORDER BY id ASC'); res.json(groups); }); app.post('/api/groups', authMiddleware, (req, res) => { const { name, display_seconds } = req.body; runExec('INSERT INTO groups (name, display_seconds, update_time) VALUES (?, ?, 0)', [name || '新分组', display_seconds || 30]); const result = db.exec("SELECT last_insert_rowid() as id"); const id = result[0].values[0][0]; res.json({ id }); }); app.put('/api/groups/:id', authMiddleware, (req, res) => { const { name, display_seconds, enabled } = req.body; const id = parseInt(req.params.id); runExec('UPDATE groups SET name = ?, display_seconds = ?, enabled = ? WHERE id = ?', [name, display_seconds, enabled, id]); res.json({ success: true }); }); app.put('/api/groups/:id/settime', authMiddleware, (req, res) => { const { display_seconds } = req.body; const id = parseInt(req.params.id); const now = Math.floor(Date.now() / 1000); runExec('UPDATE groups SET display_seconds = ?, update_time = ? WHERE id = ?', [display_seconds, now, id]); res.json({ success: true }); }); app.delete('/api/groups/:id', authMiddleware, (req, res) => { const id = parseInt(req.params.id); // 先删除分组下的图片文件 const images = runQuery('SELECT filename FROM images WHERE group_id = ?', [id]); images.forEach(img => { const filePath = path.join(__dirname, 'uploads', img.filename); if (fs.existsSync(filePath)) fs.unlinkSync(filePath); }); runExec('DELETE FROM images WHERE group_id = ?', [id]); runExec('DELETE FROM groups WHERE id = ?', [id]); res.json({ success: true }); }); // 图片上传API app.post('/api/upload', authMiddleware, upload.single('image'), (req, res) => { if (!req.file) { return res.status(400).json({ error: '没有上传文件' }); } const group_id = parseInt(req.body.group_id); if (!group_id) { return res.status(400).json({ error: '缺少分组ID' }); } const { filename, originalname, size } = req.file; runExec('INSERT INTO images (group_id, filename, original_name, size) VALUES (?, ?, ?, ?)', [group_id, filename, originalname, size || 0]); const result = db.exec("SELECT last_insert_rowid() as id"); const id = result[0].values[0][0]; res.json({ id, filename }); }); // 图片列表API app.get('/api/images', (req, res) => { const group_id = parseInt(req.query.group_id); let images; if (group_id) { images = runQuery('SELECT * FROM images WHERE group_id = ? ORDER BY id DESC', [group_id]); } else { images = runQuery('SELECT * FROM images ORDER BY id DESC'); } res.json(images); }); // 图片启用/禁用API app.put('/api/images/:id/enable', authMiddleware, (req, res) => { const id = parseInt(req.params.id); runExec('UPDATE images SET enabled = 1 WHERE id = ?', [id]); res.json({ success: true }); }); app.put('/api/images/:id/disable', authMiddleware, (req, res) => { const id = parseInt(req.params.id); runExec('UPDATE images SET enabled = 0 WHERE id = ?', [id]); res.json({ success: true }); }); // 图片删除API app.delete('/api/images/:id', authMiddleware, (req, res) => { const id = parseInt(req.params.id); const images = runQuery('SELECT filename FROM images WHERE id = ?', [id]); if (images.length > 0) { const filePath = path.join(__dirname, 'uploads', images[0].filename); if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } runExec('DELETE FROM images WHERE id = ?', [id]); res.json({ success: true }); }); // 配置API app.get('/api/config', (req, res) => { const rows = runQuery('SELECT * FROM config'); const config = {}; rows.forEach(row => { config[row.key] = row.value; }); res.json(config); }); app.put('/api/config', authMiddleware, (req, res) => { const { key, value } = req.body; runExec('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', [key, value]); res.json({ success: true }); }); // 首页展示API app.get('/api/display', (req, res) => { const configRows = runQuery("SELECT value FROM config WHERE key = 'feature_enabled'"); const featureEnabled = configRows.length > 0 && configRows[0].value === 'true'; if (!featureEnabled) { return res.json({ enabled: false, groups: [] }); } const groups = runQuery('SELECT * FROM groups WHERE enabled = 1 ORDER BY id ASC'); const now = Math.floor(Date.now() / 1000); const result = groups.filter(group => { // 展示时间为0则一直展示 if (group.display_seconds === 0) return true; // update_time为0则不展示(未设置过时间) if (group.update_time === 0) return false; // 计算剩余时间 const remaining = group.update_time + group.display_seconds - now; return remaining > 0; }).map(group => { const images = runQuery('SELECT * FROM images WHERE group_id = ? AND enabled = 1', [group.id]); const remaining = group.update_time === 0 ? 0 : Math.max(0, group.update_time + group.display_seconds - now); return { ...group, remaining, images }; }).filter(group => group.images.length > 0); res.json({ enabled: true, groups: result }); }); // 静态文件路由 app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); app.get('/login', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'login.html')); }); app.get('/admin', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'admin.html')); }); // 启动服务器 initDB().then(() => { app.listen(PORT, '0.0.0.0', () => { console.log(`服务器运行在 http://localhost:${PORT}`); }); }).catch(err => { console.error('数据库初始化失败:', err); process.exit(1); });