update: v5.0

This commit is contained in:
mei
2026-08-13 14:29:10 +08:00
parent 8ee98719ef
commit 1271627ef9
8 changed files with 732 additions and 45 deletions
+137 -3
View File
@@ -48,6 +48,11 @@ function dropSession(token) {
sessionActivity.delete(token);
}
// 下线时间列(兼容旧库:已存在则忽略)
try {
db.prepare("ALTER TABLE users ADD COLUMN last_logout TEXT DEFAULT ''").run();
} catch (e) { /* 列已存在 */ }
// 收集当前在线用户(用户ID -> 最近活跃时间戳,取最新)
function getOnlineUserMap() {
const now = Date.now();
@@ -521,6 +526,85 @@ app.get('/api/personnel/stats/summary', (req, res) => {
res.json({ success: true, data: { total, active, withAccount, departments } });
});
// ==================== 设备借用/归还 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);
});
// ==================== 通知管理 API ====================
// 获取所有通知
@@ -840,11 +924,15 @@ app.get('/api/chat/files/:id', (req, res) => {
fs.createReadStream(filePath).pipe(res);
});
// 全部用户简表(选择聊天对象/建群成员,含在线状态)
// 全部用户简表(选择聊天对象/建群成员,含在线状态与登录/下线时间)
app.get('/api/chat/users', (req, res) => {
const rows = db.prepare('SELECT id, username, display_name, role FROM users ORDER BY created_at ASC').all();
const rows = db.prepare('SELECT id, username, display_name, role, last_login, last_logout FROM users ORDER BY created_at ASC').all();
const onlineMap = getOnlineUserMap();
const data = rows.map(u => ({ ...u, is_online: onlineMap.has(u.id) }));
const data = rows.map(u => ({
...u,
is_online: onlineMap.has(u.id),
last_active: onlineMap.has(u.id) ? formatActive(onlineMap.get(u.id)) : null
}));
res.json({ success: true, data });
});
@@ -1145,6 +1233,11 @@ app.get('/api/auth/me', (req, res) => {
// 登出
app.post('/api/auth/logout', (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = token ? sessions.get(token) : null;
if (user) {
// 记录下线时间
db.prepare("UPDATE users SET last_logout=datetime('now','localtime') WHERE id=?").run(user.id);
}
if (token) dropSession(token);
res.json({ success: true, message: '登出成功' });
});
@@ -1167,6 +1260,47 @@ app.patch('/api/auth/password', (req, res) => {
res.json({ success: true, message: '密码修改成功,请使用新密码重新登录' });
});
// ==================== 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 + ' 已发布' });
});
// ==================== 天气 API ====================
function getWeatherDesc(code) {