exam.service.ts 8.9 KB

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