2
0

exam.service.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import { Server } from 'socket.io';
  2. import { AuthenticatedSocket } from '../middleware/auth.middleware';
  3. import { redisService } from './redis.service';
  4. import { AppDataSource } from '@/server/data-source';
  5. import { SubmissionRecords } from '@/server/modules/submission/submission-records.entity';
  6. import debug from 'debug';
  7. import type { Answer } from '@/client/mobile/components/Exam/types';
  8. const log = debug('socket:exam');
  9. export class ExamService {
  10. private io: Server;
  11. constructor(io: Server) {
  12. this.io = io;
  13. }
  14. async joinRoom(socket: AuthenticatedSocket, roomId: string) {
  15. try {
  16. if (!socket.user) throw new Error('用户未认证');
  17. const user = socket.user;
  18. socket.join(roomId);
  19. await redisService.addRoomMember(roomId, user.id, user.username);
  20. socket.emit('exam:joined', { roomId, message: `成功加入考试房间: ${roomId}` });
  21. socket.to(roomId).emit('exam:memberJoined', { roomId, userId: user.id, username: user.username });
  22. log(`用户 ${user.username} 加入考试房间 ${roomId}`);
  23. } catch (error) {
  24. log('加入考试房间失败:', error);
  25. socket.emit('error', '加入考试房间失败');
  26. }
  27. }
  28. async leaveRoom(socket: AuthenticatedSocket, roomId: string) {
  29. try {
  30. if (!socket.user) throw new Error('用户未认证');
  31. const user = socket.user;
  32. socket.leave(roomId);
  33. await redisService.removeRoomMember(roomId, user.id);
  34. socket.emit('exam:left', { roomId, message: `已离开考试房间: ${roomId}` });
  35. socket.to(roomId).emit('exam:memberLeft', { roomId, userId: user.id, username: user.username });
  36. log(`用户 ${user.username} 离开考试房间 ${roomId}`);
  37. } catch (error) {
  38. log('离开考试房间失败:', error);
  39. socket.emit('error', '离开考试房间失败');
  40. }
  41. }
  42. async pushQuestion(socket: AuthenticatedSocket, roomId: string, question: { date: string; price: string }) {
  43. try {
  44. if (!socket.user) throw new Error('用户未认证');
  45. await redisService.storeCurrentQuestion(roomId, question);
  46. await redisService.storePrice(roomId, question.date, question.price);
  47. const quizState = { id: question.date, date: question.date, price: question.price };
  48. socket.to(roomId).emit('exam:question', quizState);
  49. log(`用户 ${socket.user.username} 在房间 ${roomId} 推送题目`);
  50. } catch (error) {
  51. log('推送题目失败:', error);
  52. socket.emit('error', '推送题目失败');
  53. }
  54. }
  55. async storeAnswer(socket: AuthenticatedSocket, roomId: string, questionId: string, answer: Answer): Promise<boolean> {
  56. try {
  57. if (!socket.user) throw new Error('用户未认证');
  58. const user = socket.user;
  59. await redisService.storeAnswer(roomId, user.id, questionId, { ...answer, username: user.username });
  60. socket.to(roomId).emit('exam:answerUpdated', {
  61. roomId, questionId, userId: user.id, username: user.username
  62. });
  63. log(`用户 ${user.username} 在房间 ${roomId} 存储答案`);
  64. return true;
  65. } catch (error) {
  66. log('存储答案失败:', error);
  67. socket.emit('error', '存储答案失败');
  68. return false;
  69. }
  70. }
  71. async getAnswers(roomId: string, questionId?: string): Promise<Answer[]> {
  72. try {
  73. return await redisService.getAnswers(roomId, questionId);
  74. } catch (error) {
  75. log('获取答案失败:', error);
  76. return [];
  77. }
  78. }
  79. async getUserAnswers(roomId: string, userId: number): Promise<Answer[]> {
  80. try {
  81. return await redisService.getUserAnswers(roomId, userId);
  82. } catch (error) {
  83. log('获取用户答案失败:', error);
  84. return [];
  85. }
  86. }
  87. async storePrice(socket: AuthenticatedSocket, roomId: string, date: string, price: string) {
  88. try {
  89. if (!socket.user) throw new Error('用户未认证');
  90. await redisService.storePrice(roomId, date, price);
  91. log(`用户 ${socket.user.username} 存储房间 ${roomId} 的价格历史: ${date} - ${price}`);
  92. } catch (error) {
  93. log('存储价格历史失败:', error);
  94. socket.emit('error', '存储价格历史失败');
  95. }
  96. }
  97. async getPrice(roomId: string, date: string): Promise<string | null> {
  98. try {
  99. return await redisService.getPrice(roomId, date);
  100. } catch (error) {
  101. log('获取历史价格失败:', error);
  102. return null;
  103. }
  104. }
  105. async getAllPrices(roomId: string): Promise<Record<string, number>> {
  106. try {
  107. return await redisService.getAllPrices(roomId);
  108. } catch (error) {
  109. log('获取所有价格历史失败:', error);
  110. return {};
  111. }
  112. }
  113. async getCurrentQuestion(roomId: string) {
  114. try {
  115. return await redisService.getCurrentQuestion(roomId);
  116. } catch (error) {
  117. log('获取当前题目失败:', error);
  118. return null;
  119. }
  120. }
  121. async cleanupRoomData(socket: AuthenticatedSocket, roomId: string, questionId?: string) {
  122. try {
  123. if (!socket.user) throw new Error('用户未认证');
  124. const user = socket.user;
  125. // 在清理Redis数据前,先保存答题结果到数据库
  126. await this.saveAnswersToSubmissionRecords(roomId, questionId);
  127. await redisService.cleanupRoomData(roomId, questionId);
  128. socket.to(roomId).emit('exam:cleaned', {
  129. roomId,
  130. message: questionId
  131. ? `已清理房间 ${roomId} 的问题 ${questionId} 数据`
  132. : `已清理房间 ${roomId} 的所有数据`
  133. });
  134. log(`用户 ${user.username} 清理房间 ${roomId} 数据`);
  135. } catch (error) {
  136. log('清理房间数据失败:', error);
  137. socket.emit('error', '清理房间数据失败');
  138. }
  139. }
  140. async broadcastSettle(socket: AuthenticatedSocket, roomId: string) {
  141. try {
  142. if (!socket.user) throw new Error('用户未认证');
  143. const user = socket.user;
  144. socket.to(roomId).emit('exam:settle');
  145. log(`用户 ${user.username} 在房间 ${roomId} 广播结算消息`);
  146. } catch (error) {
  147. log('广播结算消息失败:', error);
  148. socket.emit('error', '广播结算消息失败');
  149. }
  150. }
  151. /**
  152. * 保存答题结果到提交记录表
  153. */
  154. private async saveAnswersToSubmissionRecords(roomId: string, questionId?: string): Promise<void> {
  155. try {
  156. // 获取所有答题数据
  157. const answers = await redisService.getAnswers(roomId, questionId);
  158. if (answers.length === 0) {
  159. log(`房间 ${roomId} 没有答题数据需要保存`);
  160. return;
  161. }
  162. const submissionRecordsRepository = AppDataSource.getRepository(SubmissionRecords);
  163. const recordsToSave: SubmissionRecords[] = [];
  164. for (const answer of answers) {
  165. // 转换Redis中的答案数据为提交记录实体
  166. const submissionRecord = new SubmissionRecords();
  167. submissionRecord.classroomNo = roomId;
  168. submissionRecord.userId = answer.userId || null;
  169. submissionRecord.score = this.calculateScore(answer);
  170. submissionRecord.code = answer.code || null;
  171. submissionRecord.trainingDate = answer.date ? new Date(answer.date) : null;
  172. submissionRecord.mark = null; // 标记字段,可根据需要设置
  173. submissionRecord.status = 1; // 状态:1-正常
  174. submissionRecord.holdingStock = answer.holdingStock || null;
  175. submissionRecord.holdingCash = answer.holdingCash || null;
  176. submissionRecord.price = Number(answer.price);
  177. submissionRecord.profitAmount = answer.profitAmount || null;
  178. submissionRecord.profitPercent = answer.profitPercent || null;
  179. submissionRecord.totalProfitAmount = answer.totalProfitAmount || null;
  180. submissionRecord.totalProfitPercent = answer.totalProfitPercent || null;
  181. recordsToSave.push(submissionRecord);
  182. }
  183. // 批量保存到数据库
  184. if (recordsToSave.length > 0) {
  185. await submissionRecordsRepository.save(recordsToSave);
  186. log(`成功保存 ${recordsToSave.length} 条答题记录到数据库,房间: ${roomId}`);
  187. }
  188. } catch (error) {
  189. log('保存答题记录到数据库失败:', error);
  190. // 不抛出错误,避免影响正常的清理操作
  191. }
  192. }
  193. /**
  194. * 计算得分(可根据业务需求自定义评分逻辑)
  195. */
  196. private calculateScore(answer: Answer): number | null {
  197. // 这里可以根据答题内容计算得分
  198. // 示例:根据收益率计算得分,收益率越高得分越高
  199. if (answer.profitPercent !== undefined && answer.profitPercent !== null) {
  200. return Math.max(0, Math.min(100, 50 + (answer.profitPercent * 2)));
  201. }
  202. return null;
  203. }
  204. }