Răsfoiți Sursa

♻️ refactor(exam): update ClassroomStatus import path

- 将ClassroomStatus导入路径从classroom-data.entity改为classroom-data.schema

♻️ refactor(submission): separate schema from entity

- 从submission-records.entity.ts中移除zod schema定义
- 创建新文件submission-records.schema.ts并迁移schema定义
- 为提交记录添加更严格的数据验证规则,包括字符长度限制和数值精度控制
yourname 6 luni în urmă
părinte
comite
2468d2f5a8

+ 1 - 1
src/client/mobile/components/Exam/ExamCard.tsx

@@ -7,7 +7,7 @@ import { classroomDataClient } from '@/client/api';
 import type { QuizState } from './types';
 import type { AnswerRecord, Answer } from './types';
 import { useAuth } from '@/client/mobile/hooks/AuthProvider';
-import { ClassroomStatus } from '@/server/modules/classroom/classroom-data.entity';
+import { ClassroomStatus } from '@/server/modules/classroom/classroom-data.schema';
 import { toast } from 'react-toastify';
 
 // 答题卡页面

+ 1 - 1
src/client/mobile/components/Exam/ExamIndex.tsx

@@ -2,7 +2,7 @@ import React, { useState, useCallback } from 'react';
 import { useNavigate } from "react-router";
 import dayjs from 'dayjs';
 import { classroomDataClient } from '@/client/api';
-import { ClassroomStatus } from '@/server/modules/classroom/classroom-data.entity';
+import { ClassroomStatus } from '@/server/modules/classroom/classroom-data.schema';
 import type { InferResponseType } from 'hono/client';
 import { toast } from 'react-toastify';
 

+ 1 - 35
src/server/modules/submission/submission-records.entity.ts

@@ -1,5 +1,4 @@
 import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
