2026-08-10 22:56:38 +08:00
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 ();
2026-08-11 10:03:40 +08:00
// 会话最近活跃时间(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 ()) } ` ;
}
2026-08-10 22:56:38 +08:00
// 登录验证中间件(仅对 /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 );
2026-08-11 10:03:40 +08:00
touchSession ( token );
2026-08-10 22:56:38 +08:00
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 = `
2026-08-11 10:03:40 +08:00
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
2026-08-10 22:56:38 +08:00
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' ;
2026-08-11 10:03:40 +08:00
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 });
2026-08-10 22:56:38 +08:00
});
// 账户操作日志(管理员)
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 ;
2026-08-11 10:03:40 +08:00
const online = getOnlineUserMap (). size ;
res . json ({ success : true , data : { total , admins , users , online } });
2026-08-10 22:56:38 +08:00
});
// 创建独立账户(管理员)
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 ()) {
2026-08-11 10:03:40 +08:00
if ( sessionUser . id === target . id ) dropSession ( token );
2026-08-10 22:56:38 +08:00
}
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 : '账户日志已清空' });
});
2026-08-11 10:03:40 +08:00
// ==================== 消息对话 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' ? '群组已解散' : '会话已删除' });
});
2026-08-10 22:56:38 +08:00
// ==================== 认证 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 );
2026-08-11 10:03:40 +08:00
touchSession ( token );
// 记录最后登录时间
db . prepare ( "UPDATE users SET last_login=datetime('now','localtime') WHERE id=?" ). run ( user . id );
2026-08-10 22:56:38 +08:00
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 ' , '' );
2026-08-11 10:03:40 +08:00
if ( token ) dropSession ( token );
2026-08-10 22:56:38 +08:00
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 );
2026-08-11 10:03:40 +08:00
// 修改成功后使当前会话失效,要求用新密码重新登录
const token = req . headers . authorization ? . replace ( 'Bearer ' , '' );
if ( token ) dropSession ( token );
res . json ({ success : true , message : '密码修改成功,请使用新密码重新登录' });
2026-08-10 22:56:38 +08:00
});
// ==================== 天气 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` );
});