2026-08-10 22:56:38 +08:00
const express = require ( 'express' );
const cors = require ( 'cors' );
const path = require ( 'path' );
2026-08-11 21:30:05 +08:00
const fs = require ( 'fs' );
const multer = require ( 'multer' );
2026-08-10 22:56:38 +08:00
const { v4 : uuidv4 } = require ( 'uuid' );
const db = require ( './database' );
const app = express ();
const PORT = process . env . PORT || 3001 ;
2026-08-11 21:30:05 +08:00
// 聊天附件存储目录
const uploadsDir = path . join ( __dirname , 'uploads' );
if ( ! fs . existsSync ( uploadsDir )) {
fs . mkdirSync ( uploadsDir , { recursive : true });
}
// multer 磁盘存储:文件名用 uuid + 安全扩展名,单文件上限 100MB
const MIME_EXT = {
'image/jpeg' : '.jpg' , 'image/png' : '.png' , 'image/gif' : '.gif' , 'image/webp' : '.webp' , 'image/bmp' : '.bmp' ,
'video/mp4' : '.mp4' , 'video/webm' : '.webm' , 'video/quicktime' : '.mov' , 'video/x-msvideo' : '.avi' , 'video/x-matroska' : '.mkv'
};
const upload = multer ({
storage : multer . diskStorage ({
destination : ( req , file , cb ) => cb ( null , uploadsDir ),
filename : ( req , file , cb ) => {
const rawExt = path . extname ( file . originalname || '' ). toLowerCase ();
const ext = MIME_EXT [ file . mimetype ] || ( /^\.[a-z0-9]{1,8}$/ . test ( rawExt ) ? rawExt : '.bin' );
cb ( null , uuidv4 () + ext );
}
}),
limits : { fileSize : 100 * 1024 * 1024 }
});
2026-08-10 22:56:38 +08:00
// 内存会话存储
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 );
}
2026-08-13 14:29:10 +08:00
// 下线时间列(兼容旧库:已存在则忽略)
try {
db . prepare ( "ALTER TABLE users ADD COLUMN last_logout TEXT DEFAULT ''" ). run ();
} catch ( e ) { /* 列已存在 */ }
2026-08-11 10:03:40 +08:00
// 收集当前在线用户(用户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 ();
}
2026-08-11 21:30:05 +08:00
// 优先取请求头 token; img/video/下载链接无法携带 Authorization 头,允许查询参数回退
const token = req . headers . authorization ? . replace ( 'Bearer ' , '' ) || req . query . token ;
2026-08-10 22:56:38 +08:00
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 || '' );
}
2026-08-11 21:30:05 +08:00
// ==================== 推送事件(客户端轮询拉取的原生推送源) ====================
// 定期清理 7 天前的推送事件
function cleanPushEvents () {
db . prepare ( "DELETE FROM push_events WHERE created_at < datetime('now','localtime','-7 day')" ). run ();
}
cleanPushEvents ();
setInterval ( cleanPushEvents , 6 * 60 * 60 * 1000 );
// 向指定用户写入一条推送事件
function pushTo ( userId , kind , title , body ) {
if ( ! userId ) return ;
db . prepare ( 'INSERT INTO push_events (user_id, kind, title, body) VALUES (?, ?, ?, ?)' )
. run ( userId , kind , String ( title || '' ), String ( body || '' ). slice ( 0 , 200 ));
}
// 向多个用户推送(自动去重、跳过发送者)
function pushToMany ( userIds , kind , title , body , excludeId ) {
const seen = new Set ();
for ( const uid of userIds ) {
if ( ! uid || uid === excludeId || seen . has ( uid )) continue ;
seen . add ( uid );
pushTo ( uid , kind , title , body );
}
}
// 向指定角色的全部用户推送
function pushToRole ( role , kind , title , body , excludeId ) {
const rows = db . prepare ( 'SELECT id FROM users WHERE role = ?' ). all ( role );
pushToMany ( rows . map ( r => r . id ), kind , title , body , excludeId );
}
// 向全部用户推送
function pushToAll ( kind , title , body , excludeId ) {
const rows = db . prepare ( 'SELECT id FROM users' ). all ();
pushToMany ( rows . map ( r => r . id ), kind , title , body , excludeId );
}
// 客户端拉取自己的新推送事件(after 游标为上次拉到的最大 id)
app . get ( '/api/push/events' , ( req , res ) => {
const after = parseInt ( req . query . after ) || 0 ;
const rows = db . prepare ( 'SELECT * FROM push_events WHERE user_id = ? AND id > ? ORDER BY id ASC LIMIT 50' )
. all ( req . user . id , after );
res . json ({ success : true , data : rows });
});
2026-08-10 22:56:38 +08:00
// 写入维修记录
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 ));
2026-08-11 21:30:05 +08:00
// 推送给全部管理员处理
pushToRole ( 'admin' , 'repair_request' , '设备报修' , ` ${ getOperator ( req ) } 报修: ${ device . name }${ remark ? '( ' + remark + ') ' : '' } ` , req . user . id );
2026-08-10 22:56:38 +08:00
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 ));
2026-08-11 21:30:05 +08:00
// 维修完成推送给全部用户
pushToAll ( 'repair_done' , '设备维修完成' , `设备「 ${ device . name } 」已维修好,状态恢复正常` , req . user . id );
2026-08-10 22:56:38 +08:00
}
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 } });
});
2026-08-13 14:29:10 +08:00
// ==================== 设备借用/归还 API ====================
// 借用记录列表(可按状态/设备筛选,借用时间倒序)
app . get ( '/api/loans' , ( req , res ) => {
const { status , device_id } = req . query ;
const conds = [];
const params = [];
if ( status ) { conds . push ( 'status = ?' ); params . push ( status ); }
if ( device_id ) { conds . push ( 'device_id = ?' ); params . push ( device_id ); }
const where = conds . length ? ' WHERE ' + conds . join ( ' AND ' ) : '' ;
const rows = db . prepare ( `SELECT * FROM device_loans ${ where } ORDER BY loan_time DESC, rowid DESC LIMIT 200` ). all (... params );
res . json ({ success : true , data : rows });
});
// 创建借用(服务器自动记录借用时间)
app . post ( '/api/loans' , ( req , res ) => {
const { device_id , borrower , borrower_id , photo , remark } = req . body || {};
const device = db . prepare ( 'SELECT * FROM devices WHERE id = ?' ). get ( device_id || '' );
if ( ! device ) {
return res . status ( 400 ). json ({ success : false , message : '请选择要借用的设备' });
}
if ( ! borrower || ! String ( borrower ). trim ()) {
return res . status ( 400 ). json ({ success : false , message : '请选择借用人' });
}
const id = uuidv4 ();
db . prepare ( `
INSERT INTO device_loans (id, device_id, device_name, borrower_id, borrower, loan_photo, remark, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
` ). run ( id , device . id , device . name , borrower_id || '' , String ( borrower ). trim (), photo || '' , remark || '' , getOperator ( req ));
const row = db . prepare ( 'SELECT * FROM device_loans WHERE id = ?' ). get ( id );
logDeviceAction ( device . id , device . name , '借用' , ` ${ row . borrower } 借走设备` , getOperator ( req ));
pushToRole ( 'admin' , 'device_loan' , '设备借用' , ` ${ row . borrower } 借用了设备「 ${ device . name } 」` , req . user . id );
res . json ({ success : true , data : row , message : '借用成功' });
});
// 归还设备(服务器自动记录归还时间;设备/借用人可重新选择)
app . post ( '/api/loans/:id/return' , ( req , res ) => {
const loan = db . prepare ( 'SELECT * FROM device_loans WHERE id = ?' ). get ( req . params . id );
if ( ! loan ) {
return res . status ( 404 ). json ({ success : false , message : '借用记录不存在' });
}
if ( loan . status === '已归还' ) {
return res . status ( 400 ). json ({ success : false , message : '该设备已归还,请勿重复操作' });
}
const { device_id , borrower , borrower_id , photo } = req . body || {};
// 归还时可重新选择设备(从已有设备中选择)
let deviceId = loan . device_id ;
let deviceName = loan . device_name ;
if ( device_id && device_id !== loan . device_id ) {
const d = db . prepare ( 'SELECT id, name FROM devices WHERE id = ?' ). get ( device_id );
if ( d ) { deviceId = d . id ; deviceName = d . name ; }
}
const borrowerName = ( borrower && String ( borrower ). trim ()) || loan . borrower ;
db . prepare ( `
UPDATE device_loans
SET device_id = ?, device_name = ?, borrower = ?, borrower_id = ?,
return_photo = ?, status = '已归还', return_time = datetime('now', 'localtime')
WHERE id = ?
` ). run ( deviceId , deviceName , borrowerName , borrower_id || loan . borrower_id , photo || '' , loan . id );
const row = db . prepare ( 'SELECT * FROM device_loans WHERE id = ?' ). get ( loan . id );
logDeviceAction ( deviceId , deviceName , '归还' , ` ${ borrowerName } 归还设备` , getOperator ( req ));
pushToRole ( 'admin' , 'device_return' , '设备归还' , ` ${ borrowerName } 已归还设备「 ${ deviceName } 」` , req . user . id );
res . json ({ success : true , data : row , message : '归还成功' });
});
// 查看借用/归还照片(所有登录用户可见)
app . get ( '/api/loans/photo/:fileId' , ( req , res ) => {
const file = db . prepare ( 'SELECT * FROM chat_files WHERE id = ?' ). get ( req . params . fileId );
if ( ! file ) {
return res . status ( 404 ). json ({ success : false , message : '照片不存在' });
}
const filePath = path . join ( uploadsDir , file . stored_name );
if ( ! fs . existsSync ( filePath )) {
return res . status ( 404 ). json ({ success : false , message : '照片文件已丢失' });
}
res . setHeader ( 'Content-Type' , file . file_mime || 'image/jpeg' );
res . sendFile ( filePath );
});
2026-08-10 22:56:38 +08:00
// ==================== 通知管理 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 ) : '' ;
}
2026-08-11 21:30:05 +08:00
// 媒体消息在会话列表的预览文案
function messagePreview ( m ) {
if ( m . msg_type === 'image' ) return '[图片]' ;
if ( m . msg_type === 'video' ) return '[视频]' ;
if ( m . msg_type === 'file' ) return '[文件] ' + ( m . content || '' );
return m . content ;
}
// 上传聊天附件(图片/视频/文件),返回 fileId 供发送消息时引用
app . post ( '/api/chat/upload' , upload . single ( 'file' ), ( req , res ) => {
if ( ! req . file ) {
return res . status ( 400 ). json ({ success : false , message : '未接收到文件' });
}
const id = uuidv4 ();
db . prepare ( 'INSERT INTO chat_files (id, uploader_id, file_name, stored_name, file_size, file_mime) VALUES (?, ?, ?, ?, ?, ?)' )
. run ( id , req . user . id , req . file . originalname , req . file . filename , req . file . size , req . file . mimetype || '' );
res . json ({
success : true ,
data : { fileId : id , fileName : req . file . originalname , fileSize : req . file . size , fileMime : req . file . mimetype || '' }
});
});
// multer 错误处理(超大文件等)
app . use ( '/api/chat/upload' , ( err , req , res , next ) => {
if ( err ) {
const msg = err . code === 'LIMIT_FILE_SIZE' ? '文件超过 100MB 限制' : '上传失败' ;
return res . status ( 400 ). json ({ success : false , message : msg });
}
next ();
});
// 下载/预览聊天附件:仅上传者可访问,或所在会话的成员可访问
app . get ( '/api/chat/files/:id' , ( req , res ) => {
const file = db . prepare ( 'SELECT * FROM chat_files WHERE id = ?' ). get ( req . params . id );
if ( ! file ) {
return res . status ( 404 ). json ({ success : false , message : '文件不存在' });
}
if ( file . uploader_id !== req . user . id ) {
const inConv = db . prepare ( `
SELECT 1 FROM conversation_members m
WHERE m.user_id = ? AND EXISTS (
SELECT 1 FROM messages msg WHERE msg.file_id = ? AND msg.conversation_id = m.conversation_id
)
` ). get ( req . user . id , file . id );
if ( ! inConv ) {
return res . status ( 403 ). json ({ success : false , message : '无权访问该文件' });
}
}
const filePath = path . join ( uploadsDir , file . stored_name );
if ( ! fs . existsSync ( filePath )) {
return res . status ( 404 ). json ({ success : false , message : '文件已丢失' });
}
const inline = ( file . file_mime || '' ). startsWith ( 'image/' ) || ( file . file_mime || '' ). startsWith ( 'video/' );
const encoded = encodeURIComponent ( file . file_name );
res . setHeader ( 'Content-Type' , file . file_mime || 'application/octet-stream' );
res . setHeader ( 'Content-Disposition' , ` ${ inline ? 'inline' : 'attachment' } ; filename*=UTF-8'' ${ encoded } ` );
fs . createReadStream ( filePath ). pipe ( res );
});
2026-08-13 14:29:10 +08:00
// 全部用户简表(选择聊天对象/建群成员,含在线状态与登录/下线时间)
2026-08-11 10:03:40 +08:00
app . get ( '/api/chat/users' , ( req , res ) => {
2026-08-13 14:29:10 +08:00
const rows = db . prepare ( 'SELECT id, username, display_name, role, last_login, last_logout FROM users ORDER BY created_at ASC' ). all ();
2026-08-11 10:03:40 +08:00
const onlineMap = getOnlineUserMap ();
2026-08-13 14:29:10 +08:00
const data = rows . map ( u => ({
... u ,
is_online : onlineMap . has ( u . id ),
last_active : onlineMap . has ( u . id ) ? formatActive ( onlineMap . get ( u . id )) : null
}));
2026-08-11 10:03:40 +08:00
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 ,
2026-08-11 21:30:05 +08:00
last_message : last ? { content : messagePreview ( last ), created_at : last . created_at , sender_name : getUserDisplayName ( last . sender_id ) } : null ,
2026-08-11 10:03:40 +08:00
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 : '会话不存在' });
}
2026-08-11 21:30:05 +08:00
// 标记已读到会话最新一条(先于返回,保证本次返回的消息自带已读回执)
const latest = db . prepare ( 'SELECT MAX(id) as maxId FROM messages WHERE conversation_id = ?' ). get ( req . params . id );
if ( latest && latest . maxId ) {
db . prepare ( 'UPDATE conversation_members SET last_read_message_id = MAX(last_read_message_id, ?) WHERE conversation_id = ? AND user_id = ?' )
. run ( latest . maxId , req . params . id , req . user . id );
}
2026-08-11 10:03:40 +08:00
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 ();
}
2026-08-11 21:30:05 +08:00
const readMap = buildReadStatusMap ( req . params . id , req . user . id );
const fileCache = {};
2026-08-11 10:03:40 +08:00
const senderCache = {};
const data = rows . map ( m => {
if ( ! senderCache [ m . sender_id ]) senderCache [ m . sender_id ] = getUserDisplayName ( m . sender_id );
2026-08-11 21:30:05 +08:00
const item = { ... m , sender_name : senderCache [ m . sender_id ], read_status : readMap [ m . id ] || null };
if ( m . file_id ) {
if ( ! fileCache [ m . file_id ]) fileCache [ m . file_id ] = db . prepare ( 'SELECT * FROM chat_files WHERE id = ?' ). get ( m . file_id );
const f = fileCache [ m . file_id ];
if ( f ) {
item . file_name = f . file_name ;
item . file_size = f . file_size ;
item . file_mime = f . file_mime ;
item . file_url = `/api/chat/files/ ${ f . id } ` ;
}
}
return item ;
2026-08-11 10:03:40 +08:00
});
res . json ({ success : true , data });
});
2026-08-11 21:30:05 +08:00
// 计算会话内各消息的已读回执:read_count=已读的其他成员数,total_others=其他成员总数,unread_users=未读成员名单
function buildReadStatusMap ( convId , meId ) {
const members = db . prepare ( 'SELECT user_id, last_read_message_id FROM conversation_members WHERE conversation_id = ?' ). all ( convId );
const others = members . filter ( m => m . user_id !== meId );
const msgs = db . prepare ( 'SELECT id, sender_id FROM messages WHERE conversation_id = ?' ). all ( convId );
const nameCache = {};
const map = {};
for ( const m of msgs ) {
const unreadUsers = others
. filter ( o => ( o . last_read_message_id || 0 ) < m . id )
. map ( o => {
if ( ! nameCache [ o . user_id ]) nameCache [ o . user_id ] = getUserDisplayName ( o . user_id );
return { id : o . user_id , name : nameCache [ o . user_id ] };
});
map [ m . id ] = {
read_count : others . length - unreadUsers . length ,
total_others : others . length ,
unread_users : unreadUsers
};
2026-08-11 10:03:40 +08:00
}
2026-08-11 21:30:05 +08:00
return map ;
}
// 轻量已读回执轮询(不标记已读,仅返回各消息的已读进度)
app . get ( '/api/chat/conversations/:id/read-status' , ( req , res ) => {
2026-08-11 10:03:40 +08:00
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 : '会话不存在' });
}
2026-08-11 21:30:05 +08:00
res . json ({ success : true , data : buildReadStatusMap ( req . params . id , req . user . id ) });
});
// 发送消息:文本(content)或媒体(type=image/video/file + fileId,引用已上传附件)
app . post ( '/api/chat/conversations/:id/messages' , ( req , res ) => {
const msgType = [ 'text' , 'image' , 'video' , 'file' ]. includes ( req . body . type ) ? req . body . type : 'text' ;
const content = String ( req . body . content || '' ). trim ();
const conv = db . prepare ( 'SELECT * FROM conversations WHERE id = ?' ). get ( req . params . id );
if ( ! conv || ! isConversationMember ( req . params . id , req . user . id )) {
return res . status ( 404 ). json ({ success : false , message : '会话不存在' });
}
let fileId = '' ;
let finalContent = content ;
if ( msgType !== 'text' ) {
const file = db . prepare ( 'SELECT * FROM chat_files WHERE id = ?' ). get ( req . body . fileId );
if ( ! file || file . uploader_id !== req . user . id ) {
return res . status ( 400 ). json ({ success : false , message : '附件不存在,请重新上传' });
}
fileId = file . id ;
if ( ! finalContent ) finalContent = file . file_name ;
} else if ( ! content ) {
return res . status ( 400 ). json ({ success : false , message : '消息不能为空' });
}
if ( finalContent . length > 2000 ) {
return res . status ( 400 ). json ({ success : false , message : '消息过长(最多 2000 字)' });
}
const result = db . prepare ( 'INSERT INTO messages (conversation_id, sender_id, content, msg_type, file_id) VALUES (?, ?, ?, ?, ?)' )
. run ( req . params . id , req . user . id , finalContent , msgType , fileId );
2026-08-11 10:03:40 +08:00
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 );
2026-08-11 21:30:05 +08:00
const data = { ... msg , sender_name : req . user . display_name || req . user . username };
if ( fileId ) {
const f = db . prepare ( 'SELECT * FROM chat_files WHERE id = ?' ). get ( fileId );
data . file_name = f . file_name ;
data . file_size = f . file_size ;
data . file_mime = f . file_mime ;
data . file_url = `/api/chat/files/ ${ f . id } ` ;
}
// 推送给会话其他成员(群聊标题=群名,单聊标题=发送者)
const memberRows = db . prepare ( 'SELECT user_id FROM conversation_members WHERE conversation_id = ?' ). all ( req . params . id );
const senderName = req . user . display_name || req . user . username ;
const pushTitle = conv . type === 'group' ? ( conv . name || '群聊消息' ) : senderName ;
pushToMany ( memberRows . map ( m => m . user_id ), 'chat' , pushTitle , ` ${ senderName } : ${ messagePreview ( msg ) } ` , req . user . id );
res . json ({ success : true , data });
2026-08-11 10:03:40 +08:00
});
// 删除/解散会话(群聊仅群主可解散,单聊任一方可删除)
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 : '只有群主可以解散群组' });
}
2026-08-11 21:30:05 +08:00
// 清理会话内附件的磁盘文件与记录
const fileRows = db . prepare ( `
SELECT f.id, f.stored_name FROM chat_files f
WHERE f.id IN (SELECT file_id FROM messages WHERE conversation_id = ? AND file_id != '')
AND NOT EXISTS (SELECT 1 FROM messages m2 WHERE m2.file_id = f.id AND m2.conversation_id != ?)
` ). all ( req . params . id , req . params . id );
for ( const f of fileRows ) {
const p = path . join ( uploadsDir , f . stored_name );
if ( fs . existsSync ( p )) fs . unlinkSync ( p );
}
if ( fileRows . length ) {
db . prepare ( `DELETE FROM chat_files WHERE id IN (SELECT file_id FROM messages WHERE conversation_id = ? AND file_id != '')` ). run ( req . params . id );
}
2026-08-11 10:03:40 +08:00
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-11 12:19:20 +08:00
// 移除群成员(群主可移除任意成员,群主本人不可被移除)
app . delete ( '/api/chat/conversations/:id/members/:userId' , ( req , res ) => {
const conv = db . prepare ( 'SELECT * FROM conversations WHERE id = ?' ). get ( req . params . id );
if ( ! conv || conv . type !== 'group' ) {
return res . status ( 404 ). json ({ success : false , message : '群聊不存在' });
}
if ( ! isConversationMember ( conv . id , req . user . id )) {
return res . status ( 403 ). json ({ success : false , message : '您不是该群聊的成员' });
}
const targetId = req . params . userId ;
if ( targetId === conv . creator_id ) {
return res . status ( 400 ). json ({ success : false , message : '群主不能被移除' });
}
if ( ! isConversationMember ( conv . id , targetId )) {
return res . status ( 404 ). json ({ success : false , message : '该用户不在群聊中' });
}
if ( req . user . id !== conv . creator_id ) {
return res . status ( 403 ). json ({ success : false , message : '只有群主可以移除群成员' });
}
db . prepare ( 'DELETE FROM conversation_members WHERE conversation_id = ? AND user_id = ?' ). run ( conv . id , targetId );
res . json ({ success : true , message : '已将该成员移出群聊' });
});
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-13 14:29:10 +08:00
const user = token ? sessions . get ( token ) : null ;
if ( user ) {
// 记录下线时间
db . prepare ( "UPDATE users SET last_logout=datetime('now','localtime') WHERE id=?" ). run ( user . id );
}
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
});
2026-08-13 14:29:10 +08:00
// ==================== App 版本检查与更新 ====================
db . exec ( `CREATE TABLE IF NOT EXISTS app_versions (
platform TEXT PRIMARY KEY,
version TEXT NOT NULL,
url TEXT DEFAULT '',
size INTEGER DEFAULT 0,
notes TEXT DEFAULT '',
released_at TEXT
)` );
// 首次启动播种当前四端 v4.0.0 版本信息(仅空表时写入)
[ 'windows' , 'android' , 'harmony' , 'ios' ]. forEach ( p => {
db . prepare ( "INSERT OR IGNORE INTO app_versions(platform, version, url, size, notes, released_at) VALUES(?, '4.0.0', '', 0, '影视星河设备管理系统 v4.0.0', datetime('now','localtime'))" ). run ( p );
});
// 查询各端最新版本(客户端登录后检查更新用)
app . get ( '/api/app/version/latest' , ( req , res ) => {
const rows = db . prepare ( 'SELECT * FROM app_versions' ). all ();
const map = {};
rows . forEach ( r => { map [ r . platform ] = r ; });
res . json ({ success : true , data : map });
});
// 管理员发布新版本:{ platform: windows|android|harmony|ios, version, url, size, notes }
app . post ( '/api/app/version' , requireAdmin , ( req , res ) => {
const { platform , version , url , size , notes } = req . body || {};
const platforms = [ 'windows' , 'android' , 'harmony' , 'ios' ];
if ( ! platforms . includes ( platform )) {
return res . status ( 400 ). json ({ success : false , message : '平台取值必须为 windows/android/harmony/ios' });
}
if ( ! version || ! /^\d+\.\d+\.\d+$/ . test ( version )) {
return res . status ( 400 ). json ({ success : false , message : '版本号格式必须为 x.y.z' });
}
db . prepare ( `INSERT INTO app_versions(platform, version, url, size, notes, released_at)
VALUES(?,?,?,?,?,datetime('now','localtime'))
ON CONFLICT(platform) DO UPDATE SET version=excluded.version, url=excluded.url, size=excluded.size, notes=excluded.notes, released_at=excluded.released_at` )
. run ( platform , version , url || '' , size || 0 , notes || '' );
res . json ({ success : true , message : platform + ' 端 v' + version + ' 已发布' });
});
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` );
});