Przeglądaj źródła

nickname字段已改为使用userAuth中的username
userId已统一改为user.id
添加了必要的非空检查

yourname 6 miesięcy temu
rodzic
commit
fd23033014

+ 1 - 1
client/admin/pages_classroom_data.tsx

@@ -160,7 +160,7 @@ export const ClassroomDataPage = () => {
 
     switch(type) {
       case 'exam':
-        url = `${baseUrl}/mobile/exam?classroom=${classroom_no}`;
+        url = `${baseUrl}/mobile/exam/card?classroom=${classroom_no}`;
         successMsg = '答题卡链接已复制';
         break;
       case 'stock':

+ 13 - 12
client/mobile/components/Exam/ExamCard.tsx

@@ -9,15 +9,16 @@ import { ClassroomStatus } from '../../../share/types_stock.ts';
 import type { ExamSocketRoomMessage } from './types.ts';
 import type { AnswerRecord, Answer } from './types.ts';
 import type { ClassroomData } from '../../../share/types_stock.ts';
+import { useAuth } from "../../hooks.tsx";
 
 
 
 
 // 答题卡页面
 export default function ExamCard() {
+  const { user } = useAuth();
   const [searchParams] = useSearchParams();
   const classroom = searchParams.get('classroom');
-  const nickname = searchParams.get('nickname');
   const {
     socketRoom: { joinRoom, leaveRoom },
     answerManagement,
@@ -47,7 +48,7 @@ export default function ExamCard() {
   });
 
   useEffect(() => {
-    if (!classroom || !nickname) {
+    if (!classroom || !user?.username) {
       globalThis.location.href = '/exam';
       return;
     }
@@ -56,7 +57,7 @@ export default function ExamCard() {
       globalThis.location.href = '/exam';
       return;
     }
-  }, [classroom, nickname, classroomData]);
+  }, [classroom, user, classroomData]);
 
   // 加入/离开房间
   useEffect(() => {
@@ -105,12 +106,12 @@ export default function ExamCard() {
     setHoldingStock('1');
     setHoldingCash('0');
     
-    if (classroom && nickname) {
+    if (classroom && user?.username) {
       const answer = {
         date: currentDate,
         holdingStock: '1',
         holdingCash: '0',
-        userId: nickname,
+        userId: String(user.id),
         price: currentPrice
       };
       
@@ -118,7 +119,7 @@ export default function ExamCard() {
         await answerManagement.storeAnswer(
           classroom as string,
           currentQuestion?.id || '',
-          nickname,
+          user.username,
           answer,
           (success) => {
             if (success) {
@@ -132,18 +133,18 @@ export default function ExamCard() {
         message.error('提交答案失败');
       }
     }
-  }, [classroom, nickname, currentDate, currentPrice, answerManagement, currentQuestion]);
+  }, [classroom, user, currentDate, currentPrice, answerManagement, currentQuestion]);
 
   const handleChooseB = useCallback(async () => {
     setHoldingStock('0');
     setHoldingCash('1');
     
-    if (classroom && nickname) {
+    if (classroom && user?.username) {
       const answer = {
         date: currentDate,
         holdingStock: '0',
         holdingCash: '1',
-        userId: nickname,
+        userId: String(user.id),
         price: currentPrice
       };
       
@@ -151,7 +152,7 @@ export default function ExamCard() {
         await answerManagement.storeAnswer(
           classroom as string,
           currentQuestion?.id || '',
-          nickname,
+          user.username,
           answer,
           (success) => {
             if (success) {
@@ -165,7 +166,7 @@ export default function ExamCard() {
         message.error('提交答案失败');
       }
     }
-  }, [classroom, nickname, currentDate, currentPrice, answerManagement, currentQuestion]);
+  }, [classroom, user, currentDate, currentPrice, answerManagement, currentQuestion]);
 
   // 初始化用户的答题记录
   useEffect(() => {
@@ -252,7 +253,7 @@ export default function ExamCard() {
         {/* 信息显示 */}
         <div className="bg-white p-6 rounded-lg shadow-md">
           <div className="grid grid-cols-2 gap-4 mb-4">
-            <div className="text-gray-600">昵称: {nickname}</div>
+            <div className="text-gray-600">昵称: {user?.username || '未知用户'}</div>
             <div className="text-gray-600">代码: {classroomData.code}</div>
           </div>
 

+ 29 - 13
client/mobile/components/Exam/hooks/useSocketClient.ts

@@ -164,20 +164,20 @@ export function useSocketClient(roomId: string | null) {
     };
   }, [client, roomId]);
 
-  // 监听用户答案变化
-  useEffect(() => {
-    if (!client || !roomId) return;
+  // // 监听用户答案变化
+  // useEffect(() => {
+  //   if (!client || !roomId) return;
 
-    const handleAnswersUpdate = async () => {
-      const answers = await getAnswers(client, roomId, 'current_state');
-      setUserAnswers(answers);
-    };
+  //   const handleAnswersUpdate = async () => {
+  //     const answers = await getAnswers(client, roomId, 'current_state');
+  //     setUserAnswers(answers);
+  //   };
 
-    client.on('exam:answers', handleAnswersUpdate);
-    return () => {
-      client.off('exam:answers', handleAnswersUpdate);
-    };
-  }, [client, roomId]);
+  //   client.on('exam:answers', handleAnswersUpdate);
+  //   return () => {
+  //     client.off('exam:answers', handleAnswersUpdate);
+  //   };
+  // }, [client, roomId]);
 
 
   // 存储答案
@@ -262,6 +262,8 @@ export function useSocketClient(roomId: string | null) {
         answer: answerWithProfit
       }, (success: boolean) => {
         callback?.(success);
+
+        setUserAnswers([...userAnswers, answerWithProfit])
       });
     }, '存储答案失败');
   }, [client, getAnswers]);
@@ -366,13 +368,27 @@ export function useSocketClient(roomId: string | null) {
     onRoomMessage
   };
 
+  // 获取用户答案
+  const getUserAnswers = useCallback(async (examId: string, userId: string): Promise<Answer[]> => {
+    if (!client || !examId || !userId) return Promise.resolve([]);
+
+    return handleAsyncOperation(async () => {
+      return new Promise((resolve) => {
+        client.emit('exam:getUserAnswers', { examId, userId }, (answers: Answer[]) => {
+          resolve(answers || []);
+        });
+      });
+    }, '获取用户答案失败');
+  }, [client]);
+
   const answerManagement = {
     storeAnswer,
     getAnswers: getAnswersCallback,
     cleanupRoom,
     sendNextQuestion,
     getCurrentQuestion: getCurrentQuestionCallback,
-    getPriceHistory
+    getPriceHistory,
+    getUserAnswers
   };
 
   // 计算累计结果

+ 2 - 2
docs/exam.md

@@ -4,8 +4,8 @@
 答题卡管理员链接打开后,加入教室号 房间
 股票 点 下一天按钮时,发送 下一天的 日期,收盘价 到房间
 答提卡每次收到消息后,显示最新日期,价格,同时重置 A,B 按钮
-答提卡点击 选A或选B后,提交答案到redis,及发消息给管理员
-答题卡答题列表中,显示 答题卡 回答记录
+答提卡点击 选A或选B后,提交答案到redis,及发消息到房间
+提交答案后,答题卡答题列表中,刷新显示 当前用户 回答记录
 管理员页收到答题消息后显示出来
 管理员点击收卷,从redis读取教室的所有答案,汇总提交后交卷后端api 接口
 最后管理员清理redis,结束所有答题卡连接

+ 90 - 14
server/routes_io_exam.ts

@@ -183,8 +183,15 @@ export function setupExamEvents({ socket, apiClient }: Variables) {
       }
 
       // 存储答案到Redis
-      const answerKey = `exam:${roomId}:${questionId}:answers`;
-      await apiClient.redis.hset(answerKey, String(user.id), JSON.stringify({
+      // 新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,
@@ -237,22 +244,33 @@ export function setupExamEvents({ socket, apiClient }: Variables) {
       }
 
       // 从Redis获取答案
-      const answerKey = `exam:${roomId}:${questionId}:answers`;
-      const answersMap = await apiClient.redis.hgetall(answerKey);
+      // 支持两种查询模式:
+      // 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[] = [];
 
-      // 转换为前端需要的格式
-      Object.entries(answersMap).forEach(([userId, answerStr]) => {
+      // 并行获取所有答案
+      await Promise.all(keys.map(async (key) => {
         try {
-          const answer = JSON.parse(answerStr);
-          formattedAnswers.push({
-            ...answer,
-            userId
-          });
+          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('answerStr转换失败:', error);
+          console.log('获取答案失败:', error);
         }
-      });
+      }));
 
       callback(formattedAnswers);
     } catch (error) {
@@ -379,4 +397,62 @@ export function setupExamEvents({ socket, apiClient }: Variables) {
       callback({});
     }
   });
-}
+
+  // 获取用户答案
+  socket.on('exam:getUserAnswers', async (data: {
+    roomId: string;
+    userId: string;
+  }, callback: (answers: Array<{
+    userId: string;
+    questionId: string;
+    answer: Answer;
+    timestamp: string;
+  }>) => 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<{
+        userId: string;
+        questionId: string;
+        answer: Answer;
+        timestamp: string;
+      }> = [];
+
+      // 并行获取所有答案
+      await Promise.all(keys.map(async (key: string) => {
+        try {
+          const answerStr = await apiClient.redis.hget(key, 'data');
+          if (answerStr) {
+            const answer = JSON.parse(answerStr);
+            // 从key中提取questionId (第五部分)
+            const questionId = key.split(':')[4];
+            userAnswers.push({
+              userId,
+              questionId,
+              answer,
+              timestamp: await apiClient.redis.hget(key, 'timestamp') || new Date().toISOString()
+            });
+          }
+        } catch (error) {
+          console.log('获取用户答案失败:', error);
+        }
+      }));
+
+      callback(userAnswers);
+    } catch (error) {
+      console.error('获取用户答案失败:', error);
+      socket.emit('error', '获取用户答案失败');
+      callback([]);
+    }
+  });
+}