Sfoglia il codice sorgente

✨ feat(routes): 添加车型枚举类型以增强类型安全

- 在route.schema.ts中定义VehicleType枚举,包含大巴(BUS)、中巴(MINIBUS)和小车(CAR)
- 将所有硬编码的车型字符串替换为枚举值,提高代码可维护性
- 更新表单组件中的车型默认值和选择项,使用枚举类型确保一致性
- 优化集成测试用例,使用枚举值替代字符串字面量
- 增强车型字段的验证逻辑,确保只能输入有效的车型类型
yourname 4 mesi fa
parent
commit
1194e59ae9

+ 6 - 6
src/client/admin/components/RouteForm.tsx

@@ -8,7 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
 import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
 import { MapPin, DollarSign, Users, Car } from 'lucide-react';
 import { format } from 'date-fns';
-import { createRouteSchema, updateRouteSchema } from '@/server/modules/routes/route.schema';
+import { createRouteSchema, updateRouteSchema, VehicleType } from '@/server/modules/routes/route.schema';
 import type { CreateRouteInput, UpdateRouteInput } from '@/server/modules/routes/route.schema';
 import { ActivitySelect } from './ActivitySelect';
 
@@ -44,7 +44,7 @@ export const RouteForm: React.FC<RouteFormProps> = ({
       pickupPoint: initialData.pickupPoint || '',
       dropoffPoint: initialData.dropoffPoint || '',
       departureTime: initialData.departureTime ? formatDateTimeForInput(initialData.departureTime) : '',
-      vehicleType: initialData.vehicleType || 'bus',
+      vehicleType: initialData.vehicleType as VehicleType || VehicleType.BUS,
       price: initialData.price || 0,
       seatCount: initialData.seatCount || 1,
       availableSeats: initialData.availableSeats || 1,
@@ -58,7 +58,7 @@ export const RouteForm: React.FC<RouteFormProps> = ({
       pickupPoint: '',
       dropoffPoint: '',
       departureTime: '',
-      vehicleType: 'bus',
+      vehicleType: VehicleType.BUS,
       price: 0,
       seatCount: 1,
       availableSeats: 1,
@@ -114,19 +114,19 @@ export const RouteForm: React.FC<RouteFormProps> = ({
                     </SelectTrigger>
                   </FormControl>
                   <SelectContent>
-                    <SelectItem value="bus">
+                    <SelectItem value={VehicleType.BUS}>
                       <div className="flex items-center gap-2">
                         <Car className="h-4 w-4 text-blue-500" />
                         <span>大巴</span>
                       </div>
                     </SelectItem>
-                    <SelectItem value="van">
+                    <SelectItem value={VehicleType.MINIBUS}>
                       <div className="flex items-center gap-2">
                         <Car className="h-4 w-4 text-green-500" />
                         <span>中巴</span>
                       </div>
                     </SelectItem>
-                    <SelectItem value="car">
+                    <SelectItem value={VehicleType.CAR}>
                       <div className="flex items-center gap-2">
                         <Car className="h-4 w-4 text-orange-500" />
                         <span>小车</span>

+ 19 - 4
src/server/modules/routes/route.schema.ts

@@ -2,6 +2,13 @@ import { z } from 'zod';
 import { DisabledStatus } from '@/share/types';
 import { ActivityType } from '../activities/activity.entity';
 
+// 车型枚举
+export enum VehicleType {
+  BUS = 'bus',        // 大巴
+  MINIBUS = 'minibus', // 中巴
+  CAR = 'car'         // 小车
+}
+
 // 路线创建Schema
 export const createRouteSchema = z.object({
   name: z.string().min(1, '路线名称不能为空').max(255, '路线名称不能超过255个字符'),
@@ -11,7 +18,9 @@ export const createRouteSchema = z.object({
   pickupPoint: z.string().min(1, '上车点不能为空').max(255, '上车点不能超过255个字符'),
   dropoffPoint: z.string().min(1, '下车点不能为空').max(255, '下车点不能超过255个字符'),
   departureTime: z.string().datetime('出发时间格式不正确'),
-  vehicleType: z.string().min(1, '车型不能为空').max(50, '车型不能超过50个字符'),
+  vehicleType: z.nativeEnum(VehicleType, {
+    message: '车型必须是有效的类型(bus/minibus/car)'
+  }),
   price: z.number().min(0, '价格不能为负数').max(99999999.99, '价格不能超过99999999.99'),
   seatCount: z.number().int().min(1, '座位数至少为1').max(1000, '座位数不能超过1000'),
   availableSeats: z.number().int().min(0, '可用座位数不能为负数').max(1000, '可用座位数不能超过1000'),
@@ -27,7 +36,9 @@ export const updateRouteSchema = z.object({
   pickupPoint: z.string().min(1, '上车点不能为空').max(255, '上车点不能超过255个字符').optional(),
   dropoffPoint: z.string().min(1, '下车点不能为空').max(255, '下车点不能超过255个字符').optional(),
   departureTime: z.string().datetime('出发时间格式不正确').optional(),
-  vehicleType: z.string().min(1, '车型不能为空').max(50, '车型不能超过50个字符').optional(),
+  vehicleType: z.nativeEnum(VehicleType, {
+    message: '车型必须是有效的类型(bus/minibus/car)'
+  }).optional(),
   price: z.number().min(0, '价格不能为负数').max(99999999.99, '价格不能超过99999999.99').optional(),
   seatCount: z.number().int().min(1, '座位数至少为1').max(1000, '座位数不能超过1000').optional(),
   availableSeats: z.number().int().min(0, '可用座位数不能为负数').max(1000, '可用座位数不能超过1000').optional(),
@@ -45,7 +56,9 @@ export const getRouteSchema = z.object({
   pickupPoint: z.string().min(1, '上车点不能为空').max(255, '上车点不能超过255个字符'),
   dropoffPoint: z.string().min(1, '下车点不能为空').max(255, '下车点不能超过255个字符'),
   departureTime: z.coerce.date(),
-  vehicleType: z.string().min(1, '车型不能为空').max(50, '车型不能超过50个字符'),
+  vehicleType: z.nativeEnum(VehicleType, {
+    message: '车型必须是有效的类型(bus/minibus/car)'
+  }),
   price: z.coerce.number().min(0, '价格不能为负数').max(99999999.99, '价格不能超过99999999.99'),
   seatCount: z.number().int().min(1, '座位数至少为1').max(1000, '座位数不能超过1000'),
   availableSeats: z.number().int().min(0, '可用座位数不能为负数').max(1000, '可用座位数不能超过1000'),
@@ -81,7 +94,9 @@ export const routeListResponseSchema = z.object({
   pickupPoint: z.string().min(1, '上车点不能为空').max(255, '上车点不能超过255个字符'),
   dropoffPoint: z.string().min(1, '下车点不能为空').max(255, '下车点不能超过255个字符'),
   departureTime: z.coerce.date(),
-  vehicleType: z.string().min(1, '车型不能为空').max(50, '车型不能超过50个字符'),
+  vehicleType: z.nativeEnum(VehicleType, {
+    message: '车型必须是有效的类型(bus/minibus/car)'
+  }),
   price: z.coerce.number().min(0, '价格不能为负数').max(99999999.99, '价格不能超过99999999.99'),
   seatCount: z.number().int().min(1, '座位数至少为1').max(1000, '座位数不能超过1000'),
   availableSeats: z.number().int().min(0, '可用座位数不能为负数').max(1000, '可用座位数不能超过1000'),

+ 10 - 9
tests/integration/server/admin/routes.integration.test.ts

@@ -9,6 +9,7 @@ import { IntegrationTestAssertions } from '~/utils/server/integration-test-utils
 import { adminRoutesRoutesExport } from '@/server/api';
 import { AuthService } from '@/server/modules/auth/auth.service';
 import { UserService } from '@/server/modules/users/user.service';
+import { VehicleType } from '@/server/modules/routes/route.schema';
 
 // 设置集成测试钩子
 setupIntegrationDatabaseHooks()
@@ -49,7 +50,7 @@ describe('路线管理API集成测试', () => {
         pickupPoint: '北京西站',
         dropoffPoint: '上海南站',
         departureTime: '2025-10-17T08:00:00.000Z',
-        vehicleType: 'bus',
+        vehicleType: VehicleType.BUS,
         price: 200,
         seatCount: 40,
         availableSeats: 40,
@@ -99,7 +100,7 @@ describe('路线管理API集成测试', () => {
         pickupPoint: '北京西站',
         dropoffPoint: '上海南站',
         departureTime: '2025-10-17T08:00:00.000Z',
-        vehicleType: 'invalid_vehicle' as any, // 无效车型
+        vehicleType: 'invalid_vehicle' as any, // 无效车型,应该验证失败
         price: 200,
         seatCount: 40,
         availableSeats: 40,
@@ -132,7 +133,7 @@ describe('路线管理API集成测试', () => {
         pickupPoint: '北京西站',
         dropoffPoint: '上海南站',
         departureTime: '2025-10-17T08:00:00.000Z',
-        vehicleType: 'bus',
+        vehicleType: VehicleType.BUS,
         price: -100, // 负数价格
         seatCount: 40,
         availableSeats: 40,
@@ -159,8 +160,8 @@ describe('路线管理API集成测试', () => {
       if (!dataSource) throw new Error('Database not initialized');
 
       // 创建几个测试路线
-      await TestDataFactory.createTestRoute(dataSource, { name: '路线1', vehicleType: 'bus' });
-      await TestDataFactory.createTestRoute(dataSource, { name: '路线2', vehicleType: 'van' });
+      await TestDataFactory.createTestRoute(dataSource, { name: '路线1', vehicleType: VehicleType.BUS });
+      await TestDataFactory.createTestRoute(dataSource, { name: '路线2', vehicleType: VehicleType.MINIBUS });
 
       const response = await client.routes.$get({
         query: {}
@@ -253,7 +254,7 @@ describe('路线管理API集成测试', () => {
         const responseData = await response.json();
         expect(responseData.name).toBe(updateData.name);
         expect(responseData.startPoint).toBe(updateData.startPoint);
-        expect(responseData.price).toBe(updateData.price);
+        expect(parseFloat(responseData.price as unknown as string)).toBe(updateData.price);
       }
 
       // 验证数据库中的更新
@@ -455,9 +456,9 @@ describe('路线管理API集成测试', () => {
       const dataSource = await IntegrationTestDatabase.getDataSource();
       if (!dataSource) throw new Error('Database not initialized');
 
-      await TestDataFactory.createTestRoute(dataSource, { name: '大巴路线1', vehicleType: 'bus' });
-      await TestDataFactory.createTestRoute(dataSource, { name: '大巴路线2', vehicleType: 'bus' });
-      await TestDataFactory.createTestRoute(dataSource, { name: '中巴路线', vehicleType: 'van' });
+      await TestDataFactory.createTestRoute(dataSource, { name: '大巴路线1', vehicleType: VehicleType.BUS });
+      await TestDataFactory.createTestRoute(dataSource, { name: '大巴路线2', vehicleType: VehicleType.BUS });
+      await TestDataFactory.createTestRoute(dataSource, { name: '中巴路线', vehicleType: VehicleType.MINIBUS });
 
       const response = await client.routes.$get({
         query: { filters: JSON.stringify({ vehicleType: 'bus' }) }

+ 2 - 1
tests/utils/server/integration-test-db.ts

@@ -4,6 +4,7 @@ import { UserEntity } from '@/server/modules/users/user.entity';
 import { Role } from '@/server/modules/users/role.entity';
 import { ActivityEntity, ActivityType } from '@/server/modules/activities/activity.entity';
 import { RouteEntity } from '@/server/modules/routes/route.entity';
+import { VehicleType } from '@/server/modules/routes/route.schema';
 import { AppDataSource } from '@/server/data-source';
 
 /**
@@ -121,7 +122,7 @@ export class TestDataFactory {
       pickupPoint: `上车点_${timestamp}`,
       dropoffPoint: `下车点_${timestamp}`,
       departureTime: departureTime,
-      vehicleType: 'bus',
+      vehicleType: VehicleType.BUS,
       price: 100,
       seatCount: 40,
       availableSeats: 40,