const express = require('express'); const cors = require('cors'); const path = require('path'); const fs = require('fs'); const multer = require('multer'); const { v4: uuidv4 } = require('uuid'); const db = require('./database'); const app = express(); const PORT = process.env.PORT || 3001; // 聊天附件存储目录 const uploadsDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } // multer 磁盘存储:文件名用 uuid + 安全扩展名,单文件上限 100MB const MIME_EXT = { 'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/webp': '.webp', 'image/bmp': '.bmp', 'video/mp4': '.mp4', 'video/webm': '.webm', 'video/quicktime': '.mov', 'video/x-msvideo': '.avi', 'video/x-matroska': '.mkv' }; const upload = multer({ storage: multer.diskStorage({ destination: (req, file, cb) => cb(null, uploadsDir), filename: (req, file, cb) => { const rawExt = path.extname(file.originalname || '').toLowerCase(); const ext = MIME_EXT[file.mimetype] || (/^\.[a-z0-9]{1,8}$/.test(rawExt) ? rawExt : '.bin'); cb(null, uuidv4() + ext); } }), limits: { fileSize: 100 * 1024 * 1024 } }); // 内存会话存储 const sessions = new Map(); // 会话最近活跃时间(token -> 时间戳),用于判断在线状态 const sessionActivity = new Map(); // 最近活跃时间在此窗口内视为在线 const ONLINE_WINDOW_MS = 10 * 60 * 1000; function touchSession(token) { sessionActivity.set(token, Date.now()); } function dropSession(token) { sessions.delete(token); sessionActivity.delete(token); } // 收集当前在线用户(用户ID -> 最近活跃时间戳,取最新) function getOnlineUserMap() { const now = Date.now(); const map = new Map(); for (const [token, sessionUser] of sessions.entries()) { const active = sessionActivity.get(token) || 0; if (now - active <= ONLINE_WINDOW_MS) { if (!map.has(sessionUser.id) || active > map.get(sessionUser.id)) { map.set(sessionUser.id, active); } } } return map; } function formatActive(ts) { const d = new Date(ts); const pad = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } // 登录验证中间件(仅对 /api 路由生效) function authMiddleware(req, res, next) { const publicPaths = ['/auth/login', '/auth/logout', '/weather']; if (publicPaths.some(path => req.path.startsWith(path))) { return next(); } // 优先取请求头 token;img/video/下载链接无法携带 Authorization 头,允许查询参数回退 const token = req.headers.authorization?.replace('Bearer ', '') || req.query.token; if (!token || !sessions.has(token)) { return res.status(401).json({ success: false, message: '未登录或登录已过期' }); } req.user = sessions.get(token); touchSession(token); next(); } // 管理员权限中间件 function requireAdmin(req, res, next) { if (req.user && req.user.role === 'admin') { return next(); } return res.status(403).json({ success: false, message: '权限不足,该操作仅管理员可用' }); } app.use(cors()); app.use(express.json()); // API 认证中间件 app.use('/api', authMiddleware); // 生产环境提供前端静态文件 const clientBuildPath = path.join(__dirname, 'client', 'dist'); app.use(express.static(clientBuildPath)); // ==================== 设备管理 API ==================== // 记录设备操作日志 function logDeviceAction(deviceId, deviceName, action, detail, operator) { db.prepare(` INSERT INTO device_logs (id, device_id, device_name, action, detail, operator) VALUES (?, ?, ?, ?, ?, ?) `).run(uuidv4(), deviceId, deviceName || '', action, detail || '', operator || ''); } // ==================== 推送事件(客户端轮询拉取的原生推送源) ==================== // 定期清理 7 天前的推送事件 function cleanPushEvents() { db.prepare("DELETE FROM push_events WHERE created_at < datetime('now','localtime','-7 day')").run(); } cleanPushEvents(); setInterval(cleanPushEvents, 6 * 60 * 60 * 1000); // 向指定用户写入一条推送事件 function pushTo(userId, kind, title, body) { if (!userId) return; db.prepare('INSERT INTO push_events (user_id, kind, title, body) VALUES (?, ?, ?, ?)') .run(userId, kind, String(title || ''), String(body || '').slice(0, 200)); } // 向多个用户推送(自动去重、跳过发送者) function pushToMany(userIds, kind, title, body, excludeId) { const seen = new Set(); for (const uid of userIds) { if (!uid || uid === excludeId || seen.has(uid)) continue; seen.add(uid); pushTo(uid, kind, title, body); } } // 向指定角色的全部用户推送 function pushToRole(role, kind, title, body, excludeId) { const rows = db.prepare('SELECT id FROM users WHERE role = ?').all(role); pushToMany(rows.map(r => r.id), kind, title, body, excludeId); } // 向全部用户推送 function pushToAll(kind, title, body, excludeId) { const rows = db.prepare('SELECT id FROM users').all(); pushToMany(rows.map(r => r.id), kind, title, body, excludeId); } // 客户端拉取自己的新推送事件(after 游标为上次拉到的最大 id) app.get('/api/push/events', (req, res) => { const after = parseInt(req.query.after) || 0; const rows = db.prepare('SELECT * FROM push_events WHERE user_id = ? AND id > ? ORDER BY id ASC LIMIT 50') .all(req.user.id, after); res.json({ success: true, data: rows }); }); // 写入维修记录 function addMaintenanceRecord(deviceId, deviceName, type, remark, operator) { db.prepare(` INSERT INTO maintenance_records (id, device_id, device_name, type, remark, operator) VALUES (?, ?, ?, ?, ?, ?) `).run(uuidv4(), deviceId, deviceName || '', type, remark || '', operator || ''); } const getOperator = (req) => (req.user ? (req.user.display_name || req.user.username) : ''); // 获取所有设备 app.get('/api/devices', (req, res) => { const { keyword, type, status } = req.query; let sql = 'SELECT * FROM devices WHERE 1=1'; const params = []; if (keyword) { sql += ' AND (name LIKE ? OR serial_number LIKE ? OR location LIKE ?)'; params.push(`%${keyword}%`, `%${keyword}%`, `%${keyword}%`); } if (type) { sql += ' AND type = ?'; params.push(type); } if (status) { sql += ' AND status = ?'; params.push(status); } sql += ' ORDER BY created_at DESC'; const devices = db.prepare(sql).all(...params); res.json({ success: true, data: devices }); }); // 报废设备列表(须定义在 /api/devices/:id 之前,避免路由遮蔽) app.get('/api/devices/scrapped', (req, res) => { const scrapped = db.prepare( "SELECT * FROM devices WHERE status = '已报废' ORDER BY scrap_date DESC, updated_at DESC" ).all(); res.json({ success: true, data: scrapped }); }); // 设备操作日志(管理员) app.get('/api/devices/logs', requireAdmin, (req, res) => { const { device_id, keyword } = req.query; let sql = 'SELECT * FROM device_logs WHERE 1=1'; const params = []; if (device_id) { sql += ' AND device_id = ?'; params.push(device_id); } if (keyword) { sql += ' AND (device_name LIKE ? OR action LIKE ? OR detail LIKE ? OR operator LIKE ?)'; params.push(`%${keyword}%`, `%${keyword}%`, `%${keyword}%`, `%${keyword}%`); } sql += ' ORDER BY created_at DESC LIMIT 500'; res.json({ success: true, data: db.prepare(sql).all(...params) }); }); // 设备统计 app.get('/api/devices/stats/summary', (req, res) => { const total = db.prepare('SELECT COUNT(*) as count FROM devices').get().count; const normal = db.prepare("SELECT COUNT(*) as count FROM devices WHERE status='正常'").get().count; const repairing = db.prepare("SELECT COUNT(*) as count FROM devices WHERE status='维修中'").get().count; const scrapped = db.prepare("SELECT COUNT(*) as count FROM devices WHERE status='已报废'").get().count; const idle = db.prepare("SELECT COUNT(*) as count FROM devices WHERE status='闲置'").get().count; const scrappedThisMonth = db.prepare( "SELECT COUNT(*) as count FROM devices WHERE status='已报废' AND substr(scrap_date,1,7) = substr(date('now','localtime'),1,7)" ).get().count; const types = db.prepare('SELECT type, COUNT(*) as count FROM devices GROUP BY type').all(); res.json({ success: true, data: { total, normal, repairing, scrapped, idle, scrappedThisMonth, types } }); }); // 获取单个设备 app.get('/api/devices/:id', (req, res) => { const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); if (device) { res.json({ success: true, data: device }); } else { res.status(404).json({ success: false, message: '设备不存在' }); } }); // 获取设备维修/保养记录 app.get('/api/devices/:id/records', (req, res) => { const records = db.prepare( 'SELECT * FROM maintenance_records WHERE device_id = ? ORDER BY created_at DESC' ).all(req.params.id); res.json({ success: true, data: records }); }); // 添加设备 app.post('/api/devices', (req, res) => { const { name, type, model, serial_number, status, location, purchase_date, warranty_date, responsible_person, description } = req.body; const id = uuidv4(); db.prepare(` INSERT INTO devices (id, name, type, model, serial_number, status, location, purchase_date, warranty_date, responsible_person, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run(id, name, type, model || '', serial_number || '', status || '正常', location || '', purchase_date || '', warranty_date || '', responsible_person || '', description || ''); logDeviceAction(id, name, '添加', '新设备入库', getOperator(req)); const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(id); res.json({ success: true, data: device, message: '设备添加成功' }); }); // 更新设备 app.put('/api/devices/:id', requireAdmin, (req, res) => { const { name, type, model, serial_number, status, location, purchase_date, warranty_date, responsible_person, description } = req.body; const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); if (!device) { return res.status(404).json({ success: false, message: '设备不存在' }); } db.prepare(` UPDATE devices SET name=?, type=?, model=?, serial_number=?, status=?, location=?, purchase_date=?, warranty_date=?, responsible_person=?, description=?, updated_at=datetime('now','localtime') WHERE id=? `).run(name, type, model || '', serial_number || '', status, location || '', purchase_date || '', warranty_date || '', responsible_person || '', description || '', req.params.id); logDeviceAction(req.params.id, name, '编辑', '设备信息已更新', getOperator(req)); const updated = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); res.json({ success: true, data: updated, message: '设备更新成功' }); }); // 删除设备(管理员) app.delete('/api/devices/:id', requireAdmin, (req, res) => { const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); if (!device) { return res.status(404).json({ success: false, message: '设备不存在' }); } db.prepare('DELETE FROM devices WHERE id = ?').run(req.params.id); db.prepare('DELETE FROM maintenance_records WHERE device_id = ?').run(req.params.id); db.prepare('DELETE FROM device_logs WHERE device_id = ?').run(req.params.id); res.json({ success: true, message: '设备删除成功' }); }); // 设备报修(普通用户+管理员):将状态置为维修中 app.patch('/api/devices/:id/repair', (req, res) => { const { remark } = req.body || {}; const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); if (!device) { return res.status(404).json({ success: false, message: '设备不存在' }); } if (device.status === '已报废') { return res.status(400).json({ success: false, message: '已报废设备不可报修' }); } db.prepare(` UPDATE devices SET status='维修中', updated_at=datetime('now','localtime') WHERE id=? `).run(req.params.id); addMaintenanceRecord(req.params.id, device.name, '报修', remark || '', getOperator(req)); logDeviceAction(req.params.id, device.name, '报修', remark || '设备已标记为维修中', getOperator(req)); // 推送给全部管理员处理 pushToRole('admin', 'repair_request', '设备报修', `${getOperator(req)} 报修:${device.name}${remark ? '(' + remark + ')' : ''}`, req.user.id); const updated = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); res.json({ success: true, data: updated, message: '报修成功,设备已标记为维修中' }); }); // 修改设备维修状态(管理员) app.patch('/api/devices/:id/status', requireAdmin, (req, res) => { const { status } = req.body || {}; const allowed = ['正常', '维修中', '已报废', '闲置']; if (!status || !allowed.includes(status)) { return res.status(400).json({ success: false, message: '无效的状态值' }); } const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); if (!device) { return res.status(404).json({ success: false, message: '设备不存在' }); } if (status === device.status) { return res.json({ success: true, data: device, message: '状态未发生变化' }); } if (status !== '已报废') { // 离开报废状态时清空报废信息 db.prepare("UPDATE devices SET status=?, scrap_reason='', scrap_date='', scrap_operator='', updated_at=datetime('now','localtime') WHERE id=?").run(status, req.params.id); } else { db.prepare("UPDATE devices SET status=?, updated_at=datetime('now','localtime') WHERE id=?").run(status, req.params.id); } if (device.status === '维修中' && status === '正常') { addMaintenanceRecord(req.params.id, device.name, '维修完成', '维修完成,状态恢复正常', getOperator(req)); // 维修完成推送给全部用户 pushToAll('repair_done', '设备维修完成', `设备「${device.name}」已维修好,状态恢复正常`, req.user.id); } logDeviceAction(req.params.id, device.name, '修改状态', `${device.status} → ${status}`, getOperator(req)); const updated = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); res.json({ success: true, data: updated, message: `维修状态已修改为:${status}` }); }); // 设备报废(管理员) app.post('/api/devices/:id/scrap', requireAdmin, (req, res) => { const { reason } = req.body || {}; const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); if (!device) { return res.status(404).json({ success: false, message: '设备不存在' }); } if (device.status === '已报废') { return res.status(400).json({ success: false, message: '该设备已处于报废状态' }); } const operator = getOperator(req); db.prepare(` UPDATE devices SET status='已报废', scrap_reason=?, scrap_date=date('now','localtime'), scrap_operator=?, updated_at=datetime('now','localtime') WHERE id=? `).run(reason || '', operator, req.params.id); addMaintenanceRecord(req.params.id, device.name, '报废', reason || '', operator); logDeviceAction(req.params.id, device.name, '报废', reason || '设备已报废', operator); const updated = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); res.json({ success: true, data: updated, message: '设备已报废' }); }); // 恢复报废设备(管理员):撤销报废,状态恢复为闲置 app.post('/api/devices/:id/restore', requireAdmin, (req, res) => { const device = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); if (!device) { return res.status(404).json({ success: false, message: '设备不存在' }); } if (device.status !== '已报废') { return res.status(400).json({ success: false, message: '仅报废状态的设备可恢复' }); } db.prepare(` UPDATE devices SET status='闲置', scrap_reason='', scrap_date='', scrap_operator='', updated_at=datetime('now','localtime') WHERE id=? `).run(req.params.id); logDeviceAction(req.params.id, device.name, '恢复', '报废已撤销,状态恢复为闲置', getOperator(req)); const updated = db.prepare('SELECT * FROM devices WHERE id = ?').get(req.params.id); res.json({ success: true, data: updated, message: '报废已撤销,设备状态恢复为闲置' }); }); // ==================== 人员管理 API ==================== // 获取所有人员(LEFT JOIN 账户信息) app.get('/api/personnel', (req, res) => { const { keyword, department, status } = req.query; let sql = ` SELECT p.*, u.id AS user_id, u.username AS account_username, u.role AS account_role FROM personnel p LEFT JOIN users u ON u.personnel_id = p.id WHERE 1=1 `; const params = []; if (keyword) { sql += ' AND (p.name LIKE ? OR p.employee_id LIKE ? OR p.phone LIKE ?)'; params.push(`%${keyword}%`, `%${keyword}%`, `%${keyword}%`); } if (department) { sql += ' AND p.department = ?'; params.push(department); } if (status) { sql += ' AND p.status = ?'; params.push(status); } sql += ' ORDER BY p.created_at DESC'; const personnel = db.prepare(sql).all(...params); res.json({ success: true, data: personnel }); }); // 获取单个人员 app.get('/api/personnel/:id', (req, res) => { const person = db.prepare('SELECT * FROM personnel WHERE id = ?').get(req.params.id); if (person) { res.json({ success: true, data: person }); } else { res.status(404).json({ success: false, message: '人员不存在' }); } }); // 添加人员(管理员),可同时创建登录账户 app.post('/api/personnel', requireAdmin, (req, res) => { const { name, employee_id, department, position, phone, email, status, entry_date, description, account } = req.body; const id = uuidv4(); // 校验账户用户名唯一 if (account && account.createAccount) { if (!account.username || !account.password) { return res.status(400).json({ success: false, message: '创建账户需填写用户名和密码' }); } const exist = db.prepare('SELECT id FROM users WHERE username = ?').get(account.username); if (exist) { return res.status(400).json({ success: false, message: `用户名 ${account.username} 已存在` }); } } db.prepare(` INSERT INTO personnel (id, name, employee_id, department, position, phone, email, status, entry_date, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run(id, name, employee_id || '', department || '', position || '', phone || '', email || '', status || '在职', entry_date || '', description || ''); // 同步创建账户 if (account && account.createAccount) { db.prepare(` INSERT INTO users (id, username, password, role, display_name, personnel_id) VALUES (?, ?, ?, ?, ?, ?) `).run(uuidv4(), account.username, account.password, account.role || 'user', name, id); } const person = db.prepare(` SELECT p.*, u.id AS user_id, u.username AS account_username, u.role AS account_role FROM personnel p LEFT JOIN users u ON u.personnel_id = p.id WHERE p.id = ? `).get(id); res.json({ success: true, data: person, message: account && account.createAccount ? '人员及账户创建成功' : '人员添加成功' }); }); // 更新人员(管理员),同步维护关联账户 app.put('/api/personnel/:id', requireAdmin, (req, res) => { const { name, employee_id, department, position, phone, email, status, entry_date, description, account } = req.body; db.prepare(` UPDATE personnel SET name=?, employee_id=?, department=?, position=?, phone=?, email=?, status=?, entry_date=?, description=?, updated_at=datetime('now','localtime') WHERE id=? `).run(name, employee_id, department || '', position || '', phone || '', email || '', status, entry_date || '', description || '', req.params.id); const existUser = db.prepare('SELECT * FROM users WHERE personnel_id = ?').get(req.params.id); if (account && account.createAccount) { // 校验用户名唯一(排除自身) const dup = db.prepare('SELECT id FROM users WHERE username = ? AND personnel_id != ?').get(account.username, req.params.id); if (dup) { return res.status(400).json({ success: false, message: `用户名 ${account.username} 已存在` }); } if (existUser) { db.prepare('UPDATE users SET username=?, role=?, display_name=? WHERE personnel_id=?') .run(account.username, account.role || 'user', name, req.params.id); // 仅当提供新密码时更新密码 if (account.password) { db.prepare('UPDATE users SET password=? WHERE personnel_id=?').run(account.password, req.params.id); } } else { if (!account.username || !account.password) { return res.status(400).json({ success: false, message: '创建账户需填写用户名和密码' }); } db.prepare(` INSERT INTO users (id, username, password, role, display_name, personnel_id) VALUES (?, ?, ?, ?, ?, ?) `).run(uuidv4(), account.username, account.password, account.role || 'user', name, req.params.id); } } else if (existUser) { // 关闭账户:删除关联用户 db.prepare('DELETE FROM users WHERE personnel_id = ?').run(req.params.id); } const person = db.prepare(` SELECT p.*, u.id AS user_id, u.username AS account_username, u.role AS account_role FROM personnel p LEFT JOIN users u ON u.personnel_id = p.id WHERE p.id = ? `).get(req.params.id); res.json({ success: true, data: person, message: '人员更新成功' }); }); // 删除人员(管理员),同时删除关联账户 app.delete('/api/personnel/:id', requireAdmin, (req, res) => { db.prepare('DELETE FROM users WHERE personnel_id = ?').run(req.params.id); db.prepare('DELETE FROM personnel WHERE id = ?').run(req.params.id); res.json({ success: true, message: '人员及关联账户删除成功' }); }); // 人员统计 app.get('/api/personnel/stats/summary', (req, res) => { const total = db.prepare('SELECT COUNT(*) as count FROM personnel').get().count; const active = db.prepare("SELECT COUNT(*) as count FROM personnel WHERE status='在职'").get().count; const withAccount = db.prepare('SELECT COUNT(*) as count FROM personnel WHERE id IN (SELECT personnel_id FROM users WHERE personnel_id IS NOT NULL)').get().count; const departments = db.prepare('SELECT department, COUNT(*) as count FROM personnel GROUP BY department').all(); res.json({ success: true, data: { total, active, withAccount, departments } }); }); // ==================== 通知管理 API ==================== // 获取所有通知 app.get('/api/notifications', (req, res) => { const { keyword, type, priority, is_read } = req.query; let sql = 'SELECT * FROM notifications WHERE 1=1'; const params = []; if (keyword) { sql += ' AND (title LIKE ? OR content LIKE ?)'; params.push(`%${keyword}%`, `%${keyword}%`); } if (type) { sql += ' AND type = ?'; params.push(type); } if (priority) { sql += ' AND priority = ?'; params.push(priority); } if (is_read !== undefined && is_read !== '') { sql += ' AND is_read = ?'; params.push(parseInt(is_read)); } sql += ' ORDER BY created_at DESC'; const notifications = db.prepare(sql).all(...params); res.json({ success: true, data: notifications }); }); // 获取单个通知 app.get('/api/notifications/:id', (req, res) => { const notif = db.prepare('SELECT * FROM notifications WHERE id = ?').get(req.params.id); if (notif) { res.json({ success: true, data: notif }); } else { res.status(404).json({ success: false, message: '通知不存在' }); } }); // 添加通知(管理员) app.post('/api/notifications', requireAdmin, (req, res) => { const { title, content, type, priority, target } = req.body; const id = uuidv4(); db.prepare(` INSERT INTO notifications (id, title, content, type, priority, is_read, target) VALUES (?, ?, ?, ?, ?, 0, ?) `).run(id, title, content, type || '普通', priority || '普通', target || '全部'); const notif = db.prepare('SELECT * FROM notifications WHERE id = ?').get(id); res.json({ success: true, data: notif, message: '通知发布成功' }); }); // 更新通知(管理员) app.put('/api/notifications/:id', requireAdmin, (req, res) => { const { title, content, type, priority, is_read, target } = req.body; db.prepare(` UPDATE notifications SET title=?, content=?, type=?, priority=?, is_read=?, target=?, updated_at=datetime('now','localtime') WHERE id=? `).run(title, content, type, priority, is_read ? 1 : 0, target, req.params.id); const notif = db.prepare('SELECT * FROM notifications WHERE id = ?').get(req.params.id); res.json({ success: true, data: notif, message: '通知更新成功' }); }); // 标记已读 app.patch('/api/notifications/:id/read', (req, res) => { db.prepare("UPDATE notifications SET is_read=1, updated_at=datetime('now','localtime') WHERE id=?").run(req.params.id); const notif = db.prepare('SELECT * FROM notifications WHERE id = ?').get(req.params.id); res.json({ success: true, data: notif, message: '已标记为已读' }); }); // 全部标记已读 app.patch('/api/notifications/read-all', (req, res) => { db.prepare("UPDATE notifications SET is_read=1, updated_at=datetime('now','localtime') WHERE is_read=0").run(); res.json({ success: true, message: '全部已标记为已读' }); }); // 删除通知(管理员) app.delete('/api/notifications/:id', requireAdmin, (req, res) => { db.prepare('DELETE FROM notifications WHERE id = ?').run(req.params.id); res.json({ success: true, message: '通知删除成功' }); }); // 通知统计 app.get('/api/notifications/stats/summary', (req, res) => { const total = db.prepare('SELECT COUNT(*) as count FROM notifications').get().count; const unread = db.prepare('SELECT COUNT(*) as count FROM notifications WHERE is_read=0').get().count; const high = db.prepare("SELECT COUNT(*) as count FROM notifications WHERE priority='高' AND is_read=0").get().count; res.json({ success: true, data: { total, unread, high } }); }); // 清空已读通知(管理员) app.delete('/api/notifications/read/clear', requireAdmin, (req, res) => { const result = db.prepare('DELETE FROM notifications WHERE is_read = 1').run(); res.json({ success: true, message: `已清理 ${result.changes} 条已读通知` }); }); // ==================== 账户管理 API ==================== // 记录账户操作日志 function logUserAction(username, action, detail, operator) { db.prepare(` INSERT INTO user_logs (id, username, action, detail, operator) VALUES (?, ?, ?, ?, ?) `).run(uuidv4(), username || '', action, detail || '', operator || ''); } // 获取所有账户(管理员,含关联人员) app.get('/api/users', requireAdmin, (req, res) => { const { keyword, role } = req.query; let sql = ` SELECT u.id, u.username, u.role, u.display_name, u.created_at, u.last_login, u.personnel_id, p.name AS personnel_name, p.department FROM users u LEFT JOIN personnel p ON u.personnel_id = p.id WHERE 1=1 `; const params = []; if (keyword) { sql += ' AND (u.username LIKE ? OR u.display_name LIKE ?)'; params.push(`%${keyword}%`, `%${keyword}%`); } if (role) { sql += ' AND u.role = ?'; params.push(role); } sql += ' ORDER BY u.created_at ASC'; const onlineMap = getOnlineUserMap(); const data = db.prepare(sql).all(...params).map(u => { const active = onlineMap.get(u.id); return { ...u, is_online: !!active, last_active: active ? formatActive(active) : null }; }); res.json({ success: true, data }); }); // 账户操作日志(管理员) app.get('/api/users/logs', requireAdmin, (req, res) => { const logs = db.prepare('SELECT * FROM user_logs ORDER BY created_at DESC LIMIT 500').all(); res.json({ success: true, data: logs }); }); // 账户统计(管理员) app.get('/api/users/stats/summary', requireAdmin, (req, res) => { const total = db.prepare('SELECT COUNT(*) as count FROM users').get().count; const admins = db.prepare("SELECT COUNT(*) as count FROM users WHERE role='admin'").get().count; const users = total - admins; const online = getOnlineUserMap().size; res.json({ success: true, data: { total, admins, users, online } }); }); // 创建独立账户(管理员) app.post('/api/users', requireAdmin, (req, res) => { const { username, password, role, display_name } = req.body; if (!username || !password) { return res.status(400).json({ success: false, message: '用户名和密码不能为空' }); } const exist = db.prepare('SELECT id FROM users WHERE username = ?').get(username); if (exist) { return res.status(400).json({ success: false, message: `用户名 ${username} 已存在` }); } const id = uuidv4(); db.prepare(` INSERT INTO users (id, username, password, role, display_name) VALUES (?, ?, ?, ?, ?) `).run(id, username, password, role === 'admin' ? 'admin' : 'user', display_name || username); logUserAction(username, '创建', `创建账户,角色:${role === 'admin' ? '管理员' : '普通用户'}`, getOperator(req)); const user = db.prepare('SELECT id, username, role, display_name, created_at FROM users WHERE id = ?').get(id); res.json({ success: true, data: user, message: '账户创建成功' }); }); // 更新账户信息(管理员:用户名/角色/显示名) app.put('/api/users/:id', requireAdmin, (req, res) => { const { username, role, display_name } = req.body; const target = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id); if (!target) { return res.status(404).json({ success: false, message: '账户不存在' }); } // 保护最后一个管理员 if (target.role === 'admin' && role === 'user') { const adminCount = db.prepare("SELECT COUNT(*) as count FROM users WHERE role='admin'").get().count; if (adminCount <= 1) { return res.status(400).json({ success: false, message: '不能降级最后一个管理员账户' }); } } const dup = db.prepare('SELECT id FROM users WHERE username = ? AND id != ?').get(username, req.params.id); if (dup) { return res.status(400).json({ success: false, message: `用户名 ${username} 已存在` }); } db.prepare('UPDATE users SET username=?, role=?, display_name=? WHERE id=?') .run(username, role === 'admin' ? 'admin' : 'user', display_name || username, req.params.id); // 同步会话中的用户信息 for (const [token, sessionUser] of sessions.entries()) { if (sessionUser.id === req.params.id) { sessions.set(token, { id: req.params.id, username, role: role === 'admin' ? 'admin' : 'user', display_name: display_name || username }); } } logUserAction(username, '修改', '账户信息已更新', getOperator(req)); const user = db.prepare('SELECT id, username, role, display_name, created_at FROM users WHERE id = ?').get(req.params.id); res.json({ success: true, data: user, message: '账户更新成功' }); }); // 重置账户密码(管理员) app.patch('/api/users/:id/reset-password', requireAdmin, (req, res) => { const { password } = req.body; if (!password || password.length < 6) { return res.status(400).json({ success: false, message: '新密码不能为空且不少于6位' }); } const target = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id); if (!target) { return res.status(404).json({ success: false, message: '账户不存在' }); } db.prepare('UPDATE users SET password=? WHERE id=?').run(password, req.params.id); logUserAction(target.username, '重置密码', '密码已被管理员重置', getOperator(req)); res.json({ success: true, message: `已重置 ${target.username} 的密码` }); }); // 删除账户(管理员) app.delete('/api/users/:id', requireAdmin, (req, res) => { const target = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id); if (!target) { return res.status(404).json({ success: false, message: '账户不存在' }); } if (req.user && req.user.id === target.id) { return res.status(400).json({ success: false, message: '不能删除当前登录的账户' }); } if (target.role === 'admin') { const adminCount = db.prepare("SELECT COUNT(*) as count FROM users WHERE role='admin'").get().count; if (adminCount <= 1) { return res.status(400).json({ success: false, message: '不能删除最后一个管理员账户' }); } } db.prepare('DELETE FROM users WHERE id = ?').run(req.params.id); // 使关联的会话失效 for (const [token, sessionUser] of sessions.entries()) { if (sessionUser.id === target.id) dropSession(token); } logUserAction(target.username, '删除', '账户已删除', getOperator(req)); res.json({ success: true, message: '账户删除成功' }); }); // 账户操作日志清理(管理员) app.delete('/api/users/logs/clear', requireAdmin, (req, res) => { db.prepare('DELETE FROM user_logs').run(); res.json({ success: true, message: '账户日志已清空' }); }); // ==================== 消息对话 API ==================== // 校验当前用户是否为会话成员 function isConversationMember(convId, userId) { return !!db.prepare('SELECT 1 FROM conversation_members WHERE conversation_id = ? AND user_id = ?').get(convId, userId); } function getUserDisplayName(userId) { const u = db.prepare('SELECT display_name, username FROM users WHERE id = ?').get(userId); return u ? (u.display_name || u.username) : ''; } // 媒体消息在会话列表的预览文案 function messagePreview(m) { if (m.msg_type === 'image') return '[图片]'; if (m.msg_type === 'video') return '[视频]'; if (m.msg_type === 'file') return '[文件] ' + (m.content || ''); return m.content; } // 上传聊天附件(图片/视频/文件),返回 fileId 供发送消息时引用 app.post('/api/chat/upload', upload.single('file'), (req, res) => { if (!req.file) { return res.status(400).json({ success: false, message: '未接收到文件' }); } const id = uuidv4(); db.prepare('INSERT INTO chat_files (id, uploader_id, file_name, stored_name, file_size, file_mime) VALUES (?, ?, ?, ?, ?, ?)') .run(id, req.user.id, req.file.originalname, req.file.filename, req.file.size, req.file.mimetype || ''); res.json({ success: true, data: { fileId: id, fileName: req.file.originalname, fileSize: req.file.size, fileMime: req.file.mimetype || '' } }); }); // multer 错误处理(超大文件等) app.use('/api/chat/upload', (err, req, res, next) => { if (err) { const msg = err.code === 'LIMIT_FILE_SIZE' ? '文件超过 100MB 限制' : '上传失败'; return res.status(400).json({ success: false, message: msg }); } next(); }); // 下载/预览聊天附件:仅上传者可访问,或所在会话的成员可访问 app.get('/api/chat/files/:id', (req, res) => { const file = db.prepare('SELECT * FROM chat_files WHERE id = ?').get(req.params.id); if (!file) { return res.status(404).json({ success: false, message: '文件不存在' }); } if (file.uploader_id !== req.user.id) { const inConv = db.prepare(` SELECT 1 FROM conversation_members m WHERE m.user_id = ? AND EXISTS ( SELECT 1 FROM messages msg WHERE msg.file_id = ? AND msg.conversation_id = m.conversation_id ) `).get(req.user.id, file.id); if (!inConv) { return res.status(403).json({ success: false, message: '无权访问该文件' }); } } const filePath = path.join(uploadsDir, file.stored_name); if (!fs.existsSync(filePath)) { return res.status(404).json({ success: false, message: '文件已丢失' }); } const inline = (file.file_mime || '').startsWith('image/') || (file.file_mime || '').startsWith('video/'); const encoded = encodeURIComponent(file.file_name); res.setHeader('Content-Type', file.file_mime || 'application/octet-stream'); res.setHeader('Content-Disposition', `${inline ? 'inline' : 'attachment'}; filename*=UTF-8''${encoded}`); fs.createReadStream(filePath).pipe(res); }); // 全部用户简表(选择聊天对象/建群成员,含在线状态) app.get('/api/chat/users', (req, res) => { const rows = db.prepare('SELECT id, username, display_name, role FROM users ORDER BY created_at ASC').all(); const onlineMap = getOnlineUserMap(); const data = rows.map(u => ({ ...u, is_online: onlineMap.has(u.id) })); res.json({ success: true, data }); }); // 当前用户的会话列表(含最后一条消息与未读数) app.get('/api/chat/conversations', (req, res) => { const me = req.user.id; const convs = db.prepare(` SELECT c.id, c.type, c.name, c.creator_id, c.created_at, m.last_read_message_id FROM conversations c JOIN conversation_members m ON m.conversation_id = c.id WHERE m.user_id = ? `).all(me); const data = convs.map(c => { const members = db.prepare(` SELECT u.id, u.display_name, u.username FROM conversation_members m JOIN users u ON u.id = m.user_id WHERE m.conversation_id = ? `).all(c.id); const last = db.prepare('SELECT * FROM messages WHERE conversation_id = ? ORDER BY id DESC LIMIT 1').get(c.id); const unread = db.prepare('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ? AND id > ? AND sender_id != ?') .get(c.id, c.last_read_message_id || 0, me).count; return { id: c.id, type: c.type, name: c.type === 'group' ? c.name : ((members.find(x => x.id !== me) || {}).display_name || ''), creator_id: c.creator_id, member_count: members.length, members, last_message: last ? { content: messagePreview(last), created_at: last.created_at, sender_name: getUserDisplayName(last.sender_id) } : null, unread }; }); // 按最后消息时间倒序(无消息的按创建时间) data.sort((a, b) => (b.last_message?.created_at || b.created_at || '').localeCompare(a.last_message?.created_at || a.created_at || '')); res.json({ success: true, data }); }); // 创建会话:单聊(type=direct + userId,已有则复用)或群聊(type=group + name + memberIds) app.post('/api/chat/conversations', (req, res) => { const { type, userId, name, memberIds } = req.body; const me = req.user; if (type === 'direct') { if (!userId || userId === me.id) { return res.status(400).json({ success: false, message: '请选择对话对象' }); } const target = db.prepare('SELECT id FROM users WHERE id = ?').get(userId); if (!target) { return res.status(404).json({ success: false, message: '用户不存在' }); } // 复用已有的单聊会话 const existing = db.prepare(` SELECT c.id FROM conversations c WHERE c.type = 'direct' AND EXISTS (SELECT 1 FROM conversation_members m WHERE m.conversation_id = c.id AND m.user_id = ?) AND EXISTS (SELECT 1 FROM conversation_members m WHERE m.conversation_id = c.id AND m.user_id = ?) `).get(me.id, userId); if (existing) { return res.json({ success: true, data: { id: existing.id }, message: '会话已存在' }); } const id = uuidv4(); db.prepare("INSERT INTO conversations (id, type, creator_id) VALUES (?, 'direct', ?)").run(id, me.id); const insMember = db.prepare('INSERT INTO conversation_members (conversation_id, user_id, role) VALUES (?, ?, ?)'); insMember.run(id, me.id, 'member'); insMember.run(id, userId, 'member'); return res.json({ success: true, data: { id }, message: '会话创建成功' }); } if (type === 'group') { if (!name || !String(name).trim()) { return res.status(400).json({ success: false, message: '请输入群名称' }); } if (!Array.isArray(memberIds) || memberIds.filter(id => id !== me.id).length < 1) { return res.status(400).json({ success: false, message: '请至少选择1名其他群成员' }); } const id = uuidv4(); db.prepare("INSERT INTO conversations (id, type, name, creator_id) VALUES (?, 'group', ?, ?)").run(id, String(name).trim(), me.id); const insMember = db.prepare('INSERT OR IGNORE INTO conversation_members (conversation_id, user_id, role) VALUES (?, ?, ?)'); insMember.run(id, me.id, 'owner'); for (const uid of memberIds) { if (uid !== me.id && db.prepare('SELECT id FROM users WHERE id = ?').get(uid)) { insMember.run(id, uid, 'member'); } } logUserAction(me.username, '创建群组', `创建群组:${String(name).trim()}`, me.display_name || me.username); return res.json({ success: true, data: { id }, message: '群组创建成功' }); } return res.status(400).json({ success: false, message: '不支持的会话类型' }); }); // 获取会话消息(默认最新 100 条;after 游标增量拉取),同时标记已读 app.get('/api/chat/conversations/:id/messages', (req, res) => { const conv = db.prepare('SELECT * FROM conversations WHERE id = ?').get(req.params.id); if (!conv || !isConversationMember(req.params.id, req.user.id)) { return res.status(404).json({ success: false, message: '会话不存在' }); } // 标记已读到会话最新一条(先于返回,保证本次返回的消息自带已读回执) const latest = db.prepare('SELECT MAX(id) as maxId FROM messages WHERE conversation_id = ?').get(req.params.id); if (latest && latest.maxId) { db.prepare('UPDATE conversation_members SET last_read_message_id = MAX(last_read_message_id, ?) WHERE conversation_id = ? AND user_id = ?') .run(latest.maxId, req.params.id, req.user.id); } const after = parseInt(req.query.after) || 0; const limit = Math.min(parseInt(req.query.limit) || 100, 200); let rows; if (after > 0) { rows = db.prepare('SELECT * FROM messages WHERE conversation_id = ? AND id > ? ORDER BY id ASC LIMIT ?').all(req.params.id, after, limit); } else { rows = db.prepare('SELECT * FROM messages WHERE conversation_id = ? ORDER BY id DESC LIMIT ?').all(req.params.id, limit).reverse(); } const readMap = buildReadStatusMap(req.params.id, req.user.id); const fileCache = {}; const senderCache = {}; const data = rows.map(m => { if (!senderCache[m.sender_id]) senderCache[m.sender_id] = getUserDisplayName(m.sender_id); const item = { ...m, sender_name: senderCache[m.sender_id], read_status: readMap[m.id] || null }; if (m.file_id) { if (!fileCache[m.file_id]) fileCache[m.file_id] = db.prepare('SELECT * FROM chat_files WHERE id = ?').get(m.file_id); const f = fileCache[m.file_id]; if (f) { item.file_name = f.file_name; item.file_size = f.file_size; item.file_mime = f.file_mime; item.file_url = `/api/chat/files/${f.id}`; } } return item; }); res.json({ success: true, data }); }); // 计算会话内各消息的已读回执:read_count=已读的其他成员数,total_others=其他成员总数,unread_users=未读成员名单 function buildReadStatusMap(convId, meId) { const members = db.prepare('SELECT user_id, last_read_message_id FROM conversation_members WHERE conversation_id = ?').all(convId); const others = members.filter(m => m.user_id !== meId); const msgs = db.prepare('SELECT id, sender_id FROM messages WHERE conversation_id = ?').all(convId); const nameCache = {}; const map = {}; for (const m of msgs) { const unreadUsers = others .filter(o => (o.last_read_message_id || 0) < m.id) .map(o => { if (!nameCache[o.user_id]) nameCache[o.user_id] = getUserDisplayName(o.user_id); return { id: o.user_id, name: nameCache[o.user_id] }; }); map[m.id] = { read_count: others.length - unreadUsers.length, total_others: others.length, unread_users: unreadUsers }; } return map; } // 轻量已读回执轮询(不标记已读,仅返回各消息的已读进度) app.get('/api/chat/conversations/:id/read-status', (req, res) => { const conv = db.prepare('SELECT * FROM conversations WHERE id = ?').get(req.params.id); if (!conv || !isConversationMember(req.params.id, req.user.id)) { return res.status(404).json({ success: false, message: '会话不存在' }); } res.json({ success: true, data: buildReadStatusMap(req.params.id, req.user.id) }); }); // 发送消息:文本(content)或媒体(type=image/video/file + fileId,引用已上传附件) app.post('/api/chat/conversations/:id/messages', (req, res) => { const msgType = ['text', 'image', 'video', 'file'].includes(req.body.type) ? req.body.type : 'text'; const content = String(req.body.content || '').trim(); const conv = db.prepare('SELECT * FROM conversations WHERE id = ?').get(req.params.id); if (!conv || !isConversationMember(req.params.id, req.user.id)) { return res.status(404).json({ success: false, message: '会话不存在' }); } let fileId = ''; let finalContent = content; if (msgType !== 'text') { const file = db.prepare('SELECT * FROM chat_files WHERE id = ?').get(req.body.fileId); if (!file || file.uploader_id !== req.user.id) { return res.status(400).json({ success: false, message: '附件不存在,请重新上传' }); } fileId = file.id; if (!finalContent) finalContent = file.file_name; } else if (!content) { return res.status(400).json({ success: false, message: '消息不能为空' }); } if (finalContent.length > 2000) { return res.status(400).json({ success: false, message: '消息过长(最多 2000 字)' }); } const result = db.prepare('INSERT INTO messages (conversation_id, sender_id, content, msg_type, file_id) VALUES (?, ?, ?, ?, ?)') .run(req.params.id, req.user.id, finalContent, msgType, fileId); const msg = db.prepare('SELECT * FROM messages WHERE id = ?').get(result.lastInsertRowid); // 自己发送的消息直接标记已读 db.prepare('UPDATE conversation_members SET last_read_message_id = ? WHERE conversation_id = ? AND user_id = ?') .run(msg.id, req.params.id, req.user.id); const data = { ...msg, sender_name: req.user.display_name || req.user.username }; if (fileId) { const f = db.prepare('SELECT * FROM chat_files WHERE id = ?').get(fileId); data.file_name = f.file_name; data.file_size = f.file_size; data.file_mime = f.file_mime; data.file_url = `/api/chat/files/${f.id}`; } // 推送给会话其他成员(群聊标题=群名,单聊标题=发送者) const memberRows = db.prepare('SELECT user_id FROM conversation_members WHERE conversation_id = ?').all(req.params.id); const senderName = req.user.display_name || req.user.username; const pushTitle = conv.type === 'group' ? (conv.name || '群聊消息') : senderName; pushToMany(memberRows.map(m => m.user_id), 'chat', pushTitle, `${senderName}:${messagePreview(msg)}`, req.user.id); res.json({ success: true, data }); }); // 删除/解散会话(群聊仅群主可解散,单聊任一方可删除) app.delete('/api/chat/conversations/:id', (req, res) => { const conv = db.prepare('SELECT * FROM conversations WHERE id = ?').get(req.params.id); if (!conv) { return res.status(404).json({ success: false, message: '会话不存在' }); } if (!isConversationMember(req.params.id, req.user.id)) { return res.status(403).json({ success: false, message: '您不是该会话的成员' }); } if (conv.type === 'group' && conv.creator_id !== req.user.id) { return res.status(403).json({ success: false, message: '只有群主可以解散群组' }); } // 清理会话内附件的磁盘文件与记录 const fileRows = db.prepare(` SELECT f.id, f.stored_name FROM chat_files f WHERE f.id IN (SELECT file_id FROM messages WHERE conversation_id = ? AND file_id != '') AND NOT EXISTS (SELECT 1 FROM messages m2 WHERE m2.file_id = f.id AND m2.conversation_id != ?) `).all(req.params.id, req.params.id); for (const f of fileRows) { const p = path.join(uploadsDir, f.stored_name); if (fs.existsSync(p)) fs.unlinkSync(p); } if (fileRows.length) { db.prepare(`DELETE FROM chat_files WHERE id IN (SELECT file_id FROM messages WHERE conversation_id = ? AND file_id != '')`).run(req.params.id); } db.prepare('DELETE FROM messages WHERE conversation_id = ?').run(req.params.id); db.prepare('DELETE FROM conversation_members WHERE conversation_id = ?').run(req.params.id); db.prepare('DELETE FROM conversations WHERE id = ?').run(req.params.id); res.json({ success: true, message: conv.type === 'group' ? '群组已解散' : '会话已删除' }); }); // 移除群成员(群主可移除任意成员,群主本人不可被移除) app.delete('/api/chat/conversations/:id/members/:userId', (req, res) => { const conv = db.prepare('SELECT * FROM conversations WHERE id = ?').get(req.params.id); if (!conv || conv.type !== 'group') { return res.status(404).json({ success: false, message: '群聊不存在' }); } if (!isConversationMember(conv.id, req.user.id)) { return res.status(403).json({ success: false, message: '您不是该群聊的成员' }); } const targetId = req.params.userId; if (targetId === conv.creator_id) { return res.status(400).json({ success: false, message: '群主不能被移除' }); } if (!isConversationMember(conv.id, targetId)) { return res.status(404).json({ success: false, message: '该用户不在群聊中' }); } if (req.user.id !== conv.creator_id) { return res.status(403).json({ success: false, message: '只有群主可以移除群成员' }); } db.prepare('DELETE FROM conversation_members WHERE conversation_id = ? AND user_id = ?').run(conv.id, targetId); res.json({ success: true, message: '已将该成员移出群聊' }); }); // ==================== 认证 API ==================== // 登录 app.post('/api/auth/login', (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ success: false, message: '用户名和密码不能为空' }); } const user = db.prepare('SELECT id, username, role, display_name FROM users WHERE username = ? AND password = ?').get(username, password); if (!user) { return res.status(401).json({ success: false, message: '用户名或密码错误' }); } const token = uuidv4(); sessions.set(token, user); touchSession(token); // 记录最后登录时间 db.prepare("UPDATE users SET last_login=datetime('now','localtime') WHERE id=?").run(user.id); res.json({ success: true, message: '登录成功', data: { ...user, token } }); }); // 获取当前登录用户 app.get('/api/auth/me', (req, res) => { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token || !sessions.has(token)) { return res.status(401).json({ success: false, message: '未登录' }); } res.json({ success: true, data: sessions.get(token) }); }); // 登出 app.post('/api/auth/logout', (req, res) => { const token = req.headers.authorization?.replace('Bearer ', ''); if (token) dropSession(token); res.json({ success: true, message: '登出成功' }); }); // 修改自己的密码(已登录用户) app.patch('/api/auth/password', (req, res) => { const { oldPassword, newPassword } = req.body; if (!oldPassword || !newPassword || newPassword.length < 6) { return res.status(400).json({ success: false, message: '请输入原密码,且新密码不少于6位' }); } const dbUser = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id); if (!dbUser || dbUser.password !== oldPassword) { return res.status(400).json({ success: false, message: '原密码不正确' }); } db.prepare('UPDATE users SET password=? WHERE id=?').run(newPassword, req.user.id); logUserAction(dbUser.username, '修改密码', '用户自行修改密码', dbUser.display_name || dbUser.username); // 修改成功后使当前会话失效,要求用新密码重新登录 const token = req.headers.authorization?.replace('Bearer ', ''); if (token) dropSession(token); res.json({ success: true, message: '密码修改成功,请使用新密码重新登录' }); }); // ==================== 天气 API ==================== function getWeatherDesc(code) { const map = { 0: '晴朗', 1: '多云', 2: '多云', 3: '阴天', 45: '雾', 48: '雾凇', 51: '毛毛雨', 53: '毛毛雨', 55: '毛毛雨', 56: '冻雨', 57: '冻雨', 61: '小雨', 63: '中雨', 65: '大雨', 66: '冻雨', 67: '冻雨', 71: '小雪', 73: '中雪', 75: '大雪', 77: '雪粒', 80: '阵雨', 81: '阵雨', 82: '强阵雨', 85: '阵雪', 86: '强阵雪', 95: '雷雨', 96: '雷雨伴冰雹', 99: '强雷雨伴冰雹' }; return map[code] || '未知'; } app.get('/api/weather/current', async (req, res) => { try { const city = req.query.city || 'beijing'; // 地理编码 const geoRes = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1&language=zh&format=json`); const geoData = await geoRes.json(); if (!geoData.results || geoData.results.length === 0) { return res.status(404).json({ success: false, message: '未找到该城市' }); } const { latitude, longitude, name, country } = geoData.results[0]; // 获取天气 const weatherRes = await fetch( `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&daily=temperature_2m_max,temperature_2m_min,weather_code&timezone=Asia/Shanghai&forecast_days=1` ); const weatherData = await weatherRes.json(); const current = weatherData.current; const daily = weatherData.daily; res.json({ success: true, data: { city: name, country, current: { temp: current.temperature_2m, humidity: current.relative_humidity_2m, windSpeed: current.wind_speed_10m, weather: getWeatherDesc(current.weather_code), code: current.weather_code }, daily: { maxTemp: daily.temperature_2m_max[0], minTemp: daily.temperature_2m_min[0], weather: getWeatherDesc(daily.weather_code[0]), code: daily.weather_code[0] } } }); } catch (err) { console.error('获取天气失败:', err); res.status(500).json({ success: false, message: '获取天气失败' }); } }); // ==================== 全局统计 API ==================== app.get('/api/dashboard/stats', (req, res) => { const deviceTotal = db.prepare('SELECT COUNT(*) as count FROM devices').get().count; const deviceNormal = db.prepare("SELECT COUNT(*) as count FROM devices WHERE status='正常'").get().count; const deviceRepairing = db.prepare("SELECT COUNT(*) as count FROM devices WHERE status='维修中'").get().count; const deviceScrapped = db.prepare("SELECT COUNT(*) as count FROM devices WHERE status='已报废'").get().count; const personnelTotal = db.prepare('SELECT COUNT(*) as count FROM personnel').get().count; const personnelActive = db.prepare("SELECT COUNT(*) as count FROM personnel WHERE status='在职'").get().count; const notifTotal = db.prepare('SELECT COUNT(*) as count FROM notifications').get().count; const notifUnread = db.prepare('SELECT COUNT(*) as count FROM notifications WHERE is_read=0').get().count; res.json({ success: true, data: { deviceTotal, deviceNormal, deviceRepairing, deviceScrapped, personnelTotal, personnelActive, notifTotal, notifUnread } }); }); // SPA 路由回退 - 所有未匹配的请求返回前端 app.get('*', (req, res) => { res.sendFile(path.join(clientBuildPath, 'index.html')); }); app.listen(PORT, '0.0.0.0', () => { console.log(`\n╔══════════════════════════════════════════╗`); console.log(`║ 设备管理系统 已启动 ║`); console.log(`║ 地址: http://localhost:${PORT} ║`); console.log(`║ 按 Ctrl+C 停止服务 ║`); console.log(`╚══════════════════════════════════════════╝\n`); });