exam.service.ts 9.7 KB

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