Prechádzať zdrojové kódy

📝 docs(architecture): 添加非通用CRUD路由规范文档

- 创建非通用CRUD路由规范文档,定义统计查询、层级数据查询、业务操作三种路由类型
- 提供完整的代码模板和Schema设计规范,包含Zod schema定义和错误处理标准
- 更新编码标准文档,添加非通用CRUD路由开发规范引用
- 完善订单管理故事文档,集成非通用CRUD路由规范要求

♻️ refactor(server): 优化订单管理模块结构和类型定义

- 修复订单实体类型定义,使用正确的实体引用(UserEntity、RouteEntity)
- 优化订单schema类型转换,使用z.coerce处理数字类型
- 修复订单统计路由初始化顺序,确保正确创建OpenAPI应用实例
- 更新数据源配置,添加订单实体注册

✨ feat(auth): 增强管理员角色管理功能

- 在AuthService中确保管理员用户拥有管理员角色
- 添加ensureAdminRole方法,自动创建和分配管理员角色
- 改进用户服务,支持角色管理和权限分配

✅ test(orders): 添加订单管理集成测试

- 创建完整的订单管理API集成测试套件
- 测试订单列表查询、详情获取、状态筛选功能
- 验证订单统计API的正确性和权限控制
- 覆盖未授权访问和权限拒绝场景
yourname 3 mesiacov pred
rodič
commit
075366f1c9

+ 8 - 0
docs/architecture/coding-standards.md

@@ -31,6 +31,14 @@
 - **路由生成**: 使用 `createCrudRoutes` 自动生成API路由
 - **用户跟踪**: 实现用户跟踪功能记录操作人信息
 
+## 非通用CRUD路由开发规范
+- **非通用路由开发**: 遵循 [非通用CRUD路由规范](./non-generic-crud-standards.md) 进行开发
+- **路由类型**: 统计查询、层级数据查询、业务操作等非标准CRUD场景
+- **Schema设计**: 使用Zod schema定义查询参数、路径参数和响应格式
+- **错误处理**: 统一错误响应格式,包含完整的错误状态码
+- **权限控制**: 复用现有中间件,实现资源所有权验证
+- **文档生成**: 自动生成OpenAPI文档,包含请求响应示例
+
 ## Taro小程序开发规范
 - **组件开发**: 遵循 [Taro小程序开发规范](./taro-mini-program-standards.md) 进行开发
 - **平台适配**: 正确处理微信小程序、H5等平台差异

+ 476 - 0
docs/architecture/non-generic-crud-standards.md

