useSocketClient.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import { useEffect, useState, useCallback } from 'react';
  2. import { useQuery, useQueryClient } from '@tanstack/react-query';
  3. import { io, Socket } from 'socket.io-client';
  4. import type {
  5. QuizContent,
  6. QuizState,
  7. ExamSocketMessage,
  8. ExamSocketRoomMessage,
  9. Answer,
  10. CumulativeResult
  11. } from '../types.ts';
  12. interface FullExamSocketMessage extends Omit<ExamSocketMessage, 'timestamp'> {
  13. id: string;
  14. from: string;
  15. timestamp: string;
  16. }
  17. import { useAuth } from "../../../hooks.tsx";
  18. // 工具函数:统一错误处理
  19. const handleAsyncOperation = async <T>(
  20. operation: () => Promise<T>,
  21. errorMessage: string
  22. ): Promise<T> => {
  23. try {
  24. return await operation();
  25. } catch (error) {
  26. console.error(`${errorMessage}:`, error);
  27. throw error;
  28. }
  29. };
  30. // 计算收益的辅助函数
  31. interface ProfitResult {
  32. profitAmount: number; // 金额收益
  33. profitPercent: number; // 百分比收益
  34. }
  35. function calculateProfit(currentPrice: number, previousPrice: number, holdingStock: string): ProfitResult {
  36. if (holdingStock === '1') {
  37. const profitAmount = currentPrice - previousPrice;
  38. const profitPercent = ((currentPrice - previousPrice) / previousPrice) * 100;
  39. return { profitAmount, profitPercent };
  40. }
  41. return { profitAmount: 0, profitPercent: 0 };
  42. }
  43. // 提前声明函数
  44. function getAnswers(client: Socket | null, roomId: string, questionId: string): Promise<Answer[]> {
  45. if (!client) return Promise.resolve([]);
  46. return new Promise((resolve) => {
  47. client.emit('exam:getAnswers', { roomId, questionId }, (answers: Answer[]) => {
  48. resolve(answers || []);
  49. });
  50. });
  51. }
  52. function getCurrentQuestion(client: Socket | null, roomId: string, getAnswersFn: typeof getAnswers): Promise<QuizState | null> {
  53. if (!client) return Promise.resolve(null);
  54. return getAnswersFn(client, roomId, 'current_state').then(answers => {
  55. const currentState = answers[0];
  56. if (currentState) {
  57. return {
  58. date: currentState.date || '',
  59. price: currentState.price || '0'
  60. };
  61. }
  62. return null;
  63. });
  64. }
  65. export function useSocketClient(roomId: string | null) {
  66. const { token } = useAuth();
  67. const queryClient = useQueryClient();
  68. const [socket, setSocket] = useState<Socket | null>(null);
  69. const [currentQuestion, setCurrentQuestion] = useState<QuizState | null>(null);
  70. const [lastMessage, setLastMessage] = useState<ExamSocketRoomMessage | null>(null);
  71. const [userAnswers, setUserAnswers] = useState<Answer[]>([]);
  72. // 初始化socket连接
  73. const { data: client } = useQuery({
  74. queryKey: ['socket-client', token],
  75. queryFn: async () => {
  76. if (!token) return null;
  77. const newSocket = io('/', {
  78. path: '/socket.io',
  79. transports: ['websocket'],
  80. query: { token },
  81. reconnection: true,
  82. reconnectionAttempts: 5,
  83. reconnectionDelay: 1000,
  84. });
  85. newSocket.on('connect', () => {
  86. console.log('Socket connected');
  87. });
  88. newSocket.on('disconnect', () => {
  89. console.log('Socket disconnected');
  90. });
  91. newSocket.on('error', (error) => {
  92. console.error('Socket error:', error);
  93. });
  94. setSocket(newSocket);
  95. return newSocket;
  96. },
  97. enabled: !!token && !!roomId,
  98. staleTime: Infinity,
  99. gcTime: 0,
  100. retry: 3,
  101. });
  102. // 加入房间
  103. const joinRoom = useCallback(async (roomId: string) => {
  104. if (client) {
  105. client.emit('exam:join', { roomId });
  106. }
  107. }, [client]);
  108. // 离开房间
  109. const leaveRoom = useCallback(async (roomId: string) => {
  110. if (client) {
  111. client.emit('exam:leave', { roomId });
  112. }
  113. }, [client]);
  114. // 发送房间消息
  115. const sendRoomMessage = useCallback(async (roomId: string, message: ExamSocketMessage) => {
  116. if (client) {
  117. client.emit('exam:message', { roomId, message });
  118. }
  119. }, [client]);
  120. // 监听房间消息
  121. const onRoomMessage = useCallback((callback: (data: ExamSocketRoomMessage) => void) => {
  122. if (client) {
  123. client.on('exam:message', (data) => {
  124. setLastMessage(data);
  125. callback(data);
  126. });
  127. }
  128. }, [client]);
  129. // 监听当前问题变化
  130. useEffect(() => {
  131. if (!client || !roomId) return;
  132. const handleQuestionUpdate = async () => {
  133. const question = await getCurrentQuestion(client, roomId, getAnswers);
  134. setCurrentQuestion(question);
  135. };
  136. client.on('exam:question', handleQuestionUpdate);
  137. return () => {
  138. client.off('exam:question', handleQuestionUpdate);
  139. };
  140. }, [client, roomId]);
  141. // 监听用户答案变化
  142. useEffect(() => {
  143. if (!client || !roomId) return;
  144. const handleAnswersUpdate = async () => {
  145. const answers = await getAnswers(client, roomId, 'current_state');
  146. setUserAnswers(answers);
  147. };
  148. client.on('exam:answers', handleAnswersUpdate);
  149. return () => {
  150. client.off('exam:answers', handleAnswersUpdate);
  151. };
  152. }, [client, roomId]);
  153. // 存储答案
  154. const storeAnswer = useCallback(async (roomId: string, questionId: string, userId: string, answer: QuizContent) => {
  155. if (!client) return;
  156. return handleAsyncOperation(async () => {
  157. // 获取历史价格数据
  158. const pricesData = await new Promise<any>((resolve) => {
  159. client.emit('exam:getPrices', { roomId }, resolve);
  160. });
  161. if (!pricesData) {
  162. // 存储初始答案
  163. const initialAnswer: Answer = {
  164. ...answer,
  165. userId,
  166. holdingStock: '0',
  167. holdingCash: '0',
  168. profitAmount: 0,
  169. profitPercent: 0,
  170. totalProfitAmount: 0,
  171. totalProfitPercent: 0
  172. };
  173. client.emit('exam:storeAnswer', {
  174. roomId,
  175. questionId,
  176. userId,
  177. answer: initialAnswer
  178. });
  179. return;
  180. }
  181. // 获取该用户的所有历史答案
  182. const dates = Object.keys(pricesData).sort();
  183. const allAnswers = await Promise.all(
  184. dates.map(date => getAnswers(client, roomId, date))
  185. );
  186. // 计算收益
  187. const userAnswers = allAnswers
  188. .flat()
  189. .filter((a: Answer) => a.userId === userId)
  190. .map((a: Answer) => ({
  191. ...a,
  192. price: pricesData[a.date || '']?.price || '0'
  193. }))
  194. .sort((a: Answer, b: Answer) => new Date(a.date || '').getTime() - new Date(b.date || '').getTime());
  195. let totalProfitAmount = 0;
  196. let totalProfitPercent = 0;
  197. if (userAnswers.length > 0) {
  198. const prevAnswer = userAnswers[userAnswers.length - 1];
  199. const { profitAmount, profitPercent } = calculateProfit(
  200. parseFloat(String(answer.price)),
  201. parseFloat(String(prevAnswer.price)),
  202. prevAnswer.holdingStock as string
  203. );
  204. totalProfitAmount = (prevAnswer.totalProfitAmount || 0) + profitAmount;
  205. totalProfitPercent = (prevAnswer.totalProfitPercent || 0) + profitPercent;
  206. }
  207. // 存储带有收益信息的答案
  208. const answerWithProfit: Answer = {
  209. ...answer,
  210. userId,
  211. profitAmount: userAnswers.length > 0 ? totalProfitAmount - (userAnswers[userAnswers.length - 1].totalProfitAmount || 0) : 0,
  212. profitPercent: userAnswers.length > 0 ? totalProfitPercent - (userAnswers[userAnswers.length - 1].totalProfitPercent || 0) : 0,
  213. totalProfitAmount,
  214. totalProfitPercent
  215. };
  216. client.emit('exam:storeAnswer', {
  217. roomId,
  218. questionId,
  219. userId,
  220. answer: answerWithProfit
  221. });
  222. }, '存储答案失败');
  223. }, [client, getAnswers]);
  224. // 清理房间数据
  225. const cleanupRoom = useCallback(async (roomId: string, questionId?: string) => {
  226. if (!client) return;
  227. await handleAsyncOperation(async () => {
  228. if (questionId) {
  229. client.emit('exam:cleanup', { roomId, questionId });
  230. } else {
  231. client.emit('exam:cleanup', { roomId });
  232. }
  233. }, '清理房间数据失败');
  234. }, [client]);
  235. // 发送下一题
  236. const sendNextQuestion = useCallback(async (roomId: string, state: QuizState) => {
  237. if (!client) return;
  238. return handleAsyncOperation(async () => {
  239. const message: FullExamSocketMessage = {
  240. id: `question-${Date.now()}`,
  241. type: 'question',
  242. from: 'system',
  243. timestamp: Date.now().toString(),
  244. content: {
  245. date: state.date,
  246. price: state.price,
  247. holdingStock: '0',
  248. holdingCash: '0',
  249. userId: 'system'
  250. }
  251. };
  252. // 存储当前问题状态
  253. await storeAnswer(roomId, 'current_state', 'system', {
  254. date: state.date,
  255. price: state.price,
  256. holdingStock: '0',
  257. holdingCash: '0',
  258. userId: 'system'
  259. });
  260. // 存储价格历史记录
  261. client.emit('exam:storePrice', {
  262. roomId,
  263. date: state.date,
  264. price: state.price
  265. });
  266. await sendRoomMessage(roomId, message);
  267. }, '发送题目失败');
  268. }, [client, sendRoomMessage, storeAnswer]);
  269. // 获取历史价格
  270. const getPriceHistory = useCallback(async (roomId: string, date: string): Promise<string> => {
  271. if (!client) return '0';
  272. return handleAsyncOperation(async () => {
  273. return new Promise((resolve) => {
  274. client.emit('exam:getPrice', { roomId, date }, (price: string) => {
  275. resolve(price || '0');
  276. });
  277. });
  278. }, '获取历史价格失败');
  279. }, [client]);
  280. // 获取答案 (封装为useCallback)
  281. const getAnswersCallback = useCallback((roomId: string, questionId: string): Promise<Answer[]> => {
  282. if (!client) return Promise.resolve([]);
  283. return handleAsyncOperation(async () => {
  284. return getAnswers(client, roomId, questionId);
  285. }, '获取答案失败');
  286. }, [client]);
  287. // 获取当前题目 (封装为useCallback)
  288. const getCurrentQuestionCallback = useCallback((roomId: string): Promise<QuizState | null> => {
  289. if (!client) return Promise.resolve(null);
  290. return handleAsyncOperation(async () => {
  291. return getCurrentQuestion(client, roomId, getAnswers);
  292. }, '获取当前题目状态失败');
  293. }, [client]);
  294. // 清理socket连接
  295. useEffect(() => {
  296. return () => {
  297. if (socket) {
  298. socket.disconnect();
  299. }
  300. };
  301. }, [socket]);
  302. // 导出所有功能作为单个对象
  303. const socketRoom = {
  304. client,
  305. joinRoom,
  306. leaveRoom,
  307. sendRoomMessage,
  308. onRoomMessage
  309. };
  310. const answerManagement = {
  311. storeAnswer,
  312. getAnswers: getAnswersCallback,
  313. cleanupRoom,
  314. sendNextQuestion,
  315. getCurrentQuestion: getCurrentQuestionCallback,
  316. getPriceHistory
  317. };
  318. // 计算累计结果
  319. // const calculateCumulativeResults = useCallback((answers: Answer[]): CumulativeResult[] => {
  320. // const userResults = new Map<string, CumulativeResult>();
  321. // answers.forEach((answer) => {
  322. // const userId = answer.userId;
  323. // if (!userResults.has(userId)) {
  324. // userResults.set(userId, {
  325. // userId,
  326. // totalProfitAmount: answer.totalProfitAmount || 0,
  327. // totalProfitPercent: answer.totalProfitPercent || 0
  328. // });
  329. // }
  330. // });
  331. // return Array.from(userResults.values());
  332. // }, []);
  333. return {
  334. socketRoom,
  335. answerManagement,
  336. // calculateCumulativeResults,
  337. currentQuestion,
  338. lastMessage,
  339. userAnswers,
  340. // 兼容旧版导入
  341. ...socketRoom,
  342. ...answerManagement
  343. };
  344. }
  345. // 保留原有其他hook实现...