2
0
Просмотр исходного кода

✨ feat(socket): implement exam socket functionality

- add exam handler with room join/leave, question push, answer management, price storage and settlement features
- create socket authentication middleware for user verification
- implement exam service for business logic processing
- develop redis service for data persistence and room management
- define socket types and event interfaces for type safety
- add shared stock types for classroom and submission data

✨ feat(types): add shared stock and classroom types

- define classroom data interface with training information
- create date note interface for stock market annotations
- implement submission record types with status enum
- add XunlianCode interface for training cases
- include pagination interfaces for list responses
yourname 7 месяцев назад
Родитель
Сommit
6072afa53c

+ 148 - 0
src/server/socket/handlers/exam.handler.ts

@@ -0,0 +1,148 @@
+import { Server } from 'socket.io';
+import { ExamService } from '../services/exam.service';
+import { AuthenticatedSocket } from '../middleware/auth.middleware';
+import debug from 'debug';
+
+const log = debug('socket:handler:exam');
+
+export class ExamHandler {
+  private examService: ExamService;
+
+  constructor(io: Server) {
+    this.examService = new ExamService(io);
+  }
+
+  register(socket: AuthenticatedSocket) {
+    // 加入考试房间
+    socket.on('exam:join', async (data) => {
+      try {
+        await this.examService.joinRoom(socket, data.roomId);
+      } catch (error) {
+        log('处理加入房间事件失败:', error);
+        socket.emit('error', '加入考试房间失败');
+      }
+    });
+
+    // 离开考试房间
+    socket.on('exam:leave', async (data) => {
+      try {
+        await this.examService.leaveRoom(socket, data.roomId);
+      } catch (error) {
+        log('处理离开房间事件失败:', error);
+        socket.emit('error', '离开考试房间失败');
+      }
+    });
+
+    // 推送题目
+    socket.on('exam:question', async (data) => {
+      try {
+        await this.examService.pushQuestion(socket, data.roomId, data.question);
+      } catch (error) {
+        log('处理推送题目事件失败:', error);
+        socket.emit('error', '推送题目失败');
+      }
+    });
+
+    // 存储答案
+    socket.on('exam:storeAnswer', async (data, callback) => {
+      try {
+        const success = await this.examService.storeAnswer(socket, data.roomId, data.questionId, data.answer);
+        if (callback) callback(success);
+      } catch (error) {
+        log('处理存储答案事件失败:', error);
+        socket.emit('error', '存储答案失败');
+        if (callback) callback(false);
+      }
+    });
+
+    // 获取答案
+    socket.on('exam:getAnswers', async (data, callback) => {
+      try {
+        const answers = await this.examService.getAnswers(data.roomId, data.questionId);
+        if (callback) callback(answers);
+      } catch (error) {
+        log('处理获取答案事件失败:', error);
+        socket.emit('error', '获取答案失败');
+        if (callback) callback([]);
+      }
+    });
+
+    // 存储价格
+    socket.on('exam:storePrice', async (data) => {
+      try {
+        await this.examService.storePrice(socket, data.roomId, data.date, data.price);
+      } catch (error) {
+        log('处理存储价格事件失败:', error);
+        socket.emit('error', '存储价格失败');
+      }
+    });
+
+    // 获取价格
+    socket.on('exam:getPrice', async (data, callback) => {
+      try {
+        const price = await this.examService.getPrice(data.roomId, data.date);
+        if (callback) callback(price || '0');
+      } catch (error) {
+        log('处理获取价格事件失败:', error);
+        socket.emit('error', '获取价格失败');
+        if (callback) callback('0');
+      }
+    });
+
+    // 获取所有价格
+    socket.on('exam:getPrices', async (data, callback) => {
+      try {
+        const prices = await this.examService.getAllPrices(data.roomId);
+        if (callback) callback(prices);
+      } catch (error) {
+        log('处理获取所有价格事件失败:', error);
+        socket.emit('error', '获取价格失败');
+        if (callback) callback({});
+      }
+    });
+
+    // 获取用户答案
+    socket.on('exam:getUserAnswers', async (data, callback) => {
+      try {
+        const answers = await this.examService.getUserAnswers(data.roomId, data.userId);
+        if (callback) callback(answers);
+      } catch (error) {
+        log('处理获取用户答案事件失败:', error);
+        socket.emit('error', '获取用户答案失败');
+        if (callback) callback([]);
+      }
+    });
+
+    // 获取当前题目
+    socket.on('exam:currentQuestion', async (data, callback) => {
+      try {
+        const question = await this.examService.getCurrentQuestion(data.roomId);
+        if (callback) callback(question);
+      } catch (error) {
+        log('处理获取当前题目事件失败:', error);
+        socket.emit('error', '获取当前题目失败');
+        if (callback) callback(null);
+      }
+    });
+
+    // 结算
+    socket.on('exam:settle', async (data) => {
+      try {
+        await this.examService.broadcastSettle(socket, data.roomId);
+      } catch (error) {
+        log('处理结算事件失败:', error);
+        socket.emit('error', '结算失败');
+      }
+    });
+
+    // 清理数据
+    socket.on('exam:cleanup', async (data) => {
+      try {
+        await this.examService.cleanupRoomData(socket, data.roomId, data.questionId);
+      } catch (error) {
+        log('处理清理数据事件失败:', error);
+        socket.emit('error', '清理数据失败');
+      }
+    });
+  }
+}

