1031 lines
44 KiB
JavaScript
1031 lines
44 KiB
JavaScript
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}¤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`);
|
||
});
|