Files
showpic/server.js
T

372 lines
12 KiB
JavaScript
Raw Normal View History

2026-07-16 22:51:02 +08:00
const express = require('express');
const path = require('path');
2026-07-16 23:13:56 +08:00
const os = require('os');
2026-07-16 22:51:02 +08:00
const multer = require('multer');
const initSqlJs = require('sql.js');
const { v4: uuidv4 } = require('uuid');
const fs = require('fs');
2026-07-16 22:58:46 +08:00
const WebSocket = require('ws');
2026-07-16 22:51:02 +08:00
2026-07-16 22:58:46 +08:00
const ADMIN_PORT = 16888;
const FRONT_PORT = 80;
2026-07-16 22:51:02 +08:00
let db;
2026-07-16 23:13:56 +08:00
const DB_PATH = path.join(os.homedir(), '.showpic.db');
2026-07-16 22:51:02 +08:00
async function initDB() {
const SQL = await initSqlJs();
2026-07-16 23:13:56 +08:00
if (fs.existsSync(DB_PATH)) {
db = new SQL.Database(fs.readFileSync(DB_PATH));
2026-07-16 22:51:02 +08:00
} else {
db = new SQL.Database();
}
2026-07-16 22:58:46 +08:00
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]);
}
2026-07-16 22:51:02 +08:00
2026-07-16 22:58:46 +08:00
// 确保有默认管理员账号
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']);
2026-07-16 22:51:02 +08:00
}
saveDB();
}
function saveDB() {
const data = db.export();
2026-07-16 23:13:56 +08:00
fs.writeFileSync(DB_PATH, Buffer.from(data));
2026-07-16 22:51:02 +08:00
}
function runQuery(sql, params) {
const stmt = db.prepare(sql);
2026-07-16 22:58:46 +08:00
if (params && params.length > 0) stmt.bind(params);
2026-07-16 22:51:02 +08:00
const results = [];
2026-07-16 22:58:46 +08:00
while (stmt.step()) results.push(stmt.getAsObject());
2026-07-16 22:51:02 +08:00
stmt.free();
return results;
}
function runExec(sql, params) {
db.run(sql, params || []);
2026-07-16 22:58:46 +08:00
}
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)]);
2026-07-16 22:51:02 +08:00
saveDB();
}
2026-07-16 22:58:46 +08:00
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) return;
2026-07-16 22:58:46 +08:00
// 首次启动,记录开始时间并广播
2026-07-16 22:58:46 +08:00
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;
// 时间到了,切换分组并广播
2026-07-16 22:58:46 +08:00
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();
2026-07-16 22:58:46 +08:00
}
}, 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;
2026-07-16 22:51:02 +08:00
}
2026-07-16 22:58:46 +08:00
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}`);
2026-07-16 22:51:02 +08:00
});
2026-07-16 22:58:46 +08:00
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')));
2026-07-16 22:51:02 +08:00
// 路由 - 后台直接在根路径,未登录跳转登录页
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 }));
2026-07-16 22:51:02 +08:00
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
2026-07-16 22:58:46 +08:00
if (!token || token !== 'admin-token') return res.status(401).json({ error: '未授权' });
2026-07-16 22:51:02 +08:00
next();
};
2026-07-16 22:58:46 +08:00
// 登录
adminApp.post('/api/login', (req, res) => {
2026-07-16 22:51:02 +08:00
const { username, password } = req.body;
2026-07-16 22:58:46 +08:00
const users = runQuery('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]);
if (users.length > 0) {
2026-07-16 22:51:02 +08:00
res.json({ success: true, token: 'admin-token' });
} else {
res.status(401).json({ success: false, error: '用户名或密码错误' });
}
});
2026-07-16 22:58:46 +08:00
// 修改密码
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'));
2026-07-16 22:51:02 +08:00
});
2026-07-16 22:58:46 +08:00
adminApp.post('/api/groups', authMiddleware, (req, res) => {
2026-07-16 22:51:02 +08:00
const { name, display_seconds } = req.body;
2026-07-16 22:58:46 +08:00
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();
2026-07-16 22:51:02 +08:00
res.json({ id });
});
2026-07-16 22:58:46 +08:00
adminApp.put('/api/groups/:id', authMiddleware, (req, res) => {
2026-07-16 22:51:02 +08:00
const { name, display_seconds, enabled } = req.body;
2026-07-16 22:58:46 +08:00
runExec('UPDATE groups SET name=?, display_seconds=?, enabled=? WHERE id=?',
[name, display_seconds, enabled, parseInt(req.params.id)]);
saveDB();
broadcastState();
2026-07-16 22:51:02 +08:00
res.json({ success: true });
});
2026-07-16 22:58:46 +08:00
adminApp.put('/api/groups/:id/settime', authMiddleware, (req, res) => {
2026-07-16 22:51:02 +08:00
const { display_seconds } = req.body;
2026-07-16 22:58:46 +08:00
runExec('UPDATE groups SET display_seconds=? WHERE id=?', [display_seconds, parseInt(req.params.id)]);
saveDB();
broadcastState();
2026-07-16 22:51:02 +08:00
res.json({ success: true });
});
2026-07-16 22:58:46 +08:00
adminApp.delete('/api/groups/:id', authMiddleware, (req, res) => {
2026-07-16 22:51:02 +08:00
const id = parseInt(req.params.id);
2026-07-16 22:58:46 +08:00
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);
2026-07-16 22:51:02 +08:00
});
2026-07-16 22:58:46 +08:00
runExec('DELETE FROM images WHERE group_id=?', [id]);
runExec('DELETE FROM groups WHERE id=?', [id]);
saveDB();
resetCarousel();
2026-07-16 22:51:02 +08:00
res.json({ success: true });
});
2026-07-16 22:58:46 +08:00
// 图片
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'));
2026-07-16 22:51:02 +08:00
}
});
2026-07-16 22:58:46 +08:00
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 });
2026-07-16 22:51:02 +08:00
});
2026-07-16 22:58:46 +08:00
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 });
2026-07-16 22:51:02 +08:00
});
2026-07-16 22:58:46 +08:00
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 });
2026-07-16 22:51:02 +08:00
});
2026-07-16 22:58:46 +08:00
adminApp.delete('/api/images/:id', authMiddleware, (req, res) => {
2026-07-16 22:51:02 +08:00
const id = parseInt(req.params.id);
2026-07-16 22:58:46 +08:00
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);
2026-07-16 22:51:02 +08:00
}
2026-07-16 22:58:46 +08:00
runExec('DELETE FROM images WHERE id=?', [id]);
saveDB(); broadcastState(); res.json({ success: true });
2026-07-16 22:51:02 +08:00
});
2026-07-16 22:58:46 +08:00
// 配置
adminApp.get('/api/config', (req, res) => {
2026-07-16 22:51:02 +08:00
const rows = runQuery('SELECT * FROM config');
const config = {};
2026-07-16 22:58:46 +08:00
rows.forEach(r => config[r.key] = r.value);
2026-07-16 22:51:02 +08:00
res.json(config);
});
2026-07-16 22:58:46 +08:00
adminApp.put('/api/config', authMiddleware, (req, res) => {
setConfig(req.body.key, req.body.value);
broadcastState();
2026-07-16 22:51:02 +08:00
res.json({ success: true });
});
2026-07-16 22:58:46 +08:00
// 重置轮播
adminApp.post('/api/carousel/reset', authMiddleware, (req, res) => {
resetCarousel();
res.json({ success: true });
2026-07-16 22:51:02 +08:00
});
2026-07-16 22:58:46 +08:00
// 路由 - 后台直接在根路径,未登录跳转登录页
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')));
2026-07-16 22:51:02 +08:00
2026-07-16 22:58:46 +08:00
adminApp.listen(ADMIN_PORT, '0.0.0.0', () => {
console.log(`后台运行在 http://0.0.0.0:${ADMIN_PORT}`);
2026-07-16 22:51:02 +08:00
});
2026-07-16 22:58:46 +08:00
// ========== 启动 ==========
2026-07-16 22:51:02 +08:00
initDB().then(() => {
2026-07-16 22:58:46 +08:00
startCarouselTimer();
console.log('数据库初始化完成');
2026-07-16 22:51:02 +08:00
}).catch(err => {
2026-07-16 22:58:46 +08:00
console.error('启动失败:', err);
2026-07-16 22:51:02 +08:00
process.exit(1);
});