| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675 |
- 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/exam.ts';
- interface LoaderData {
- token: string;
- serverUrl: string;
- }
- // 工具函数:统一错误处理
- const handleAsyncOperation = async <T>(
- operation: () => Promise<T>,
- errorMessage: string
- ): Promise<T> => {
- 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, serverUrl } = {
- token: '',
- serverUrl: '/wss'
- };
- 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<Answer[]> => {
- 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<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);
- }, '获取历史价格失败');
- }, [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<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
- };
- }
- // 使用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
- };
- }
|