ソースを参照

✨ feat(passengers): 实现乘客管理功能

- 创建用户端乘客API路由,包含列表、创建、详情、更新、删除和设置默认乘客功能
- 添加乘客管理API到主路由配置
- 更新需求文档,细化Taro小程序乘客管理页面开发任务
- 实现用户只能管理自己乘客的权限控制逻辑
- 支持按姓名、手机号、证件号搜索乘客功能
yourname 3 ヶ月 前
コミット
88d36eba71
3 ファイル変更406 行追加8 行削除
  1. 23 8
      docs/stories/005.006.story.md
  2. 3 0
      src/server/api.ts
  3. 380 0
      src/server/api/passengers/index.ts

+ 23 - 8
docs/stories/005.006.story.md

@@ -20,16 +20,31 @@ Approved
   - [ ] 使用通用CRUD规范创建用户端乘客API
   - [ ] 实现用户只能管理自己乘客的权限控制
   - [ ] 支持默认乘客设置功能
-- [ ] 创建Taro小程序乘客管理页面 (AC: 1, 2, 3, 4)
-  - [ ] 创建 `mini/src/pages/passengers/passengers.tsx` 页面组件
-  - [ ] 实现乘客列表显示
-  - [ ] 实现添加乘客表单
-  - [ ] 实现编辑乘客功能
-  - [ ] 实现删除乘客功能
+- [ ] 创建Taro小程序乘客管理主页面 (AC: 1, 2, 3, 4)
+  - [ ] 迁移 `mini-demo/pages/passenger-management/passenger-management` 页面到 `mini/src/pages/passengers/passengers.tsx`
+  - [ ] 遵循 [mini-demo迁移指导规范](docs/architecture/mini-demo-migration-guide.md) 进行技术栈转换
+  - [ ] 组件映射:原生小程序组件 → Taro组件
+  - [ ] 样式转换:WXSS → Tailwind CSS(精确样式保留)
+  - [ ] 事件处理:`bindtap` → `onClick`,`bindinput` → `onInput`
+  - [ ] 状态管理:小程序data → React useState
+  - [ ] 实现乘客列表显示(包含姓名、证件类型、手机号)
+  - [ ] 实现搜索功能(按姓名、手机号、证件号搜索)
+  - [ ] 实现模态框添加/编辑乘客功能
+  - [ ] 实现删除乘客功能(确认对话框)
   - [ ] 实现设置默认乘客功能
-- [ ] 迁移添加乘客页面 (AC: 1, 2, 3, 4)
+  - [ ] 集成真实的后端API替换模拟数据
+  - [ ] 保持原有样式和用户体验
+- [ ] 创建Taro小程序独立添加乘客页面 (AC: 1, 2, 3, 4)
   - [ ] 迁移 `mini-demo/pages/add-passenger/add-passenger` 页面到 `mini/src/pages/passengers/add-passenger.tsx`
-  - [ ] 适配Taro框架和React技术栈
+  - [ ] 遵循 [mini-demo迁移指导规范](docs/architecture/mini-demo-migration-guide.md) 进行技术栈转换
+  - [ ] 组件映射:原生小程序组件 → Taro组件
+  - [ ] 样式转换:WXSS → Tailwind CSS(精确样式保留)
+  - [ ] 事件处理:`bindtap` → `onClick`,`bindinput` → `onInput`
+  - [ ] 状态管理:小程序data → React useState
+  - [ ] 实现独立添加乘客表单
+  - [ ] 支持从不同来源页面跳转(订单页面、管理页面)
+  - [ ] 实现表单验证(姓名、证件类型、证件号码、手机号)
+  - [ ] 支持多种证件类型选择器
   - [ ] 集成真实的后端API
   - [ ] 保持原有样式和用户体验
 - [ ] 集成乘客页面到小程序路由 (AC: 1)

+ 3 - 0
src/server/api.ts

@@ -10,6 +10,7 @@ import { routesRoutes as adminRoutesRoutes } from './api/admin/routes'
 import areasRoutes from './api/admin/areas'
 import locationsRoutes from './api/admin/locations'
 import { passengersRoutes as adminPassengersRoutes } from './api/admin/passengers'
+import passengersRoutes from './api/passengers/index'
 import routesRoutes from './api/routes'
 import areasUserRoutes from './api/areas'
 import locationsUserRoutes from './api/locations'
