ソースを参照

📝 docs(architecture): update entity design coding standard
- 修改实体设计规范,要求使用TypeORM装饰器定义字段并添加comment配置

📝 docs(story): update order management task status
- 更新订单管理功能开发任务状态,标记已完成的任务

✨ feat(orders): implement admin order management API
- 创建订单相关基础文件:
- 订单实体 `order.entity.ts`
- 订单Zod Schema `order.schema.ts`
- 共享类型 `order.types.ts`
- 实现订单服务 `order.service.ts`,包含统计和筛选功能
- 创建管理后台订单API路由,支持CRUD操作和状态统计
- 添加管理员权限中间件 `admin.middleware.ts`

yourname 3 ヶ月 前
コミット
41ec733fe0

+ 1 - 1
docs/architecture/coding-standards.md

@@ -26,7 +26,7 @@
 
 ## 通用CRUD开发规范
 - **CRUD开发**: 遵循 [通用CRUD规范](./generic-crud-standards.md) 进行开发
-- **实体设计**: 所有实体必须继承 `ObjectLiteral`,包含时间戳字段
+- **实体设计**: 使用TypeORM装饰器定义字段,为所有字段添加 `comment` 配置,包含时间戳字段
 - **Schema设计**: 创建、更新、响应使用不同的Zod schema
 - **路由生成**: 使用 `createCrudRoutes` 自动生成API路由
 - **用户跟踪**: 实现用户跟踪功能记录操作人信息

+ 10 - 10
docs/stories/005.007.story.md

@@ -15,16 +15,16 @@ Approved
 4. 支持订单状态统计和报表
 
 ## Tasks / Subtasks
-- [ ] 创建订单相关基础文件 (前置任务)
-  - [ ] 创建订单实体 `packages/server/src/modules/orders/order.entity.ts`
-  - [ ] 创建订单Zod Schema `packages/server/src/modules/orders/order.schema.ts`
-  - [ ] 创建共享类型 `packages/server/src/share/order.types.ts`
-
-- [ ] 创建管理后台订单API路由 (AC: 1, 2, 3, 4)
-  - [ ] 使用通用CRUD规范创建管理后台订单API
-  - [ ] 实现订单列表查询(支持分页、搜索、状态筛选)
-  - [ ] 实现订单详情查询(包含关联的用户、路线、乘客信息)
-  - [ ] 实现订单状态统计API
+- [x] 创建订单相关基础文件 (前置任务)
+  - [x] 创建订单实体 `packages/server/src/modules/orders/order.entity.ts`
+  - [x] 创建订单Zod Schema `packages/server/src/modules/orders/order.schema.ts`
+  - [x] 创建共享类型 `packages/server/src/share/order.types.ts`
+
+- [x] 创建管理后台订单API路由 (AC: 1, 2, 3, 4)
+  - [x] 使用通用CRUD规范创建管理后台订单API
+  - [x] 实现订单列表查询(支持分页、搜索、状态筛选)
+  - [x] 实现订单详情查询(包含关联的用户、路线、乘客信息)
+  - [x] 实现订单状态统计API
 - [ ] 编写管理后台订单API集成测试 (AC: 1, 2, 3, 4)
   - [ ] 编写订单列表API集成测试
   - [ ] 编写订单状态筛选功能集成测试

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