+ 65 - 0
src/server/socket/middleware/auth.middleware.ts

@@ -0,0 +1,65 @@
+import { Socket } from 'socket.io';
+import { UserService } from '@/server/modules/users/user.service';
+import { AppDataSource } from '@/server/data-source';
+import debug from 'debug';
+import { UserEntity } from '@/server/modules/users/user.entity';
+import { AuthService } from '@/server/modules/auth/auth.service';
+
+const log = debug('socket:auth');
+
+export interface AuthenticatedSocket extends Socket {
+  user?: UserEntity;
+}
+
+export const createSocketAuthMiddleware = () => {
+  return async (socket: AuthenticatedSocket, next: (err?: Error) => void) => {
+    try {
+      // 获取 token
+      const token = socket.handshake.auth?.token || socket.handshake.query?.socket_token;
+      
+      if (!token) {
+        log('未提供token,拒绝连接');
+        return next(new Error('未授权'));
+      }
+
+      // 使用 AuthService 统一验证 token - 与 HTTP API 保持一致
+      const userService = new UserService(AppDataSource);
+      const authService = new AuthService(userService);
+      const decoded = authService.verifyToken(token as string);
+
+      // 获取用户信息
+      const user = await userService.getUserById(decoded.id);
+
+      if (!user) {
+        log('无效用户,拒绝连接');
+        return next(new Error('无效凭证'));
+      }
+
+      // 检查用户状态
+      if (user.isDisabled === 1) {
+        log('用户被禁用,拒绝连接');
+        return next(new Error('用户被禁用'));
+      }
+
+      if (user.isDeleted === 1) {
+        log('用户已删除,拒绝连接');
+        return next(new Error('用户不存在'));
+      }
+
+      // 设置用户信息到 socket
+      socket.user = user;
+      
+      log(`用户认证成功: ${user.username} (ID: ${user.id})`);
+      next();
+    } catch (error) {
+      log('认证错误:', error);
+      
+      const err = error as Error;
+      if (err.name === 'JsonWebTokenError' || err.name === 'TokenExpiredError') {
+        return next(new Error('无效的token'));
+      }
+      
+      next(new Error('认证失败'));
+    }
+  };
+};

+ 179 - 0
src/server/socket/services/exam.service.ts

@@ -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', '广播结算消息失败');
+    }
+  }
+}

+ 196 - 0
src/server/socket/services/redis.service.ts

