处理发送消息重复的问题

This commit is contained in:
2026-07-22 10:29:47 +08:00
parent 494977d376
commit 972804edba
2 changed files with 37 additions and 20 deletions

View File

@@ -192,7 +192,7 @@
let currentRoom = null; let currentRoom = null;
let rooms = []; let rooms = [];
let pendingUploads = []; let pendingUploads = [];
const sentMsgIds = new Set(); // 已发送成功的消息 ID 集合,防止重复 const sentMsgIds = new Set(); // 本地已处理的消息 ID防止广播重复
function getToken() { return localStorage.getItem('token'); } function getToken() { return localStorage.getItem('token'); }
function saveAuth(data) { function saveAuth(data) {
@@ -303,8 +303,7 @@
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data.type === 'new_message') { if (data.type === 'new_message') {
const msg = data.message; const msg = data.message;
// 去重:如果消息 ID 已在发送确认集合中,忽略广播 // 发送者本人不会收到自己的广播(已由后端过滤),因此直接添加
if (sentMsgIds.has(msg.id)) return;
if (currentRoom === msg.room_id) { if (currentRoom === msg.room_id) {
appendMessage(msg); appendMessage(msg);
} }
@@ -483,7 +482,7 @@
body: JSON.stringify({ text: text || '', attachments: pendingUploads }) body: JSON.stringify({ text: text || '', attachments: pendingUploads })
}); });
const newMsg = await res.json(); const newMsg = await res.json();
sentMsgIds.add(newMsg.id); // 记录已发送,避免广播重复 // 不记录 sentMsgIds,因为后端已经过滤,我们直接 append
appendMessage(newMsg); appendMessage(newMsg);
input.value = ''; input.value = '';
pendingUploads = []; pendingUploads = [];
@@ -492,13 +491,12 @@
} }
} }
// 选图后自动压缩上传,然后调用 sendMessage确保占位 // 选图后自动压缩上传,显示占位,然后发送
async function handleFileSelect(event) { async function handleFileSelect(event) {
const files = event.target.files; const files = event.target.files;
if (!files.length) return; if (!files.length) return;
const token = getToken(); const token = getToken();
// 立即显示占位消息
const placeholderId = 'placeholder_' + Date.now(); const placeholderId = 'placeholder_' + Date.now();
const placeholderMsg = { const placeholderMsg = {
id: placeholderId, id: placeholderId,
@@ -529,11 +527,11 @@
const data = await res.json(); const data = await res.json();
if (data.path) pendingUploads.push(data.path); if (data.path) pendingUploads.push(data.path);
} }
// 上传完毕,移除占位并发送正式消息 // 移除占位
window._messagesCache = window._messagesCache.filter(m => m.id !== placeholderId); window._messagesCache = window._messagesCache.filter(m => m.id !== placeholderId);
sendMessage(); // 会自动带上 pendingUploads // 发送正式消息
sendMessage();
} catch(e) { } catch(e) {
// 失败则移除占位
window._messagesCache = window._messagesCache.filter(m => m.id !== placeholderId); window._messagesCache = window._messagesCache.filter(m => m.id !== placeholderId);
renderMessages(window._messagesCache); renderMessages(window._messagesCache);
alert('图片发送失败,请重试'); alert('图片发送失败,请重试');

View File

@@ -42,7 +42,7 @@ const storage = multer.diskStorage({
}); });
const upload = multer({ const upload = multer({
storage, storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (req, file, cb) => { fileFilter: (req, file, cb) => {
const allowed = ['image/jpeg','image/png','image/gif']; const allowed = ['image/jpeg','image/png','image/gif'];
cb(null, allowed.includes(file.mimetype)); cb(null, allowed.includes(file.mimetype));
@@ -80,7 +80,6 @@ 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 });
}); });
// 群聊列表(带最后消息)
app.get('/api/rooms', auth, (req, res) => { app.get('/api/rooms', auth, (req, res) => {
const rooms = db.prepare('SELECT * FROM rooms').all(); const rooms = db.prepare('SELECT * FROM rooms').all();
const result = rooms.map(room => { const result = rooms.map(room => {
@@ -126,19 +125,26 @@ app.post('/api/rooms/:roomId/messages', auth, (req, res) => {
attachments: attachments || [], attachments: attachments || [],
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);
broadcastToRoom(roomId, { const preview = {
room_id: roomId,
last_message: lastMsg ? lastMsg.text : '',
last_time: lastMsg ? lastMsg.timestamp : ''
};
// 广播给房间内其他成员,但不包括发送者自己
broadcastToRoomExcludeSelf(roomId, username, {
type: 'new_message', type: 'new_message',
message: msg, message: msg,
room_preview: { room_preview: preview
room_id: roomId,
last_message: lastMsg ? lastMsg.text : '',
last_time: lastMsg ? lastMsg.timestamp : ''
}
}); });
// 加入对话历史 // 加入对话历史
addToHistory(roomId, 'user', `${username}: ${text || '图片'}`); addToHistory(roomId, 'user', `${username}: ${text || '图片'}`);
// 返回消息给发送者
res.json(msg); res.json(msg);
}); });
@@ -186,7 +192,8 @@ app.get('/api/rooms/:roomId/purchases/export', auth, (req, res) => {
}); });
// WebSocket 管理 // WebSocket 管理
const clients = new Map(); const clients = new Map(); // ws -> { username, roomId }
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');
@@ -220,6 +227,17 @@ wss.on('connection', (ws, req) => {
}); });
}); });
// 广播给房间内所有成员,排除指定用户
function broadcastToRoomExcludeSelf(roomId, excludeUsername, data) {
const message = JSON.stringify(data);
clients.forEach((info, ws) => {
if (info.roomId === roomId && info.username !== excludeUsername && ws.readyState === 1) {
ws.send(message);
}
});
}
// 广播给房间内所有人
function broadcastToRoom(roomId, data) { function broadcastToRoom(roomId, data) {
const message = JSON.stringify(data); const message = JSON.stringify(data);
clients.forEach((info, ws) => { clients.forEach((info, ws) => {
@@ -229,6 +247,7 @@ function broadcastToRoom(roomId, data) {
}); });
} }
// 全局广播
function broadcast(data) { function broadcast(data) {
const message = JSON.stringify(data); const message = JSON.stringify(data);
clients.forEach((ws) => { clients.forEach((ws) => {
@@ -236,7 +255,7 @@ function broadcast(data) {
}); });
} }
// 文本消息存储并广播(用于 AI 回复等内部消息 // 内部消息存储并广播(用于 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);
@@ -266,7 +285,7 @@ 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 处理相关函数(保持不变 // AI 处理(保持原逻辑
async function analyzeWithDeepSeek(text, historyMessages, username) { async function analyzeWithDeepSeek(text, historyMessages, username) {
const systemPrompt = `你是智能财务助手“小财”。结合对话历史理解用户意图。只返回JSON。 const systemPrompt = `你是智能财务助手“小财”。结合对话历史理解用户意图。只返回JSON。