|
|
@@ -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实现...
|