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. const log = debug('socket:exam');
  8. export class ExamService {
  9. private io: Server;
  10. constructor(io: Server) {
  11. this.io = io;
  12. }
  13. async joinRoom(socket: AuthenticatedSocket, roomId: string) {
  14. try {
  15. if (!socket.user) throw new Error('用户未认证');
  16. const user = socket.user;
  17. socket.join(roomId);
  18. await redisService.addRoomMember(roomId, user.id, user.username);
  19. socket.emit('exam:joined', { roomId, message: `成功加入考试房间: ${roomId}` });
  20. socket.to(roomId).emit('exam:memberJoined', { roomId, userId: user.id, username: user.username });
  21. log(`用户 ${user.username} 加入考试房间 ${roomId}`);
  22. } catch (error) {
  23. log('加入考试房间失败:', error);
  24. socket.emit('error', '加入考试房间失败');
  25. }
  26. }
  27. async leaveRoom(socket: AuthenticatedSocket, roomId: string) {
  28. try {
  29. if (!socket.user) throw new Error('用户未认证');
  30. const user = socket.user;
  31. socket.leave(roomId);
  32. await redisService.removeRoomMember(roomId, user.id);
  33. socket.emit('exam:left', { roomId, message: `已离开考试房间: ${roomId}` });
  34. socket.to(roomId).emit('exam:memberLeft', { roomId, userId: user.id, username: user.username });
  35. log(`用户 ${user.username} 离开考试房间 ${roomId}`);
  36. } catch (error) {
  37. log('离开考试房间失败:', error);
  38. socket.emit('error', '离开考试房间失败');
  39. }
  40. }
  41. async pushQuestion(socket: AuthenticatedSocket, roomId: string, question: { date: string; price: string }) {
  42. try {
  43. if (!socket.user) throw new Error('用户未认证');
  44. await redisService.storeCurrentQuestion(roomId, question);
  45. await redisService.storePrice(roomId, question.date, question.price);
  46. const quizState = { id: question.date, date: question.date, price: question.price };
  47. socket.to(roomId).emit('exam:question', quizState);
  48. log(`用户 ${socket.user.username} 在房间 ${roomId} 推送题目`);
  49. } catch (error) {
  50. log('推送题目失败:', error);
  51. socket.emit('error', '推送题目失败');
  52. }
  53. }
  54. async storeAnswer(socket: AuthenticatedSocket, roomId: string, questionId: string, answer: any): Promise<boolean> {
  55. try {
  56. if (!socket.user) throw new Error('用户未认证');
  57. const user = socket.user;
  58. await redisService.storeAnswer(roomId, user.id, questionId, { ...answer, username: user.username });
  59. socket.to(roomId).emit('exam:answerUpdated', {
  60. roomId, questionId, userId: user.id, username: user.username
  61. });
  62. log(`用户 ${user.username} 在房间 ${roomId} 存储答案`);
  63. return true;
  64. } catch (error) {
  65. log('存储答案失败:', error);
  66. socket.emit('error', '存储答案失败');
  67. return false;
  68. }
  69. }
  70. async getAnswers(roomId: string, questionId?: string) {
  71. try {
  72. return await redisService.getAnswers(roomId, questionId);
  73. } catch (error) {
  74. log('获取答案失败:', error);
  75. return [];
  76. }
  77. }
  78. async getUserAnswers(roomId: string, userId: number) {
  79. try {
  80. return await redisService.getUserAnswers(roomId, userId);
  81. } catch (error) {
  82. log('获取用户答案失败:', error);
  83. return [];
  84. }
  85. }
  86. async storePrice(socket: AuthenticatedSocket, roomId: string, date: string, price: string) {
  87. try {
  88. if (!socket.user) throw new Error('用户未认证');
  89. await redisService.storePrice(roomId, date, price);
  90. log(`用户 ${socket.user.username} 存储房间 ${roomId} 的价格历史: ${date} - ${price}`);
  91. } catch (error) {
  92. log('存储价格历史失败:', error);
  93. socket.emit('error', '存储价格历史失败');
  94. }
  95. }
  96. async getPrice(roomId: string, date: string): Promise<string | null> {
  97. try {
  98. return await redisService.getPrice(roomId, date);
  99. } catch (error) {
  100. log('获取历史价格失败:', error);
  101. return null;
  102. }
  103. }
  104. async getAllPrices(roomId: string): Promise<Record<string, number>> {
  105. try {
  106. return await redisService.getAllPrices(roomId);
  107. } catch (error) {
  108. log('获取所有价格历史失败:', error);
  109. return {};
  110. }
  111. }
  112. async getCurrentQuestion(roomId: string) {
  113. try {
  114. return await redisService.getCurrentQuestion(roomId);
  115. } catch (error) {
  116. log('获取当前题目失败:', error);
  117. return null;
  118. }
  119. }
  120. async cleanupRoomData(socket: AuthenticatedSocket, roomId: string, questionId?: string) {
  121. try {
  122. if (!socket.user) throw new Error('用户未认证');
  123. const user = socket.user;
  124. // 在清理Redis数据前,先保存答题结果到数据库
  125. await this.saveAnswersToSubmissionRecords(roomId, questionId);
  126. await redisService.cleanupRoomData(roomId, questionId);
  127. socket.to(roomId).emit('exam:cleaned', {
  128. roomId,
  129. message: questionId
  130. ? `已清理房间 ${roomId} 的问题 ${questionId} 数据`
  131. : `已清理房间 ${roomId} 的所有数据`
  132. });
  133. log(`用户 ${user.username} 清理房间 ${roomId} 数据`);
  134. } catch (error) {
  135. log('清理房间数据失败:', error);
  136. socket.emit('error', '清理房间数据失败');
  137. }
  138. }
  139. async broadcastSettle(socket: AuthenticatedSocket, roomId: string) {
  140. try {
  141. if (!socket.user) throw new Error('用户未认证');
  142. const user = socket.user;
  143. socket.to(roomId).emit('exam:settle');
  144. log(`用户 ${user.username} 在房间 ${roomId} 广播结算消息`);
  145. } catch (error) {
  146. log('广播结算消息失败:', error);
  147. socket.emit('error', '广播结算消息失败');
  148. }
  149. }
  150. /**
  151. * 保存答题结果到提交记录表
  152. */
  153. private async saveAnswersToSubmissionRecords(roomId: string, questionId?: string): Promise<void> {
  154. try {
  155. // 获取所有答题数据
  156. const answers = await redisService.getAnswers(roomId, questionId);
  157. if (answers.length === 0) {
  158. log(`房间 ${roomId} 没有答题数据需要保存`);
  159. return;
  160. }
  161. const submissionRecordsRepository = AppDataSource.getRepository(SubmissionRecords);
  162. const recordsToSave: SubmissionRecords[] = [];
  163. for (const answer of answers) {
  164. // 转换Redis中的答案数据为提交记录实体
  165. const submissionRecord = new SubmissionRecords();
  166. submissionRecord.classroomNo = roomId;
  167. submissionRecord.userId = answer.userId?.toString() || null;
  168. submissionRecord.nickname = answer.username || 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 = answer.price ? parseFloat(answer.price) : null;
  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: any): 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. }