@@ -0,0 +1,476 @@
+# 非通用 CRUD 路由规范
+
+## 版本信息
+| 版本 | 日期 | 描述 | 作者 |
+|------|------|------|------|
+| 1.0 | 2025-10-23 | 创建非通用 CRUD 路由规范文档 | Winston |
+
+## 概述
+
+非通用 CRUD 路由是指那些不遵循标准 CRUD 操作模式的 API 端点,包括统计查询、业务操作、复杂查询等场景。这些路由需要特定的业务逻辑和自定义处理,无法通过通用 CRUD 服务自动生成。
+
+## 设计原则
+
+### 核心原则
+- **业务导向**: 路由设计应反映业务需求而非技术实现
+- **一致性**: 保持与通用 CRUD 相同的错误处理、认证和文档标准
+- **类型安全**: 使用 Zod schema 确保输入输出类型安全
+- **文档完整**: 自动生成 OpenAPI 文档,包含完整的请求响应示例
+
+### 架构模式
+- **单一职责**: 每个路由文件专注于特定业务功能
+- **服务分离**: 业务逻辑封装在服务层,路由层只负责 HTTP 处理
+- **中间件复用**: 复用现有的认证、授权中间件
+
+## 非通用 CRUD 路由类型
+
+### 1. 统计查询路由 (Stats Routes)
+
+**适用场景**: 获取聚合数据、统计信息、报表数据
+
+**示例文件**: `packages/server/src/api/admin/orders/stats.ts`
+
+**规范要求**:
+- 使用 `GET` 方法
+- 路径以 `/stats` 或 `/analytics` 结尾
+- 返回聚合数据而非分页列表
+- 包含完整的错误处理
+
+**代码模板**:
+```typescript
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { authMiddleware } from '../../../middleware/auth.middleware';
+import { adminMiddleware } from '../../../middleware/admin.middleware';
+import { OrderService } from '../../../modules/orders/order.service';
+import { OrderStatsSchema } from '../../../modules/orders/order.schema';
+import { ErrorSchema } from '../../../utils/errorHandler';
+
+// 统计路由定义
+const statsRoute = createRoute({
+  method: 'get',
+  path: '/',
+  middleware: [authMiddleware, adminMiddleware],
+  responses: {
+    200: {
+      description: '成功获取统计信息',
+      content: {
+        'application/json': {
+          schema: OrderStatsSchema
+        }
+      }
+    },
+    401: {
+      description: '未授权',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    403: {
+      description: '权限不足',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+const app = new OpenAPIHono().openapi(statsRoute, async (c) => {
+  try {
+    const orderService = new OrderService();
+    const stats = await orderService.getOrderStats();
+    return c.json(stats, 200);
+  } catch (error) {
+    console.error('获取统计信息失败:', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '获取统计信息失败'
+    }, 500);
+  }
+});
+
+export default app;
+```
+
+### 2. 层级数据查询路由 (Hierarchical Data Routes)
+
+**适用场景**: 树形结构数据、省市区数据、分类数据
+
+**示例文件**: `packages/server/src/api/areas/index.ts`
+
+**规范要求**:
+- 使用 `GET` 方法
+- 路径反映数据层级关系(如 `/provinces`, `/cities`, `/districts`)
+- 支持分页和父级ID筛选
+- 返回标准化的分页响应格式
+
+**代码模板**:
+```typescript
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AreaService } from '../../modules/areas/area.service';
+
+// 查询参数Schema
+const getCitiesSchema = z.object({
+  provinceId: z.coerce.number<number>().int().positive('省份ID必须为正整数').openapi({
+    example: 1,
+    description: '省份ID'
+  }),
+  page: z.coerce.number<number>().int().min(1).default(1).openapi({
+    example: 1,
+    description: '页码'
+  }),
+  pageSize: z.coerce.number<number>().int().min(1).max(100).default(50).openapi({
+    example: 50,
+    description: '每页数量'
+  })
+});
+
+// 响应Schema
+const citiesResponseSchema = z.object({
+  success: z.boolean(),
+  data: z.object({
+    cities: z.array(areaResponseSchema),
+    pagination: z.object({
+      page: z.number(),
+      pageSize: z.number(),
+      total: z.number(),
+      totalPages: z.number()
+    })
+  }),
+  message: z.string()
+});
+
+// 创建城市查询路由
+const getCitiesRoute = createRoute({
+  method: 'get',
+  path: '/cities',
+  request: {
+    query: getCitiesSchema
+  },
+  responses: {
+    200: {
+      description: '获取城市列表成功',
+      content: { 'application/json': { schema: citiesResponseSchema } }
+    },
+    400: {
+      description: '参数错误',
+      content: { 'application/json': { schema: errorSchema } }
+    },
+    500: {
+      description: '获取城市列表失败',
+      content: { 'application/json': { schema: errorSchema } }
+    }
+  }
+});
+
+const app = new OpenAPIHono()
+  .openapi(getCitiesRoute, async (c) => {
+    try {
+      const { provinceId, page, pageSize } = c.req.valid('query');
+      const areaService = new AreaService();
+
+      // 业务逻辑处理
+      const allCities = await areaService.getAreaTreeByLevel(AreaLevel.CITY);
+      const cities = allCities.filter(city => city.parentId === provinceId);
+
+      // 分页处理
+      const startIndex = (page - 1) * pageSize;
+      const endIndex = startIndex + pageSize;
+      const paginatedCities = cities.slice(startIndex, endIndex);
+
+      return c.json({
+        success: true,
+        data: {
+          cities: paginatedCities,
+          pagination: {
+            page,
+            pageSize,
+            total: cities.length,
+            totalPages: Math.ceil(cities.length / pageSize)
+          }
+        },
+        message: '获取城市列表成功'
+      }, 200);
+    } catch (error) {
+      console.error('获取城市列表失败:', error);
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '获取城市列表失败'
+      }, 500);
+    }
+  });
+
+export default app;
+```
+
+### 3. 业务操作路由 (Business Action Routes)
+
+**适用场景**: 设置默认项、状态变更、批量操作等业务动作
+
+**示例文件**: `packages/server/src/api/passengers/index.ts`
+
+**规范要求**:
+- 使用 `POST` 方法表示动作执行
+- 路径格式为 `/{id}/{action-name}`
+- 包含完整的权限验证
+- 返回操作结果
+
+**代码模板**:
+```typescript
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { authMiddleware } from '../../middleware/auth.middleware';
+import { PassengerService } from '../../modules/passengers/passenger.service';
+
+// 设置默认乘客路由
+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 } }
+    },
+    403: {
+      description: '无权设置',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '乘客不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>()
+  .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);
+      if (!result) {
+        return c.json({ code: 500, message: '设置默认乘客失败' }, 500);
+      }
+      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;
+```
+
+## Schema 设计规范
+
+### 1. 查询参数 Schema
+- 使用 `z.coerce` 处理类型转换
+- 为所有参数添加 `.openapi()` 配置,包含示例和描述
+- 设置合理的默认值和验证规则
+
+```typescript
+const querySchema = 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: '搜索关键词'
+  })
+});
+```
+
+### 2. 路径参数 Schema
+- 使用 `z.coerce` 处理类型转换
+- 包含 `param` 配置指定参数位置
+
+```typescript
+const paramsSchema = z.object({
+  id: z.coerce.number<number>().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '实体ID'
+  })
+});
+```
+
+### 3. 响应 Schema
+- 使用标准化的响应格式
+- 包含 `success`、`data`、`message` 字段
+- 为分页数据包含 `pagination` 信息
+
+```typescript
+const responseSchema = z.object({
+  success: z.boolean(),
+  data: z.object({
+    // 业务数据字段
+    items: z.array(itemSchema),
+    pagination: z.object({
+      page: z.number(),
+      pageSize: z.number(),
+      total: z.number(),
+      totalPages: z.number()
+    }).optional()
+  }),
+  message: z.string()
+});
+```
+
+## 错误处理规范
+
+### 1. 标准错误响应
+所有非通用 CRUD 路由必须使用统一的错误处理:
+
+```typescript
+return c.json({
+  code: 500,
+  message: error instanceof Error ? error.message : '操作失败'
+}, 500);
+```
+
+### 2. 常见错误状态码
+- `400`: 参数错误
+- `401`: 未授权
+- `403`: 权限不足
+- `404`: 资源不存在
+- `500`: 服务器错误
+
+### 3. 错误日志记录
+- 使用 `console.error` 记录详细错误信息
+- 包含操作上下文信息
+- 避免在生产环境暴露敏感信息
+
+## 权限控制规范
+
+### 1. 中间件使用
+- 复用现有的认证中间件 `authMiddleware`
+- 管理员权限使用 `adminMiddleware`
+- 自定义权限中间件应遵循相同模式
+
+### 2. 资源所有权验证
+- 验证用户对资源的访问权限
+- 在服务层或路由层实现权限检查
+- 返回明确的权限错误信息
+
+```typescript
+// 检查资源所有权
+if (resource.userId !== user.id) {
+  return c.json({ code: 403, message: '无权访问该资源' }, 403);
+}
+```
+
+## 最佳实践
+
+### 1. 文件组织
+- 每个非通用路由功能创建独立文件
+- 文件名反映功能用途(如 `stats.ts`, `actions.ts`)
+- 在模块的 `index.ts` 中统一导出
+
+### 2. 代码结构
+- 路由定义在前,处理函数在后
+- 使用 try-catch 包装所有业务逻辑
+- 保持错误处理的一致性
+
+### 3. 性能考虑
+- 为统计查询添加适当的缓存策略
+- 复杂查询使用数据库索引优化
+- 避免 N+1 查询问题
+
+### 4. 测试策略
+- 为每个非通用路由编写集成测试
+- 测试所有可能的错误场景
+- 验证权限控制和业务逻辑
+
+## 扩展和自定义
+
+### 1. 自定义中间件
+对于需要特殊权限验证的路由,可以创建自定义中间件:
+
+```typescript
+const ownerMiddleware = createMiddleware(async (c, next) => {
+  const user = c.get('user');
+  const { id } = c.req.param();
+
+  const resource = await resourceService.getById(id);
+  if (!resource || resource.userId !== user.id) {
+    return c.json({ code: 403, message: '无权访问' }, 403);
+  }
+
+  c.set('resource', resource);
+  await next();
+});
+```
+
+### 2. 批量操作路由
+对于批量操作,创建专门的批量处理路由:
+
+```typescript
+const bulkUpdateRoute = createRoute({
+  method: 'post',
+  path: '/bulk-update',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: BulkUpdateSchema }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '批量更新成功',
+      content: { 'application/json': { schema: BulkUpdateResponseSchema } }
+    }
+  }
+});
+```
+
+## 相关文档
+
+- [通用 CRUD 规范](./generic-crud-standards.md)
+- [API 设计规范](./api-design-integration.md)
+- [数据模型规范](./data-model-schema-changes.md)
+- [测试策略](./testing-strategy.md)
+
+---
+
+**文档状态**: 正式版
+**下次评审**: 2025-11-23

