import { Variables } from './router_io.ts'; import type { QuizContent, QuizState, Answer } from '../client/mobile/components/Exam/types.ts'; interface ExamRoomData { roomId: string; userId?: string; } interface ExamQuestionData extends ExamRoomData { question: QuizContent; } interface ExamAnswerData extends ExamRoomData { questionId: string; answer: Answer; } interface ExamPriceData extends ExamRoomData { date: string; price: string; } export function setupExamEvents({ socket, apiClient }: Variables) { // 加入考试房间 socket.on('exam:join', async (data: ExamRoomData) => { try { const { roomId } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); return; } // 加入房间 socket.join(roomId); // 通知用户加入成功 socket.emit('exam:joined', { roomId, message: `成功加入考试房间: ${roomId}` }); // 通知房间其他用户有新成员加入 socket.to(roomId).emit('exam:memberJoined', { roomId, userId: user.id, username: user.username }); console.log(`用户 ${user.username} 加入考试房间 ${roomId}`); } catch (error) { console.error('加入考试房间失败:', error); socket.emit('error', '加入考试房间失败'); } }); // 离开考试房间 socket.on('exam:leave', async (data: ExamRoomData) => { try { const { roomId } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); return; } // 离开房间 socket.leave(roomId); // 通知用户离开成功 socket.emit('exam:left', { roomId, message: `已离开考试房间: ${roomId}` }); // 通知房间其他用户有成员离开 socket.to(roomId).emit('exam:memberLeft', { roomId, userId: user.id, username: user.username }); console.log(`用户 ${user.username} 离开考试房间 ${roomId}`); } catch (error) { console.error('离开考试房间失败:', error); socket.emit('error', '离开考试房间失败'); } }); // 发送考试房间消息 socket.on('exam:message', async (data: { roomId: string; message: { type: string; content: any; } }) => { try { const { roomId, message } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); return; } // 广播消息到房间 socket.to(roomId).emit('exam:message', { roomId, message: { ...message, from: user.id, username: user.username, timestamp: new Date().toISOString() } }); console.log(`用户 ${user.username} 在房间 ${roomId} 发送消息: ${message.type}`); } catch (error) { console.error('发送考试消息失败:', error); socket.emit('error', '发送考试消息失败'); } }); // 推送题目 socket.on('exam:question', async (data: ExamQuestionData) => { try { const { roomId, question } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); return; } // 存储当前问题状态到Redis const questionKey = `exam:${roomId}:current_question`; await apiClient.redis.hset(questionKey, 'date', String(question.date)); await apiClient.redis.hset(questionKey, 'price', String(question.price)); await apiClient.redis.hset(questionKey, 'updated_at', new Date().toISOString()); // TODO: 需要Redis服务端配置自动过期或通过其他方式实现TTL // 广播题目到房间 // socket.to(roomId).emit('exam:question', { // roomId, // question: { // ...question, // timestamp: new Date().toISOString() // } // }); // 存储价格到Redis const priceKey = `exam:${roomId}:prices`; await apiClient.redis.hset(priceKey, question.date, String(question.price)); // console.log(`用户 ${user.username} 存储房间 ${roomId} 的价格历史: ${date} - ${price}`); const quizState:QuizState = { // id: `question-${Date.now()}`, id: question.date, date: question.date, price: question.price } socket.to(roomId).emit('exam:question', quizState); console.log(`用户 ${user.username} 在房间 ${roomId} 推送题目`); } catch (error) { console.error('推送题目失败:', error); socket.emit('error', '推送题目失败'); } }); // 存储答案 socket.on('exam:storeAnswer', async (data: ExamAnswerData, callback: (success: boolean) => void) => { try { const { roomId, questionId, answer } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); callback(false); return; } // 存储答案到Redis // 新key格式: exam:answers:{roomId}:{userId}:{questionId} // 优点: // 1. 更清晰的命名空间划分(exam:answers开头) // 2. 支持两种查询模式: // - 按用户查询: exam:answers:{roomId}:{userId}:* // - 按题目查询: exam:answers:{roomId}:*:{questionId} // 3. 每个用户答案独立存储,避免大hash性能问题 const answerKey = `exam:answers:${roomId}:${user.id}:${questionId}`; await apiClient.redis.hset(answerKey, 'data', JSON.stringify({ username: user.username, date: answer.date, price: answer.price, holdingStock: answer.holdingStock, holdingCash: answer.holdingCash, profitAmount: answer.profitAmount, profitPercent: answer.profitPercent, totalProfitAmount: answer.totalProfitAmount, totalProfitPercent: answer.totalProfitPercent })); // 广播答案更新到房间 socket.to(roomId).emit('exam:answerUpdated', { roomId, questionId, userId: user.id, username: user.username }); // // 通知管理员有新答案提交 // socket.to(`admin-${roomId}`).emit('exam:newAnswer', { // roomId, // questionId, // userId: user.id, // username: user.username, // timestamp: new Date().toISOString() // }); console.log(`用户 ${user.username} 在房间 ${roomId} 存储答案`); callback(true); } catch (error) { console.error('存储答案失败:', error); socket.emit('error', '存储答案失败'); callback(false); } }); // 获取答案 socket.on('exam:getAnswers', async (data: { roomId: string; questionId: string; }, callback: (answers: Answer[]) => void) => { try { const { roomId, questionId } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); return; } // 从Redis获取答案 // 支持两种查询模式: // 1. 查询特定问题的所有答案: exam:answers:{roomId}:{questionId}:* // 2. 查询房间的所有答案: exam:answers:{roomId}:* const pattern = questionId ? `exam:answers:${roomId}:*:${questionId}` : `exam:answers:${roomId}:*`; const keys = await apiClient.redis.keys(pattern); const formattedAnswers:Answer[] = []; // 并行获取所有答案 await Promise.all(keys.map(async (key) => { try { const answerStr = await apiClient.redis.hget(key, 'data'); if (answerStr) { const answer = JSON.parse(answerStr); // 从key中提取userId (第四部分) const userId = key.split(':')[3]; formattedAnswers.push({ ...answer, userId }); } } catch (error) { console.log('获取答案失败:', error); } })); callback(formattedAnswers); } catch (error) { console.error('获取答案失败:', error); socket.emit('error', '获取答案失败'); callback([]); } }); // 清理房间数据 socket.on('exam:cleanup', async (data: { roomId: string; questionId?: string; }) => { try { const { roomId, questionId } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); return; } if (questionId) { // 清理特定问题的数据 const answerKey = `exam:${roomId}:${questionId}:answers`; const keys = await apiClient.redis.keys(answerKey); if (keys.length > 0) { await Promise.all(keys.map((key) => apiClient.redis.del(key))) } } else { // 清理整个房间的数据 const keys = await apiClient.redis.keys(`exam:${roomId}:*`); if (keys.length > 0) { await Promise.all(keys.map((key) => apiClient.redis.del(key))) } } socket.emit('exam:cleaned', { roomId, message: questionId ? `已清理房间 ${roomId} 的问题 ${questionId} 数据` : `已清理房间 ${roomId} 的所有数据` }); console.log(`用户 ${user.username} 清理房间 ${roomId} 数据`); } catch (error) { console.error('清理房间数据失败:', error); socket.emit('error', '清理房间数据失败'); } }); // 存储价格历史 socket.on('exam:storePrice', async (data: ExamPriceData) => { try { const { roomId, date, price } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); return; } // 存储价格到Redis const priceKey = `exam:${roomId}:prices`; await apiClient.redis.hset(priceKey, date, price); console.log(`用户 ${user.username} 存储房间 ${roomId} 的价格历史: ${date} - ${price}`); } catch (error) { console.error('存储价格历史失败:', error); socket.emit('error', '存储价格历史失败'); } }); // 获取历史价格 socket.on('exam:getPrice', async (data: { roomId: string; date: string; }, callback: (price: string) => void) => { try { const { roomId, date } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); return; } // 从Redis获取价格 const priceKey = `exam:${roomId}:prices`; const price = await apiClient.redis.hget(priceKey, date); callback(price || '0'); } catch (error) { console.error('获取历史价格失败:', error); socket.emit('error', '获取历史价格失败'); callback('0'); } }); // 获取所有价格历史 socket.on('exam:getPrices', async (data: { roomId: string; }, callback: (prices: Record) => void) => { try { const { roomId } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); return; } // 从Redis获取所有价格 const priceKey = `exam:${roomId}:prices`; const priceMap = await apiClient.redis.hgetall(priceKey); const entries = Object.entries(priceMap); const convertedEntries = entries.map(([date, price]) => [date, Number(price)]); const convertedMap = Object.fromEntries(convertedEntries); callback(convertedMap); } catch (error) { console.error('获取所有价格历史失败:', error); socket.emit('error', '获取所有价格历史失败'); callback({}); } }); // 获取用户答案 socket.on('exam:getUserAnswers', async (data: { roomId: string; userId: string; }, callback: (answers: Array) => void) => { try { const { roomId, userId } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); callback([]); return; } // 从Redis获取该用户的所有答案 const pattern = `exam:answers:${roomId}:${userId}:*`; const keys = await apiClient.redis.keys(pattern); const userAnswers: Array = []; // 并行获取所有答案 await Promise.all(keys.map(async (key: string) => { try { const answerStr = await apiClient.redis.hget(key, 'data'); if (answerStr) { const answer = JSON.parse(answerStr) as Answer; userAnswers.push(answer) } } catch (error) { console.log('获取用户答案失败:', error); } })); callback(userAnswers); } catch (error) { console.error('获取用户答案失败:', error); socket.emit('error', '获取用户答案失败'); callback([]); } }); // 获取当前问题 socket.on('exam:currentQuestion', async (data: ExamRoomData, callback: (question: QuizState | null) => void) => { try { const { roomId } = data; const user = socket.user; if (!user) { socket.emit('error', '未授权访问'); callback(null); return; } // 从Redis获取当前问题数据 const questionKey = `exam:${roomId}:current_question`; const date = await apiClient.redis.hget(questionKey, 'date'); const price = await apiClient.redis.hget(questionKey, 'price'); if (!date || !price) { callback(null); return; } const quizState: QuizState = { id: date, date, price }; callback(quizState); console.log(`用户 ${user.username} 获取房间 ${roomId} 的当前问题`); } catch (error) { console.error('获取当前问题失败:', error); socket.emit('error', '获取当前问题失败'); callback(null); } }); }