| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479 |
- 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<string, number>) => 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<Answer>) => 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<Answer> = [];
- // 并行获取所有答案
- 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);
- }
- });
- }
|