import { useEffect, useState, useCallback } from 'react'; import { APIClient } from '@d8d-appcontainer/api'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { QuizContent, QuizState, ExamSocketMessage, ExamSocketRoomMessage, Answer } from '../types.ts'; import { useAuth } from "../../../hooks.tsx"; interface LoaderData { token: string; serverUrl: string; } // 工具函数:统一错误处理 const handleAsyncOperation = async ( operation: () => Promise, errorMessage: string ): Promise => { try { return await operation(); } catch (error) { console.error(`${errorMessage}:`, error); throw error; } }; // 计算收益的辅助函数 interface ProfitResult { profitAmount: number; // 金额收益 profitPercent: number; // 百分比收益 } 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 }; } return { profitAmount: 0, profitPercent: 0 }; } // 使用react-query管理socket客户端 export function useSocketClient(roomId: string | null) { const { token } = useAuth(); const serverUrl = '/'; const { data: client } = useQuery({ queryKey: ['socket-client'], queryFn: async () => { if (!token || !serverUrl) return null; const apiClient = new APIClient({ scope: 'user', config: { serverUrl, type: 'socket', token, } }); await apiClient.connect(); return apiClient; }, enabled: !!token && !!serverUrl && !!roomId, staleTime: Infinity, retry: 3, gcTime: 0 }); const joinRoom = useCallback(async (roomId: string) => { if (client) { await client.socket.joinRoom(roomId); } }, [client]); const leaveRoom = useCallback(async (roomId: string) => { if (client) { await client.socket.leaveRoom(roomId); } }, [client]); const sendRoomMessage = useCallback(async (roomId: string, message: ExamSocketMessage) => { if (client) { await client.socket.sendRoomMessage(roomId, message as any); } }, [client]); const onRoomMessage = useCallback((callback: (data: ExamSocketRoomMessage) => void) => { if (client) { client.socket.onRoomMessage(callback); } }, [client]); const getAnswers = useCallback(async (roomId: string, questionId: string): Promise => { if (!client) 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 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; } // 获取该用户的所有历史答案 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 }; }) .sort((a: Answer, b: Answer) => new Date(a.date || '').getTime() - new Date(b.date || '').getTime()); // 计算收益 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, 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 }; if (client?.redis) { await client.redis.hset( `quiz:${roomId}:answers:${questionId}`, userId, JSON.stringify(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}`); } else { await Promise.all([ client.redis.delByPattern(`quiz:${roomId}:answers:*`), client.redis.del(`quiz:${roomId}:prices`) ]); } }, '清理房间数据失败'); }, [client]); const sendNextQuestion = useCallback(async (roomId: string, state: QuizState) => { if (!client) return; return handleAsyncOperation(async () => { const message = { type: 'question', content: { date: state.date, price: state.price } }; // 存储当前问题状态 await storeAnswer(roomId, 'current_state', 'system', { date: state.date, price: state.price }); // 存储价格历史记录 await client.redis.hset( `quiz:${roomId}:prices`, state.date, JSON.stringify({ price: state.price }) ); await sendRoomMessage(roomId, message); }, '发送题目失败'); }, [client, sendRoomMessage, storeAnswer]); const getCurrentQuestion = useCallback(async (roomId: string): Promise => { 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 => { 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); }, '获取历史价格失败'); }, [client]); return { client, joinRoom, leaveRoom, sendRoomMessage, onRoomMessage, storeAnswer, getAnswers, cleanupRoom, sendNextQuestion, getCurrentQuestion, 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 }); return { currentQuestion, refetchQuestion: refetch }; } // 使用react-query管理房间消息 export function useRoomMessages(roomId: string | null) { const socketClient = useSocketClient(roomId); const queryClient = useQueryClient(); const [lastMessage, setLastMessage] = useState(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 }; } // 使用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 }; }