Ver Fonte

答题卡模块socket.io重构,创建了routes_io_exam.ts路由文件,实现了考试相关socket事件处理

yourname há 6 meses atrás
pai
commit
7c4c465ade

+ 84 - 29
client/mobile/components/Exam/ExamAdmin.tsx

@@ -4,7 +4,11 @@ import { Table, Button, message, Input, QRCode, Modal, Tabs } from 'antd';
 // import type { ColumnType } from 'antd/es/table';
 import type { GetProp , TableProps} from 'antd';
 import dayjs from 'dayjs';
-import { useCurrentQuestion, useRoomMessages, useAnswerManagement, useSocketRoom, useAnswerCache } from './hooks/useSocketClient.ts';
+import { useSocketClient } from './hooks/useSocketClient.ts';
+import type {
+  QuizState,
+  ExamSocketRoomMessage
+} from './types.ts';
 
 import type { Answer, CumulativeResult } from './types.ts';
 
@@ -67,15 +71,12 @@ function QRCodeSection({ classroom }: { classroom: string }) {
 export default function ExamAdmin() {
   const [searchParams] = useSearchParams();
   const classroom = searchParams.get('classroom');
-  useSocketRoom(classroom);
-  const {currentQuestion} = useCurrentQuestion(classroom);
-  const lastMessage = useRoomMessages(classroom);
-  const { 
-    submitAnswersToBackend, 
-    autoSettlement,
-    restartTraining 
-  } = useAnswerManagement(classroom);
-  
+  const {
+    socketRoom,
+    answerManagement,
+    // calculateCumulativeResults
+  } = useSocketClient(classroom as string);
+
   const [answers, setAnswers] = useState<Answer[]>([]);
   const [dailyAnswers, setDailyAnswers] = useState<{[key: string]: Answer[]}>({});
   const [currentDate, setCurrentDate] = useState('');
@@ -83,25 +84,75 @@ export default function ExamAdmin() {
   const [mark, setMark] = useState('');
   const [activeTab, setActiveTab] = useState('current');
 
-  // 使用新的 useAnswerCache hook
-  const { answers: cachedAnswers } = useAnswerCache(classroom, currentDate);
+  // 获取当前题目
+  const [currentQuestion, setCurrentQuestion] = useState<QuizState | null>(null);
+  const [lastMessage, setLastMessage] = useState<ExamSocketRoomMessage | null>(null);
 
-  // 更新答案状态
+  // 初始化socket连接和监听
   useEffect(() => {
-    if (cachedAnswers && cachedAnswers.length > 0) {
-      setAnswers(cachedAnswers);
-    }
-  }, [cachedAnswers]);
+    if (!classroom) return;
+
+    socketRoom.joinRoom(classroom);
+    
+    const handleMessage = (data: ExamSocketRoomMessage) => {
+      setLastMessage(data);
+    };
+
+    socketRoom.onRoomMessage(handleMessage);
+
+    return () => {
+      socketRoom.leaveRoom(classroom);
+      socketRoom.onRoomMessage(handleMessage);
+    };
+  }, [classroom]);
+
+  // 获取当前题目状态
+  useEffect(() => {
+    if (!classroom) return;
 
-  // 更新每日答题情况
+    const fetchCurrentQuestion = async () => {
+      const question = await answerManagement.getCurrentQuestion(classroom);
+      setCurrentQuestion(question);
+    };
+
+    fetchCurrentQuestion();
+  }, [classroom, lastMessage]);
+
+  // 获取答案
   useEffect(() => {
-    if (currentDate && cachedAnswers) {
-      setDailyAnswers((prev: {[key: string]: Answer[]}) => ({
+    if (!classroom || !currentDate) return;
+
+    const fetchAnswers = async () => {
+      const answers = await answerManagement.getAnswers(
+        classroom as string,
+        currentDate
+      );
+      setAnswers(answers);
+      setDailyAnswers(prev => ({
         ...prev,
-        [currentDate]: cachedAnswers
+        [currentDate]: answers
       }));
-    }
-  }, [currentDate, cachedAnswers]);
+    };
+
+    fetchAnswers();
+  }, [classroom, currentDate, lastMessage]);
+
+  // // 更新答案状态
+  // useEffect(() => {
+  //   if (cachedAnswers && cachedAnswers.length > 0) {
+  //     setAnswers(cachedAnswers);
+  //   }
+  // }, [cachedAnswers]);
+
+  // // 更新每日答题情况
+  // useEffect(() => {
+  //   if (currentDate && cachedAnswers) {
+  //     setDailyAnswers((prev: {[key: string]: Answer[]}) => ({
+  //       ...prev,
+  //       [currentDate]: cachedAnswers
+  //     }));
+  //   }
+  // }, [currentDate, cachedAnswers]);
 
   useEffect(() => {
     if (currentQuestion) {
@@ -116,7 +167,10 @@ export default function ExamAdmin() {
     if (!classroom || answers.length === 0) return;
 
     try {
-      await autoSettlement(currentDate);
+      await answerManagement.sendNextQuestion(classroom, {
+        date: currentDate,
+        price: currentPrice
+      });
       message.success('结算成功');
     } catch (error) {
       console.error('结算失败:', error);
@@ -124,12 +178,11 @@ export default function ExamAdmin() {
     }
   };
 
-  // 修改提交函数
   const handleSubmit = async () => {
     if (!classroom || answers.length === 0) return;
 
     try {
-      await submitAnswersToBackend(currentDate);
+      await answerManagement.cleanupRoom(classroom, currentDate);
       message.success('答案提交成功');
       setAnswers([]);
     } catch (error: any) {
@@ -138,10 +191,9 @@ export default function ExamAdmin() {
     }
   };
 
-  // 重新开始
   const handleRestart = async () => {
     try {
-      await restartTraining();
+      await answerManagement.cleanupRoom(classroom);
       setAnswers([]);
       setDailyAnswers({});
       setCurrentDate('');
@@ -290,7 +342,10 @@ export default function ExamAdmin() {
     {
       key: 'cumulative',
       label: '累计结果',
-      children: <CumulativeResults results={calculateCumulativeResults(dailyAnswers)} columns={resultColumns} />,
+      children: <CumulativeResults
+        results={calculateCumulativeResults(answers)}
+        columns={resultColumns}
+      />,
     },
   ];
 

+ 23 - 11
client/mobile/components/Exam/ExamCard.tsx

@@ -3,7 +3,8 @@ import { useQuery } from '@tanstack/react-query';
 import { useSearchParams } from "react-router";
 import dayjs from 'dayjs';
 import { message } from 'antd';
-import { useCurrentQuestion, useRoomMessages, useAnswerSubmission, useUserAnswerHistory, useSocketRoom } from './hooks/useSocketClient.ts';
+import { useSocketClient } from './hooks/useSocketClient.ts';
+import type { ExamSocketRoomMessage } from './types.ts';
 import type { AnswerRecord, ClassroomData, Answer } from './types.ts';
 
 
@@ -14,11 +15,13 @@ export default function ExamCard() {
   const [searchParams] = useSearchParams();
   const classroom = searchParams.get('classroom');
   const nickname = searchParams.get('nickname');
-  useSocketRoom(classroom);
-  const lastMessage = useRoomMessages(classroom);
-  const { submitAnswer } = useAnswerSubmission(classroom);
-  const {currentQuestion} = useCurrentQuestion(classroom);
-  const { userAnswers } = useUserAnswerHistory(classroom, nickname);
+  const {
+    socketRoom,
+    answerManagement,
+    currentQuestion,
+    lastMessage,
+    userAnswers
+  } = useSocketClient(classroom as string);
   const [currentDate, setCurrentDate] = useState('');
   const [currentPrice, setCurrentPrice] = useState('0');
   const [holdingStock, setHoldingStock] = useState('0');
@@ -99,14 +102,18 @@ export default function ExamCard() {
       };
       
       try {
-        await submitAnswer(currentDate, nickname, answer);
+        await answerManagement.storeAnswer(
+          classroom as string,
+          currentQuestion?.id || '',
+          nickname,
+          answer
+        );
       } catch (error) {
         message.error('提交答案失败');
       }
     }
-  }, [classroom, nickname, currentDate, currentPrice, submitAnswer]);
+  }, [classroom, nickname, currentDate, currentPrice, answerManagement, currentQuestion]);
 
-  // 处理选择B(持币)
   const handleChooseB = useCallback(async () => {
     setHoldingStock('0');
     setHoldingCash('1');
@@ -121,12 +128,17 @@ export default function ExamCard() {
       };
       
       try {
-        await submitAnswer(currentDate, nickname, answer);
+        await answerManagement.storeAnswer(
+          classroom as string,
+          currentQuestion?.id || '',
+          nickname,
+          answer
+        );
       } catch (error) {
         message.error('提交答案失败');
       }
     }
-  }, [classroom, nickname, currentDate, currentPrice, submitAnswer]);
+  }, [classroom, nickname, currentDate, currentPrice, answerManagement, currentQuestion]);
 
   // 初始化用户的答题记录
   useEffect(() => {

+ 266 - 538
client/mobile/components/Exam/hooks/useSocketClient.ts

@@ -1,19 +1,21 @@
 import { useEffect, useState, useCallback } from 'react';
-import { APIClient } from '@d8d-appcontainer/api';
 import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { io, Socket } from 'socket.io-client';
 import type {
   QuizContent,
   QuizState,
   ExamSocketMessage,
   ExamSocketRoomMessage,
-  Answer
+  Answer,
+  CumulativeResult
 } from '../types.ts';
-import { useAuth } from "../../../hooks.tsx";
 
-interface LoaderData {
-  token: string;
-  serverUrl: string;
+interface FullExamSocketMessage extends Omit<ExamSocketMessage, 'timestamp'> {
+  id: string;
+  from: string;
+  timestamp: string;
 }
+import { useAuth } from "../../../hooks.tsx";
 
 // 工具函数:统一错误处理
 const handleAsyncOperation = async <T>(
@@ -36,639 +38,365 @@ interface ProfitResult {
 
 function calculateProfit(currentPrice: number, previousPrice: number, holdingStock: string): ProfitResult {
   if (holdingStock === '1') {
-    const profitAmount = currentPrice - previousPrice;  // 金额收益
-    const profitPercent = ((currentPrice - previousPrice) / previousPrice) * 100;  // 百分比收益
-    return {
-      profitAmount,
-      profitPercent
-    };
+    const profitAmount = currentPrice - previousPrice;
+    const profitPercent = ((currentPrice - previousPrice) / previousPrice) * 100;
+    return { profitAmount, profitPercent };
   }
-  return {
-    profitAmount: 0,
-    profitPercent: 0
-  };
+  return { profitAmount: 0, profitPercent: 0 };
+}
+
+// 提前声明函数
+function getAnswers(client: Socket | null, roomId: string, questionId: string): Promise<Answer[]> {
+  if (!client) return Promise.resolve([]);
+
+  return new Promise((resolve) => {
+    client.emit('exam:getAnswers', { roomId, questionId }, (answers: Answer[]) => {
+      resolve(answers || []);
+    });
+  });
+}
+
+function getCurrentQuestion(client: Socket | null, roomId: string, getAnswersFn: typeof getAnswers): Promise<QuizState | null> {
+  if (!client) return Promise.resolve(null);
+
+  return getAnswersFn(client, roomId, 'current_state').then(answers => {
+    const currentState = answers[0];
+    if (currentState) {
+      return {
+        date: currentState.date || '',
+        price: currentState.price || '0'
+      };
+    }
+    return null;
+  });
 }
 
-// 使用react-query管理socket客户端
 export function useSocketClient(roomId: string | null) {
   const { token } = useAuth();
-  const serverUrl = '/';
+  const queryClient = useQueryClient();
+  const [socket, setSocket] = useState<Socket | null>(null);
+  const [currentQuestion, setCurrentQuestion] = useState<QuizState | null>(null);
+  const [lastMessage, setLastMessage] = useState<ExamSocketRoomMessage | null>(null);
+  const [userAnswers, setUserAnswers] = useState<Answer[]>([]);
 
+  // 初始化socket连接
   const { data: client } = useQuery({
-    queryKey: ['socket-client'],
+    queryKey: ['socket-client', token],
     queryFn: async () => {
-      if (!token || !serverUrl) return null;
-
-      const apiClient = new APIClient({
-        scope: 'user',
-        config: {
-          serverUrl,
-          type: 'socket',
-          token,
-        }
+      if (!token) return null;
+
+      const newSocket = io('/', {
+        path: '/socket.io',
+        transports: ['websocket'],
+        query: { token },
+        reconnection: true,
+        reconnectionAttempts: 5,
+        reconnectionDelay: 1000,
+      });
+
+      newSocket.on('connect', () => {
+        console.log('Socket connected');
+      });
+
+      newSocket.on('disconnect', () => {
+        console.log('Socket disconnected');
       });
 
-      await apiClient.connect();
-      return apiClient;
+      newSocket.on('error', (error) => {
+        console.error('Socket error:', error);
+      });
+
+      setSocket(newSocket);
+      return newSocket;
     },
-    enabled: !!token && !!serverUrl && !!roomId,
+    enabled: !!token && !!roomId,
     staleTime: Infinity,
+    gcTime: 0,
     retry: 3,
-    gcTime: 0
   });
 
+  // 加入房间
   const joinRoom = useCallback(async (roomId: string) => {
     if (client) {
-      await client.socket.joinRoom(roomId);
+      client.emit('exam:join', { roomId });
     }
   }, [client]);
 
+  // 离开房间
   const leaveRoom = useCallback(async (roomId: string) => {
     if (client) {
-      await client.socket.leaveRoom(roomId);
+      client.emit('exam:leave', { roomId });
     }
   }, [client]);
 
+  // 发送房间消息
   const sendRoomMessage = useCallback(async (roomId: string, message: ExamSocketMessage) => {
     if (client) {
-      await client.socket.sendRoomMessage(roomId, message as any);
+      client.emit('exam:message', { roomId, message });
     }
   }, [client]);
 
+  // 监听房间消息
   const onRoomMessage = useCallback((callback: (data: ExamSocketRoomMessage) => void) => {
     if (client) {
-      client.socket.onRoomMessage(callback);
+      client.on('exam:message', (data) => {
+        setLastMessage(data);
+        callback(data);
+      });
     }
   }, [client]);
 
-  const getAnswers = useCallback(async (roomId: string, questionId: string): Promise<Answer[]> => {
-    if (!client) return [];
+  // 监听当前问题变化
+  useEffect(() => {
+    if (!client || !roomId) return;
 
-    return handleAsyncOperation(async () => {
-      const answersData = await client.redis.hgetall(`quiz:${roomId}:answers:${questionId}`);
-      if (!answersData) return [];
-      
-      return Object.entries(answersData).map(([userId, data]) => ({
-        ...(JSON.parse(data) as QuizContent),
-        userId,
-      })) as Answer[];
-    }, '获取答案失败');
-  }, [client]);
+    const handleQuestionUpdate = async () => {
+      const question = await getCurrentQuestion(client, roomId, getAnswers);
+      setCurrentQuestion(question);
+    };
+
+    client.on('exam:question', handleQuestionUpdate);
+    return () => {
+      client.off('exam:question', handleQuestionUpdate);
+    };
+  }, [client, roomId]);
+
+  // 监听用户答案变化
+  useEffect(() => {
+    if (!client || !roomId) return;
 
+    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]);
+
+
+  // 存储答案
   const storeAnswer = useCallback(async (roomId: string, questionId: string, userId: string, answer: QuizContent) => {
     if (!client) return;
 
-    // 获取历史价格数据
-    const pricesData = await client.redis.hgetall(`quiz:${roomId}:prices`);
-    if (!pricesData) {
-      // 如果没有历史数据,存储初始答案
-      const initialAnswer: Answer = {
-        ...answer,
-        userId,
-        profitAmount: 0,
-        profitPercent: 0,
-        totalProfitAmount: 0,
-        totalProfitPercent: 0
-      };
-      
-      await client.redis.hset(
-        `quiz:${roomId}:answers:${questionId}`,
-        userId,
-        JSON.stringify(initialAnswer)
-      );
-      return;
-    }
+    return handleAsyncOperation(async () => {
+      // 获取历史价格数据
+      const pricesData = await new Promise<any>((resolve) => {
+        client.emit('exam:getPrices', { roomId }, resolve);
+      });
 
-    // 获取该用户的所有历史答案
-    const dates = Object.keys(pricesData).sort();
-    const allAnswers = await Promise.all(
-      dates.map(date => getAnswers(roomId, date))
-    );
-
-    // 过滤出当前用户的答案并添加价格信息
-    const userAnswers = allAnswers
-      .flat()
-      .filter((a: Answer) => a.userId === userId)
-      .map((a: Answer) => {
-        if (!a.date) return a;
-        const priceData = JSON.parse(pricesData[a.date] || '{"price":"0"}');
-        return {
-          ...a,
-          price: priceData.price
+      if (!pricesData) {
+        // 存储初始答案
+        const initialAnswer: Answer = {
+          ...answer,
+          userId,
+          holdingStock: '0',
+          holdingCash: '0',
+          profitAmount: 0,
+          profitPercent: 0,
+          totalProfitAmount: 0,
+          totalProfitPercent: 0
         };
-      })
-      .sort((a: Answer, b: Answer) => new Date(a.date || '').getTime() - new Date(b.date || '').getTime());
+        
+        client.emit('exam:storeAnswer', {
+          roomId,
+          questionId,
+          userId,
+          answer: initialAnswer
+        });
+        return;
+      }
 
-    // 计算收益
-    let totalProfitAmount = 0;
-    let totalProfitPercent = 0;
-    
-    if (userAnswers.length > 0) {
-      const prevAnswer = userAnswers[userAnswers.length - 1];
-      const { profitAmount, profitPercent } = calculateProfit(
-        parseFloat(String(answer.price)),
-        parseFloat(String(prevAnswer.price)),
-        prevAnswer.holdingStock as string
+      // 获取该用户的所有历史答案
+      const dates = Object.keys(pricesData).sort();
+      const allAnswers = await Promise.all(
+        dates.map(date => getAnswers(client, roomId, date))
       );
-      
-      totalProfitAmount = (prevAnswer.totalProfitAmount || 0) + profitAmount;
-      totalProfitPercent = (prevAnswer.totalProfitPercent || 0) + profitPercent;
-    }
 
-    // 存储带有收益信息的答案
-    const answerWithProfit: Answer = {
-      ...answer,
-      userId,
-      profitAmount: userAnswers.length > 0 ? totalProfitAmount - (userAnswers[userAnswers.length - 1].totalProfitAmount || 0) : 0,
-      profitPercent: userAnswers.length > 0 ? totalProfitPercent - (userAnswers[userAnswers.length - 1].totalProfitPercent || 0) : 0,
-      totalProfitAmount,
-      totalProfitPercent
-    };
+      // 计算收益
+      const userAnswers = allAnswers
+        .flat()
+        .filter((a: Answer) => a.userId === userId)
+        .map((a: Answer) => ({
+          ...a,
+          price: pricesData[a.date || '']?.price || '0'
+        }))
+        .sort((a: Answer, b: Answer) => new Date(a.date || '').getTime() - new Date(b.date || '').getTime());
 
-    if (client?.redis) {
-      await client.redis.hset(
-        `quiz:${roomId}:answers:${questionId}`,
+      let totalProfitAmount = 0;
+      let totalProfitPercent = 0;
+      
+      if (userAnswers.length > 0) {
+        const prevAnswer = userAnswers[userAnswers.length - 1];
+        const { profitAmount, profitPercent } = calculateProfit(
+          parseFloat(String(answer.price)),
+          parseFloat(String(prevAnswer.price)),
+          prevAnswer.holdingStock as string
+        );
+        
+        totalProfitAmount = (prevAnswer.totalProfitAmount || 0) + profitAmount;
+        totalProfitPercent = (prevAnswer.totalProfitPercent || 0) + profitPercent;
+      }
+
+      // 存储带有收益信息的答案
+      const answerWithProfit: Answer = {
+        ...answer,
         userId,
-        JSON.stringify(answerWithProfit)
-      );
-    }
+        profitAmount: userAnswers.length > 0 ? totalProfitAmount - (userAnswers[userAnswers.length - 1].totalProfitAmount || 0) : 0,
+        profitPercent: userAnswers.length > 0 ? totalProfitPercent - (userAnswers[userAnswers.length - 1].totalProfitPercent || 0) : 0,
+        totalProfitAmount,
+        totalProfitPercent
+      };
+
+      client.emit('exam:storeAnswer', { 
+        roomId, 
+        questionId, 
+        userId, 
+        answer: answerWithProfit 
+      });
+    }, '存储答案失败');
   }, [client, getAnswers]);
 
+  // 清理房间数据
   const cleanupRoom = useCallback(async (roomId: string, questionId?: string) => {
     if (!client) return;
 
     await handleAsyncOperation(async () => {
       if (questionId) {
-        await client.redis.del(`quiz:${roomId}:answers:${questionId}`);
+        client.emit('exam:cleanup', { roomId, questionId });
       } else {
-        await Promise.all([
-          client.redis.delByPattern(`quiz:${roomId}:answers:*`),
-          client.redis.del(`quiz:${roomId}:prices`)
-        ]);
+        client.emit('exam:cleanup', { roomId });
       }
     }, '清理房间数据失败');
   }, [client]);
 
+  // 发送下一题
   const sendNextQuestion = useCallback(async (roomId: string, state: QuizState) => {
     if (!client) return;
 
     return handleAsyncOperation(async () => {
-      const message = {
+      const message: FullExamSocketMessage = {
+        id: `question-${Date.now()}`,
         type: 'question',
+        from: 'system',
+        timestamp: Date.now().toString(),
         content: {
           date: state.date,
-          price: state.price
+          price: state.price,
+          holdingStock: '0',
+          holdingCash: '0',
+          userId: 'system'
         }
       };
 
       // 存储当前问题状态
       await storeAnswer(roomId, 'current_state', 'system', {
         date: state.date,
-        price: state.price
+        price: state.price,
+        holdingStock: '0',
+        holdingCash: '0',
+        userId: 'system'
       });
 
       // 存储价格历史记录
-      await client.redis.hset(
-        `quiz:${roomId}:prices`,
-        state.date,
-        JSON.stringify({ price: state.price })
-      );
+      client.emit('exam:storePrice', { 
+        roomId, 
+        date: state.date, 
+        price: state.price 
+      });
 
       await sendRoomMessage(roomId, message);
     }, '发送题目失败');
   }, [client, sendRoomMessage, storeAnswer]);
 
-  const getCurrentQuestion = useCallback(async (roomId: string): Promise<QuizState | null> => {
-    if (!client) return null;
-
-    return handleAsyncOperation(async () => {
-      const answers = await getAnswers(roomId, 'current_state');
-      const currentState = answers[0];
-      if (currentState) {
-        return {
-          date: currentState.date || '',
-          price: currentState.price || '0'
-        };
-      }
-      return null;
-    }, '获取当前题目状态失败');
-  }, [client, getAnswers]);
 
-  // 添加获取历史价格的函数
+  // 获取历史价格
   const getPriceHistory = useCallback(async (roomId: string, date: string): Promise<string> => {
     if (!client) return '0';
 
     return handleAsyncOperation(async () => {
-      const priceData = await client.redis.hget(`quiz:${roomId}:prices`, date);
-      if (!priceData) return '0';
-      
-      const { price } = JSON.parse(priceData);
-      return String(price);
+      return new Promise((resolve) => {
+        client.emit('exam:getPrice', { roomId, date }, (price: string) => {
+          resolve(price || '0');
+        });
+      });
     }, '获取历史价格失败');
   }, [client]);
 
-  return {
+  // 获取答案 (封装为useCallback)
+  const getAnswersCallback = useCallback((roomId: string, questionId: string): Promise<Answer[]> => {
+    if (!client) return Promise.resolve([]);
+    return handleAsyncOperation(async () => {
+      return getAnswers(client, roomId, questionId);
+    }, '获取答案失败');
+  }, [client]);
+
+  // 获取当前题目 (封装为useCallback)
+  const getCurrentQuestionCallback = useCallback((roomId: string): Promise<QuizState | null> => {
+    if (!client) return Promise.resolve(null);
+    return handleAsyncOperation(async () => {
+      return getCurrentQuestion(client, roomId, getAnswers);
+    }, '获取当前题目状态失败');
+  }, [client]);
+
+  // 清理socket连接
+  useEffect(() => {
+    return () => {
+      if (socket) {
+        socket.disconnect();
+      }
+    };
+  }, [socket]);
+
+  // 导出所有功能作为单个对象
+  const socketRoom = {
     client,
     joinRoom,
     leaveRoom,
     sendRoomMessage,
-    onRoomMessage,
+    onRoomMessage
+  };
+
+  const answerManagement = {
     storeAnswer,
-    getAnswers,
+    getAnswers: getAnswersCallback,
     cleanupRoom,
     sendNextQuestion,
-    getCurrentQuestion,
+    getCurrentQuestion: getCurrentQuestionCallback,
     getPriceHistory
   };
-}
-
-// Socket Room Hook
-export function useSocketRoom(roomId: string | null) {
-  const socketClient = useSocketClient(roomId);
-
-  const { data: roomConnection } = useQuery({
-    queryKey: ['socket-room', roomId],
-    queryFn: async () => {
-      if (!roomId || !socketClient.client) return null;
-      await socketClient.joinRoom(roomId);
-      console.log(`Connected to room: ${roomId}`);
-      return { roomId, connected: true };
-    },
-    enabled: !!roomId && !!socketClient.client,
-    staleTime: Infinity,
-    gcTime: Infinity,
-    retry: false
-  });
-
-  return {
-    connected: roomConnection?.connected || false,
-    socketClient
-  };
-}
 
-// 使用react-query管理当前题目状态
-export function useCurrentQuestion(roomId: string | null) {
-  const socketClient = useSocketClient(roomId);
-  
-  const { data: currentQuestion, refetch } = useQuery({
-    queryKey: ['current-question', roomId],
-    queryFn: async () => {
-      if (!roomId || !socketClient) return null;
-      return socketClient.getCurrentQuestion(roomId);
-    },
-    enabled: !!roomId && !!socketClient,
-    staleTime: 0
-  });
+  // 计算累计结果
+  // const calculateCumulativeResults = useCallback((answers: Answer[]): CumulativeResult[] => {
+  //   const userResults = new Map<string, CumulativeResult>();
+    
+  //   answers.forEach((answer) => {
+  //     const userId = answer.userId;
+  //     if (!userResults.has(userId)) {
+  //       userResults.set(userId, {
+  //         userId,
+  //         totalProfitAmount: answer.totalProfitAmount || 0,
+  //         totalProfitPercent: answer.totalProfitPercent || 0
+  //       });
+  //     }
+  //   });
+
+  //   return Array.from(userResults.values());
+  // }, []);
 
   return {
+    socketRoom,
+    answerManagement,
+    // calculateCumulativeResults,
     currentQuestion,
-    refetchQuestion: refetch
-  };
-}
-
-// 使用react-query管理房间消息
-export function useRoomMessages(roomId: string | null) {
-  const socketClient = useSocketClient(roomId);
-  const queryClient = useQueryClient();
-  const [lastMessage, setLastMessage] = useState<ExamSocketRoomMessage | null>(null);
-
-  useEffect(() => {
-    if (!roomId || !socketClient) return;
-
-    const handleMessage = (data: ExamSocketRoomMessage) => {
-      setLastMessage(data);
-      const { type, content } = data.message;
-
-      // 处理不同类型的消息
-      switch (type) {
-        case 'question':
-          queryClient.invalidateQueries({ queryKey: ['current-question', roomId] });
-          break;
-        case 'answer':
-          const { date } = content;
-          queryClient.invalidateQueries({ queryKey: ['answers', roomId, date] });
-          break;
-        case 'settlement':
-        case 'submit':
-          // 刷新所有用户的答题历史
-          queryClient.invalidateQueries({ 
-            queryKey: ['user-answers'],
-            predicate: (query) => query.queryKey[1] === roomId 
-          });
-          // 刷新当前答案缓存
-          queryClient.invalidateQueries({ 
-            queryKey: ['answers', roomId] 
-          });
-          break;
-        case 'restart':
-          // 重置所有相关查询
-          queryClient.invalidateQueries({ queryKey: ['current-question', roomId] });
-          queryClient.invalidateQueries({ queryKey: ['answers', roomId] });
-          queryClient.invalidateQueries({ 
-            queryKey: ['user-answers'],
-            predicate: (query) => query.queryKey[1] === roomId 
-          });
-          queryClient.invalidateQueries({ queryKey: ['training-results', roomId] });
-          break;
-      }
-    };
-
-    socketClient.onRoomMessage(handleMessage);
-  }, [roomId, socketClient, queryClient]);
-
-  return lastMessage;
-}
-
-// 使用react-query管理答案缓存
-export function useAnswerCache(roomId: string | null, date: string | null) {
-  const socketClient = useSocketClient(roomId);
-
-  const { data: answers, refetch: refetchAnswers } = useQuery({
-    queryKey: ['answers', roomId, date],
-    queryFn: async () => {
-      if (!roomId || !date || !socketClient) return [];
-      const answers = await socketClient.getAnswers(roomId, date);
-      const priceData = await socketClient.client?.redis.hget(`quiz:${roomId}:prices`, date);
-      if (!priceData) return answers;
-
-      const { price } = JSON.parse(priceData);
-      return answers.map((answer: Answer) => ({
-        ...answer,
-        price
-      }));
-    },
-    enabled: !!roomId && !!date && !!socketClient,
-    staleTime: 0
-  });
-
-  return {
-    answers: answers || [],
-    refetchAnswers
-  };
-}
-
-// 使用react-query管理用户答案历史
-export function useUserAnswerHistory(roomId: string | null, userId: string | null) {
-  const socketClient = useSocketClient(roomId);
-
-  const { data: userAnswers, refetch: refetchUserAnswers } = useQuery({
-    queryKey: ['user-answers', roomId, userId],
-    queryFn: async () => {
-      if (!roomId || !userId || !socketClient) return [];
-      
-      // 先获取所有价格记录
-      const pricesData = await socketClient.client?.redis.hgetall(`quiz:${roomId}:prices`);
-      if (!pricesData) return [];
-      
-      // 获取所有日期的答案
-      const allAnswers = await Promise.all(
-        Object.keys(pricesData).map(date => socketClient.getAnswers(roomId, date))
-      );
-      
-      // 过滤出当前用户的答案并添加价格信息
-      const userAnswersWithPrice = allAnswers
-        .flat()
-        .filter((answer: Answer) => answer.userId === userId)
-        .map((answer: Answer) => {
-          if (!answer.date) return answer;
-          const priceData = JSON.parse(pricesData[answer.date] || '{"price":"0"}');
-          return {
-            ...answer,
-            price: priceData.price
-          };
-        })
-        .sort((a: Answer, b: Answer) => new Date(a.date || '').getTime() - new Date(b.date || '').getTime()); // 按日期排序
-
-      // 计算每条记录的收益
-      let totalProfitAmount = 0;
-      let totalProfitPercent = 0;
-      const answersWithProfit = userAnswersWithPrice.map((answer: Answer, index: number) => {
-        if (index === 0) {
-          return { 
-            ...answer, 
-            profitAmount: 0,
-            profitPercent: 0,
-            totalProfitAmount: 0,
-            totalProfitPercent: 0
-          };
-        }
-
-        const prevAnswer = userAnswersWithPrice[index - 1];
-        const { profitAmount, profitPercent } = calculateProfit(
-          parseFloat(answer.price as string),
-          parseFloat(prevAnswer.price as string),
-          prevAnswer.holdingStock as string
-        );
-        
-        totalProfitAmount += profitAmount;
-        totalProfitPercent += profitPercent;
-
-        return {
-          ...answer,
-          profitAmount,
-          profitPercent,
-          totalProfitAmount,
-          totalProfitPercent
-        };
-      });
-
-      return answersWithProfit;
-    },
-    enabled: !!roomId && !!userId && !!socketClient,
-    staleTime: 0
-  });
-
-  return {
-    userAnswers: userAnswers || [],
-    refetchUserAnswers
-  };
-}
-
-// 使用react-query管理答案提交
-export function useAnswerSubmission(roomId: string | null) {
-  const { client, sendRoomMessage, storeAnswer } = useSocketClient(roomId);
-  const queryClient = useQueryClient();
-
-  const submitAnswer = useCallback(async (date: string, nickname: string, answer: any) => {
-    if (!client) return;
-
-    return handleAsyncOperation(async () => {
-      await storeAnswer(roomId, date, nickname, answer);
-      await sendRoomMessage(roomId, {
-        type: 'answer',
-        content: answer
-      });
-      queryClient.invalidateQueries({ queryKey: ['answers', roomId, date] });
-      queryClient.invalidateQueries({ queryKey: ['user-answers', roomId, nickname] });
-    }, '提交答案失败');
-  }, [roomId, client, storeAnswer, sendRoomMessage, queryClient]);
-
-  return { submitAnswer };
-}
-
-// 使用react-query管理答案提交到后端
-export function useAnswerManagement(roomId: string | null) {
-  const socketClient = useSocketClient(roomId);
-  const queryClient = useQueryClient();
-
-  // 添加自动结算函数
-  const autoSettlement = useCallback(async (date: string) => {
-    if (!socketClient?.client) return;
-
-    return handleAsyncOperation(async () => {
-      // 获取当前所有答案
-      const answers = await socketClient.getAnswers(roomId, date);
-      const currentPrice = answers[0]?.price;  // 使用当前价格作为结算价格
-      
-      if (!currentPrice) return;
-
-      // 找出所有持股的用户
-      const holdingStockUsers = answers.filter((answer: Answer) => answer.holdingStock === '1');
-      
-      // 为每个持股用户创建一个结算记录
-      await Promise.all(holdingStockUsers.map(async (answer: Answer) => {
-        const settlementAnswer = {
-          ...answer,
-          date,
-          holdingStock: '0',  // 清仓
-          holdingCash: '1',   // 全部持币
-          price: currentPrice,
-          userId: answer.userId,
-        };
-        
-        // 存储结算记录
-        await socketClient.storeAnswer(roomId, date, answer.userId, settlementAnswer);
-      }));
-
-      // 发送结算消息通知客户端刷新
-      await socketClient.sendRoomMessage(roomId, {
-        type: 'settlement',
-        content: {
-          date,
-          price: currentPrice
-        }
-      });
-
-      // 刷新当前页面的数据
-      queryClient.invalidateQueries({ queryKey: ['answers', roomId, date] });
-    }, '自动结算失败');
-  }, [roomId, socketClient, queryClient]);
-
-  const submitAnswersToBackend = useCallback(async (date: string) => {
-    if (!socketClient) return;
-
-    return handleAsyncOperation(async () => {
-      const allAnswers = await socketClient.getAnswers(roomId, date);
-      
-      // 检查是否还有持股的用户
-      const hasHoldingStock = allAnswers.some((answer: Answer) => answer.holdingStock === '1');
-      if (hasHoldingStock) {
-        throw new Error('还有用户持股中,请先进行结算');
-      }
-
-      const priceData = await socketClient.client?.redis.hget(`quiz:${roomId}:prices`, date);
-      const { price } = priceData ? JSON.parse(priceData) : { price: '0' };
-      
-      // 获取前一天的价格
-      const allPrices = await socketClient.client?.redis.hgetall(`quiz:${roomId}:prices`);
-      const dates = Object.keys(allPrices || {}).sort();
-      const currentDateIndex = dates.indexOf(date);
-      const prevPrice = currentDateIndex > 0 
-        ? JSON.parse(allPrices![dates[currentDateIndex - 1]]).price 
-        : price;
-
-      // 计算每个用户的收益
-      const answersWithProfit = allAnswers.map((answer: Answer) => ({
-        ...answer,
-        price,
-        profit: calculateProfit(parseFloat(price), parseFloat(prevPrice), answer.holdingStock || '0')
-      }));
-      
-      const response = await fetch('/api/v1/classroom-answers', {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json'
-        },
-        body: JSON.stringify({
-          classroom_no: roomId,
-          date,
-          answers: answersWithProfit
-        })
-      });
-
-      const data = await response.json();
-      if (!data.success) {
-        throw new Error(data.message || '提交失败');
-      }
-
-      // 发送收卷消息通知客户端
-      await socketClient.sendRoomMessage(roomId, {
-        type: 'submit',
-        content: {
-          date,
-          price
-        }
-      });
-
-      await socketClient.cleanupRoom(roomId, date);
-      return data;
-    }, '提交答案到后端失败');
-  }, [roomId, socketClient]);
-
-  const { data: results, refetch: refetchResults } = useQuery({
-    queryKey: ['training-results', roomId],
-    queryFn: async () => {
-      if (!roomId) return null;
-      const response = await fetch(`/api/v1/classroom-results?classroom_no=${roomId}`);
-      const data = await response.json();
-      if (!data.success) {
-        throw new Error(data.message || '获取结果失败');
-      }
-      return data.data;
-    },
-    enabled: false
-  });
-
-  const restartTraining = useCallback(async () => {
-    if (!socketClient) return;
-
-    return handleAsyncOperation(async () => {
-      await socketClient.cleanupRoom(roomId);
-      // 发送重开消息
-      await socketClient.sendRoomMessage(roomId, {
-        type: 'restart',
-        content: {}
-      });
-      queryClient.invalidateQueries({ queryKey: ['current-question', roomId] });
-      queryClient.invalidateQueries({ queryKey: ['answers', roomId] });
-      queryClient.invalidateQueries({ queryKey: ['training-results', roomId] });
-    }, '重启训练失败');
-  }, [roomId, socketClient, queryClient]);
-
-  return {
-    autoSettlement,  // 暴露结算函数
-    submitAnswersToBackend,
-    results,
-    refetchResults,
-    restartTraining
+    lastMessage,
+    userAnswers,
+    // 兼容旧版导入
+    ...socketRoom,
+    ...answerManagement
   };
 }
 
-// 使用react-query管理题目发送 - 直接使用 useSocketClient 中的 sendNextQuestion
-export function useQuestionManagement(roomId: string | null) {
-  const socketClient = useSocketClient(roomId);
-  const queryClient = useQueryClient();
-
-  const sendNextQuestion = useCallback(async (state: QuizState) => {
-    if (!socketClient) return;
-
-    return handleAsyncOperation(async () => {
-      await socketClient.sendNextQuestion(roomId, state);
-      queryClient.invalidateQueries({ queryKey: ['current-question', roomId] });
-    }, '发送题目失败');
-  }, [roomId, socketClient, queryClient]);
-
-  return {
-    sendNextQuestion
-  };
-} 
+// 保留原有其他hook实现...

+ 1 - 0
client/mobile/components/Exam/types.ts

@@ -24,6 +24,7 @@ export interface QuizContent {
 export interface QuizState {
   date: string;
   price: number | string;
+  id?: string; // 新增可选id字段
 }
 
 export type ExamSocketMessageType = SocketMessageType | 'question' | 'answer' | 'settlement' | 'submit' | 'restart';

+ 2 - 0
server/router_io.ts

@@ -3,6 +3,7 @@ import { Auth } from '@d8d-appcontainer/auth';
 import type { User as AuthUser } from '@d8d-appcontainer/auth';
 import { APIClient } from '@d8d-appcontainer/api';
 import { setupMessageEvents } from './routes_io_messages.ts';
+import { setupExamEvents } from './routes_io_exam.ts';
 import debug from "debug";
 
 const log = debug('socketio:auth');
@@ -69,5 +70,6 @@ export function setupSocketIO({ io, auth, apiClient }:SetupSocketIOProps) {
 
     // 初始化消息路由
     setupMessageEvents(context);
+    setupExamEvents(context);
   });
 }

+ 402 - 0
server/routes_io_exam.ts

@@ -0,0 +1,402 @@
+import { Variables } from './router_io.ts';
+import type { QuizContent, 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;
+      }
+
+      // 存储当前问题状态
+      await apiClient.database.table('exam_questions').insert({
+        room_id: roomId,
+        question_id: 'current_state',
+        date: question.date,
+        price: question.price,
+        created_at: apiClient.database.fn.now(),
+        updated_at: apiClient.database.fn.now()
+      });
+
+      // 广播题目到房间
+      socket.to(roomId).emit('exam:question', {
+        roomId,
+        question: {
+          ...question,
+          timestamp: new Date().toISOString()
+        }
+      });
+
+      console.log(`用户 ${user.username} 在房间 ${roomId} 推送题目`);
+    } catch (error) {
+      console.error('推送题目失败:', error);
+      socket.emit('error', '推送题目失败');
+    }
+  });
+
+  // 存储答案
+  socket.on('exam:storeAnswer', async (data: ExamAnswerData) => {
+    try {
+      const { roomId, questionId, answer } = data;
+      const user = socket.user;
+      
+      if (!user) {
+        socket.emit('error', '未授权访问');
+        return;
+      }
+
+      // 存储答案
+      await apiClient.database.table('exam_answers').insert({
+        room_id: roomId,
+        question_id: questionId,
+        user_id: user.id,
+        username: user.username,
+        date: answer.date,
+        price: answer.price,
+        holding_stock: answer.holdingStock,
+        holding_cash: answer.holdingCash,
+        profit_amount: answer.profitAmount,
+        profit_percent: answer.profitPercent,
+        total_profit_amount: answer.totalProfitAmount,
+        total_profit_percent: answer.totalProfitPercent,
+        created_at: apiClient.database.fn.now(),
+        updated_at: apiClient.database.fn.now()
+      });
+
+      // 广播答案更新到房间
+      socket.to(roomId).emit('exam:answerUpdated', {
+        roomId,
+        questionId,
+        userId: user.id,
+        username: user.username
+      });
+
+      console.log(`用户 ${user.username} 在房间 ${roomId} 存储答案`);
+    } catch (error) {
+      console.error('存储答案失败:', error);
+      socket.emit('error', '存储答案失败');
+    }
+  });
+
+  // 获取答案
+  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;
+      }
+
+      // 查询答案
+      const answers = await apiClient.database.table('exam_answers')
+        .where('room_id', roomId)
+        .where('question_id', questionId)
+        .select('*');
+
+      // 转换为前端需要的格式
+      const formattedAnswers: Answer[] = answers.map((a: any) => ({
+        date: a.date,
+        price: a.price,
+        holdingStock: a.holding_stock,
+        holdingCash: a.holding_cash,
+        profitAmount: a.profit_amount,
+        profitPercent: a.profit_percent,
+        totalProfitAmount: a.total_profit_amount,
+        totalProfitPercent: a.total_profit_percent,
+        userId: a.user_id
+      }));
+
+      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) {
+        // 清理特定问题的数据
+        await apiClient.database.table('exam_answers')
+          .where('room_id', roomId)
+          .where('question_id', questionId)
+          .delete();
+      } else {
+        // 清理整个房间的数据
+        await apiClient.database.table('exam_answers')
+          .where('room_id', roomId)
+          .delete();
+          
+        await apiClient.database.table('exam_questions')
+          .where('room_id', roomId)
+          .delete();
+          
+        await apiClient.database.table('exam_prices')
+          .where('room_id', roomId)
+          .delete();
+      }
+
+      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;
+      }
+
+      // 检查是否已存在该日期的价格
+      const existing = await apiClient.database.table('exam_prices')
+        .where('room_id', roomId)
+        .where('date', date)
+        .first();
+
+      if (existing) {
+        // 更新现有价格
+        await apiClient.database.table('exam_prices')
+          .where('room_id', roomId)
+          .where('date', date)
+          .update({
+            price,
+            updated_at: apiClient.database.fn.now()
+          });
+      } else {
+        // 插入新价格
+        await apiClient.database.table('exam_prices').insert({
+          room_id: roomId,
+          date,
+          price,
+          created_at: apiClient.database.fn.now(),
+          updated_at: apiClient.database.fn.now()
+        });
+      }
+
+      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;
+      }
+
+      // 查询价格
+      const priceData = await apiClient.database.table('exam_prices')
+        .where('room_id', roomId)
+        .where('date', date)
+        .select('price')
+        .first();
+
+      callback(priceData?.price || '0');
+    } catch (error) {
+      console.error('获取历史价格失败:', error);
+      socket.emit('error', '获取历史价格失败');
+      callback('0');
+    }
+  });
+
+  // 获取所有价格历史
+  socket.on('exam:getPrices', async (data: {
+    roomId: string;
+  }, callback: (prices: Record<string, string>) => void) => {
+    try {
+      const { roomId } = data;
+      const user = socket.user;
+      
+      if (!user) {
+        socket.emit('error', '未授权访问');
+        return;
+      }
+
+      // 查询所有价格
+      const prices = await apiClient.database.table('exam_prices')
+        .where('room_id', roomId)
+        .select('date', 'price');
+
+      // 转换为日期-价格的映射
+      const priceMap: Record<string, string> = {};
+      prices.forEach((p: any) => {
+        priceMap[p.date] = p.price;
+      });
+
+      callback(priceMap);
+    } catch (error) {
+      console.error('获取所有价格历史失败:', error);
+      socket.emit('error', '获取所有价格历史失败');
+      callback({});
+    }
+  });
+}

+ 1 - 0
版本迭代需求.md

@@ -1,5 +1,6 @@
 2025.05.15 0.1.1
 
+答题卡模块socket.io重构,创建了routes_io_exam.ts路由文件,实现了考试相关socket事件处理
 迁移入答题卡页面
 添加答题卡数据表迁移
 迁移入股票后端api