+ 41 - 0
docs/stories/005.007.story.md

@@ -87,6 +87,38 @@ export enum PaymentStatus {
 ### 通用CRUD规范要求
 基于 [docs/architecture/generic-crud-standards.md#使用指南],管理后台订单管理必须遵循通用CRUD规范,需要创建以下文件:
 
+### 非通用CRUD路由规范要求
+基于 [docs/architecture/non-generic-crud-standards.md#统计查询路由],订单状态统计API必须遵循非通用CRUD路由规范,需要创建以下文件:
+
+**统计查询路由规范** [Source: architecture/non-generic-crud-standards.md#统计查询路由]:
+- 使用 `GET` 方法,路径为 `/stats`
+- 返回聚合数据而非分页列表
+- 包含完整的错误处理和权限控制
+- 自动生成OpenAPI文档
+
+**统计路由代码模板** [Source: architecture/non-generic-crud-standards.md#代码模板]:
+```typescript
+// 统计路由定义
+const statsRoute = createRoute({
+  method: 'get',
+  path: '/',
+  middleware: [authMiddleware, adminMiddleware],
+  responses: {
+    200: {
+      description: '成功获取统计信息',
+      content: {
+        'application/json': {
+          schema: OrderStatsSchema
+        }
+      }
+    },
+    401: { description: '未授权' },
+    403: { description: '权限不足' },
+    500: { description: '服务器错误' }
+  }
+});
+```
+
 **实体设计** [Source: architecture/generic-crud-standards.md#实体设计]:
 - 使用TypeORM装饰器定义字段
 - 为所有字段添加 `comment` 配置,说明字段用途
@@ -171,6 +203,14 @@ export const orderRoutes = createCrudRoutes({
 - 创建、更新、响应使用不同的Zod schema
 - 使用 `createCrudRoutes` 自动生成API路由
 
+**非通用CRUD路由开发** [Source: architecture/coding-standards.md#非通用crud路由开发规范]:
+- 遵循非通用CRUD路由规范进行开发
+- 统计查询、层级数据查询、业务操作等非标准CRUD场景
+- 使用Zod schema定义查询参数、路径参数和响应格式
+- 统一错误响应格式,包含完整的错误状态码
+- 复用现有中间件,实现资源所有权验证
+- 自动生成OpenAPI文档,包含请求响应示例
+
 **管理后台开发** [Source: architecture/coding-standards.md#管理后台开发规范]:
 - 遵循管理后台开发规范进行开发
 - 统一页面结构和布局标准
@@ -209,6 +249,7 @@ export const orderRoutes = createCrudRoutes({
 ## Change Log
 | Date | Version | Description | Author |
 |------|---------|-------------|--------|
+| 2025-10-23 | 1.3 | 添加非通用CRUD路由规范引用,更新API开发标准 | Winston (Architect) |
 | 2025-10-23 | 1.2 | 更新故事状态为Approved,准备实施 | Sarah (PO) |
 | 2025-10-23 | 1.1 | 修正文件存在性错误,添加基础文件创建任务 | Sarah (PO) |
 | 2025-10-23 | 1.0 | 初始故事创建,基于史诗005 US005-07需求 | Bob (Scrum Master) |

+ 10 - 0
packages/server/package.json

@@ -11,6 +11,16 @@
       "require": "./src/index.ts",
       "types": "./src/index.ts"
     },
+    "./api": {
+      "import": "./src/index.ts",
+      "require": "./src/index.ts",
+      "types": "./src/index.ts"
+    },
+    "./data-source": {
+      "import": "./src/data-source.ts",
+      "require": "./src/data-source.ts",
+      "types": "./src/data-source.ts"
+    },
     "./modules/*": {
       "import": "./src/modules/*",
       "require": "./src/modules/*",

+ 5 - 5
packages/server/src/api/admin/orders/index.ts

@@ -1,14 +1,14 @@
-import { createCrudRoutes } from '../../../../utils/generic-crud.routes';
-import { authMiddleware } from '../../../../middleware/auth.middleware';
-import { adminMiddleware } from '../../../../middleware/admin.middleware';
-import { Order } from '../../../../modules/orders/order.entity';
+import { createCrudRoutes } from '../../../utils/generic-crud.routes';
+import { authMiddleware } from '../../../middleware/auth.middleware';
+import { adminMiddleware } from '../../../middleware/admin.middleware';
+import { Order } from '../../../modules/orders/order.entity';
 import {
   OrderCreateSchema,
   OrderUpdateSchema,
   OrderGetSchema,
   OrderResponseSchema,
   OrderListSchema
-} from '../../../../modules/orders/order.schema';
+} from '../../../modules/orders/order.schema';
 import { OpenAPIHono } from '@hono/zod-openapi';
 import statsRoutes from './stats';
 

+ 1 - 2
packages/server/src/api/admin/orders/stats.ts

@@ -5,7 +5,6 @@ import { OrderService } from '../../../modules/orders/order.service';
 import { OrderStatsSchema } from '../../../modules/orders/order.schema';
 import { ErrorSchema } from '../../../utils/errorHandler';
 
-const app = new OpenAPIHono();
 
 // 订单统计路由
 const statsRoute = createRoute({
@@ -36,7 +35,7 @@ const statsRoute = createRoute({
   }
 });
 
-app.openapi(statsRoute, async (c) => {
+const app = new OpenAPIHono().openapi(statsRoute, async (c) => {
   try {
     const orderService = new OrderService();
     const stats = await orderService.getOrderStats();

+ 3 - 2
packages/server/src/data-source.ts

@@ -11,6 +11,7 @@ import { RouteEntity } from "./modules/routes/route.entity"
 import { AreaEntity } from "./modules/areas/area.entity"
 import { LocationEntity } from "./modules/locations/location.entity"
 import { Passenger } from "./modules/passengers/passenger.entity"
+import { Order } from "./modules/orders/order.entity"
 
 // 在测试环境下使用测试数据库配置
 const isTestEnv = process.env.NODE_ENV === 'test';
@@ -21,7 +22,7 @@ const dataSource = isTestEnv && testDatabaseUrl
   ? new DataSource({
       type: "postgres",
       url: testDatabaseUrl,
-      entities: [User, Role, File, ActivityEntity, RouteEntity, AreaEntity, LocationEntity, Passenger],
+      entities: [User, Role, File, ActivityEntity, RouteEntity, AreaEntity, LocationEntity, Passenger, Order],
       migrations: [],
       synchronize: true, // 测试环境总是同步schema
       dropSchema: true,  // 测试环境每次重新创建schema
@@ -34,7 +35,7 @@ const dataSource = isTestEnv && testDatabaseUrl
       username: process.env.DB_USERNAME || "postgres",
       password: process.env.DB_PASSWORD || "",
       database: process.env.DB_DATABASE || "postgres",
-      entities: [User, Role, File, ActivityEntity, RouteEntity, AreaEntity, LocationEntity, Passenger],
+      entities: [User, Role, File, ActivityEntity, RouteEntity, AreaEntity, LocationEntity, Passenger, Order],
       migrations: [],
       synchronize: process.env.DB_SYNCHRONIZE !== "false",
       logging: process.env.DB_LOGGING === "true",

+ 3 - 0
packages/server/src/index.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 ordersRoutes from './api/admin/orders'
 import passengersRoutes from './api/passengers/index'
 import routesRoutes from './api/routes'
 import areasUserRoutes from './api/areas'
@@ -123,6 +124,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 adminOrdersRoutesExport = api.route('/api/v1/admin/orders', ordersRoutes)
 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)
@@ -137,6 +139,7 @@ export type AdminRoutesRoutes = typeof adminRoutesRoutesExport
 export type AdminAreasRoutes = typeof adminAreasRoutesExport
 export type AdminLocationsRoutes = typeof adminLocationsRoutesExport
 export type AdminPassengersRoutes = typeof adminPassengersRoutesExport
+export type AdminOrdersRoutes = typeof adminOrdersRoutesExport
 export type PassengersRoutes = typeof passengersRoutesExport
 export type RoutesRoutes = typeof routesRoutesExport
 export type AreasUserRoutes = typeof areasUserRoutesExport

+ 11 - 1
packages/server/src/modules/auth/auth.service.ts

@@ -30,9 +30,19 @@ export class AuthService {
           nickname: '系统管理员',
           isDisabled: DisabledStatus.ENABLED
         });
+
+        // 确保管理员角色存在并分配给用户
+        await this.userService.ensureAdminRole(admin);
+
         logger.info('Default admin account created successfully');
+      } else {
+        // 确保现有管理员用户有管理员角色
+        await this.userService.ensureAdminRole(admin);
       }
-      return admin;
+
+      // 重新加载用户以获取角色关系
+      admin = await this.userService.getUserByUsername(ADMIN_USERNAME);
+      return admin!;
     } catch (error) {
       logger.error('Failed to ensure admin account exists:', error);
       throw error;

+ 12 - 13
packages/server/src/modules/orders/order.entity.ts

@@ -1,7 +1,6 @@
 import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
-import { User } from '../users/user.entity';
-import { Route } from '../routes/route.entity';
-import { ObjectLiteral } from 'typeorm/common/ObjectLiteral';
+import { UserEntity } from '../users/user.entity';
+import { RouteEntity } from '../routes/route.entity';
 
 export enum OrderStatus {
   PENDING_PAYMENT = '待支付',
@@ -19,17 +18,17 @@ export enum PaymentStatus {
 }
 
 @Entity('orders')
-export class Order implements ObjectLiteral {
+export class Order {
   @PrimaryGeneratedColumn({ comment: '订单ID' })
   id!: number;
 
-  @Column({ comment: '用户ID' })
+  @Column({ type: 'int', unsigned: true, comment: '用户ID' })
   userId!: number;
 
-  @Column({ comment: '路线ID' })
+  @Column({ type: 'int', unsigned: true, comment: '路线ID' })
   routeId!: number;
 
-  @Column({ comment: '乘客数量' })
+  @Column({ type: 'int', unsigned: true, comment: '乘客数量' })
   passengerCount!: number;
 
   @Column('decimal', { precision: 10, scale: 2, comment: '订单总金额' })
@@ -57,10 +56,10 @@ export class Order implements ObjectLiteral {
   @Column('json', { comment: '路线信息快照(下单时的路线信息)' })
   routeSnapshot!: any;
 
-  @Column({ nullable: true, comment: '创建人ID' })
+  @Column({ type: 'int', unsigned: true, nullable: true, comment: '创建人ID' })
   createdBy?: number;
 
-  @Column({ nullable: true, comment: '更新人ID' })
+  @Column({ type: 'int', unsigned: true, nullable: true, comment: '更新人ID' })
   updatedBy?: number;
 
   @CreateDateColumn({ comment: '创建时间' })
@@ -69,11 +68,11 @@ export class Order implements ObjectLiteral {
   @UpdateDateColumn({ comment: '更新时间' })
   updatedAt!: Date;
 
-  @ManyToOne(() => User)
+  @ManyToOne(() => UserEntity)
   @JoinColumn({ name: 'userId' })
-  user!: User;
+  user!: UserEntity;
 
-  @ManyToOne(() => Route)
+  @ManyToOne(() => RouteEntity)
   @JoinColumn({ name: 'routeId' })
-  route!: Route;
+  route!: RouteEntity;
 }

+ 5 - 5
packages/server/src/modules/orders/order.schema.ts

@@ -39,24 +39,24 @@ export const OrderResponseSchema = z.object({
   userId: z.number(),
   routeId: z.number(),
   passengerCount: z.number(),
-  totalAmount: z.number(),
+  totalAmount: z.coerce.number(),
   status: z.nativeEnum(OrderStatus),
   paymentStatus: z.nativeEnum(PaymentStatus),
   passengerSnapshots: z.array(z.any()),
   routeSnapshot: z.any(),
-  createdBy: z.number().optional(),
-  updatedBy: z.number().optional(),
+  createdBy: z.number().nullable().optional(),
+  updatedBy: z.number().nullable().optional(),
   createdAt: z.date(),
   updatedAt: z.date(),
   user: z.object({
     id: z.number(),
     username: z.string(),
-    phone: z.string().optional()
+    phone: z.string().nullable().optional()
   }).optional(),
   route: z.object({
     id: z.number(),
     name: z.string(),
-    description: z.string().optional()
+    description: z.string().nullable().optional()
   }).optional()
 });
 

+ 35 - 0
packages/server/src/modules/users/user.service.ts

@@ -136,4 +136,39 @@ export class UserService {
       throw new Error('Failed to get user by account');
     }
   }
+
+  async ensureAdminRole(user: User): Promise<void> {
+    try {
+      // 检查管理员角色是否存在
+      let adminRole = await this.roleRepository.findOne({ where: { name: 'admin' } });
+
+      if (!adminRole) {
+        // 创建管理员角色
+        adminRole = this.roleRepository.create({
+          name: 'admin',
+          description: '系统管理员',
+          permissions: ['*'] // 所有权限
+        });
+        adminRole = await this.roleRepository.save(adminRole);
+      }
+
+      // 检查用户是否已经有管理员角色
+      const userWithRoles = await this.getUserById(user.id);
+      if (userWithRoles && userWithRoles.roles) {
+        const hasAdminRole = userWithRoles.roles.some(role => role.name === 'admin');
+        if (!hasAdminRole) {
+          // 分配管理员角色
+          userWithRoles.roles = [...userWithRoles.roles, adminRole];
+          await this.userRepository.save(userWithRoles);
+        }
+      } else {
+        // 用户没有角色,直接分配管理员角色
+        user.roles = [adminRole];
+        await this.userRepository.save(user);
+      }
+    } catch (error) {
+      console.error('Error ensuring admin role:', error);
+      throw new Error('Failed to ensure admin role');
+    }
+  }
 }

+ 299 - 0
web/tests/integration/server/admin/orders.integration.test.ts

@@ -0,0 +1,299 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import {
+  IntegrationTestDatabase,
+  setupIntegrationDatabaseHooks,
+  TestDataFactory
+} from '~/utils/server/integration-test-db';
+import { adminOrdersRoutesExport } from '@d8d/server/api';
+import { AuthService } from '@d8d/server/modules/auth/auth.service';
+import { UserService } from '@d8d/server/modules/users/user.service';
+import { OrderStatus, PaymentStatus } from '@d8d/server/modules/orders/order.entity';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooks()
+
+describe('订单管理API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof adminOrdersRoutesExport>>['api']['v1']['admin'];
+  let testToken: string;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(adminOrdersRoutesExport).api.v1.admin;
+
+    // 创建测试用户并生成token
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    const userService = new UserService(dataSource);
+    const authService = new AuthService(userService);
+
+    // 确保admin用户存在
+    const user = await authService.ensureAdminExists();
+
+    // 生成admin用户的token
+    testToken = authService.generateToken(user);
+  });
+
+  describe('订单列表查询测试', () => {
+    it('应该成功获取订单列表', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+
+      // 创建测试数据
+      const testUser = await TestDataFactory.createTestUser(dataSource);
+      const testRoute = await TestDataFactory.createTestRoute(dataSource);
+
+      // 创建测试订单
+      await dataSource.getRepository('Order').save({
+        userId: testUser.id,
+        routeId: testRoute.id,
+        passengerCount: 2,
+        totalAmount: 100.00,
+        status: OrderStatus.PENDING_PAYMENT,
+        paymentStatus: PaymentStatus.PENDING,
+        passengerSnapshots: [
+          { name: '乘客1', phone: '13800138001' },
+          { name: '乘客2', phone: '13800138002' }
+        ],
+        routeSnapshot: {
+          name: testRoute.name,
+          description: testRoute.description
+        }
+      });
+
+      const response = await client.orders.$get({
+        query: {
+          page: 1,
+          pageSize: 10
+        }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      if (response.status !== 200) {
+        const error = await response.json();
+        console.debug('Error response:', JSON.stringify(error, null, 2));
+      }
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const result = await response.json();
+        expect(Array.isArray(result.data)).toBe(true);
+        expect(result.pagination).toHaveProperty('total');
+        expect(result.pagination).toHaveProperty('current');
+        expect(result.pagination).toHaveProperty('pageSize');
+      }
+    });
+
+    it('应该支持按订单状态筛选', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+
+      // 创建测试数据
+      const testUser = await TestDataFactory.createTestUser(dataSource);
+      const testRoute = await TestDataFactory.createTestRoute(dataSource);
+
+      // 创建不同状态的测试订单
+      await dataSource.getRepository('Order').save([
+        {
+          userId: testUser.id,
+          routeId: testRoute.id,
+          passengerCount: 1,
+          totalAmount: 50.00,
+          status: OrderStatus.PENDING_PAYMENT,
+          paymentStatus: PaymentStatus.PENDING,
+          passengerSnapshots: [{ name: '乘客A', phone: '13800138003' }],
+          routeSnapshot: { name: testRoute.name }
+        },
+        {
+          userId: testUser.id,
+          routeId: testRoute.id,
+          passengerCount: 3,
+          totalAmount: 150.00,
+          status: OrderStatus.COMPLETED,
+          paymentStatus: PaymentStatus.PAID,
+          passengerSnapshots: [
+            { name: '乘客B', phone: '13800138004' },
+            { name: '乘客C', phone: '13800138005' },
+            { name: '乘客D', phone: '13800138006' }
+          ],
+          routeSnapshot: { name: testRoute.name }
+        }
+      ]);
+
+      const response = await client.orders.$get({
+        query: {
+          page: 1,
+          pageSize: 10,
+          filters: JSON.stringify({ status: OrderStatus.PENDING_PAYMENT })
+        }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const result = await response.json();
+        expect(result.data).toHaveLength(1);
+        expect(result.data[0].status).toBe(OrderStatus.PENDING_PAYMENT);
+      }
+    });
+  });
+
+  describe('订单详情查询测试', () => {
+    it('应该成功获取订单详情', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+
+      // 创建测试数据
+      const testUser = await TestDataFactory.createTestUser(dataSource);
+      const testRoute = await TestDataFactory.createTestRoute(dataSource);
+
+      // 创建测试订单
+      const order = await dataSource.getRepository('Order').save({
+        userId: testUser.id,
+        routeId: testRoute.id,
+        passengerCount: 2,
+        totalAmount: 100.00,
+        status: OrderStatus.PENDING_PAYMENT,
+        paymentStatus: PaymentStatus.PENDING,
+        passengerSnapshots: [
+          { name: '乘客1', phone: '13800138001' },
+          { name: '乘客2', phone: '13800138002' }
+        ],
+        routeSnapshot: {
+          name: testRoute.name,
+          description: testRoute.description
+        }
+      });
+
+      const response = await client.orders[':id'].$get({
+        param: { id: order.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const result = await response.json();
+        expect(result.id).toBe(order.id);
+        expect(result.userId).toBe(testUser.id);
+        expect(result.routeId).toBe(testRoute.id);
+        expect(result.passengerCount).toBe(2);
+        expect(result.totalAmount).toBe(100.00);
+        expect(result.status).toBe(OrderStatus.PENDING_PAYMENT);
+        expect(result.paymentStatus).toBe(PaymentStatus.PENDING);
+        expect(Array.isArray(result.passengerSnapshots)).toBe(true);
+        expect(result.passengerSnapshots).toHaveLength(2);
+      }
+    });
+
+    it('应该返回404当订单不存在时', async () => {
+      const response = await client.orders[':id'].$get({
+        param: { id: 99999 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(404);
+    });
+  });
+
+  describe('订单统计API测试', () => {
+    it('应该成功获取订单统计信息', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+
+      // 创建测试数据
+      const testUser = await TestDataFactory.createTestUser(dataSource);
+      const testRoute = await TestDataFactory.createTestRoute(dataSource);
+
+      // 创建不同状态的测试订单
+      await dataSource.getRepository('Order').save([
+        {
+          userId: testUser.id,
+          routeId: testRoute.id,
+          passengerCount: 1,
+          totalAmount: 50.00,
+          status: OrderStatus.PENDING_PAYMENT,
+          paymentStatus: PaymentStatus.PENDING,
+          passengerSnapshots: [{ name: '乘客A', phone: '13800138003' }],
+          routeSnapshot: { name: testRoute.name }
+        },
+        {
+          userId: testUser.id,
+          routeId: testRoute.id,
+          passengerCount: 2,
+          totalAmount: 100.00,
+          status: OrderStatus.COMPLETED,
+          paymentStatus: PaymentStatus.PAID,
+          passengerSnapshots: [
+            { name: '乘客B', phone: '13800138004' },
+            { name: '乘客C', phone: '13800138005' }
+          ],
+          routeSnapshot: { name: testRoute.name }
+        }
+      ]);
+
+      const response = await client.orders.stats.$get({}, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const result = await response.json();
+        expect(result).toHaveProperty('total');
+        expect(result).toHaveProperty('pendingPayment');
+        expect(result).toHaveProperty('waitingDeparture');
+        expect(result).toHaveProperty('inProgress');
+        expect(result).toHaveProperty('completed');
+        expect(result).toHaveProperty('cancelled');
+
+        expect(result.total).toBeGreaterThanOrEqual(2);
+        expect(result.pendingPayment).toBeGreaterThanOrEqual(1);
+        expect(result.completed).toBeGreaterThanOrEqual(1);
+      }
+    });
+  });
+
+  describe('权限控制测试', () => {
+    it('应该拒绝未授权访问', async () => {
+      const response = await client.orders.$get({
+        query: {
+          page: 1,
+          pageSize: 10
+        }
+      });
+      expect(response.status).toBe(401);
+    });
+
+    it('应该拒绝非管理员访问', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const userService = new UserService(dataSource);
+      const authService = new AuthService(userService);
+
+      // 创建普通用户
+      const regularUser = await TestDataFactory.createTestUser(dataSource);
+      const regularToken = authService.generateToken(regularUser);
+
+      const response = await client.orders.$get({
+        query: {
+          page: 1,
+          pageSize: 10
+        }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${regularToken}`
+        }
+      });
+
+      expect(response.status).toBe(403);
+    });
+  });
+});