@@ -0,0 +1,196 @@
+import Redis from 'ioredis';
+import debug from 'debug';
+
+const log = debug('socket:redis');
+
+export class RedisService {
+  private client: Redis;
+  private isConnected = false;
+
+  constructor() {
+    this.client = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
+
+    this.client.on('error', (err: Error) => {
+      log('Redis Client Error:', err);
+    });
+
+    this.client.on('connect', () => {
+      log('Redis Client Connected');
+      this.isConnected = true;
+    });
+
+    this.client.on('disconnect', () => {
+      log('Redis Client Disconnected');
+      this.isConnected = false;
+    });
+  }
+
+  // 考试相关的方法
+  async storeCurrentQuestion(roomId: string, question: { date: string; price: string }) {
+    const key = `exam:${roomId}:current_question`;
+    await this.client.hset(key, {
+      date: question.date,
+      price: question.price,
+      updated_at: new Date().toISOString(),
+    });
+    // 设置过期时间,24小时
+    await this.client.expire(key, 24 * 60 * 60);
+  }
+
+  async getCurrentQuestion(roomId: string): Promise<{ date: string; price: string } | null> {
+    const key = `exam:${roomId}:current_question`;
+    const data = await this.client.hgetall(key);
+    
+    if (!data.date || !data.price) {
+      return null;
+    }
+
+    return {
+      date: data.date,
+      price: data.price,
+    };
+  }
+
+  async storePrice(roomId: string, date: string, price: string) {
+    const key = `exam:${roomId}:prices`;
+    await this.client.hset(key, date, price);
+    // 设置过期时间,7天
+    await this.client.expire(key, 7 * 24 * 60 * 60);
+  }
+
+  async getPrice(roomId: string, date: string): Promise<string | null> {
+    const key = `exam:${roomId}:prices`;
+    return await this.client.hget(key, date);
+  }
+
+  async getAllPrices(roomId: string): Promise<Record<string, number>> {
+    const key = `exam:${roomId}:prices`;
+    const prices = await this.client.hgetall(key);
+    
+    const result: Record<string, number> = {};
+    for (const [date, price] of Object.entries(prices)) {
+      const numPrice = parseFloat(price);
+      if (!isNaN(numPrice)) {
+        result[date] = numPrice;
+      }
+    }
+    
+    return result;
+  }
+
+  async storeAnswer(
+    roomId: string,
+    userId: number,
+    questionId: string,
+    answer: any
+  ) {
+    const key = `exam:${roomId}:answers:${userId}:${questionId}`;
+    await this.client.hset(key, 'data', JSON.stringify(answer));
+    // 设置过期时间,30天
+    await this.client.expire(key, 30 * 24 * 60 * 60);
+  }
+
+  async getAnswers(roomId: string, questionId?: string): Promise<any[]> {
+    const pattern = questionId
+      ? `exam:${roomId}:answers:*:${questionId}`
+      : `exam:${roomId}:answers:*`;
+    
+    const keys = await this.client.keys(pattern);
+    const answers: any[] = [];
+
+    for (const key of keys) {
+      const data = await this.client.hget(key, 'data');
+      if (data) {
+        try {
+          const answer = JSON.parse(data);
+          // 从 key 中提取 userId
+          const parts = key.split(':');
+          const userId = parts[3];
+          answers.push({
+            ...answer,
+            userId,
+          });
+        } catch (error) {
+          log('解析答案数据失败:', error);
+        }
+      }
+    }
+
+    return answers;
+  }
+
+  async getUserAnswers(roomId: string, userId: number): Promise<any[]> {
+    const pattern = `exam:${roomId}:answers:${userId}:*`;
+    const keys = await this.client.keys(pattern);
+    const answers: any[] = [];
+
+    for (const key of keys) {
+      const data = await this.client.hget(key, 'data');
+      if (data) {
+        try {
+          const answer = JSON.parse(data);
+          answers.push(answer);
+        } catch (error) {
+          log('解析用户答案数据失败:', error);
+        }
+      }
+    }
+
+    return answers;
+  }
+
+  async cleanupRoomData(roomId: string, questionId?: string) {
+    if (questionId) {
+      // 清理特定问题的数据
+      const pattern = `exam:${roomId}:${questionId}:*`;
+      const keys = await this.client.keys(pattern);
+      
+      if (keys.length > 0) {
+        await this.client.del(keys);
+      }
+    } else {
+      // 清理整个房间的数据
+      const pattern = `exam:${roomId}:*`;
+      const keys = await this.client.keys(pattern);
+      
+      if (keys.length > 0) {
+        await this.client.del(keys);
+      }
+    }
+  }
+
+  // 房间成员管理
+  async addRoomMember(roomId: string, userId: number, username: string) {
+    const key = `exam:${roomId}:members`;
+    await this.client.hset(key, userId.toString(), username);
+    await this.client.expire(key, 24 * 60 * 60);
+  }
+
+  async removeRoomMember(roomId: string, userId: number) {
+    const key = `exam:${roomId}:members`;
+    await this.client.hdel(key, userId.toString());
+  }
+
+  async getRoomMembers(roomId: string): Promise<Record<string, string>> {
+    const key = `exam:${roomId}:members`;
+    return await this.client.hgetall(key);
+  }
+
+  // 通用方法
+  async ping(): Promise<boolean> {
+    try {
+      const result = await this.client.ping();
+      return result === 'PONG';
+    } catch (error) {
+      log('Redis ping failed:', error);
+      return false;
+    }
+  }
+
+  async disconnect() {
+    await this.client.disconnect();
+  }
+}
+
+// 单例实例
+export const redisService = new RedisService();