@@ -122,6 +123,7 @@ export const adminRoutesRoutesExport = api.route('/api/v1/admin/routes', adminRo
 export const adminAreasRoutesExport = api.route('/api/v1/admin/areas', areasRoutes)
 export const adminLocationsRoutesExport = api.route('/api/v1/admin/locations', locationsRoutes)
 export const adminPassengersRoutesExport = api.route('/api/v1/admin/passengers', adminPassengersRoutes)
+export const passengersRoutesExport = api.route('/api/v1/passengers', passengersRoutes)
 export const routesRoutesExport = api.route('/api/v1/routes', routesRoutes)
 export const areasUserRoutesExport = api.route('/api/v1/areas', areasUserRoutes)
 export const locationsUserRoutesExport = api.route('/api/v1/locations', locationsUserRoutes)
@@ -135,6 +137,7 @@ export type AdminRoutesRoutes = typeof adminRoutesRoutesExport
 export type AdminAreasRoutes = typeof adminAreasRoutesExport
 export type AdminLocationsRoutes = typeof adminLocationsRoutesExport
 export type AdminPassengersRoutes = typeof adminPassengersRoutesExport
+export type PassengersRoutes = typeof passengersRoutesExport
 export type RoutesRoutes = typeof routesRoutesExport
 export type AreasUserRoutes = typeof areasUserRoutesExport
 export type LocationsUserRoutes = typeof locationsUserRoutesExport

+ 380 - 0
src/server/api/passengers/index.ts

@@ -0,0 +1,380 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { PassengerService } from '@/server/modules/passengers/passenger.service';
+import {
+  PassengerCreateSchema,
+  PassengerUpdateSchema,
+  PassengerResponseSchema,
+  PassengerListResponseSchema,
+  PassengerListParams
+} from '@/server/modules/passengers/passenger.schema';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AuthContext } from '@/server/types/context';
+
+// 创建用户端乘客API路由
+
+// 获取用户乘客列表路由
+const listRoute = createRoute({
+  method: 'get',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    query: z.object({
+      page: z.coerce.number<number>().int().positive().default(1).openapi({
+        example: 1,
+        description: '页码,从1开始'
+      }),
+      pageSize: z.coerce.number<number>().int().positive().default(20).openapi({
+        example: 20,
+        description: '每页数量'
+      }),
+      keyword: z.string().optional().openapi({
+        example: '搜索关键词',
+        description: '搜索关键词'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取乘客列表',
+      content: {
+        'application/json': {
+          schema: PassengerListResponseSchema
+        }
+      }
+    },
+    401: {
+      description: '未授权',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 创建乘客路由
+const createRouteDef = createRoute({
+  method: 'post',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: PassengerCreateSchema }
+      }
+    }
+  },
+  responses: {
+    201: {
+      description: '创建乘客成功',
+      content: { 'application/json': { schema: PassengerResponseSchema } }
+    },
+    400: {
+      description: '输入数据无效',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    401: {
+      description: '未授权',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 获取乘客详情路由
+const getRouteDef = createRoute({
+  method: 'get',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number<number>().openapi({
+        param: { name: 'id', in: 'path' },
+        example: 1,
+        description: '乘客ID'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '成功获取乘客详情',
+      content: { 'application/json': { schema: PassengerResponseSchema } }
+    },
+    401: {
+      description: '未授权',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '乘客不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 更新乘客路由
+const updateRouteDef = createRoute({
+  method: 'put',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number<number>().openapi({
+        param: { name: 'id', in: 'path' },
+        example: 1,
+        description: '乘客ID'
+      })
+    }),
+    body: {
+      content: {
+        'application/json': { schema: PassengerUpdateSchema }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '更新乘客成功',
+      content: { 'application/json': { schema: PassengerResponseSchema } }
+    },
+    400: {
+      description: '无效输入',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    401: {
+      description: '未授权',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '乘客不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 删除乘客路由
+const deleteRouteDef = createRoute({
+  method: 'delete',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number<number>().openapi({
+        param: { name: 'id', in: 'path' },
+        example: 1,
+        description: '乘客ID'
+      })
+    })
+  },
+  responses: {
+    204: { description: '删除乘客成功' },
+    401: {
+      description: '未授权',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '乘客不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 设置默认乘客路由
+const setDefaultRoute = createRoute({
+  method: 'post',
+  path: '/{id}/set-default',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.coerce.number<number>().openapi({
+        param: { name: 'id', in: 'path' },
+        example: 1,
+        description: '乘客ID'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '设置默认乘客成功',
+      content: { 'application/json': { schema: PassengerResponseSchema } }
+    },
+    401: {
+      description: '未授权',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '乘客不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 注册路由处理函数
+const passengerService = new PassengerService();
+
+const app = new OpenAPIHono<AuthContext>()
+  .openapi(listRoute, async (c) => {
+    try {
+      const user = c.get('user');
+      const query = c.req.valid('query');
+
+      // 用户只能查看自己的乘客
+      const params: PassengerListParams = {
+        ...query,
+        userId: user.id
+      };
+
+      const result = await passengerService.getPassengers(params);
+      return c.json(result, 200);
+    } catch (error) {
+      console.error('获取乘客列表失败:', error);
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '获取乘客列表失败'
+      }, 500);
+    }
+  })
+  .openapi(createRouteDef, async (c) => {
+    try {
+      const user = c.get('user');
+      const data = c.req.valid('json');
+
+      // 用户只能为自己创建乘客
+      const passengerData = {
+        ...data,
+        userId: user.id
+      };
+
+      const result = await passengerService.createPassenger(passengerData);
+      return c.json(result, 201);
+    } catch (error) {
+      console.error('创建乘客失败:', error);
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '创建乘客失败'
+      }, 500);
+    }
+  })
+  .openapi(getRouteDef, async (c) => {
+    try {
+      const user = c.get('user');
+      const { id } = c.req.valid('param');
+
+      const passenger = await passengerService.getPassengerById(id);
+
+      if (!passenger) {
+        return c.json({ code: 404, message: '乘客不存在' }, 404);
+      }
+
+      // 用户只能查看自己的乘客
+      if (passenger.userId !== user.id) {
+        return c.json({ code: 403, message: '无权访问该乘客信息' }, 403);
+      }
+
+      return c.json(passenger, 200);
+    } catch (error) {
+      console.error('获取乘客详情失败:', error);
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '获取乘客详情失败'
+      }, 500);
+    }
+  })
+  .openapi(updateRouteDef, async (c) => {
+    try {
+      const user = c.get('user');
+      const { id } = c.req.valid('param');
+      const data = c.req.valid('json');
+
+      // 先检查乘客是否存在且属于当前用户
+      const existingPassenger = await passengerService.getPassengerById(id);
+      if (!existingPassenger) {
+        return c.json({ code: 404, message: '乘客不存在' }, 404);
+      }
+
+      if (existingPassenger.userId !== user.id) {
+        return c.json({ code: 403, message: '无权修改该乘客信息' }, 403);
+      }
+
+      const result = await passengerService.updatePassenger(id, data);
+      return c.json(result, 200);
+    } catch (error) {
+      console.error('更新乘客失败:', error);
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '更新乘客失败'
+      }, 500);
+    }
+  })
+  .openapi(deleteRouteDef, async (c) => {
+    try {
+      const user = c.get('user');
+      const { id } = c.req.valid('param');
+
+      // 先检查乘客是否存在且属于当前用户
+      const existingPassenger = await passengerService.getPassengerById(id);
+      if (!existingPassenger) {
+        return c.json({ code: 404, message: '乘客不存在' }, 404);
+      }
+
+      if (existingPassenger.userId !== user.id) {
+        return c.json({ code: 403, message: '无权删除该乘客信息' }, 403);
+      }
+
+      await passengerService.deletePassenger(id);
+      return c.body(null, 204);
+    } catch (error) {
+      console.error('删除乘客失败:', error);
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '删除乘客失败'
+      }, 500);
+    }
+  })
+  .openapi(setDefaultRoute, async (c) => {
+    try {
+      const user = c.get('user');
+      const { id } = c.req.valid('param');
+
+      // 先检查乘客是否存在且属于当前用户
+      const existingPassenger = await passengerService.getPassengerById(id);
+      if (!existingPassenger) {
+        return c.json({ code: 404, message: '乘客不存在' }, 404);
+      }
+
+      if (existingPassenger.userId !== user.id) {
+        return c.json({ code: 403, message: '无权设置该乘客为默认' }, 403);
+      }
+
+      const result = await passengerService.setDefaultPassenger(user.id, id);
+      return c.json(result, 200);
+    } catch (error) {
+      console.error('设置默认乘客失败:', error);
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '设置默认乘客失败'
+      }, 500);
+    }
+  });
+
+export default app;