|
|
@@ -0,0 +1,179 @@
|
|
|
+import { Server } from 'socket.io';
|
|
|
+import { AuthenticatedSocket } from '../middleware/auth.middleware';
|
|
|
+import { redisService } from './redis.service';
|
|
|
+import debug from 'debug';
|
|
|
+
|
|
|
+const log = debug('socket:exam');
|
|
|
+
|
|
|
+export class ExamService {
|
|
|
+ private io: Server;
|
|
|
+
|
|
|
+ constructor(io: Server) {
|
|
|
+ this.io = io;
|
|
|
+ }
|
|
|
+
|
|
|
+ async joinRoom(socket: AuthenticatedSocket, roomId: string) {
|
|
|
+ try {
|
|
|
+ if (!socket.user) throw new Error('用户未认证');
|
|
|
+
|
|
|
+ const user = socket.user;
|
|
|
+ socket.join(roomId);
|
|
|
+ await redisService.addRoomMember(roomId, user.id, user.username);
|
|
|
+
|
|
|
+ socket.emit('exam:joined', { roomId, message: `成功加入考试房间: ${roomId}` });
|
|
|
+ socket.to(roomId).emit('exam:memberJoined', { roomId, userId: user.id, username: user.username });
|
|
|
+
|
|
|
+ log(`用户 ${user.username} 加入考试房间 ${roomId}`);
|
|
|
+ } catch (error) {
|
|
|
+ log('加入考试房间失败:', error);
|
|
|
+ socket.emit('error', '加入考试房间失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async leaveRoom(socket: AuthenticatedSocket, roomId: string) {
|
|
|
+ try {
|
|
|
+ if (!socket.user) throw new Error('用户未认证');
|
|
|
+
|
|
|
+ const user = socket.user;
|
|
|
+ socket.leave(roomId);
|
|
|
+ await redisService.removeRoomMember(roomId, user.id);
|
|
|
+
|
|
|
+ socket.emit('exam:left', { roomId, message: `已离开考试房间: ${roomId}` });
|
|
|
+ socket.to(roomId).emit('exam:memberLeft', { roomId, userId: user.id, username: user.username });
|
|
|
+
|
|
|
+ log(`用户 ${user.username} 离开考试房间 ${roomId}`);
|
|
|
+ } catch (error) {
|
|
|
+ log('离开考试房间失败:', error);
|
|
|
+ socket.emit('error', '离开考试房间失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async pushQuestion(socket: AuthenticatedSocket, roomId: string, question: { date: string; price: string }) {
|
|
|
+ try {
|
|
|
+ if (!socket.user) throw new Error('用户未认证');
|
|
|
+
|
|
|
+ await redisService.storeCurrentQuestion(roomId, question);
|
|
|
+ await redisService.storePrice(roomId, question.date, question.price);
|
|
|
+
|
|
|
+ const quizState = { id: question.date, date: question.date, price: question.price };
|
|
|
+ socket.to(roomId).emit('exam:question', quizState);
|
|
|
+
|
|
|
+ log(`用户 ${socket.user.username} 在房间 ${roomId} 推送题目`);
|
|
|
+ } catch (error) {
|
|
|
+ log('推送题目失败:', error);
|
|
|
+ socket.emit('error', '推送题目失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async storeAnswer(socket: AuthenticatedSocket, roomId: string, questionId: string, answer: any): Promise<boolean> {
|
|
|
+ try {
|
|
|
+ if (!socket.user) throw new Error('用户未认证');
|
|
|
+
|
|
|
+ const user = socket.user;
|
|
|
+ await redisService.storeAnswer(roomId, user.id, questionId, { ...answer, username: user.username });
|
|
|
+
|
|
|
+ socket.to(roomId).emit('exam:answerUpdated', {
|
|
|
+ roomId, questionId, userId: user.id, username: user.username
|
|
|
+ });
|
|
|
+
|
|
|
+ log(`用户 ${user.username} 在房间 ${roomId} 存储答案`);
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ log('存储答案失败:', error);
|
|
|
+ socket.emit('error', '存储答案失败');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async getAnswers(roomId: string, questionId?: string) {
|
|
|
+ try {
|
|
|
+ return await redisService.getAnswers(roomId, questionId);
|
|
|
+ } catch (error) {
|
|
|
+ log('获取答案失败:', error);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async getUserAnswers(roomId: string, userId: number) {
|
|
|
+ try {
|
|
|
+ return await redisService.getUserAnswers(roomId, userId);
|
|
|
+ } catch (error) {
|
|
|
+ log('获取用户答案失败:', error);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async storePrice(socket: AuthenticatedSocket, roomId: string, date: string, price: string) {
|
|
|
+ try {
|
|
|
+ if (!socket.user) throw new Error('用户未认证');
|
|
|
+
|
|
|
+ await redisService.storePrice(roomId, date, price);
|
|
|
+ log(`用户 ${socket.user.username} 存储房间 ${roomId} 的价格历史: ${date} - ${price}`);
|
|
|
+ } catch (error) {
|
|
|
+ log('存储价格历史失败:', error);
|
|
|
+ socket.emit('error', '存储价格历史失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async getPrice(roomId: string, date: string): Promise<string | null> {
|
|
|
+ try {
|
|
|
+ return await redisService.getPrice(roomId, date);
|
|
|
+ } catch (error) {
|
|
|
+ log('获取历史价格失败:', error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async getAllPrices(roomId: string): Promise<Record<string, number>> {
|
|
|
+ try {
|
|
|
+ return await redisService.getAllPrices(roomId);
|
|
|
+ } catch (error) {
|
|
|
+ log('获取所有价格历史失败:', error);
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async getCurrentQuestion(roomId: string) {
|
|
|
+ try {
|
|
|
+ return await redisService.getCurrentQuestion(roomId);
|
|
|
+ } catch (error) {
|
|
|
+ log('获取当前题目失败:', error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async cleanupRoomData(socket: AuthenticatedSocket, roomId: string, questionId?: string) {
|
|
|
+ try {
|
|
|
+ if (!socket.user) throw new Error('用户未认证');
|
|
|
+
|
|
|
+ const user = socket.user;
|
|
|
+ await redisService.cleanupRoomData(roomId, questionId);
|
|
|
+
|
|
|
+ socket.to(roomId).emit('exam:cleaned', {
|
|
|
+ roomId,
|
|
|
+ message: questionId
|
|
|
+ ? `已清理房间 ${roomId} 的问题 ${questionId} 数据`
|
|
|
+ : `已清理房间 ${roomId} 的所有数据`
|
|
|
+ });
|
|
|
+
|
|
|
+ log(`用户 ${user.username} 清理房间 ${roomId} 数据`);
|
|
|
+ } catch (error) {
|
|
|
+ log('清理房间数据失败:', error);
|
|
|
+ socket.emit('error', '清理房间数据失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async broadcastSettle(socket: AuthenticatedSocket, roomId: string) {
|
|
|
+ try {
|
|
|
+ if (!socket.user) throw new Error('用户未认证');
|
|
|
+
|
|
|
+ const user = socket.user;
|
|
|
+ socket.to(roomId).emit('exam:settle');
|
|
|
+
|
|
|
+ log(`用户 ${user.username} 在房间 ${roomId} 广播结算消息`);
|
|
|
+ } catch (error) {
|
|
|
+ log('广播结算消息失败:', error);
|
|
|
+ socket.emit('error', '广播结算消息失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|