+ 91 - 0
src/server/socket/types/socket.types.ts

@@ -0,0 +1,91 @@
+import { Socket } from 'socket.io';
+import { UserEntity } from '@/server/modules/users/user.entity';
+
+export interface AuthenticatedSocket extends Socket {
+  user?: UserEntity;
+}
+
+export interface SocketContext {
+  socket: AuthenticatedSocket;
+  user: UserEntity;
+}
+
+// 考试相关的事件类型
+export interface ExamRoomData {
+  roomId: string;
+}
+
+export interface ExamQuestionData {
+  roomId: string;
+  question: QuizContent;
+}
+
+export interface ExamAnswerData {
+  roomId: string;
+  questionId: string;
+  answer: Answer;
+}
+
+export interface ExamPriceData {
+  roomId: string;
+  date: string;
+  price: string;
+}
+
+// 与前端共享的类型
+export interface QuizContent {
+  date: string;
+  price: string;
+}
+
+export interface QuizState {
+  id: string;
+  date: string;
+  price: string;
+}
+
+export interface Answer {
+  username: string;
+  date: string;
+  price: string;
+  holdingStock: number;
+  holdingCash: number;
+  profitAmount: number;
+  profitPercent: number;
+  totalProfitAmount: number;
+  totalProfitPercent: number;
+}
+
+// Socket.IO 事件映射
+export interface ClientToServerEvents {
+  'exam:join': (data: ExamRoomData) => void;
+  'exam:leave': (data: ExamRoomData) => void;
+  'exam:question': (data: ExamQuestionData) => void;
+  'exam:storeAnswer': (data: ExamAnswerData, callback: (success: boolean) => void) => void;
+  'exam:getAnswers': (data: { roomId: string; questionId?: string }, callback: (answers: Answer[]) => void) => void;
+  'exam:storePrice': (data: ExamPriceData) => void;
+  'exam:getPrice': (data: { roomId: string; date: string }, callback: (price: string) => void) => void;
+  'exam:getPrices': (data: { roomId: string }, callback: (prices: Record<string, number>) => void) => void;
+  'exam:getUserAnswers': (data: { roomId: string; userId: string }, callback: (answers: Answer[]) => void) => void;
+  'exam:currentQuestion': (data: ExamRoomData, callback: (question: QuizState | null) => void) => void;
+  'exam:settle': (data: ExamRoomData) => void;
+  'exam:cleanup': (data: { roomId: string; questionId?: string }) => void;
+}
+
+export interface ServerToClientEvents {
+  'exam:joined': (data: { roomId: string; message: string }) => void;
+  'exam:left': (data: { roomId: string; message: string }) => void;
+  'exam:memberJoined': (data: { roomId: string; userId: number; username: string }) => void;
+  'exam:memberLeft': (data: { roomId: string; userId: number; username: string }) => void;
+  'exam:question': (quizState: QuizState) => void;
+  'exam:answerUpdated': (data: { roomId: string; questionId: string; userId: number; username: string }) => void;
+  'exam:cleaned': (data: { roomId: string; message: string }) => void;
+  'exam:settle': () => void;
+  'error': (message: string) => void;
+}
+
+export interface InterServerEvents {}
+
+export interface SocketData {
+  user: UserEntity;
+}

