删除操作加入管理员权限控制
This commit is contained in:
135
server/server.js
135
server/server.js
@@ -76,11 +76,8 @@ const auth = (req, res, next) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 管理员权限检查中间件
|
|
||||||
const adminOnly = (req, res, next) => {
|
const adminOnly = (req, res, next) => {
|
||||||
if (!req.user.isAdmin) {
|
if (!req.user.isAdmin) return res.status(403).json({ error: '仅管理员可执行此操作' });
|
||||||
return res.status(403).json({ error: '仅管理员可执行此操作' });
|
|
||||||
}
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -150,6 +147,12 @@ app.post('/api/rooms/:roomId/messages', auth, async (req, res) => {
|
|||||||
addToHistory(roomId, 'user', `${username}: ${text || '图片'}`);
|
addToHistory(roomId, 'user', `${username}: ${text || '图片'}`);
|
||||||
res.json(msg);
|
res.json(msg);
|
||||||
|
|
||||||
|
// 检查是否有待确认操作且用户回复了“确认”
|
||||||
|
if (text && text.trim() === '确认' && hasPendingAction(roomId, username)) {
|
||||||
|
executePendingAction(roomId, username);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
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 }));
|
||||||
@@ -205,7 +208,6 @@ 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) => {
|
app.post('/api/rooms/:roomId/purchases/clear', auth, adminOnly, (req, res) => {
|
||||||
const roomId = req.params.roomId;
|
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_attachments WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId);
|
||||||
@@ -217,7 +219,6 @@ app.post('/api/rooms/:roomId/purchases/clear', auth, adminOnly, (req, res) => {
|
|||||||
|
|
||||||
// WebSocket 管理
|
// WebSocket 管理
|
||||||
const clients = new Map();
|
const clients = new Map();
|
||||||
|
|
||||||
wss.on('connection', (ws, req) => {
|
wss.on('connection', (ws, req) => {
|
||||||
console.log('✅ WebSocket 客户端已连接');
|
console.log('✅ WebSocket 客户端已连接');
|
||||||
const url = new URL(req.url, 'http://localhost');
|
const url = new URL(req.url, 'http://localhost');
|
||||||
@@ -234,7 +235,7 @@ wss.on('connection', (ws, req) => {
|
|||||||
|
|
||||||
ws.username = username;
|
ws.username = username;
|
||||||
ws.isAdmin = ADMINS.includes(username);
|
ws.isAdmin = ADMINS.includes(username);
|
||||||
clients.set(ws, { username, roomId: null, isAdmin: ADMINS.includes(username) });
|
clients.set(ws, { username, roomId: null, isAdmin: ws.isAdmin });
|
||||||
|
|
||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
try {
|
try {
|
||||||
@@ -293,6 +294,59 @@ function storeAndBroadcastText(roomId, user, text) {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 待处理操作管理 ----------
|
||||||
|
const pendingActions = new Map(); // key: `${roomId}:${username}`, value: { type, data }
|
||||||
|
|
||||||
|
function hasPendingAction(roomId, username) {
|
||||||
|
return pendingActions.has(`${roomId}:${username}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPendingAction(roomId, username, action) {
|
||||||
|
pendingActions.set(`${roomId}:${username}`, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPendingAction(roomId, username) {
|
||||||
|
pendingActions.delete(`${roomId}:${username}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function executePendingAction(roomId, username) {
|
||||||
|
const action = pendingActions.get(`${roomId}:${username}`);
|
||||||
|
if (!action) return false;
|
||||||
|
clearPendingAction(roomId, username);
|
||||||
|
|
||||||
|
const now = new Date().toLocaleString('zh-CN', { hour12: false });
|
||||||
|
try {
|
||||||
|
if (action.type === 'delete') {
|
||||||
|
const { itemName, purchaseIds } = action.data;
|
||||||
|
const deleteAttachments = db.prepare('DELETE FROM purchase_attachments WHERE purchase_id = ?');
|
||||||
|
const deleteHistory = db.prepare('DELETE FROM purchase_history WHERE purchase_id = ?');
|
||||||
|
const deletePurchase = db.prepare('DELETE FROM purchases WHERE id = ?');
|
||||||
|
for (const id of purchaseIds) {
|
||||||
|
deleteAttachments.run(id);
|
||||||
|
deleteHistory.run(id);
|
||||||
|
deletePurchase.run(id);
|
||||||
|
}
|
||||||
|
const reply = `✅ 已删除采购记录:${itemName}(共 ${purchaseIds.length} 条)`;
|
||||||
|
storeAndBroadcastText(roomId, '小财', reply);
|
||||||
|
addToHistory(roomId, 'assistant', reply);
|
||||||
|
} else if (action.type === 'clear') {
|
||||||
|
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 true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('执行待处理操作失败:', e);
|
||||||
|
storeAndBroadcastText(roomId, '小财', '❌ 操作执行失败,请重试。');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话历史
|
||||||
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, []);
|
||||||
@@ -311,16 +365,20 @@ async function analyzeWithDeepSeek(text, historyMessages, username, isAdmin) {
|
|||||||
- 采购/付款/发票相关:action="purchase",提取purchase_item, amount, method, status, applicant, created_time
|
- 采购/付款/发票相关: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="clear_all"
|
||||||
- 忽略:action="ignore"
|
- 忽略:action="ignore"
|
||||||
|
|
||||||
|
重要权限规则:
|
||||||
|
- 删除和清空操作只有管理员可以执行。当前用户${username}的管理员状态:${isAdmin}。
|
||||||
|
- 如果用户要求删除或清空,但当前用户不是管理员,你必须直接回复拒绝,返回action="chat",reply说明需要管理员权限。
|
||||||
|
- 如果用户是管理员,返回delete或clear_all action。
|
||||||
|
|
||||||
采购规则:
|
采购规则:
|
||||||
- 金额缺失=0,方式缺失="未指定",申请人缺失="${username}"。
|
- 金额缺失=0,方式缺失="未指定",申请人缺失="${username}"。
|
||||||
- 状态:已付→已采购,发票→发票已收,否则待采购。
|
- 状态:已付→已采购,发票→发票已收,否则待采购。
|
||||||
- 时间提取:如果用户提到具体时间,设置created_time字段,格式yyyy/MM/dd HH:mm:ss。
|
- 用户补充信息时更新已有记录。
|
||||||
|
- 时间提取:如果用户提到具体时间,设置created_time字段,格式yyyy/MM/dd HH:mm:ss。`;
|
||||||
当前用户${username},管理员状态:${isAdmin}。`;
|
|
||||||
|
|
||||||
const messages = [
|
const messages = [
|
||||||
{ role: 'system', content: systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
@@ -348,40 +406,47 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
|||||||
addToHistory(roomId, 'assistant', aiResult.question);
|
addToHistory(roomId, 'assistant', aiResult.question);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除指定物品(管理员权限)
|
||||||
if (aiResult.action === 'delete') {
|
if (aiResult.action === 'delete') {
|
||||||
const itemName = aiResult.delete_item;
|
const itemName = aiResult.delete_item;
|
||||||
const purchases = db.prepare('SELECT * FROM purchases WHERE room_id = ? AND item LIKE ?').all(roomId, `%${itemName}%`);
|
const purchases = db.prepare('SELECT id, item, amount, method, status, applicant, created_at FROM purchases WHERE room_id = ? AND item LIKE ?').all(roomId, `%${itemName}%`);
|
||||||
if (purchases.length === 0) {
|
if (purchases.length === 0) {
|
||||||
storeAndBroadcastText(roomId, '小财', `没有找到与“${itemName}”相关的采购记录。`);
|
storeAndBroadcastText(roomId, '小财', `没有找到与“${itemName}”相关的采购记录。`);
|
||||||
addToHistory(roomId, 'assistant', `没有找到与“${itemName}”相关的采购记录。`);
|
addToHistory(roomId, 'assistant', `没有找到与“${itemName}”相关的采购记录。`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const deleteAttachments = db.prepare('DELETE FROM purchase_attachments WHERE purchase_id = ?');
|
// 构建确认消息
|
||||||
const deleteHistory = db.prepare('DELETE FROM purchase_history WHERE purchase_id = ?');
|
let confirmText = `⚠️ 即将删除以下 ${purchases.length} 条采购记录,请回复“确认”继续:\n\n`;
|
||||||
const deletePurchase = db.prepare('DELETE FROM purchases WHERE id = ?');
|
purchases.forEach(p => {
|
||||||
for (const p of purchases) {
|
confirmText += `• ${p.item} | ¥${p.amount} | ${p.status} | ${p.applicant} | ${p.created_at}\n`;
|
||||||
deleteAttachments.run(p.id);
|
});
|
||||||
deleteHistory.run(p.id);
|
confirmText += `\n如果不删除,请忽略此消息。`;
|
||||||
deletePurchase.run(p.id);
|
storeAndBroadcastText(roomId, '小财', confirmText);
|
||||||
}
|
addToHistory(roomId, 'assistant', confirmText);
|
||||||
const reply = `已删除采购记录:${itemName}(共 ${purchases.length} 条)`;
|
// 存储待处理操作
|
||||||
storeAndBroadcastText(roomId, '小财', reply);
|
setPendingAction(roomId, username, {
|
||||||
addToHistory(roomId, 'assistant', reply);
|
type: 'delete',
|
||||||
broadcastToRoom(roomId, { type: 'purchase_updated' });
|
data: { itemName, purchaseIds: purchases.map(p => p.id) }
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空所有采购数据(管理员权限)
|
||||||
if (aiResult.action === 'clear_all') {
|
if (aiResult.action === 'clear_all') {
|
||||||
// 管理员才可以清空,但已经由 AI 判断了权限,这里再检查一次
|
const count = db.prepare('SELECT COUNT(*) as count FROM purchases WHERE room_id = ?').get(roomId).count;
|
||||||
// 清空所有采购数据(当前房间)
|
if (count === 0) {
|
||||||
db.prepare('DELETE FROM purchase_attachments WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId);
|
storeAndBroadcastText(roomId, '小财', '当前房间没有采购记录,无需清空。');
|
||||||
db.prepare('DELETE FROM purchase_history WHERE purchase_id IN (SELECT id FROM purchases WHERE room_id = ?)').run(roomId);
|
addToHistory(roomId, 'assistant', '当前房间没有采购记录,无需清空。');
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
const confirmText = `⚠️ 即将清空当前房间的 ${count} 条采购数据,请回复“确认”继续,否则忽略。`;
|
||||||
|
storeAndBroadcastText(roomId, '小财', confirmText);
|
||||||
|
addToHistory(roomId, 'assistant', confirmText);
|
||||||
|
setPendingAction(roomId, username, { type: 'clear', data: {} });
|
||||||
|
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}%`, '已完成');
|
||||||
@@ -410,6 +475,7 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
|||||||
broadcastToRoom(roomId, { type: 'purchase_updated' });
|
broadcastToRoom(roomId, { type: 'purchase_updated' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aiResult.action === 'query') {
|
if (aiResult.action === 'query') {
|
||||||
const purchaseData = db.prepare('SELECT item, amount, method, status FROM purchases WHERE room_id = ? ORDER BY created_at DESC').all(roomId)
|
const purchaseData = db.prepare('SELECT item, amount, method, status FROM purchases WHERE room_id = ? ORDER BY created_at DESC').all(roomId)
|
||||||
.map(p => `${p.item} ¥${p.amount} ${p.status}`).join('\n');
|
.map(p => `${p.item} ¥${p.amount} ${p.status}`).join('\n');
|
||||||
@@ -420,6 +486,7 @@ function handleAIResult(aiResult, roomId, username, originalText, attachments) {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aiResult.action === 'chat') {
|
if (aiResult.action === 'chat') {
|
||||||
const reply = aiResult.reply || '好的,有什么需要帮助的吗?';
|
const reply = aiResult.reply || '好的,有什么需要帮助的吗?';
|
||||||
storeAndBroadcastText(roomId, '小财', reply);
|
storeAndBroadcastText(roomId, '小财', reply);
|
||||||
|
|||||||
Reference in New Issue
Block a user