useSocketClient.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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. export function useSocketClient(roomId: string | null) {
  53. const { token } = useAuth();
  54. const [socket, setSocket] = useState<Socket | null>(null);
  55. const [isConnected, setIsConnected] = useState(false);
  56. // 初始化socket连接
  57. const { data: client } = useQuery({
  58. queryKey: ['socket-client', token],
  59. queryFn: async () => {
  60. if (!token) return null;
  61. const newSocket = io('/', {
  62. path: '/socket.io',
  63. transports: ['websocket'],
  64. withCredentials: true,
  65. query: {
  66. socket_token: token
  67. },
  68. reconnection: true,
  69. reconnectionAttempts: 5,
  70. reconnectionDelay: 1000,
  71. });
  72. newSocket.on('connect', () => {
  73. console.log('Socket connected');
  74. setIsConnected(true);
  75. });
  76. newSocket.on('disconnect', () => {
  77. console.log('Socket disconnected');
  78. setIsConnected(false);
  79. });
  80. newSocket.on('error', (error) => {
  81. console.error('Socket error:', error);
  82. });
  83. setSocket(newSocket);
  84. return newSocket;
  85. },
  86. enabled: !!token && !!roomId,
  87. staleTime: Infinity,
  88. gcTime: 0,
  89. retry: 3,
  90. });
  91. // 加入房间
  92. const joinRoom = useCallback(async (roomId: string) => {
  93. if (client) {
  94. client.emit('exam:join', { roomId });
  95. }
  96. }, [client]);
  97. // 离开房间
  98. const leaveRoom = useCallback(async (roomId: string) => {
  99. if (client) {
  100. client.emit('exam:leave', { roomId });
  101. }
  102. }, [client]);
  103. // // 发送房间消息
  104. // const sendRoomMessage = useCallback(async (roomId: string, message: ExamSocketMessage) => {
  105. // if (client) {
  106. // client.emit('exam:message', { roomId, message });
  107. // }
  108. // }, [client]);
  109. // // 监听房间消息
  110. // const onRoomMessage = useCallback((callback: (data: ExamSocketRoomMessage) => void) => {
  111. // if (client) {
  112. // client.on('exam:message', (data) => {
  113. // setLastMessage(data);
  114. // callback(data);
  115. // });
  116. // }
  117. // }, [client]);
  118. // 存储答案
  119. const storeAnswer = useCallback(async (roomId: string, questionId: string, userId: string, answer: QuizContent, callback?: (success: Answer[]) => void) => {
  120. if (!client) return;
  121. return handleAsyncOperation(async () => {
  122. // // 获取历史价格数据
  123. // const pricesData = await new Promise<any>((resolve) => {
  124. // client.emit('exam:getPrices', { roomId }, resolve);
  125. // });
  126. // if (!pricesData) {
  127. // // 存储初始答案
  128. // const initialAnswer: Answer = {
  129. // ...answer,
  130. // userId,
  131. // holdingStock: '0',
  132. // holdingCash: '0',
  133. // profitAmount: 0,
  134. // profitPercent: 0,
  135. // totalProfitAmount: 0,
  136. // totalProfitPercent: 0
  137. // };
  138. // client.emit('exam:storeAnswer', {
  139. // roomId,
  140. // questionId,
  141. // userId,
  142. // answer: initialAnswer
  143. // }, (success: boolean) => {
  144. // callback?.([initialAnswer]);
  145. // });
  146. // return;
  147. // }
  148. // 获取该用户的所有历史答案
  149. // const dates = Object.keys(pricesData).sort();
  150. const allUserAnswers = await getUserAnswers(roomId, userId);
  151. const userAnswers = allUserAnswers
  152. .filter((a: Answer) => a.date !== answer.date)
  153. // .filter((a: Answer) => dates.includes(a.date || ''))
  154. .map((a: Answer) => ({
  155. ...a,
  156. // price: pricesData[a.date || '']?.price || '0'
  157. }))
  158. .sort((a: Answer, b: Answer) => new Date(a.date || '').getTime() - new Date(b.date || '').getTime());
  159. let totalProfitAmount = 0;
  160. let totalProfitPercent = 0;
  161. if (userAnswers.length > 0) {
  162. const prevAnswer = userAnswers[userAnswers.length - 1];
  163. const { profitAmount, profitPercent } = calculateProfit(
  164. parseFloat(String(answer.price)),
  165. parseFloat(String(prevAnswer.price)),
  166. prevAnswer.holdingStock as string
  167. );
  168. totalProfitAmount = (prevAnswer.totalProfitAmount || 0) + profitAmount;
  169. totalProfitPercent = (prevAnswer.totalProfitPercent || 0) + profitPercent;
  170. }
  171. // 存储带有收益信息的答案
  172. const answerWithProfit: Answer = {
  173. ...answer,
  174. userId,
  175. profitAmount: userAnswers.length > 0 ? totalProfitAmount - (userAnswers[userAnswers.length - 1].totalProfitAmount || 0) : 0,
  176. profitPercent: userAnswers.length > 0 ? totalProfitPercent - (userAnswers[userAnswers.length - 1].totalProfitPercent || 0) : 0,
  177. totalProfitAmount,
  178. totalProfitPercent
  179. };
  180. client.emit('exam:storeAnswer', {
  181. roomId,
  182. questionId,
  183. userId,
  184. answer: answerWithProfit
  185. }, (success: boolean) => {
  186. callback?.([...userAnswers, answerWithProfit]);
  187. });
  188. }, '存储答案失败');
  189. }, [client]);
  190. // 清理房间数据
  191. const cleanupRoom = useCallback(async (roomId: string, questionId?: string) => {
  192. if (!client) return;
  193. await handleAsyncOperation(async () => {
  194. if (questionId) {
  195. client.emit('exam:cleanup', { roomId, questionId });
  196. } else {
  197. client.emit('exam:cleanup', { roomId });
  198. }
  199. }, '清理房间数据失败');
  200. }, [client]);
  201. // // 发送下一题
  202. // const sendNextQuestion = useCallback(async (roomId: string, state: QuizState) => {
  203. // if (!client) return;
  204. // return handleAsyncOperation(async () => {
  205. // const message: FullExamSocketMessage = {
  206. // id: `question-${Date.now()}`,
  207. // type: 'question',
  208. // from: 'system',
  209. // timestamp: Date.now().toString(),
  210. // content: {
  211. // date: state.date,
  212. // price: state.price,
  213. // holdingStock: '0',
  214. // holdingCash: '0',
  215. // userId: 'system'
  216. // }
  217. // };
  218. // // 存储当前问题状态
  219. // await storeAnswer(roomId, 'current_state', 'system', {
  220. // date: state.date,
  221. // price: state.price,
  222. // holdingStock: '0',
  223. // holdingCash: '0',
  224. // userId: 'system'
  225. // });
  226. // // 存储价格历史记录
  227. // client.emit('exam:storePrice', {
  228. // roomId,
  229. // date: state.date,
  230. // price: state.price
  231. // });
  232. // await sendRoomMessage(roomId, message);
  233. // }, '发送题目失败');
  234. // }, [client, sendRoomMessage, storeAnswer]);
  235. // 获取历史价格
  236. const getPriceHistory = useCallback(async (roomId: string, date: string): Promise<string> => {
  237. if (!client) return '0';
  238. return handleAsyncOperation(async () => {
  239. return new Promise((resolve) => {
  240. client.emit('exam:getPrice', { roomId, date }, (price: string) => {
  241. resolve(price || '0');
  242. });
  243. });
  244. }, '获取历史价格失败');
  245. }, [client]);
  246. // 获取答案 (封装为useCallback)
  247. const getAnswersCallback = useCallback((roomId: string, questionId: string): Promise<Answer[]> => {
  248. if (!client) return Promise.resolve([]);
  249. return handleAsyncOperation(async () => {
  250. return getAnswers(client, roomId, questionId);
  251. }, '获取答案失败');
  252. }, [client]);
  253. // 清理socket连接
  254. useEffect(() => {
  255. return () => {
  256. if (socket) {
  257. socket.disconnect();
  258. }
  259. };
  260. }, [socket]);
  261. // 导出所有功能作为单个对象
  262. const socketRoom = {
  263. client,
  264. joinRoom,
  265. leaveRoom,
  266. // sendRoomMessage,
  267. // onRoomMessage
  268. };
  269. // 获取用户答案
  270. const getUserAnswers = useCallback(async (roomId: string, userId: string): Promise<Answer[]> => {
  271. if (!client || !roomId || !userId) return Promise.resolve([]);
  272. return handleAsyncOperation(async () => {
  273. return new Promise((resolve) => {
  274. client.emit('exam:getUserAnswers', { roomId, userId }, (answers: Answer[]) => {
  275. resolve(answers || []);
  276. });
  277. });
  278. }, '获取用户答案失败');
  279. }, [client]);
  280. const getCurrentQuestion = useCallback(async (roomId: string): Promise<QuizState> => {
  281. if (!client) return Promise.reject(new Error('Socket not connected'));
  282. return handleAsyncOperation(async () => {
  283. return new Promise((resolve, reject) => {
  284. client.emit('exam:currentQuestion', { roomId }, (question: QuizState) => {
  285. if (!question) {
  286. reject(new Error('No current question available'));
  287. return;
  288. }
  289. resolve(question);
  290. });
  291. });
  292. }, '获取当前问题失败');
  293. }, [client])
  294. const sendSettleExam = async (roomId: string) => {
  295. if (!client) return;
  296. return handleAsyncOperation(async () => {
  297. client.emit('exam:settle', { roomId });
  298. }, '发送结算消息失败');
  299. }
  300. const answerManagement = {
  301. storeAnswer,
  302. getAnswers: getAnswersCallback,
  303. cleanupRoom,
  304. // sendNextQuestion,
  305. getPriceHistory,
  306. getUserAnswers,
  307. getCurrentQuestion,
  308. sendSettleExam,
  309. };
  310. // 计算累计结果
  311. // const calculateCumulativeResults = useCallback((answers: Answer[]): CumulativeResult[] => {
  312. // const userResults = new Map<string, CumulativeResult>();
  313. // answers.forEach((answer) => {
  314. // const userId = answer.userId;
  315. // if (!userResults.has(userId)) {
  316. // userResults.set(userId, {
  317. // userId,
  318. // totalProfitAmount: answer.totalProfitAmount || 0,
  319. // totalProfitPercent: answer.totalProfitPercent || 0
  320. // });
  321. // }
  322. // });
  323. // return Array.from(userResults.values());
  324. // }, []);
  325. return {
  326. socketRoom,
  327. answerManagement,
  328. // calculateCumulativeResults,
  329. // currentQuestion,
  330. // setCurrentQuestion,
  331. // lastMessage,
  332. ...socketRoom,
  333. ...answerManagement,
  334. isConnected,
  335. };
  336. }