+ 172 - 0
src/share/types_stock.ts

@@ -0,0 +1,172 @@
+
+  
+  // 教室数据接口
+  export interface ClassroomData {
+    /** 主键ID */
+    id: number;
+    
+    /** 教室号 */
+    classroom_no: string;
+    
+    /** 训练日期 */
+    training_date: string;
+    
+    /** 持股 */
+    holding_stock?: string;
+    
+    /** 持币 */
+    holding_cash?: string;
+    
+    /** 价格 */
+    price?: string;
+    
+    /** 代码 */
+    code?: string;
+    
+    /** 状态 */
+    status: ClassroomStatus;
+    
+    /** 备用字段 */
+    spare?: string;
+    
+    /** 提交用户ID */
+    submit_user?: number;
+    
+    /** 创建时间 */
+    created_at: string;
+    
+    /** 更新时间 */
+    updated_at: string;
+  }
+  
+  // 教室数据列表响应
+  export interface ClassroomDataListResponse {
+    data: ClassroomData[];
+    pagination: {
+      current: number;
+      pageSize: number;
+      total: number;
+      totalPages: number;
+    };
+  }
+  
+  // 日期备注接口
+  export interface DateNote {
+    /** 主键ID */
+    id: number;
+    
+    /** 股票代码 */
+    code: string;
+    
+    /** 备注日期 */
+    note_date: string;
+    
+    /** 备注内容 */
+    note: string;
+    
+    /** 创建时间 */
+    created_at: string;
+    
+    /** 更新时间 */
+    updated_at: string;
+  }
+  
+  // 日期备注列表响应
+  export interface DateNoteListResponse {
+    data: DateNote[];
+    pagination: {
+      current: number;
+      pageSize: number;
+      total: number;
+      totalPages: number;
+    };
+  }
+  
+  // 提交记录状态枚举
+  export enum SubmissionRecordStatus {
+    PENDING = 0,   // 待处理
+    APPROVED = 1,  // 已通过
+    REJECTED = 2   // 已拒绝
+  }
+  
+  // 提交记录状态中文映射
+  export const SubmissionRecordStatusNameMap: Record<SubmissionRecordStatus, string> = {
+    [SubmissionRecordStatus.PENDING]: '待处理',
+    [SubmissionRecordStatus.APPROVED]: '已通过',
+    [SubmissionRecordStatus.REJECTED]: '已拒绝'
+  };
+  
+  // 提交记录实体
+  export interface SubmissionRecord {
+    /** 主键ID */
+    id: number;
+    
+    /** 用户ID */
+    user_id: number;
+    
+    /** 用户昵称 */
+    nickname: string;
+    
+    /** 成绩 */
+    score: number;
+    
+    /** 代码 */
+    code: string;
+    
+    /** 训练日期 */
+    training_date: string;
+    
+    /** 标记 */
+    mark?: string;
+    
+    /** 状态 */
+    status: SubmissionRecordStatus;
+    
+    /** 创建时间 */
+    created_at: string;
+    
+    /** 更新时间 */
+    updated_at: string;
+  }
+  
+  // 提交记录列表响应
+  export interface SubmissionRecordListResponse {
+    data: SubmissionRecord[];
+    pagination: {
+      current: number;
+      pageSize: number;
+      total: number;
+      totalPages: number;
+    };
+  }
+  
+  // 训练代码实体
+  export interface XunlianCode {
+    /** 主键ID */
+    id: number;
+    
+    /** 股票代码 */
+    code: string;
+    
+    /** 股票名称 */
+    stock_name: string;
+    
+    /** 案例名称 */
+    name: string;
+    
+    /** 案例类型 */
+    type?: string;
+    
+    /** 案例描述 */
+    description?: string;
+    
+    /** 交易日期 */
+    trade_date: string;
+    
+    /** 创建时间 */
+    created_at: string;
+    
+    /** 更新时间 */
+    updated_at: string;
+  }
+