-import { z } from '@hono/zod-openapi';
 
 @Entity('submission_records')
 export class SubmissionRecords {
@@ -56,37 +55,4 @@ export class SubmissionRecords {
 
   @Column({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP' })
   updatedAt!: Date;
-}
-
-export const SubmissionRecordsSchema = z.object({
-  id: z.number().int().positive().openapi({ description: '数据ID', example: 1 }),
-  classroomNo: z.string().max(255).nullable().openapi({ description: '教室号', example: 'class01' }),
-  userId: z.string().max(255).nullable().openapi({ description: '用户id', example: '1001' }),
-  nickname: z.string().max(255).nullable().openapi({ description: '昵称', example: 'student1' }),
-  score: z.number().nullable().openapi({ description: '成绩', example: 95.5 }),
-  code: z.string().max(255).nullable().openapi({ description: '代码', example: '001339' }),
-  trainingDate: z.date().nullable().openapi({ description: '训练日期', example: '2025-05-21T08:00:00Z' }),
-  mark: z.string().max(255).nullable().openapi({ description: '标记', example: '优秀' }),
-  status: z.number().nullable().openapi({ description: '状态', example: 1 }),
-  holdingStock: z.string().max(255).nullable().openapi({ description: '持股', example: '100股' }),
-  holdingCash: z.string().max(255).nullable().openapi({ description: '持币', example: '10000元' }),
-  price: z.number().nullable().openapi({ description: '价格', example: 15.68 }),
-  profitAmount: z.number().nullable().openapi({ description: '收益金额', example: 500.00 }),
-  profitPercent: z.number().nullable().openapi({ description: '收益率', example: 5.25 }),
-  totalProfitAmount: z.number().nullable().openapi({ description: '累计收益金额', example: 2500.00 }),
-  totalProfitPercent: z.number().nullable().openapi({ description: '累计收益率', example: 12.5 }),
-  createdAt: z.date().openapi({ description: '创建时间', example: '2025-05-21T16:44:36Z' }),
-  updatedAt: z.date().openapi({ description: '更新时间', example: '2025-05-21T21:22:06Z' })
-});
-
-export const CreateSubmissionRecordsDto = SubmissionRecordsSchema.omit({
-  id: true,
-  createdAt: true,
-  updatedAt: true
-}).partial();
-
-export const UpdateSubmissionRecordsDto = SubmissionRecordsSchema.omit({
-  id: true,
-  createdAt: true,
-  updatedAt: true
-}).partial();
+}

+ 205 - 0
src/server/modules/submission/submission-records.schema.ts

@@ -0,0 +1,205 @@
+import { z } from '@hono/zod-openapi';
+
+// 提交记录Schema定义
+export const SubmissionRecordsSchema = z.object({
+  id: z.number().int('必须是整数').positive('必须是正整数').openapi({
+    description: '提交记录ID',
+    example: 1
+  }),
+  classroomNo: z.string().max(255, '教室号最多255个字符').nullable().openapi({
+    description: '教室号',
+    example: 'A101'
+  }),
+  userId: z.string().max(255, '用户ID最多255个字符').nullable().openapi({
+    description: '用户ID',
+    example: 'user123'
+  }),
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().openapi({
+    description: '昵称',
+    example: '张三'
+  }),
+  score: z.coerce.number<number>().multipleOf(0.01, '成绩最多保留两位小数').nullable().openapi({
+    description: '成绩',
+    example: 95.5
+  }),
+  code: z.string().max(255, '代码最多255个字符').nullable().openapi({
+    description: '代码',
+    example: '600000.SH'
+  }),
+  trainingDate: z.coerce.date<Date>().nullable().openapi({
+    description: '训练日期',
+    example: '2024-01-15T08:00:00Z'
+  }),
+  mark: z.string().max(255, '标记最多255个字符').nullable().openapi({
+    description: '标记',
+    example: '优秀'
+  }),
+  status: z.coerce.number<number>().int('状态必须是整数').nullable().openapi({
+    description: '状态',
+    example: 1
+  }),
+  holdingStock: z.string().max(255, '持股最多255个字符').nullable().openapi({
+    description: '持股',
+    example: '600000.SH'
+  }),
+  holdingCash: z.string().max(255, '持币最多255个字符').nullable().openapi({
+    description: '持币',
+    example: '10000.00'
+  }),
+  price: z.coerce.number<number>().multipleOf(0.01, '价格最多保留两位小数').nullable().openapi({
+    description: '价格',
+    example: 10.5
+  }),
+  profitAmount: z.coerce.number<number>().multipleOf(0.01, '收益金额最多保留两位小数').nullable().openapi({
+    description: '收益金额',
+    example: 500.25
+  }),
+  profitPercent: z.coerce.number<number>().multipleOf(0.01, '收益率最多保留两位小数').nullable().openapi({
+    description: '收益率',
+    example: 5.25
+  }),
+  totalProfitAmount: z.coerce.number<number>().multipleOf(0.01, '累计收益金额最多保留两位小数').nullable().openapi({
+    description: '累计收益金额',
+    example: 2500.75
+  }),
+  totalProfitPercent: z.coerce.number<number>().multipleOf(0.01, '累计收益率最多保留两位小数').nullable().openapi({
+    description: '累计收益率',
+    example: 12.5
+  }),
+  createdAt: z.coerce.date<Date>().openapi({
+    description: '创建时间',
+    example: '2024-01-15T08:00:00Z'
+  }),
+  updatedAt: z.coerce.date<Date>().openapi({
+    description: '更新时间',
+    example: '2024-01-15T08:30:00Z'
+  })
+});
+
+// 创建提交记录DTO
+export const CreateSubmissionRecordsDto = z.object({
+  classroomNo: z.string().max(255, '教室号最多255个字符').nullable().optional().openapi({
+    description: '教室号',
+    example: 'A101'
+  }),
+  userId: z.string().max(255, '用户ID最多255个字符').nullable().optional().openapi({
+    description: '用户ID',
+    example: 'user123'
+  }),
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().optional().openapi({
+    description: '昵称',
+    example: '张三'
+  }),
+  score: z.coerce.number<number>().multipleOf(0.01, '成绩最多保留两位小数').nullable().optional().openapi({
+    description: '成绩',
+    example: 95.5
+  }),
+  code: z.string().max(255, '代码最多255个字符').nullable().optional().openapi({
+    description: '代码',
+    example: '600000.SH'
+  }),
+  trainingDate: z.coerce.date<Date>().nullable().optional().openapi({
+    description: '训练日期',
+    example: '2024-01-15T08:00:00Z'
+  }),
+  mark: z.string().max(255, '标记最多255个字符').nullable().optional().openapi({
+    description: '标记',
+    example: '优秀'
+  }),
+  status: z.coerce.number<number>().int('状态必须是整数').nullable().optional().openapi({
+    description: '状态',
+    example: 1
+  }),
+  holdingStock: z.string().max(255, '持股最多255个字符').nullable().optional().openapi({
+    description: '持股',
+    example: '600000.SH'
+  }),
+  holdingCash: z.string().max(255, '持币最多255个字符').nullable().optional().openapi({
+    description: '持币',
+    example: '10000.00'
+  }),
+  price: z.coerce.number<number>().multipleOf(0.01, '价格最多保留两位小数').nullable().optional().openapi({
+    description: '价格',
+    example: 10.5
+  }),
+  profitAmount: z.coerce.number<number>().multipleOf(0.01, '收益金额最多保留两位小数').nullable().optional().openapi({
+    description: '收益金额',
+    example: 500.25
+  }),
+  profitPercent: z.coerce.number<number>().multipleOf(0.01, '收益率最多保留两位小数').nullable().optional().openapi({
+    description: '收益率',
+    example: 5.25
+  }),
+  totalProfitAmount: z.coerce.number<number>().multipleOf(0.01, '累计收益金额最多保留两位小数').nullable().optional().openapi({
+    description: '累计收益金额',
+    example: 2500.75
+  }),
+  totalProfitPercent: z.coerce.number<number>().multipleOf(0.01, '累计收益率最多保留两位小数').nullable().optional().openapi({
+    description: '累计收益率',
+    example: 12.5
+  })
+});
+
+// 更新提交记录DTO
+export const UpdateSubmissionRecordsDto = z.object({
+  classroomNo: z.string().max(255, '教室号最多255个字符').nullable().optional().openapi({
+    description: '教室号',
+    example: 'A101'
+  }),
+  userId: z.string().max(255, '用户ID最多255个字符').nullable().optional().openapi({
+    description: '用户ID',
+    example: 'user123'
+  }),
+  nickname: z.string().max(255, '昵称最多255个字符').nullable().optional().openapi({
+    description: '昵称',
+    example: '张三'
+  }),
+  score: z.coerce.number<number>().multipleOf(0.01, '成绩最多保留两位小数').nullable().optional().openapi({
+    description: '成绩',
+    example: 95.5
+  }),
+  code: z.string().max(255, '代码最多255个字符').nullable().optional().openapi({
+    description: '代码',
+    example: '600000.SH'
+  }),
+  trainingDate: z.coerce.date<Date>().nullable().optional().openapi({
+    description: '训练日期',
+    example: '2024-01-15T08:00:00Z'
+  }),
+  mark: z.string().max(255, '标记最多255个字符').nullable().optional().openapi({
+    description: '标记',
+    example: '优秀'
+  }),
+  status: z.coerce.number<number>().int('状态必须是整数').nullable().optional().openapi({
+    description: '状态',
+    example: 1
+  }),
+  holdingStock: z.string().max(255, '持股最多255个字符').nullable().optional().openapi({
+    description: '持股',
+    example: '600000.SH'
+  }),
+  holdingCash: z.string().max(255, '持币最多255个字符').nullable().optional().openapi({
+    description: '持币',
+    example: '10000.00'
+  }),
+  price: z.coerce.number<number>().multipleOf(0.01, '价格最多保留两位小数').nullable().optional().openapi({
+    description: '价格',
+    example: 10.5
+  }),
+  profitAmount: z.coerce.number<number>().multipleOf(0.01, '收益金额最多保留两位小数').nullable().optional().openapi({
+    description: '收益金额',
+    example: 500.25
+  }),
+  profitPercent: z.coerce.number<number>().multipleOf(0.01, '收益率最多保留两位小数').nullable().optional().openapi({
+    description: '收益率',
+    example: 5.25
+  }),
+  totalProfitAmount: z.coerce.number<number>().multipleOf(0.01, '累计收益金额最多保留两位小数').nullable().optional().openapi({
+    description: '累计收益金额',
+    example: 2500.75
+  }),
+  totalProfitPercent: z.coerce.number<number>().multipleOf(0.01, '累计收益率最多保留两位小数').nullable().optional().openapi({
+    description: '累计收益率',
+    example: 12.5
+  })
+});