routes_io_exam.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import { Variables } from './router_io.ts';
  2. import type { QuizContent, Answer } from '../client/mobile/components/Exam/types.ts';
  3. interface ExamRoomData {
  4. roomId: string;
  5. userId?: string;
  6. }
  7. interface ExamQuestionData extends ExamRoomData {
  8. question: QuizContent;
  9. }
  10. interface ExamAnswerData extends ExamRoomData {
  11. questionId: string;
  12. answer: Answer;
  13. }
  14. interface ExamPriceData extends ExamRoomData {
  15. date: string;
  16. price: string;
  17. }
  18. export function setupExamEvents({ socket, apiClient }: Variables) {
  19. // 加入考试房间
  20. socket.on('exam:join', async (data: ExamRoomData) => {
  21. try {
  22. const { roomId } = data;
  23. const user = socket.user;
  24. if (!user) {
  25. socket.emit('error', '未授权访问');
  26. return;
  27. }
  28. // 加入房间
  29. socket.join(roomId);
  30. // 通知用户加入成功
  31. socket.emit('exam:joined', {
  32. roomId,
  33. message: `成功加入考试房间: ${roomId}`
  34. });
  35. // 通知房间其他用户有新成员加入
  36. socket.to(roomId).emit('exam:memberJoined', {
  37. roomId,
  38. userId: user.id,
  39. username: user.username
  40. });
  41. console.log(`用户 ${user.username} 加入考试房间 ${roomId}`);
  42. } catch (error) {
  43. console.error('加入考试房间失败:', error);
  44. socket.emit('error', '加入考试房间失败');
  45. }
  46. });
  47. // 离开考试房间
  48. socket.on('exam:leave', async (data: ExamRoomData) => {
  49. try {
  50. const { roomId } = data;
  51. const user = socket.user;
  52. if (!user) {
  53. socket.emit('error', '未授权访问');
  54. return;
  55. }
  56. // 离开房间
  57. socket.leave(roomId);
  58. // 通知用户离开成功
  59. socket.emit('exam:left', {
  60. roomId,
  61. message: `已离开考试房间: ${roomId}`
  62. });
  63. // 通知房间其他用户有成员离开
  64. socket.to(roomId).emit('exam:memberLeft', {
  65. roomId,
  66. userId: user.id,
  67. username: user.username
  68. });
  69. console.log(`用户 ${user.username} 离开考试房间 ${roomId}`);
  70. } catch (error) {
  71. console.error('离开考试房间失败:', error);
  72. socket.emit('error', '离开考试房间失败');
  73. }
  74. });
  75. // 发送考试房间消息
  76. socket.on('exam:message', async (data: {
  77. roomId: string;
  78. message: {
  79. type: string;
  80. content: any;
  81. }
  82. }) => {
  83. try {
  84. const { roomId, message } = data;
  85. const user = socket.user;
  86. if (!user) {
  87. socket.emit('error', '未授权访问');
  88. return;
  89. }
  90. // 广播消息到房间
  91. socket.to(roomId).emit('exam:message', {
  92. roomId,
  93. message: {
  94. ...message,
  95. from: user.id,
  96. username: user.username,
  97. timestamp: new Date().toISOString()
  98. }
  99. });
  100. console.log(`用户 ${user.username} 在房间 ${roomId} 发送消息: ${message.type}`);
  101. } catch (error) {
  102. console.error('发送考试消息失败:', error);
  103. socket.emit('error', '发送考试消息失败');
  104. }
  105. });
  106. // 推送题目
  107. socket.on('exam:question', async (data: ExamQuestionData) => {
  108. try {
  109. const { roomId, question } = data;
  110. const user = socket.user;
  111. if (!user) {
  112. socket.emit('error', '未授权访问');
  113. return;
  114. }
  115. // 存储当前问题状态
  116. await apiClient.database.table('exam_questions').insert({
  117. room_id: roomId,
  118. question_id: 'current_state',
  119. date: question.date,
  120. price: question.price,
  121. created_at: apiClient.database.fn.now(),
  122. updated_at: apiClient.database.fn.now()
  123. });
  124. // 广播题目到房间
  125. socket.to(roomId).emit('exam:question', {
  126. roomId,
  127. question: {
  128. ...question,
  129. timestamp: new Date().toISOString()
  130. }
  131. });
  132. console.log(`用户 ${user.username} 在房间 ${roomId} 推送题目`);
  133. } catch (error) {
  134. console.error('推送题目失败:', error);
  135. socket.emit('error', '推送题目失败');
  136. }
  137. });
  138. // 存储答案
  139. socket.on('exam:storeAnswer', async (data: ExamAnswerData) => {
  140. try {
  141. const { roomId, questionId, answer } = data;
  142. const user = socket.user;
  143. if (!user) {
  144. socket.emit('error', '未授权访问');
  145. return;
  146. }
  147. // 存储答案
  148. await apiClient.database.table('exam_answers').insert({
  149. room_id: roomId,
  150. question_id: questionId,
  151. user_id: user.id,
  152. username: user.username,
  153. date: answer.date,
  154. price: answer.price,
  155. holding_stock: answer.holdingStock,
  156. holding_cash: answer.holdingCash,
  157. profit_amount: answer.profitAmount,
  158. profit_percent: answer.profitPercent,
  159. total_profit_amount: answer.totalProfitAmount,
  160. total_profit_percent: answer.totalProfitPercent,
  161. created_at: apiClient.database.fn.now(),
  162. updated_at: apiClient.database.fn.now()
  163. });
  164. // 广播答案更新到房间
  165. socket.to(roomId).emit('exam:answerUpdated', {
  166. roomId,
  167. questionId,
  168. userId: user.id,
  169. username: user.username
  170. });
  171. console.log(`用户 ${user.username} 在房间 ${roomId} 存储答案`);
  172. } catch (error) {
  173. console.error('存储答案失败:', error);
  174. socket.emit('error', '存储答案失败');
  175. }
  176. });
  177. // 获取答案
  178. socket.on('exam:getAnswers', async (data: {
  179. roomId: string;
  180. questionId: string;
  181. }, callback: (answers: Answer[]) => void) => {
  182. try {
  183. const { roomId, questionId } = data;
  184. const user = socket.user;
  185. if (!user) {
  186. socket.emit('error', '未授权访问');
  187. return;
  188. }
  189. // 查询答案
  190. const answers = await apiClient.database.table('exam_answers')
  191. .where('room_id', roomId)
  192. .where('question_id', questionId)
  193. .select('*');
  194. // 转换为前端需要的格式
  195. const formattedAnswers: Answer[] = answers.map((a: any) => ({
  196. date: a.date,
  197. price: a.price,
  198. holdingStock: a.holding_stock,
  199. holdingCash: a.holding_cash,
  200. profitAmount: a.profit_amount,
  201. profitPercent: a.profit_percent,
  202. totalProfitAmount: a.total_profit_amount,
  203. totalProfitPercent: a.total_profit_percent,
  204. userId: a.user_id
  205. }));
  206. callback(formattedAnswers);
  207. } catch (error) {
  208. console.error('获取答案失败:', error);
  209. socket.emit('error', '获取答案失败');
  210. callback([]);
  211. }
  212. });
  213. // 清理房间数据
  214. socket.on('exam:cleanup', async (data: {
  215. roomId: string;
  216. questionId?: string;
  217. }) => {
  218. try {
  219. const { roomId, questionId } = data;
  220. const user = socket.user;
  221. if (!user) {
  222. socket.emit('error', '未授权访问');
  223. return;
  224. }
  225. if (questionId) {
  226. // 清理特定问题的数据
  227. await apiClient.database.table('exam_answers')
  228. .where('room_id', roomId)
  229. .where('question_id', questionId)
  230. .delete();
  231. } else {
  232. // 清理整个房间的数据
  233. await apiClient.database.table('exam_answers')
  234. .where('room_id', roomId)
  235. .delete();
  236. await apiClient.database.table('exam_questions')
  237. .where('room_id', roomId)
  238. .delete();
  239. await apiClient.database.table('exam_prices')
  240. .where('room_id', roomId)
  241. .delete();
  242. }
  243. socket.emit('exam:cleaned', {
  244. roomId,
  245. message: questionId
  246. ? `已清理房间 ${roomId} 的问题 ${questionId} 数据`
  247. : `已清理房间 ${roomId} 的所有数据`
  248. });
  249. console.log(`用户 ${user.username} 清理房间 ${roomId} 数据`);
  250. } catch (error) {
  251. console.error('清理房间数据失败:', error);
  252. socket.emit('error', '清理房间数据失败');
  253. }
  254. });
  255. // 存储价格历史
  256. socket.on('exam:storePrice', async (data: ExamPriceData) => {
  257. try {
  258. const { roomId, date, price } = data;
  259. const user = socket.user;
  260. if (!user) {
  261. socket.emit('error', '未授权访问');
  262. return;
  263. }
  264. // 检查是否已存在该日期的价格
  265. const existing = await apiClient.database.table('exam_prices')
  266. .where('room_id', roomId)
  267. .where('date', date)
  268. .first();
  269. if (existing) {
  270. // 更新现有价格
  271. await apiClient.database.table('exam_prices')
  272. .where('room_id', roomId)
  273. .where('date', date)
  274. .update({
  275. price,
  276. updated_at: apiClient.database.fn.now()
  277. });
  278. } else {
  279. // 插入新价格
  280. await apiClient.database.table('exam_prices').insert({
  281. room_id: roomId,
  282. date,
  283. price,
  284. created_at: apiClient.database.fn.now(),
  285. updated_at: apiClient.database.fn.now()
  286. });
  287. }
  288. console.log(`用户 ${user.username} 存储房间 ${roomId} 的价格历史: ${date} - ${price}`);
  289. } catch (error) {
  290. console.error('存储价格历史失败:', error);
  291. socket.emit('error', '存储价格历史失败');
  292. }
  293. });
  294. // 获取历史价格
  295. socket.on('exam:getPrice', async (data: {
  296. roomId: string;
  297. date: string;
  298. }, callback: (price: string) => void) => {
  299. try {
  300. const { roomId, date } = data;
  301. const user = socket.user;
  302. if (!user) {
  303. socket.emit('error', '未授权访问');
  304. return;
  305. }
  306. // 查询价格
  307. const priceData = await apiClient.database.table('exam_prices')
  308. .where('room_id', roomId)
  309. .where('date', date)
  310. .select('price')
  311. .first();
  312. callback(priceData?.price || '0');
  313. } catch (error) {
  314. console.error('获取历史价格失败:', error);
  315. socket.emit('error', '获取历史价格失败');
  316. callback('0');
  317. }
  318. });
  319. // 获取所有价格历史
  320. socket.on('exam:getPrices', async (data: {
  321. roomId: string;
  322. }, callback: (prices: Record<string, string>) => void) => {
  323. try {
  324. const { roomId } = data;
  325. const user = socket.user;
  326. if (!user) {
  327. socket.emit('error', '未授权访问');
  328. return;
  329. }
  330. // 查询所有价格
  331. const prices = await apiClient.database.table('exam_prices')
  332. .where('room_id', roomId)
  333. .select('date', 'price');
  334. // 转换为日期-价格的映射
  335. const priceMap: Record<string, string> = {};
  336. prices.forEach((p: any) => {
  337. priceMap[p.date] = p.price;
  338. });
  339. callback(priceMap);
  340. } catch (error) {
  341. console.error('获取所有价格历史失败:', error);
  342. socket.emit('error', '获取所有价格历史失败');
  343. callback({});
  344. }
  345. });
  346. }