|
@@ -1,5 +1,8 @@
|
|
|
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
|
|
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
|
|
|
import { z } from '@hono/zod-openapi';
|
|
import { z } from '@hono/zod-openapi';
|
|
|
|
|
+import { AppDataSource } from '@/server/data-source';
|
|
|
|
|
+import { RouteService } from '@/server/modules/routes/route.service';
|
|
|
|
|
+import { VehicleType } from '@/server/modules/routes/route.schema';
|
|
|
|
|
|
|
|
// 路线搜索参数Schema
|
|
// 路线搜索参数Schema
|
|
|
const searchRoutesSchema = z.object({
|
|
const searchRoutesSchema = z.object({
|
|
@@ -11,12 +14,20 @@ const searchRoutesSchema = z.object({
|
|
|
example: 2,
|
|
example: 2,
|
|
|
description: '目的地ID'
|
|
description: '目的地ID'
|
|
|
}),
|
|
}),
|
|
|
|
|
+ startAreaIds: z.string().optional().openapi({
|
|
|
|
|
+ example: '[1,34,1001]',
|
|
|
|
|
+ description: '出发地区ID数组 [provinceId,cityId,districtId]'
|
|
|
|
|
+ }),
|
|
|
|
|
+ endAreaIds: z.string().optional().openapi({
|
|
|
|
|
+ example: '[2,35,1002]',
|
|
|
|
|
+ description: '目的地区ID数组 [provinceId,cityId,districtId]'
|
|
|
|
|
+ }),
|
|
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, '日期格式必须为YYYY-MM-DD').optional().openapi({
|
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, '日期格式必须为YYYY-MM-DD').optional().openapi({
|
|
|
example: '2025-10-18',
|
|
example: '2025-10-18',
|
|
|
description: '出发日期'
|
|
description: '出发日期'
|
|
|
}),
|
|
}),
|
|
|
- routeType: z.enum(['去程', '返程']).optional().openapi({
|
|
|
|
|
- example: '去程',
|
|
|
|
|
|
|
+ routeType: z.enum(['departure', 'return']).optional().openapi({
|
|
|
|
|
+ example: 'departure',
|
|
|
description: '路线类型'
|
|
description: '路线类型'
|
|
|
}),
|
|
}),
|
|
|
minPrice: z.coerce.number().int().min(0, '最低价格不能为负数').optional().openapi({
|
|
minPrice: z.coerce.number().int().min(0, '最低价格不能为负数').optional().openapi({
|
|
@@ -51,41 +62,69 @@ const routeSearchResultSchema = z.object({
|
|
|
data: z.object({
|
|
data: z.object({
|
|
|
routes: z.array(z.object({
|
|
routes: z.array(z.object({
|
|
|
id: z.number(),
|
|
id: z.number(),
|
|
|
|
|
+ name: z.string(),
|
|
|
|
|
+ description: z.string().nullable(),
|
|
|
|
|
+ startLocationId: z.number(),
|
|
|
|
|
+ endLocationId: z.number(),
|
|
|
startLocation: z.object({
|
|
startLocation: z.object({
|
|
|
id: z.number(),
|
|
id: z.number(),
|
|
|
name: z.string(),
|
|
name: z.string(),
|
|
|
|
|
+ provinceId: z.number(),
|
|
|
|
|
+ cityId: z.number(),
|
|
|
|
|
+ districtId: z.number(),
|
|
|
address: z.string()
|
|
address: z.string()
|
|
|
}),
|
|
}),
|
|
|
endLocation: z.object({
|
|
endLocation: z.object({
|
|
|
id: z.number(),
|
|
id: z.number(),
|
|
|
name: z.string(),
|
|
name: z.string(),
|
|
|
|
|
+ provinceId: z.number(),
|
|
|
|
|
+ cityId: z.number(),
|
|
|
|
|
+ districtId: z.number(),
|
|
|
address: z.string()
|
|
address: z.string()
|
|
|
}),
|
|
}),
|
|
|
|
|
+ pickupPoint: z.string(),
|
|
|
|
|
+ dropoffPoint: z.string(),
|
|
|
departureTime: z.string(),
|
|
departureTime: z.string(),
|
|
|
- vehicleType: z.string(),
|
|
|
|
|
|
|
+ vehicleType: z.nativeEnum(VehicleType),
|
|
|
price: z.number(),
|
|
price: z.number(),
|
|
|
seatCount: z.number(),
|
|
seatCount: z.number(),
|
|
|
- routeType: z.string(),
|
|
|
|
|
|
|
+ availableSeats: z.number(),
|
|
|
|
|
+ activityId: z.number(),
|
|
|
activity: z.object({
|
|
activity: z.object({
|
|
|
id: z.number(),
|
|
id: z.number(),
|
|
|
name: z.string(),
|
|
name: z.string(),
|
|
|
- description: z.string(),
|
|
|
|
|
|
|
+ description: z.string().nullable(),
|
|
|
|
|
+ venueLocationId: z.number(),
|
|
|
venueLocation: z.object({
|
|
venueLocation: z.object({
|
|
|
id: z.number(),
|
|
id: z.number(),
|
|
|
name: z.string(),
|
|
name: z.string(),
|
|
|
|
|
+ provinceId: z.number(),
|
|
|
|
|
+ cityId: z.number(),
|
|
|
|
|
+ districtId: z.number(),
|
|
|
address: z.string()
|
|
address: z.string()
|
|
|
}),
|
|
}),
|
|
|
startDate: z.string(),
|
|
startDate: z.string(),
|
|
|
endDate: z.string()
|
|
endDate: z.string()
|
|
|
- })
|
|
|
|
|
|
|
+ }),
|
|
|
|
|
+ routeType: z.enum(['departure', 'return']),
|
|
|
|
|
+ isDisabled: z.number(),
|
|
|
|
|
+ isDeleted: z.number(),
|
|
|
|
|
+ createdBy: z.number().nullable(),
|
|
|
|
|
+ updatedBy: z.number().nullable(),
|
|
|
|
|
+ createdAt: z.string(),
|
|
|
|
|
+ updatedAt: z.string()
|
|
|
})),
|
|
})),
|
|
|
activities: z.array(z.object({
|
|
activities: z.array(z.object({
|
|
|
id: z.number(),
|
|
id: z.number(),
|
|
|
name: z.string(),
|
|
name: z.string(),
|
|
|
- description: z.string(),
|
|
|
|
|
|
|
+ description: z.string().nullable(),
|
|
|
|
|
+ venueLocationId: z.number(),
|
|
|
venueLocation: z.object({
|
|
venueLocation: z.object({
|
|
|
id: z.number(),
|
|
id: z.number(),
|
|
|
name: z.string(),
|
|
name: z.string(),
|
|
|
|
|
+ provinceId: z.number(),
|
|
|
|
|
+ cityId: z.number(),
|
|
|
|
|
+ districtId: z.number(),
|
|
|
address: z.string()
|
|
address: z.string()
|
|
|
}),
|
|
}),
|
|
|
startDate: z.string(),
|
|
startDate: z.string(),
|
|
@@ -142,6 +181,8 @@ const app = new OpenAPIHono()
|
|
|
const {
|
|
const {
|
|
|
startLocationId,
|
|
startLocationId,
|
|
|
endLocationId,
|
|
endLocationId,
|
|
|
|
|
+ startAreaIds,
|
|
|
|
|
+ endAreaIds,
|
|
|
routeType,
|
|
routeType,
|
|
|
minPrice,
|
|
minPrice,
|
|
|
maxPrice,
|
|
maxPrice,
|
|
@@ -151,102 +192,34 @@ const app = new OpenAPIHono()
|
|
|
pageSize
|
|
pageSize
|
|
|
} = c.req.valid('query');
|
|
} = c.req.valid('query');
|
|
|
|
|
|
|
|
- // 这里需要调用路线服务进行查询
|
|
|
|
|
- // 由于路线服务尚未实现,这里返回模拟数据
|
|
|
|
|
- const mockRoutes = [
|
|
|
|
|
- {
|
|
|
|
|
- id: 1,
|
|
|
|
|
- startLocation: { id: 1, name: '北京南站', address: '北京市丰台区北京南站' },
|
|
|
|
|
- endLocation: { id: 2, name: '上海虹桥站', address: '上海市闵行区虹桥站' },
|
|
|
|
|
- departureTime: '2025-10-18T08:00:00Z',
|
|
|
|
|
- vehicleType: '大巴',
|
|
|
|
|
- price: 200,
|
|
|
|
|
- seatCount: 40,
|
|
|
|
|
- routeType: '去程',
|
|
|
|
|
- activity: {
|
|
|
|
|
- id: 1,
|
|
|
|
|
- name: '北京到上海商务会议',
|
|
|
|
|
- description: '前往上海参加商务会议',
|
|
|
|
|
- venueLocation: { id: 2, name: '上海虹桥站', address: '上海市闵行区虹桥站' },
|
|
|
|
|
- startDate: '2025-10-18T09:00:00Z',
|
|
|
|
|
- endDate: '2025-10-20T18:00:00Z'
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 2,
|
|
|
|
|
- startLocation: { id: 2, name: '上海虹桥站', address: '上海市闵行区虹桥站' },
|
|
|
|
|
- endLocation: { id: 1, name: '北京南站', address: '北京市丰台区北京南站' },
|
|
|
|
|
- departureTime: '2025-10-20T18:00:00Z',
|
|
|
|
|
- vehicleType: '大巴',
|
|
|
|
|
- price: 200,
|
|
|
|
|
- seatCount: 40,
|
|
|
|
|
- routeType: '返程',
|
|
|
|
|
- activity: {
|
|
|
|
|
- id: 1,
|
|
|
|
|
- name: '北京到上海商务会议',
|
|
|
|
|
- description: '前往上海参加商务会议',
|
|
|
|
|
- venueLocation: { id: 2, name: '上海虹桥站', address: '上海市闵行区虹桥站' },
|
|
|
|
|
- startDate: '2025-10-18T09:00:00Z',
|
|
|
|
|
- endDate: '2025-10-20T18:00:00Z'
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- ];
|
|
|
|
|
-
|
|
|
|
|
- // 简单的筛选逻辑(在实际实现中应该在数据库层面进行)
|
|
|
|
|
- let filteredRoutes = mockRoutes;
|
|
|
|
|
|
|
+ // 初始化路线服务
|
|
|
|
|
+ const routeService = new RouteService(AppDataSource);
|
|
|
|
|
|
|
|
- if (startLocationId) {
|
|
|
|
|
- filteredRoutes = filteredRoutes.filter(route => route.startLocation.id === startLocationId);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (endLocationId) {
|
|
|
|
|
- filteredRoutes = filteredRoutes.filter(route => route.endLocation.id === endLocationId);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 解析省市区ID数组
|
|
|
|
|
+ const parsedStartAreaIds = startAreaIds ? JSON.parse(startAreaIds) : undefined;
|
|
|
|
|
+ const parsedEndAreaIds = endAreaIds ? JSON.parse(endAreaIds) : undefined;
|
|
|
|
|
|
|
|
- if (routeType) {
|
|
|
|
|
- filteredRoutes = filteredRoutes.filter(route => route.routeType === routeType);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (minPrice !== undefined) {
|
|
|
|
|
- filteredRoutes = filteredRoutes.filter(route => route.price >= minPrice);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (maxPrice !== undefined) {
|
|
|
|
|
- filteredRoutes = filteredRoutes.filter(route => route.price <= maxPrice);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 排序
|
|
|
|
|
- filteredRoutes.sort((a, b) => {
|
|
|
|
|
- if (sortBy === 'price') {
|
|
|
|
|
- return sortOrder === 'ASC' ? a.price - b.price : b.price - a.price;
|
|
|
|
|
- } else {
|
|
|
|
|
- return sortOrder === 'ASC'
|
|
|
|
|
- ? new Date(a.departureTime).getTime() - new Date(b.departureTime).getTime()
|
|
|
|
|
- : new Date(b.departureTime).getTime() - new Date(a.departureTime).getTime();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 调用路线服务进行查询
|
|
|
|
|
+ const result = await routeService.searchRoutes({
|
|
|
|
|
+ startLocationId,
|
|
|
|
|
+ endLocationId,
|
|
|
|
|
+ startAreaIds: parsedStartAreaIds,
|
|
|
|
|
+ endAreaIds: parsedEndAreaIds,
|
|
|
|
|
+ routeType: routeType as 'departure' | 'return',
|
|
|
|
|
+ minPrice,
|
|
|
|
|
+ maxPrice,
|
|
|
|
|
+ sortBy: sortBy as 'price' | 'departureTime',
|
|
|
|
|
+ sortOrder: sortOrder as 'ASC' | 'DESC',
|
|
|
|
|
+ page,
|
|
|
|
|
+ pageSize
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 分页
|
|
|
|
|
- const startIndex = (page - 1) * pageSize;
|
|
|
|
|
- const endIndex = startIndex + pageSize;
|
|
|
|
|
- const paginatedRoutes = filteredRoutes.slice(startIndex, endIndex);
|
|
|
|
|
-
|
|
|
|
|
- // 去重后的活动列表
|
|
|
|
|
- const uniqueActivities = Array.from(
|
|
|
|
|
- new Map(paginatedRoutes.map(route => [route.activity.id, route.activity])).values()
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
return c.json({
|
|
return c.json({
|
|
|
success: true,
|
|
success: true,
|
|
|
data: {
|
|
data: {
|
|
|
- routes: paginatedRoutes,
|
|
|
|
|
- activities: uniqueActivities,
|
|
|
|
|
- pagination: {
|
|
|
|
|
- page,
|
|
|
|
|
- pageSize,
|
|
|
|
|
- total: filteredRoutes.length,
|
|
|
|
|
- totalPages: Math.ceil(filteredRoutes.length / pageSize)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ routes: result.routes,
|
|
|
|
|
+ activities: result.activities,
|
|
|
|
|
+ pagination: result.pagination
|
|
|
},
|
|
},
|
|
|
message: '搜索路线成功'
|
|
message: '搜索路线成功'
|
|
|
}, 200);
|
|
}, 200);
|