2
0
Преглед на файлове

♻️ refactor(classroom): separate schema from entity and optimize

- move ClassroomStatus enum and schema definitions from entity to new classroom-data.schema.ts file
- update schema to use z.enum instead of z.nativeEnum for better zod v4 compatibility
- add type generics to z.coerce methods (z.coerce.date<Date>(), z.coerce.number<number>())
- add validation messages to schema fields for better error feedback
- update example values to be more descriptive and consistent

📝 docs(commands): update generic-crud documentation

- replace device status example with classroom status example
- add note about correct enum usage in zod schemas
- add documentation for z.coerce type generics requirement in zod v4
yourname преди 6 месеца
родител
ревизия
f57ee6896e

+ 16 - 18
.roo/commands/generic-crud-创建实体验证规则.md

@@ -37,26 +37,24 @@ description: "创建已有实体的schema指令"
 4. 实体中import用到的枚举,your-entity.schema.ts 中也要import用上
     示例:
     ```typescript
-    import { 
-        DeviceStatus, 
-        NetworkStatus, 
-        packetLoss
-    } from '@/share/monitorTypes';
+    // 教室状态枚举
+    export enum ClassroomStatus {
+    CLOSED = 0,  // 关闭
+    OPEN = 1     // 开放
+    }
     // 在当前zod v4中 z.enum 代替了 z.nativeEnum;  
     z.object({
-        // z.enum(DeviceStatus) 等价于 z.nativeEnum(DeviceStatus)
-        deviceStatus: z.enum(DeviceStatus).nullable().openapi({
-            description: '设备状态 (0正常 1维护中 2故障 3下线)',
-            example: DeviceStatus.NORMAL
+        // z.enum(ClassroomStatus) 等价于 z.nativeEnum(ClassroomStatus), 不要 z.enum([ClassroomStatus.CLOSED, ClassroomStatus.OPEN]),应该是 z.enum(ClassroomStatus)
+        z.enum(ClassroomStatus).nullable().openapi({
+            description: '状态 (0关闭 1开放)',
+            example: ClassroomStatus.OPEN
         }),
-        networkStatus: z.enum(NetworkStatus).nullable().openapi({
-            description: '网络状态 (0已连接 1已断开 2不稳定)',
-            example: NetworkStatus.CONNECTED
-        }),
-        packetLoss: z.enum(PacketLossStatus).nullable().openapi({
-            description: '丢包率 (0正常 1丢包)',
-            example: PacketLossStatus.NORMAL
-        }),
-
     })
+    ```
+
+5. 在当前 zod v4中, z.coerce.date(), z.coerce.number() 等,都要添加类型泛型指定
+    示例:
+    ```typescript
+    z.coerce.date<Date>()
+    z.coerce.number<number>()
     ```

+ 2 - 53
src/server/modules/classroom/classroom-data.entity.ts

@@ -1,17 +1,5 @@
 import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
