加入删除相关功能

This commit is contained in:
2026-07-22 10:49:52 +08:00
parent 0727236085
commit 81c4706f1a

View File

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