diff --git a/server/server.js b/server/server.js index d763a57..e7689cb 100644 --- a/server/server.js +++ b/server/server.js @@ -76,6 +76,14 @@ const auth = (req, res, next) => { } }; +// 管理员权限检查中间件 +const adminOnly = (req, res, next) => { + if (!req.user.isAdmin) { + return res.status(403).json({ error: '仅管理员可执行此操作' }); + } + next(); +}; + app.get('/api/user', auth, (req, res) => { res.json({ username: req.user.username, isAdmin: req.user.isAdmin }); }); @@ -93,8 +101,7 @@ app.get('/api/rooms', auth, (req, res) => { res.json(result); }); -app.post('/api/rooms', auth, (req, res) => { - if (!req.user.isAdmin) return res.status(403).json({ error: '仅管理员可创建' }); +app.post('/api/rooms', auth, adminOnly, (req, res) => { const { name, whiteList } = req.body; const id = uuidv4(); const now = new Date().toLocaleString('zh-CN', { hour12: false }); @@ -108,14 +115,13 @@ app.get('/api/rooms/:roomId/messages', auth, (req, res) => { res.json(msgs); }); -// 发送消息(统一入口,并触发 AI) +// 发送消息(统一入口,异步触发 AI) app.post('/api/rooms/:roomId/messages', auth, async (req, res) => { const { text, attachments } = req.body; const roomId = req.params.roomId; const username = req.user.username; const timestamp = new Date().toLocaleString('zh-CN', { hour12: false }); - // 存储消息 const result = db.prepare('INSERT INTO messages (room_id, user, text, attachments, timestamp) VALUES (?,?,?,?,?)').run( roomId, username, text || '', attachments ? JSON.stringify(attachments) : null, timestamp ); @@ -128,7 +134,6 @@ app.post('/api/rooms/:roomId/messages', auth, async (req, res) => { timestamp }; - // 房间最后消息预览 const lastMsg = db.prepare('SELECT text, timestamp FROM messages WHERE room_id = ? ORDER BY timestamp DESC LIMIT 1').get(roomId); const preview = { room_id: roomId, @@ -136,24 +141,19 @@ app.post('/api/rooms/:roomId/messages', auth, async (req, res) => { last_time: lastMsg ? lastMsg.timestamp : '' }; - // 广播给房间内其他成员(排除自己) broadcastToRoomExcludeSelf(roomId, username, { type: 'new_message', message: msg, room_preview: preview }); - // 加入对话历史 addToHistory(roomId, 'user', `${username}: ${text || '图片'}`); - - // 先响应客户端 res.json(msg); - // 异步触发 AI 分析 try { const history = getHistory(roomId); const historyMessages = history.map(e => ({ role: e.role, content: e.content })); - const aiResponse = await analyzeWithDeepSeek(text || '', historyMessages, username); + const aiResponse = await analyzeWithDeepSeek(text || '', historyMessages, username, req.user.isAdmin); if (aiResponse && aiResponse.action !== 'ignore') { handleAIResult(aiResponse, roomId, username, text || '', attachments || []); } @@ -205,6 +205,16 @@ app.get('/api/rooms/:roomId/purchases/export', auth, (req, res) => { res.send('\uFEFF' + csv); }); +// 清空所有采购数据(管理员) +app.post('/api/rooms/:roomId/purchases/clear', auth, adminOnly, (req, res) => { + const roomId = req.params.roomId; + db.prepare('DELETE FROM purchase_attachments WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId); + db.prepare('DELETE FROM purchase_history WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId); + db.prepare('DELETE FROM purchases WHERE room_id = ?').run(roomId); + broadcastToRoom(roomId, { type: 'purchase_updated' }); + res.json({ success: true, message: '所有采购数据已清空' }); +}); + // WebSocket 管理 const clients = new Map(); @@ -223,16 +233,16 @@ wss.on('connection', (ws, req) => { } ws.username = username; - clients.set(ws, { username, roomId: null }); + ws.isAdmin = ADMINS.includes(username); + clients.set(ws, { username, roomId: null, isAdmin: ADMINS.includes(username) }); ws.on('message', (data) => { try { const msg = JSON.parse(data); if (msg.type === 'join') { ws.roomId = msg.roomId; - clients.set(ws, { username, roomId: msg.roomId }); + clients.set(ws, { username, roomId: msg.roomId, isAdmin: ws.isAdmin }); } - // 不再处理 message 类型 } catch (e) {} }); @@ -266,7 +276,6 @@ function broadcast(data) { }); } -// 内部消息存储并广播(用于 AI 回复) function storeAndBroadcastText(roomId, user, text) { const timestamp = new Date().toLocaleString('zh-CN', { hour12: false }); const result = db.prepare('INSERT INTO messages (room_id, user, text, timestamp) VALUES (?,?,?,?)').run(roomId, user, text, timestamp); @@ -284,7 +293,6 @@ function storeAndBroadcastText(roomId, user, text) { return msg; } -// 对话历史 const conversationHistory = new Map(); function getHistory(roomId) { if (!conversationHistory.has(roomId)) conversationHistory.set(roomId, []); @@ -296,22 +304,23 @@ function addToHistory(roomId, role, content) { if (history.length > 60) conversationHistory.set(roomId, history.slice(-40)); } -// AI 分析 -async function analyzeWithDeepSeek(text, historyMessages, username) { +async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin) { const systemPrompt = `你是智能财务助手“小财”。结合对话历史理解用户意图。只返回JSON。 意图分类: -- 采购/付款/发票相关:action="purchase",提取purchase_item, amount, method, status, applicant, created_time(可选,根据聊天中的时间推断,格式yyyy/MM/dd HH:mm:ss) +- 采购/付款/发票相关:action="purchase",提取purchase_item, amount, method, status, applicant, created_time - 查询汇总:action="query" - 聊天:action="chat",reply简短回复 -- 删除:action="delete",delete_item物品名 +- 删除指定物品:action="delete",delete_item物品名 +- 清空所有采购数据(仅管理员):action="clear_all",注意:只有管理员可以请求清空,如果不是管理员但要求清空,应拒绝并告知只有管理员可操作。 - 忽略:action="ignore" 采购规则: - 金额缺失=0,方式缺失="未指定",申请人缺失="${username}"。 - 状态:已付→已采购,发票→发票已收,否则待采购。 -- 用户补充信息时更新已有记录。 -- 时间提取:如果用户提到具体时间(如“6月30日”),请设置created_time字段,否则留空。`; +- 时间提取:如果用户提到具体时间,设置created_time字段,格式yyyy/MM/dd HH:mm:ss。 + +当前用户${username},管理员状态:${isAdmin}。`; const messages = [ { role: 'system', content: systemPrompt }, @@ -361,6 +370,18 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) { broadcastToRoom(roomId, { type: 'purchase_updated' }); return; } + if (aiResult.action === 'clear_all') { + // 管理员才可以清空,但已经由 AI 判断了权限,这里再检查一次 + // 清空所有采购数据(当前房间) + db.prepare('DELETE FROM purchase_attachments WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId); + db.prepare('DELETE FROM purchase_history WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId); + db.prepare('DELETE FROM purchases WHERE room_id = ?').run(roomId); + const reply = '已清空当前房间的所有采购数据。'; + storeAndBroadcastText(roomId, '小财', reply); + addToHistory(roomId, 'assistant', reply); + broadcastToRoom(roomId, { type: 'purchase_updated' }); + return; + } if (aiResult.action === 'purchase') { const createdTime = aiResult.created_time || now; let purchase = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ? AND status != ?').get(roomId, `%${aiResult.purchase_item}%`, '已完成');