-import { z } from '@hono/zod-openapi';
-
-// 教室状态枚举
-export enum ClassroomStatus {
-  CLOSED = 0,  // 关闭
-  OPEN = 1     // 开放
-}
-
-// 教室状态中文映射
-export const ClassroomStatusNameMap: Record<ClassroomStatus, string> = {
-  [ClassroomStatus.CLOSED]: '关闭',
-  [ClassroomStatus.OPEN]: '开放'
-};
+import { ClassroomStatus } from './classroom-data.schema';
 
 @Entity('classroom_data')
 export class ClassroomData {
@@ -56,43 +44,4 @@ export class ClassroomData {
 
   @Column({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP' })
   updatedAt!: Date;
-}
-
-export const ClassroomDataSchema = z.object({
-  id: z.number().int().positive().openapi({ description: '数据ID', example: 1 }),
-  classroomNo: z.string().max(255).nullable().openapi({ description: '教室号', example: 'test01' }),
-  trainingDate: z.date().nullable().openapi({ description: '训练日期', example: '2025-05-21T08:00:00Z' }),
-  holdingStock: z.string().max(255).nullable().openapi({ description: '持股', example: '100股' }),
-  holdingCash: z.string().max(255).nullable().openapi({ description: '持币', example: '10000元' }),
-  price: z.string().max(255).nullable().openapi({ description: '价格', example: '15.68' }),
-  code: z.string().max(255).nullable().openapi({ description: '代码', example: '001339' }),
-  status: z.nativeEnum(ClassroomStatus).nullable().openapi({ description: '状态', example: ClassroomStatus.OPEN }),
-  spare: z.string().max(255).nullable().openapi({ description: '备用', example: '' }),
-  submitUser: z.string().max(255).nullable().openapi({ description: '提交用户', example: '' }),
-  createdBy: z.number().int().positive().nullable().openapi({ description: '创建用户ID', example: 1 }),
-  updatedBy: z.number().int().positive().nullable().openapi({ description: '更新用户ID', example: 1 }),
-  createdAt: z.date().openapi({ description: '创建时间', example: '2025-05-21T16:44:36Z' }),
-  updatedAt: z.date().openapi({ description: '更新时间', example: '2025-05-21T21:22:06Z' })
-});
-
-export const CreateClassroomDataDto = z.object({
-  classroomNo: z.string().max(255).optional().nullable().openapi({ description: '教室号', example: 'test01' }),
-  trainingDate: z.coerce.date().optional().nullable().openapi({ description: '训练日期', example: '2025-05-21T08:00:00Z' }),
-  holdingStock: z.string().max(255).optional().nullable().openapi({ description: '持股', example: '100股' }),
-  holdingCash: z.string().max(255).optional().nullable().openapi({ description: '持币', example: '10000元' }),
-  price: z.string().max(255).optional().nullable().openapi({ description: '价格', example: '15.68' }),
-  code: z.string().max(255).optional().nullable().openapi({ description: '代码', example: '001339' }),
-  status: z.nativeEnum(ClassroomStatus).optional().nullable().openapi({ description: '状态', example: ClassroomStatus.OPEN }),
-  spare: z.string().max(255).optional().nullable().openapi({ description: '备用', example: '' })
-});
-
-export const UpdateClassroomDataDto = z.object({
-  classroomNo: z.string().max(255).optional().nullable().openapi({ description: '教室号', example: 'test01' }),
-  trainingDate: z.coerce.date().optional().nullable().openapi({ description: '训练日期', example: '2025-05-21T08:00:00Z' }),
-  holdingStock: z.string().max(255).optional().nullable().openapi({ description: '持股', example: '100股' }),
-  holdingCash: z.string().max(255).optional().nullable().openapi({ description: '持币', example: '10000元' }),
-  price: z.string().max(255).optional().nullable().openapi({ description: '价格', example: '15.68' }),
-  code: z.string().max(255).optional().nullable().openapi({ description: '代码', example: '001339' }),
-  status: z.nativeEnum(ClassroomStatus).optional().nullable().openapi({ description: '状态', example: ClassroomStatus.OPEN }),
-  spare: z.string().max(255).optional().nullable().openapi({ description: '备用', example: '' })
-});
+}

+ 153 - 0
src/server/modules/classroom/classroom-data.schema.ts

@@ -0,0 +1,153 @@
+import { z } from '@hono/zod-openapi';
+
+// 教室状态枚举
+export enum ClassroomStatus {
+  CLOSED = 0,  // 关闭
+  OPEN = 1     // 开放
+}
+
+// 教室状态中文映射
+export const ClassroomStatusNameMap: Record<ClassroomStatus, string> = {
+  [ClassroomStatus.CLOSED]: '关闭',
+  [ClassroomStatus.OPEN]: '开放'
+};
+
+// 实体完整Schema
+export const ClassroomDataSchema = z.object({
+  id: z.number().int('必须是整数').positive('必须是正整数').openapi({
+    description: '教室数据ID',
+    example: 1
+  }),
+  classroomNo: z.string().max(255, '教室号最多255个字符').nullable().openapi({
+    description: '教室号',
+    example: 'A101'
+  }),
+  trainingDate: z.coerce.date<Date>().nullable().openapi({
+    description: '训练日期',
+    example: '2024-01-15T08:00:00Z'
+  }),
+  holdingStock: z.string().max(255, '持股信息最多255个字符').nullable().openapi({
+    description: '持股',
+    example: 'AAPL,100股'
+  }),
+  holdingCash: z.string().max(255, '持币信息最多255个字符').nullable().openapi({
+    description: '持币',
+    example: 'USD,5000'
+  }),
+  price: z.string().max(255, '价格信息最多255个字符').nullable().openapi({
+    description: '价格',
+    example: '150.25'
+  }),
+  code: z.string().max(255, '代码最多255个字符').nullable().openapi({
+    description: '代码',
+    example: 'AAPL'
+  }),
+  status: z.enum(ClassroomStatus).nullable().openapi({
+    description: '状态 (0关闭 1开放)',
+    example: ClassroomStatus.OPEN
+  }),
+  spare: z.string().max(255, '备用信息最多255个字符').nullable().openapi({
+    description: '备用',
+    example: '备用信息'
+  }),
+  submitUser: z.string().max(255, '提交用户最多255个字符').nullable().openapi({
+    description: '提交用户',
+    example: 'user123'
+  }),
+  createdBy: z.coerce.number<number>().int('创建用户ID必须是整数').positive('创建用户ID必须是正整数').nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.coerce.number<number>().int('更新用户ID必须是整数').positive('更新用户ID必须是正整数').nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  }),
+  createdAt: z.coerce.date<Date>().openapi({
+    description: '创建时间',
+    example: '2024-01-15T08:00:00Z'
+  }),
+  updatedAt: z.coerce.date<Date>().openapi({
+    description: '更新时间',
+    example: '2024-01-15T08:00:00Z'
+  })
+});
+
+// 创建教室数据DTO
+export const CreateClassroomDataDto = z.object({
+  classroomNo: z.string().max(255, '教室号最多255个字符').nullable().optional().openapi({
+    description: '教室号',
+    example: 'A101'
+  }),
+  trainingDate: z.coerce.date<Date>().nullable().optional().openapi({
+    description: '训练日期',
+    example: '2024-01-15T08:00:00Z'
+  }),
+  holdingStock: z.string().max(255, '持股信息最多255个字符').nullable().optional().openapi({
+    description: '持股',
+    example: 'AAPL,100股'
+  }),
+  holdingCash: z.string().max(255, '持币信息最多255个字符').nullable().optional().openapi({
+    description: '持币',
+    example: 'USD,5000'
+  }),
+  price: z.string().max(255, '价格信息最多255个字符').nullable().optional().openapi({
+    description: '价格',
+    example: '150.25'
+  }),
+  code: z.string().max(255, '代码最多255个字符').nullable().optional().openapi({
+    description: '代码',
+    example: 'AAPL'
+  }),
+  status: z.enum(ClassroomStatus).nullable().optional().openapi({
+    description: '状态 (0关闭 1开放)',
+    example: ClassroomStatus.OPEN
+  }),
+  spare: z.string().max(255, '备用信息最多255个字符').nullable().optional().openapi({
+    description: '备用',
+    example: '备用信息'
+  }),
+  submitUser: z.string().max(255, '提交用户最多255个字符').nullable().optional().openapi({
+    description: '提交用户',
+    example: 'user123'
+  })
+});
+
+// 更新教室数据DTO
+export const UpdateClassroomDataDto = z.object({
+  classroomNo: z.string().max(255, '教室号最多255个字符').nullable().optional().openapi({
+    description: '教室号',
+    example: 'A101'
+  }),
+  trainingDate: z.coerce.date<Date>().nullable().optional().openapi({
+    description: '训练日期',
+    example: '2024-01-15T08:00:00Z'
+  }),
+  holdingStock: z.string().max(255, '持股信息最多255个字符').nullable().optional().openapi({
+    description: '持股',
+    example: 'AAPL,100股'
+  }),
+  holdingCash: z.string().max(255, '持币信息最多255个字符').nullable().optional().openapi({
+    description: '持币',
+    example: 'USD,5000'
+  }),
+  price: z.string().max(255, '价格信息最多255个字符').nullable().optional().openapi({
+    description: '价格',
+    example: '150.25'
+  }),
+  code: z.string().max(255, '代码最多255个字符').nullable().optional().openapi({
+    description: '代码',
+    example: 'AAPL'
+  }),
+  status: z.enum(ClassroomStatus).nullable().optional().openapi({
+    description: '状态 (0关闭 1开放)',
+    example: ClassroomStatus.OPEN
+  }),
+  spare: z.string().max(255, '备用信息最多255个字符').nullable().optional().openapi({
+    description: '备用',
+    example: '备用信息'
+  }),
+  submitUser: z.string().max(255, '提交用户最多255个字符').nullable().optional().openapi({
+    description: '提交用户',
+    example: 'user123'
+  })
+});