@@ -0,0 +1,34 @@
+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';
+import { OpenAPIHono } from '@hono/zod-openapi';
+import statsRoutes from './stats';
+
+// 使用通用CRUD路由创建订单管理API
+const orderRoutes = createCrudRoutes({
+  entity: Order,
+  createSchema: OrderCreateSchema,
+  updateSchema: OrderUpdateSchema,
+  getSchema: OrderResponseSchema,
+  listSchema: OrderResponseSchema,
+  searchFields: ['id', 'user.username', 'user.phone'],
+  relations: ['user', 'route'],
+  middleware: [authMiddleware, adminMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+})
+
+export default new OpenAPIHono()
+  // 合并统计路由
+  .route('/stats', statsRoutes)
+  .route('/', orderRoutes);

+ 54 - 0
packages/server/src/api/admin/orders/stats.ts

@@ -0,0 +1,54 @@
+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 app = new OpenAPIHono();
+
+// 订单统计路由
+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 } }
+    }
+  }
+});
+
+app.openapi(statsRoute, async (c) => {
+  try {
+    const orderService = new OrderService();
+    const stats = await orderService.getOrderStats();
+
+    return c.json(stats, 200);
+  } catch (error) {
+    console.error('Get order stats error:', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '获取订单统计失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 26 - 0
packages/server/src/middleware/admin.middleware.ts

@@ -0,0 +1,26 @@
+import { Context, Next } from 'hono';
+import { AuthContext } from '../types/context';
+
+export async function adminMiddleware(c: Context<AuthContext>, next: Next) {
+  try {
+    const user = c.get('user');
+
+    if (!user) {
+      return c.json({ message: 'Authentication required' }, 401);
+    }
+
+    // 检查用户是否有管理员角色
+    const isAdmin = user.roles?.some((role: any) =>
+      role.name === 'admin' || role.name === 'administrator'
+    );
+
+    if (!isAdmin) {
+      return c.json({ message: 'Admin access required' }, 403);
+    }
+
+    await next();
+  } catch (error) {
+    console.error('Admin middleware error:', error);
+    return c.json({ message: 'Admin verification failed' }, 403);
+  }
+}

+ 79 - 0
packages/server/src/modules/orders/order.entity.ts

@@ -0,0 +1,79 @@
+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';
+
+export enum OrderStatus {
+  PENDING_PAYMENT = '待支付',
+  WAITING_DEPARTURE = '待出发',
+  IN_PROGRESS = '行程中',
+  COMPLETED = '已完成',
+  CANCELLED = '已取消'
+}
+
+export enum PaymentStatus {
+  PENDING = '待支付',
+  PAID = '已支付',
+  FAILED = '支付失败',
+  REFUNDED = '已退款'
+}
+
+@Entity('orders')
+export class Order implements ObjectLiteral {
+  @PrimaryGeneratedColumn({ comment: '订单ID' })
+  id!: number;
+
+  @Column({ comment: '用户ID' })
+  userId!: number;
+
+  @Column({ comment: '路线ID' })
+  routeId!: number;
+
+  @Column({ comment: '乘客数量' })
+  passengerCount!: number;
+
+  @Column('decimal', { precision: 10, scale: 2, comment: '订单总金额' })
+  totalAmount!: number;
+
+  @Column({
+    type: 'enum',
+    enum: OrderStatus,
+    default: OrderStatus.PENDING_PAYMENT,
+    comment: '订单状态'
+  })
+  status!: OrderStatus;
+
+  @Column({
+    type: 'enum',
+    enum: PaymentStatus,
+    default: PaymentStatus.PENDING,
+    comment: '支付状态'
+  })
+  paymentStatus!: PaymentStatus;
+
+  @Column('json', { comment: '乘客信息快照数组(下单时的多个乘客信息)' })
+  passengerSnapshots!: any[];
+
+  @Column('json', { comment: '路线信息快照(下单时的路线信息)' })
+  routeSnapshot!: any;
+
+  @Column({ nullable: true, comment: '创建人ID' })
+  createdBy?: number;
+
+  @Column({ nullable: true, comment: '更新人ID' })
+  updatedBy?: number;
+
+  @CreateDateColumn({ comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ comment: '更新时间' })
+  updatedAt!: Date;
+
+  @ManyToOne(() => User)
+  @JoinColumn({ name: 'userId' })
+  user!: User;
+
+  @ManyToOne(() => Route)
+  @JoinColumn({ name: 'routeId' })
+  route!: Route;
+}

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

@@ -0,0 +1,78 @@
+import { z } from 'zod';
+import { OrderStatus, PaymentStatus } from './order.entity';
+
+// 创建订单Schema
+export const OrderCreateSchema = z.object({
+  userId: z.number().int().positive(),
+  routeId: z.number().int().positive(),
+  passengerCount: z.number().int().min(1),
+  totalAmount: z.number().positive(),
+  passengerSnapshots: z.array(z.any()),
+  routeSnapshot: z.any(),
+  createdBy: z.number().int().positive().optional()
+});
+
+// 更新订单Schema
+export const OrderUpdateSchema = z.object({
+  status: z.nativeEnum(OrderStatus).optional(),
+  paymentStatus: z.nativeEnum(PaymentStatus).optional(),
+  updatedBy: z.number().int().positive().optional()
+});
+
+// 获取订单Schema
+export const OrderGetSchema = z.object({
+  id: z.number().int().positive()
+});
+
+// 订单列表查询Schema
+export const OrderListSchema = z.object({
+  page: z.number().int().positive().default(1),
+  pageSize: z.number().int().positive().max(100).default(20),
+  status: z.nativeEnum(OrderStatus).optional(),
+  paymentStatus: z.nativeEnum(PaymentStatus).optional(),
+  search: z.string().optional()
+});
+
+// 订单响应Schema
+export const OrderResponseSchema = z.object({
+  id: z.number(),
+  userId: z.number(),
+  routeId: z.number(),
+  passengerCount: z.number(),
+  totalAmount: z.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(),
+  createdAt: z.date(),
+  updatedAt: z.date(),
+  user: z.object({
+    id: z.number(),
+    username: z.string(),
+    phone: z.string().optional()
+  }).optional(),
+  route: z.object({
+    id: z.number(),
+    name: z.string(),
+    description: z.string().optional()
+  }).optional()
+});
+
+// 订单统计Schema
+export const OrderStatsSchema = z.object({
+  total: z.number(),
+  pendingPayment: z.number(),
+  waitingDeparture: z.number(),
+  inProgress: z.number(),
+  completed: z.number(),
+  cancelled: z.number()
+});
+
+export type OrderCreateInput = z.infer<typeof OrderCreateSchema>;
+export type OrderUpdateInput = z.infer<typeof OrderUpdateSchema>;
+export type OrderGetInput = z.infer<typeof OrderGetSchema>;
+export type OrderListInput = z.infer<typeof OrderListSchema>;
+export type OrderResponse = z.infer<typeof OrderResponseSchema>;
+export type OrderStats = z.infer<typeof OrderStatsSchema>;

+ 60 - 0
packages/server/src/modules/orders/order.service.ts

@@ -0,0 +1,60 @@
+import { AppDataSource } from '../../data-source';
+import { Order, OrderStatus } from './order.entity';
+import { OrderStats } from './order.schema';
+
+export class OrderService {
+  private orderRepository = AppDataSource.getRepository(Order);
+
+  /**
+   * 获取订单统计信息
+   */
+  async getOrderStats(): Promise<OrderStats> {
+    const total = await this.orderRepository.count();
+    const pendingPayment = await this.orderRepository.count({
+      where: { status: OrderStatus.PENDING_PAYMENT }
+    });
+    const waitingDeparture = await this.orderRepository.count({
+      where: { status: OrderStatus.WAITING_DEPARTURE }
+    });
+    const inProgress = await this.orderRepository.count({
+      where: { status: OrderStatus.IN_PROGRESS }
+    });
+    const completed = await this.orderRepository.count({
+      where: { status: OrderStatus.COMPLETED }
+    });
+    const cancelled = await this.orderRepository.count({
+      where: { status: OrderStatus.CANCELLED }
+    });
+
+    return {
+      total,
+      pendingPayment,
+      waitingDeparture,
+      inProgress,
+      completed,
+      cancelled
+    };
+  }
+
+  /**
+   * 根据状态筛选订单
+   */
+  async getOrdersByStatus(status: OrderStatus, page: number = 1, pageSize: number = 20) {
+    const skip = (page - 1) * pageSize;
+
+    const [orders, total] = await this.orderRepository.findAndCount({
+      where: { status },
+      relations: ['user', 'route'],
+      order: { id: 'DESC' },
+      skip,
+      take: pageSize
+    });
+
+    return {
+      data: orders,
+      total,
+      page,
+      pageSize
+    };
+  }
+}

+ 27 - 0
packages/server/src/share/order.types.ts

@@ -0,0 +1,27 @@
+import { OrderStatus, PaymentStatus } from '../modules/orders/order.entity';
+import { OrderResponse, OrderStats } from '../modules/orders/order.schema';
+
+export { OrderStatus, PaymentStatus };
+export type { OrderResponse, OrderStats };
+
+// 订单列表响应类型
+export interface OrderListResponse {
+  data: OrderResponse[];
+  total: number;
+  page: number;
+  pageSize: number;
+}
+
+// 订单统计响应类型
+export interface OrderStatsResponse {
+  stats: OrderStats;
+}
+
+// 订单筛选参数
+export interface OrderFilterParams {
+  status?: OrderStatus;
+  paymentStatus?: PaymentStatus;
+  search?: string;
+  page?: number;
+  pageSize?: number;
+}