Files
dms/server.js
T
2026-08-11 10:03:40 +08:00

1031 lines
44 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const cors = require('cors');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const db = require('./database');
const app = express();
const PORT = process.env.PORT || 3001;
// 内存会话存储
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();
}
const token = req.headers.authorization?.replace('Bearer ', '');
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 || '');
}
// 写入维修记录
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));
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));
}
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) : '';
}
// 全部用户简表(选择聊天对象/建群成员,含在线状态)
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: last.content, 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 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 senderCache = {};
const data = rows.map(m => {
if (!senderCache[m.sender_id]) senderCache[m.sender_id] = getUserDisplayName(m.sender_id);
return { ...m, sender_name: senderCache[m.sender_id] };
});
// 标记已读到最新一条
const maxId = data.length ? data[data.length - 1].id : after;
if (maxId > 0) {
db.prepare('UPDATE conversation_members SET last_read_message_id = MAX(last_read_message_id, ?) WHERE conversation_id = ? AND user_id = ?')
.run(maxId, req.params.id, req.user.id);
}
res.json({ success: true, data });
});
// 发送消息
app.post('/api/chat/conversations/:id/messages', (req, res) => {
const content = String(req.body.content || '').trim();
if (!content) {
return res.status(400).json({ success: false, message: '消息不能为空' });
}
if (content.length > 2000) {
return res.status(400).json({ success: false, message: '消息过长(最多 2000 字)' });
}
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 result = db.prepare('INSERT INTO messages (conversation_id, sender_id, content) VALUES (?, ?, ?)')
.run(req.params.id, req.user.id, content);
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);
res.json({ success: true, data: { ...msg, sender_name: req.user.display_name || req.user.username } });
});
// 删除/解散会话(群聊仅群主可解散,单聊任一方可删除)
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: '只有群主可以解散群组' });
}
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' ? '群组已解散' : '会话已删除' });
});
// ==================== 认证 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}&current=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`);
});