update: v5.0
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user