const express = require('express'); const path = require('path'); const os = require('os'); const multer = require('multer'); const initSqlJs = require('sql.js'); const { v4: uuidv4 } = require('uuid'); const fs = require('fs'); const WebSocket = require('ws'); const ADMIN_PORT = 16888; const FRONT_PORT = 80; let db; const DB_PATH = path.join(os.homedir(), '.showpic.db'); async function initDB() { const SQL = await initSqlJs(); if (fs.existsSync(DB_PATH)) { db = new SQL.Database(fs.readFileSync(DB_PATH)); } else { db = new SQL.Database(); } db.run(fs.readFileSync(path.join(__dirname, 'database', 'schema.sql'), 'utf8')); const defaults = { 'feature_enabled': 'false', 'current_group_index': '0', 'current_group_start_time': '0' }; for (const [k, v] of Object.entries(defaults)) { const r = runQuery("SELECT value FROM config WHERE key=?", [k]); if (r.length === 0) db.run("INSERT INTO config (key,value) VALUES (?,?)", [k, v]); } // 确保有默认管理员账号 const userCheck = runQuery("SELECT COUNT(*) as cnt FROM users WHERE username=?", ['admin']); if (userCheck.length === 0 || userCheck[0].cnt === 0) { db.run("INSERT INTO users (username, password) VALUES (?,?)", ['admin', 'admin123']); } saveDB(); } function saveDB() { const data = db.export(); fs.writeFileSync(DB_PATH, Buffer.from(data)); } 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 || []); } function getConfig(key) { const r = runQuery("SELECT value FROM config WHERE key=?", [key]); return r.length > 0 ? r[0].value : null; } function setConfig(key, value) { runExec("INSERT OR REPLACE INTO config (key,value) VALUES (?,?)", [key, String(value)]); saveDB(); } function getEnabledGroups() { return runQuery(` SELECT g.*, (SELECT COUNT(*) FROM images WHERE group_id=g.id AND enabled=1) as img_count FROM groups g WHERE g.enabled=1 ORDER BY g.id ASC `).filter(g => g.img_count > 0); } // ========== 轮播逻辑 ========== let wsClients = new Set(); function broadcast(data) { const msg = JSON.stringify(data); wsClients.forEach(ws => { if (ws.readyState === WebSocket.OPEN) ws.send(msg); }); } function getCurrentState() { const featureEnabled = getConfig('feature_enabled') === 'true'; if (!featureEnabled) return { type: 'state', playing: false, groups: [] }; const groups = getEnabledGroups(); if (groups.length === 0) return { type: 'state', playing: false, groups: [] }; const idx = parseInt(getConfig('current_group_index') || '0') % groups.length; const startTime = parseInt(getConfig('current_group_start_time') || '0'); const now = Math.floor(Date.now() / 1000); const currentGroup = groups[idx]; let remaining = 0; if (currentGroup.display_seconds === 0) { remaining = -1; } else if (startTime > 0) { remaining = Math.max(0, startTime + currentGroup.display_seconds - now); } const images = runQuery('SELECT * FROM images WHERE group_id=? AND enabled=1', [currentGroup.id]); return { type: 'state', playing: true, current_index: idx, total: groups.length, group_name: currentGroup.name, group_id: currentGroup.id, display_seconds: currentGroup.display_seconds, remaining: remaining, images: images }; } function broadcastState() { broadcast(getCurrentState()); } function startCarouselTimer() { setInterval(() => { if (getConfig('feature_enabled') !== 'true') return; const groups = getEnabledGroups(); if (groups.length === 0) return; const idx = parseInt(getConfig('current_group_index') || '0') % groups.length; const startTime = parseInt(getConfig('current_group_start_time') || '0'); const currentGroup = groups[idx]; if (currentGroup.display_seconds === 0) { broadcastState(); return; } if (startTime === 0) { setConfig('current_group_start_time', Math.floor(Date.now() / 1000)); broadcastState(); return; } const now = Math.floor(Date.now() / 1000); const elapsed = now - startTime; if (elapsed >= currentGroup.display_seconds) { const nextIdx = findNextGroupIndex(groups, idx); setConfig('current_group_index', nextIdx); setConfig('current_group_start_time', Math.floor(Date.now() / 1000)); } broadcastState(); }, 1000); } function findNextGroupIndex(groups, currentIdx) { for (let i = 1; i <= groups.length; i++) { const nextIdx = (currentIdx + i) % groups.length; if (groups[nextIdx].display_seconds > 0) return nextIdx; } return currentIdx; } function resetCarousel() { setConfig('current_group_index', 0); setConfig('current_group_start_time', Math.floor(Date.now() / 1000)); broadcastState(); } // ========== Multer ========== const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, path.join(__dirname, 'uploads')), filename: (req, file, cb) => cb(null, uuidv4() + path.extname(file.originalname)) }); const upload = multer({ storage }); // ========== 前台服务 (端口 80) ========== const frontApp = express(); frontApp.use(express.static(path.join(__dirname, 'public'))); frontApp.use('/uploads', express.static(path.join(__dirname, 'uploads'))); frontApp.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); const frontServer = frontApp.listen(FRONT_PORT, '0.0.0.0', () => { console.log(`前台运行在 http://0.0.0.0:${FRONT_PORT}`); }); const wss = new WebSocket.Server({ server: frontServer }); wss.on('connection', (ws) => { wsClients.add(ws); ws.send(JSON.stringify(getCurrentState())); ws.on('close', () => wsClients.delete(ws)); }); // ========== 后台服务 (端口 18888) ========== const adminApp = express(); adminApp.use(express.json()); adminApp.use(express.urlencoded({ extended: true })); adminApp.use('/uploads', express.static(path.join(__dirname, 'uploads'))); // 路由 - 后台直接在根路径,未登录跳转登录页 adminApp.get('/login', (req, res) => res.sendFile(path.join(__dirname, 'public', 'login.html'))); adminApp.get('/', (req, res) => res.sendFile(path.join(__dirname, 'public', 'admin.html'))); // 静态文件(不自动serve index.html) adminApp.use(express.static(path.join(__dirname, 'public'), { index: false })); const authMiddleware = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token || token !== 'admin-token') return res.status(401).json({ error: '未授权' }); next(); }; // 登录 adminApp.post('/api/login', (req, res) => { const { username, password } = req.body; const users = runQuery('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]); if (users.length > 0) { res.json({ success: true, token: 'admin-token' }); } else { res.status(401).json({ success: false, error: '用户名或密码错误' }); } }); // 修改密码 adminApp.put('/api/change-password', authMiddleware, (req, res) => { const { oldPassword, newPassword } = req.body; if (!oldPassword || !newPassword) { return res.status(400).json({ error: '请输入旧密码和新密码' }); } const users = runQuery('SELECT * FROM users WHERE username = ? AND password = ?', ['admin', oldPassword]); if (users.length === 0) { return res.status(400).json({ error: '旧密码错误' }); } runExec('UPDATE users SET password = ? WHERE username = ?', [newPassword, 'admin']); saveDB(); res.json({ success: true }); }); // 分组 adminApp.get('/api/groups', (req, res) => { res.json(runQuery('SELECT * FROM groups ORDER BY id ASC')); }); adminApp.post('/api/groups', authMiddleware, (req, res) => { const { name, display_seconds } = req.body; runExec('INSERT INTO groups (name, display_seconds) VALUES (?, ?)', [name || '新分组', display_seconds || 30]); saveDB(); const id = db.exec("SELECT last_insert_rowid()")[0].values[0][0]; broadcastState(); res.json({ id }); }); adminApp.put('/api/groups/:id', authMiddleware, (req, res) => { const { name, display_seconds, enabled } = req.body; runExec('UPDATE groups SET name=?, display_seconds=?, enabled=? WHERE id=?', [name, display_seconds, enabled, parseInt(req.params.id)]); saveDB(); broadcastState(); res.json({ success: true }); }); adminApp.put('/api/groups/:id/settime', authMiddleware, (req, res) => { const { display_seconds } = req.body; runExec('UPDATE groups SET display_seconds=? WHERE id=?', [display_seconds, parseInt(req.params.id)]); saveDB(); broadcastState(); res.json({ success: true }); }); adminApp.delete('/api/groups/:id', authMiddleware, (req, res) => { const id = parseInt(req.params.id); const imgs = runQuery('SELECT filename FROM images WHERE group_id=?', [id]); imgs.forEach(img => { const fp = path.join(__dirname, 'uploads', img.filename); if (fs.existsSync(fp)) fs.unlinkSync(fp); }); runExec('DELETE FROM images WHERE group_id=?', [id]); runExec('DELETE FROM groups WHERE id=?', [id]); saveDB(); resetCarousel(); res.json({ success: true }); }); // 图片 adminApp.get('/api/images', (req, res) => { const gid = parseInt(req.query.group_id); if (gid) { res.json(runQuery('SELECT * FROM images WHERE group_id=? ORDER BY id DESC', [gid])); } else { res.json(runQuery('SELECT * FROM images ORDER BY id DESC')); } }); adminApp.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' }); runExec('INSERT INTO images (group_id, filename, original_name, size) VALUES (?,?,?,?)', [group_id, req.file.filename, req.file.originalname, req.file.size || 0]); saveDB(); const id = db.exec("SELECT last_insert_rowid()")[0].values[0][0]; broadcastState(); res.json({ id, filename: req.file.filename }); }); adminApp.put('/api/images/:id/enable', authMiddleware, (req, res) => { runExec('UPDATE images SET enabled=1 WHERE id=?', [parseInt(req.params.id)]); saveDB(); broadcastState(); res.json({ success: true }); }); adminApp.put('/api/images/:id/disable', authMiddleware, (req, res) => { runExec('UPDATE images SET enabled=0 WHERE id=?', [parseInt(req.params.id)]); saveDB(); broadcastState(); res.json({ success: true }); }); adminApp.delete('/api/images/:id', authMiddleware, (req, res) => { const id = parseInt(req.params.id); const imgs = runQuery('SELECT filename FROM images WHERE id=?', [id]); if (imgs.length > 0) { const fp = path.join(__dirname, 'uploads', imgs[0].filename); if (fs.existsSync(fp)) fs.unlinkSync(fp); } runExec('DELETE FROM images WHERE id=?', [id]); saveDB(); broadcastState(); res.json({ success: true }); }); // 配置 adminApp.get('/api/config', (req, res) => { const rows = runQuery('SELECT * FROM config'); const config = {}; rows.forEach(r => config[r.key] = r.value); res.json(config); }); adminApp.put('/api/config', authMiddleware, (req, res) => { setConfig(req.body.key, req.body.value); broadcastState(); res.json({ success: true }); }); // 重置轮播 adminApp.post('/api/carousel/reset', authMiddleware, (req, res) => { resetCarousel(); res.json({ success: true }); }); // 路由 - 后台直接在根路径,未登录跳转登录页 adminApp.get('/login', (req, res) => res.sendFile(path.join(__dirname, 'public', 'login.html'))); adminApp.get('/', (req, res) => res.sendFile(path.join(__dirname, 'public', 'admin.html'))); adminApp.listen(ADMIN_PORT, '0.0.0.0', () => { console.log(`后台运行在 http://0.0.0.0:${ADMIN_PORT}`); }); // ========== 启动 ========== initDB().then(() => { startCarouselTimer(); console.log('数据库初始化完成'); }).catch(err => { console.error('启动失败:', err); process.exit(1); });