Quellcode durchsuchen

feat: 添加商城相关API和模块

- 添加广告、商品、订单、物流等商城API路由
- 添加商城相关实体和服务模块
- 完善商城功能模块结构

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
yourname vor 2 Monaten
Ursprung
Commit
37a82dbcf2
76 geänderte Dateien mit 5016 neuen und 0 gelöschten Zeilen
  1. 20 0
      packages/server/src/api/advertisement-types/index.ts
  2. 21 0
      packages/server/src/api/advertisements/index.ts
  3. 20 0
      packages/server/src/api/agents/index.ts
  4. 21 0
      packages/server/src/api/cards/index.ts
  5. 20 0
      packages/server/src/api/cities/index.ts
  6. 20 0
      packages/server/src/api/configs/index.ts
  7. 21 0
      packages/server/src/api/delivery-address/index.ts
  8. 20 0
      packages/server/src/api/express-companies/index.ts
  9. 21 0
      packages/server/src/api/goods-categories/index.ts
  10. 37 0
      packages/server/src/api/goods/index.ts
  11. 113 0
      packages/server/src/api/goods/random.ts
  12. 20 0
      packages/server/src/api/merchants/index.ts
  13. 21 0
      packages/server/src/api/orders-goods/index.ts
  14. 21 0
      packages/server/src/api/orders-refund/index.ts
  15. 73 0
      packages/server/src/api/orders/create-order.ts
  16. 31 0
      packages/server/src/api/orders/index.ts
  17. 20 0
      packages/server/src/api/organizations/index.ts
  18. 20 0
      packages/server/src/api/suppliers/index.ts
  19. 21 0
      packages/server/src/api/user-card-balance-records/index.ts
  20. 21 0
      packages/server/src/api/user-cards/index.ts
  21. 72 0
      packages/server/src/modules/advertisements/advertisement-type.entity.ts
  22. 81 0
      packages/server/src/modules/advertisements/advertisement-type.schema.ts
  23. 9 0
      packages/server/src/modules/advertisements/advertisement-type.service.ts
  24. 125 0
      packages/server/src/modules/advertisements/advertisement.entity.ts
  25. 145 0
      packages/server/src/modules/advertisements/advertisement.schema.ts
  26. 9 0
      packages/server/src/modules/advertisements/advertisement.service.ts
  27. 52 0
      packages/server/src/modules/agent/agent.entity.ts
  28. 119 0
      packages/server/src/modules/agent/agent.schema.ts
  29. 9 0
      packages/server/src/modules/agent/agent.service.ts
  30. 42 0
      packages/server/src/modules/card/card.entity.ts
  31. 99 0
      packages/server/src/modules/card/card.schema.ts
  32. 9 0
      packages/server/src/modules/card/card.service.ts
  33. 71 0
      packages/server/src/modules/delivery-address/delivery-address.entity.ts
  34. 293 0
      packages/server/src/modules/delivery-address/delivery-address.schema.ts
  35. 58 0
      packages/server/src/modules/delivery-address/delivery-address.service.ts
  36. 39 0
      packages/server/src/modules/goods/goods-category.entity.ts
  37. 91 0
      packages/server/src/modules/goods/goods-category.schema.ts
  38. 9 0
      packages/server/src/modules/goods/goods-category.service.ts
  39. 115 0
      packages/server/src/modules/goods/goods.entity.ts
  40. 291 0
      packages/server/src/modules/goods/goods.schema.ts
  41. 9 0
      packages/server/src/modules/goods/goods.service.ts
  42. 59 0
      packages/server/src/modules/goods/schemas/random.schema.ts
  43. 31 0
      packages/server/src/modules/logistics/express-company.entity.ts
  44. 75 0
      packages/server/src/modules/logistics/express-company.schema.ts
  45. 9 0
      packages/server/src/modules/logistics/express-company.service.ts
  46. 58 0
      packages/server/src/modules/merchant/merchant.entity.ts
  47. 143 0
      packages/server/src/modules/merchant/merchant.schema.ts
  48. 9 0
      packages/server/src/modules/merchant/merchant.service.ts
  49. 82 0
      packages/server/src/modules/orders/order-goods.entity.ts
  50. 246 0
      packages/server/src/modules/orders/order-goods.schema.ts
  51. 9 0
      packages/server/src/modules/orders/order-goods.service.ts
  52. 40 0
      packages/server/src/modules/orders/order-refund.entity.ts
  53. 128 0
      packages/server/src/modules/orders/order-refund.schema.ts
  54. 9 0
      packages/server/src/modules/orders/order-refund.service.ts
  55. 139 0
      packages/server/src/modules/orders/order.entity.ts
  56. 497 0
      packages/server/src/modules/orders/order.schema.ts
  57. 178 0
      packages/server/src/modules/orders/order.service.ts
  58. 66 0
      packages/server/src/modules/orders/schemas/create-order.schema.ts
  59. 31 0
      packages/server/src/modules/organization/organization.entity.ts
  60. 75 0
      packages/server/src/modules/organization/organization.schema.ts
  61. 9 0
      packages/server/src/modules/organization/organization.service.ts
  62. 52 0
      packages/server/src/modules/supplier/supplier.entity.ts
  63. 119 0
      packages/server/src/modules/supplier/supplier.schema.ts
  64. 9 0
      packages/server/src/modules/supplier/supplier.service.ts
  65. 34 0
      packages/server/src/modules/system/city.entity.ts
  66. 87 0
      packages/server/src/modules/system/city.schema.ts
  67. 9 0
      packages/server/src/modules/system/city.service.ts
  68. 28 0
      packages/server/src/modules/system/config.entity.ts
  69. 63 0
      packages/server/src/modules/system/config.schema.ts
  70. 9 0
      packages/server/src/modules/system/config.service.ts
  71. 53 0
      packages/server/src/modules/user-card-balance-records/user-card-balance-record.entity.ts
  72. 125 0
      packages/server/src/modules/user-card-balance-records/user-card-balance-record.schema.ts
  73. 9 0
      packages/server/src/modules/user-card-balance-records/user-card-balance-record.service.ts
  74. 56 0
      packages/server/src/modules/user-cards/user-card.entity.ts
  75. 132 0
      packages/server/src/modules/user-cards/user-card.schema.ts
  76. 9 0
      packages/server/src/modules/user-cards/user-card.service.ts

+ 20 - 0
packages/server/src/api/advertisement-types/index.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { AdvertisementType } from '@/server/modules/advertisements/advertisement-type.entity';
+import { AdvertisementTypeSchema, CreateAdvertisementTypeDto, UpdateAdvertisementTypeDto } from '@/server/modules/advertisements/advertisement-type.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const advertisementTypeRoutes = createCrudRoutes({
+  entity: AdvertisementType,
+  createSchema: CreateAdvertisementTypeDto,
+  updateSchema: UpdateAdvertisementTypeDto,
+  getSchema: AdvertisementTypeSchema,
+  listSchema: AdvertisementTypeSchema,
+  searchFields: ['name', 'code'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default advertisementTypeRoutes;

+ 21 - 0
packages/server/src/api/advertisements/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Advertisement } from '@/server/modules/advertisements/advertisement.entity';
+import { AdvertisementSchema, CreateAdvertisementDto, UpdateAdvertisementDto } from '@/server/modules/advertisements/advertisement.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const advertisementRoutes = createCrudRoutes({
+  entity: Advertisement,
+  createSchema: CreateAdvertisementDto,
+  updateSchema: UpdateAdvertisementDto,
+  getSchema: AdvertisementSchema,
+  listSchema: AdvertisementSchema,
+  searchFields: ['title', 'code'],
+  relations: ['imageFile', 'advertisementType'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default advertisementRoutes;

+ 20 - 0
packages/server/src/api/agents/index.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Agent } from '@/server/modules/agent/agent.entity';
+import { AgentSchema, CreateAgentDto, UpdateAgentDto } from '@/server/modules/agent/agent.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const agentRoutes = createCrudRoutes({
+  entity: Agent,
+  createSchema: CreateAgentDto,
+  updateSchema: UpdateAgentDto,
+  getSchema: AgentSchema,
+  listSchema: AgentSchema,
+  searchFields: ['name', 'username', 'realname', 'phone'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default agentRoutes;

+ 21 - 0
packages/server/src/api/cards/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Card } from '@/server/modules/card/card.entity';
+import { CardSchema, CreateCardDto, UpdateCardDto } from '@/server/modules/card/card.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const cardRoutes = createCrudRoutes({
+  entity: Card,
+  createSchema: CreateCardDto,
+  updateSchema: UpdateCardDto,
+  getSchema: CardSchema,
+  listSchema: CardSchema,
+  searchFields: ['card_no', 'card_type'],
+  relations: ['agent'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default cardRoutes;

+ 20 - 0
packages/server/src/api/cities/index.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { City } from '@/server/modules/system/city.entity';
+import { CitySchema, CreateCityDto, UpdateCityDto } from '@/server/modules/system/city.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const cityRoutes = createCrudRoutes({
+  entity: City,
+  createSchema: CreateCityDto,
+  updateSchema: UpdateCityDto,
+  getSchema: CitySchema,
+  listSchema: CitySchema,
+  searchFields: ['name'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default cityRoutes;

+ 20 - 0
packages/server/src/api/configs/index.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Config } from '@/server/modules/system/config.entity';
+import { ConfigSchema, CreateConfigDto, UpdateConfigDto } from '@/server/modules/system/config.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const configRoutes = createCrudRoutes({
+  entity: Config,
+  createSchema: CreateConfigDto,
+  updateSchema: UpdateConfigDto,
+  getSchema: ConfigSchema,
+  listSchema: ConfigSchema,
+  searchFields: ['key', 'value'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default configRoutes;

+ 21 - 0
packages/server/src/api/delivery-address/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { DeliveryAddress } from '@/server/modules/delivery-address/delivery-address.entity';
+import { DeliveryAddressSchema, CreateDeliveryAddressDto, UpdateDeliveryAddressDto } from '@/server/modules/delivery-address/delivery-address.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const deliveryAddressRoutes = createCrudRoutes({
+  entity: DeliveryAddress,
+  createSchema: CreateDeliveryAddressDto,
+  updateSchema: UpdateDeliveryAddressDto,
+  getSchema: DeliveryAddressSchema,
+  listSchema: DeliveryAddressSchema,
+  searchFields: ['name', 'phone', 'address'],
+  relations: ['user', 'province', 'city', 'district', 'town'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default deliveryAddressRoutes;

+ 20 - 0
packages/server/src/api/express-companies/index.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { ExpressCompany } from '@/server/modules/logistics/express-company.entity';
+import { ExpressCompanySchema, CreateExpressCompanyDto, UpdateExpressCompanyDto } from '@/server/modules/logistics/express-company.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const expressCompanyRoutes = createCrudRoutes({
+  entity: ExpressCompany,
+  createSchema: CreateExpressCompanyDto,
+  updateSchema: UpdateExpressCompanyDto,
+  getSchema: ExpressCompanySchema,
+  listSchema: ExpressCompanySchema,
+  searchFields: ['name', 'code'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default expressCompanyRoutes;

+ 21 - 0
packages/server/src/api/goods-categories/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { GoodsCategory } from '@/server/modules/goods/goods-category.entity';
+import { GoodsCategorySchema, CreateGoodsCategoryDto, UpdateGoodsCategoryDto } from '@/server/modules/goods/goods-category.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const goodsCategoryRoutes = createCrudRoutes({
+  entity: GoodsCategory,
+  createSchema: CreateGoodsCategoryDto,
+  updateSchema: UpdateGoodsCategoryDto,
+  getSchema: GoodsCategorySchema,
+  listSchema: GoodsCategorySchema,
+  searchFields: ['name'],
+  relations: ['imageFile'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default goodsCategoryRoutes;

+ 37 - 0
packages/server/src/api/goods/index.ts

@@ -0,0 +1,37 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { Goods } from '@/server/modules/goods/goods.entity';
+import { GoodsSchema, CreateGoodsDto, UpdateGoodsDto } from '@/server/modules/goods/goods.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { File } from '@/server/modules/files/file.entity';
+import randomRoute from './random';
+
+// 创建基础CRUD路由
+const goodsRoutes = createCrudRoutes({
+  entity: Goods,
+  createSchema: CreateGoodsDto,
+  updateSchema: UpdateGoodsDto,
+  getSchema: GoodsSchema,
+  listSchema: GoodsSchema,
+  searchFields: ['name', 'instructions'],
+  relations: ['category1', 'category2', 'category3', 'supplier', 'imageFile.uploadUser', 'slideImages.uploadUser'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  },
+  relationFields: {
+    slideImageIds: {
+      relationName: 'slideImages',
+      targetEntity: File,
+      joinTableName: 'goods_slide_images'
+    }
+  }
+});
+
+// 聚合基础CRUD路由和扩展路由
+const app = new OpenAPIHono()
+  .route('/random', randomRoute)  // 随机商品列表路由
+  .route('/', goodsRoutes)          // 基础CRUD路由
+
+export default app;

+ 113 - 0
packages/server/src/api/goods/random.ts

@@ -0,0 +1,113 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { GoodsSchema } from '@/server/modules/goods/goods.schema';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { Goods } from '@/server/modules/goods/goods.entity';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { RandomGoodsQuerySchema, RandomGoodsResponseSchema } from '@/server/modules/goods/schemas/random.schema';
+import { parseWithAwait } from '@/server/utils/parseWithAwait';
+
+// 定义随机商品列表路由
+const routeDef = createRoute({
+  method: 'get',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    query: RandomGoodsQuerySchema
+  },
+  responses: {
+    200: {
+      description: '成功获取随机商品列表',
+      content: {
+        'application/json': {
+          schema: RandomGoodsResponseSchema
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const query = c.req.valid('query');
+    const { limit, categoryId, includeImages } = query;
+
+    // 创建查询构建器
+    const queryBuilder = AppDataSource.getRepository(Goods)
+      .createQueryBuilder('goods')
+      .where('goods.state = :state', { state: 1 }) // 只获取可用的商品
+      .orderBy('RAND()') // 使用随机排序
+      .limit(limit);
+
+    // 如果指定了分类ID,添加分类过滤
+    if (categoryId) {
+      queryBuilder.andWhere(
+        'goods.category_id1 = :categoryId OR goods.category_id2 = :categoryId OR goods.category_id3 = :categoryId',
+        { categoryId }
+      );
+    }
+
+    // 如果需要包含关联数据
+    if (includeImages) {
+      queryBuilder
+        .leftJoinAndSelect('goods.imageFile', 'imageFile')
+        .leftJoinAndSelect('imageFile.uploadUser', 'imageUploadUser')
+        .leftJoinAndSelect('goods.category1', 'category1')
+        .leftJoinAndSelect('goods.category2', 'category2')
+        .leftJoinAndSelect('goods.category3', 'category3')
+        .leftJoinAndSelect('goods.supplier', 'supplier');
+    }
+
+    // 获取随机商品
+    const goods = await queryBuilder.getMany();
+
+    // 获取总数(用于分页参考)
+    const totalQuery = AppDataSource.getRepository(Goods)
+      .createQueryBuilder('goods')
+      .where('goods.state = :state', { state: 1 });
+
+    if (categoryId) {
+      totalQuery.andWhere(
+        'goods.category_id1 = :categoryId OR goods.category_id2 = :categoryId OR goods.category_id3 = :categoryId',
+        { categoryId }
+      );
+    }
+
+    const total = await totalQuery.getCount();
+
+    // 使用 parseWithAwait 确保数据格式正确
+    const validatedGoods = await parseWithAwait(z.array(GoodsSchema), goods);
+
+    return c.json({
+      data: validatedGoods,
+      total
+    }, 200);
+  } catch (error) {
+    console.error('获取随机商品列表失败:', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '获取随机商品列表失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 20 - 0
packages/server/src/api/merchants/index.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Merchant } from '@/server/modules/merchant/merchant.entity';
+import { MerchantSchema, CreateMerchantDto, UpdateMerchantDto } from '@/server/modules/merchant/merchant.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const merchantRoutes = createCrudRoutes({
+  entity: Merchant,
+  createSchema: CreateMerchantDto,
+  updateSchema: UpdateMerchantDto,
+  getSchema: MerchantSchema,
+  listSchema: MerchantSchema,
+  searchFields: ['name', 'username', 'realname', 'phone'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default merchantRoutes;

+ 21 - 0
packages/server/src/api/orders-goods/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { OrderGoods } from '@/server/modules/orders/order-goods.entity';
+import { OrderGoodsSchema, CreateOrderGoodsDto, UpdateOrderGoodsDto } from '@/server/modules/orders/order-goods.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const orderGoodsRoutes = createCrudRoutes({
+  entity: OrderGoods,
+  createSchema: CreateOrderGoodsDto,
+  updateSchema: UpdateOrderGoodsDto,
+  getSchema: OrderGoodsSchema,
+  listSchema: OrderGoodsSchema,
+  searchFields: ['goodsName', 'orderNo'],
+  relations: ['order', 'goods', 'supplier', 'imageFile'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default orderGoodsRoutes;

+ 21 - 0
packages/server/src/api/orders-refund/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { OrderRefund } from '@/server/modules/orders/order-refund.entity';
+import { OrderRefundSchema, CreateOrderRefundDto, UpdateOrderRefundDto } from '@/server/modules/orders/order-refund.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const orderRefundRoutes = createCrudRoutes({
+  entity: OrderRefund,
+  createSchema: CreateOrderRefundDto,
+  updateSchema: UpdateOrderRefundDto,
+  getSchema: OrderRefundSchema,
+  listSchema: OrderRefundSchema,
+  searchFields: ['orderNo', 'refundOrderNo'],
+  relations: ['order'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default orderRefundRoutes;

+ 73 - 0
packages/server/src/api/orders/create-order.ts

@@ -0,0 +1,73 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { CreateOrderRequestDto, CreateOrderResponseDto } from '@/server/modules/orders/schemas/create-order.schema';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { OrderService } from '@/server/modules/orders/order.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const routeDef = createRoute({
+  method: 'post',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: CreateOrderRequestDto
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '订单创建成功',
+      content: {
+        'application/json': {
+          schema: CreateOrderResponseDto
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const data = await c.req.json();
+    const user = c.get('user');
+
+    const service = new OrderService(AppDataSource);
+    const result = await service.createOrder(data, user.id);
+
+    return c.json({
+      success: true,
+      orderId: result.orderId,
+      orderNo: result.orderNo,
+      amount: result.amount,
+      payAmount: result.payAmount,
+      message: '订单创建成功'
+    }, 200);
+  } catch (error) {
+    const message = error instanceof Error ? error.message : '创建订单失败';
+    return c.json({ code: 500, message }, 500);
+  }
+});
+
+export default app;

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

@@ -0,0 +1,31 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { OpenAPIHono } from '@hono/zod-openapi';
+import { Order } from '@/server/modules/orders/order.entity';
+import { OrderSchema, CreateOrderDto, UpdateOrderDto } from '@/server/modules/orders/order.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+// 基础CRUD路由
+const orderRoutes = createCrudRoutes({
+  entity: Order,
+  createSchema: CreateOrderDto,
+  updateSchema: UpdateOrderDto,
+  getSchema: OrderSchema,
+  listSchema: OrderSchema,
+  searchFields: ['orderNo', 'userPhone', 'recevierName'],
+  relations: ['user', 'merchant', 'supplier', 'deliveryAddress'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+// 导入扩展路由
+import createOrderRoute from './create-order';
+
+// 聚合所有路由
+const app = new OpenAPIHono()
+  .route('/create-order', createOrderRoute)  // 创建订单专用路由
+  .route('/', orderRoutes);  // 基础CRUD路由
+
+export default app;

+ 20 - 0
packages/server/src/api/organizations/index.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Organization } from '@/server/modules/organization/organization.entity';
+import { OrganizationSchema, CreateOrganizationDto, UpdateOrganizationDto } from '@/server/modules/organization/organization.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const organizationRoutes = createCrudRoutes({
+  entity: Organization,
+  createSchema: CreateOrganizationDto,
+  updateSchema: UpdateOrganizationDto,
+  getSchema: OrganizationSchema,
+  listSchema: OrganizationSchema,
+  searchFields: ['name'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default organizationRoutes;

+ 20 - 0
packages/server/src/api/suppliers/index.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Supplier } from '@/server/modules/supplier/supplier.entity';
+import { SupplierSchema, CreateSupplierDto, UpdateSupplierDto } from '@/server/modules/supplier/supplier.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const supplierRoutes = createCrudRoutes({
+  entity: Supplier,
+  createSchema: CreateSupplierDto,
+  updateSchema: UpdateSupplierDto,
+  getSchema: SupplierSchema,
+  listSchema: SupplierSchema,
+  searchFields: ['name', 'username', 'realname'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: 'updated_by'
+  }
+});
+
+export default supplierRoutes;

+ 21 - 0
packages/server/src/api/user-card-balance-records/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { UserCardBalanceRecord } from '@/server/modules/user-card-balance-records/user-card-balance-record.entity';
+import { UserCardBalanceRecordSchema, CreateUserCardBalanceRecordDto, UpdateUserCardBalanceRecordDto } from '@/server/modules/user-card-balance-records/user-card-balance-record.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const userCardBalanceRecordRoutes = createCrudRoutes({
+  entity: UserCardBalanceRecord,
+  createSchema: CreateUserCardBalanceRecordDto,
+  updateSchema: UpdateUserCardBalanceRecordDto,
+  getSchema: UserCardBalanceRecordSchema,
+  listSchema: UserCardBalanceRecordSchema,
+  searchFields: ['cardNo', 'orderNo', 'remark'],
+  relations: ['user', 'userCard'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default userCardBalanceRecordRoutes;

+ 21 - 0
packages/server/src/api/user-cards/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { UserCard } from '@/server/modules/user-cards/user-card.entity';
+import { UserCardSchema, CreateUserCardDto, UpdateUserCardDto } from '@/server/modules/user-cards/user-card.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const userCardRoutes = createCrudRoutes({
+  entity: UserCard,
+  createSchema: CreateUserCardDto,
+  updateSchema: UpdateUserCardDto,
+  getSchema: UserCardSchema,
+  listSchema: UserCardSchema,
+  searchFields: ['cardNo', 'sjtCardNo'],
+  relations: ['user', 'agent'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default userCardRoutes;

+ 72 - 0
packages/server/src/modules/advertisements/advertisement-type.entity.ts

@@ -0,0 +1,72 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('ad_type')
+export class AdvertisementType {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ 
+    name: 'name', 
+    type: 'varchar', 
+    length: 50,
+    comment: '类型名称'
+  })
+  name!: string;
+
+  @Column({ 
+    name: 'code', 
+    type: 'varchar', 
+    length: 20,
+    comment: '调用别名'
+  })
+  code!: string;
+
+  @Column({ 
+    name: 'remark', 
+    type: 'varchar', 
+    length: 100,
+    nullable: true,
+    comment: '备注'
+  })
+  remark!: string | null;
+
+  @CreateDateColumn({ 
+    name: 'created_at',
+    type: 'timestamp',
+    comment: '创建时间'
+  })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ 
+    name: 'updated_at',
+    type: 'timestamp',
+    comment: '更新时间'
+  })
+  updatedAt!: Date;
+
+  @Column({ 
+    name: 'created_by', 
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '创建用户ID'
+  })
+  createdBy!: number | null;
+
+  @Column({ 
+    name: 'updated_by', 
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '更新用户ID'
+  })
+  updatedBy!: number | null;
+
+  @Column({ 
+    name: 'status', 
+    type: 'int',
+    default: 0,
+    comment: '状态 0禁用 1启用'
+  })
+  status!: number;
+}

+ 81 - 0
packages/server/src/modules/advertisements/advertisement-type.schema.ts

@@ -0,0 +1,81 @@
+import { z } from '@hono/zod-openapi';
+
+// 广告类型实体Schema
+export const AdvertisementTypeSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '广告类型ID',
+    example: 1
+  }),
+  name: z.string().max(50).openapi({
+    description: '类型名称',
+    example: '首页轮播'
+  }),
+  code: z.string().max(20).openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  remark: z.string().max(100).nullable().openapi({
+    description: '备注',
+    example: '用于首页轮播图展示'
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  }),
+  status: z.number().int().min(0).max(1).default(0).openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  })
+});
+
+// 创建广告类型DTO
+export const CreateAdvertisementTypeDto = z.object({
+  name: z.string().min(1).max(50).openapi({
+    description: '类型名称',
+    example: '首页轮播'
+  }),
+  code: z.string().min(1).max(20).openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  remark: z.string().max(100).nullable().optional().openapi({
+    description: '备注',
+    example: '用于首页轮播图展示'
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).default(0).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  })
+});
+
+// 更新广告类型DTO
+export const UpdateAdvertisementTypeDto = z.object({
+  name: z.string().min(1).max(50).optional().openapi({
+    description: '类型名称',
+    example: '首页轮播'
+  }),
+  code: z.string().min(1).max(20).optional().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  remark: z.string().max(100).nullable().optional().openapi({
+    description: '备注',
+    example: '用于首页轮播图展示'
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  })
+});

+ 9 - 0
packages/server/src/modules/advertisements/advertisement-type.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { AdvertisementType } from './advertisement-type.entity';
+
+export class AdvertisementTypeService extends GenericCrudService<AdvertisementType> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, AdvertisementType);
+  }
+}

+ 125 - 0
packages/server/src/modules/advertisements/advertisement.entity.ts

@@ -0,0 +1,125 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { File } from '@/server/modules/files/file.entity';
+import { AdvertisementType } from './advertisement-type.entity';
+
+@Entity('ad')
+export class Advertisement {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ 
+    name: 'title', 
+    type: 'varchar', 
+    length: 30,
+    nullable: true,
+    comment: '标题'
+  })
+  title!: string | null;
+
+  @Column({ 
+    name: 'type_id', 
+    type: 'int',
+    nullable: true,
+    unsigned: true,
+    comment: '广告类型'
+  })
+  typeId!: number | null;
+
+  @Column({ 
+    name: 'code', 
+    type: 'varchar', 
+    length: 20,
+    nullable: true,
+    comment: '调用别名'
+  })
+  code!: string | null;
+
+  @Column({ 
+    name: 'url', 
+    type: 'varchar', 
+    length: 255,
+    nullable: true,
+    comment: 'url'
+  })
+  url!: string | null;
+
+  @Column({
+    name: 'image_file_id',
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '图片文件ID'
+  })
+  imageFileId!: number | null;
+
+  @ManyToOne(() => File, { nullable: true })
+  @JoinColumn({
+    name: 'image_file_id',
+    referencedColumnName: 'id'
+  })
+  imageFile!: File | null;
+
+  @ManyToOne(() => AdvertisementType, { nullable: true })
+  @JoinColumn({
+    name: 'type_id',
+    referencedColumnName: 'id'
+  })
+  advertisementType!: AdvertisementType | null;
+
+  @Column({ 
+    name: 'sort', 
+    type: 'int',
+    default: 0,
+    comment: '排序'
+  })
+  sort!: number;
+
+  @CreateDateColumn({ 
+    name: 'created_at',
+    type: 'timestamp',
+    comment: '创建时间'
+  })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ 
+    name: 'updated_at',
+    type: 'timestamp',
+    comment: '更新时间'
+  })
+  updatedAt!: Date;
+
+  @Column({ 
+    name: 'created_by', 
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '创建用户ID'
+  })
+  createdBy!: number | null;
+
+  @Column({ 
+    name: 'updated_by', 
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '更新用户ID'
+  })
+  updatedBy!: number | null;
+
+  @Column({ 
+    name: 'status', 
+    type: 'int',
+    unsigned: true,
+    default: 0,
+    comment: '状态'
+  })
+  status!: number;
+
+  @Column({ 
+    name: 'action_type', 
+    type: 'int',
+    default: 1,
+    comment: '跳转类型 0 不跳转 1webview 2小程序页面'
+  })
+  actionType!: number;
+}

+ 145 - 0
packages/server/src/modules/advertisements/advertisement.schema.ts

@@ -0,0 +1,145 @@
+import { z } from '@hono/zod-openapi';
+
+// 广告实体Schema
+export const AdvertisementSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '广告ID',
+    example: 1
+  }),
+  title: z.string().max(30).nullable().openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.number().int().positive().nullable().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().max(20).nullable().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.number().int().positive().nullable().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  imageFile: z.object({
+    id: z.number().int().positive().openapi({ description: '文件ID' }),
+    name: z.string().max(255).openapi({ description: '文件名', example: 'banner.jpg' }),
+    fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/banner.jpg' }),
+    type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+    size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+  }).nullable().optional().openapi({
+    description: '图片文件信息'
+  }),
+  advertisementType: z.object({
+    id: z.number().int().positive().openapi({ description: '广告类型ID' }),
+    name: z.string().max(50).openapi({ description: '类型名称', example: '首页轮播' }),
+    code: z.string().max(20).openapi({ description: '类型编码', example: 'home_banner' })
+  }).nullable().optional().openapi({
+    description: '广告类型信息'
+  }),
+  sort: z.number().int().default(0).openapi({
+    description: '排序值',
+    example: 10
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  }),
+  status: z.number().int().min(0).max(1).default(0).openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  }),
+  actionType: z.number().int().min(0).max(2).default(1).openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});
+
+// 创建广告DTO
+export const CreateAdvertisementDto = z.object({
+  title: z.string().min(1).max(30).openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.coerce.number<number>().int().positive().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().min(1).max(20).openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().optional().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  sort: z.coerce.number<number>().int().default(0).optional().openapi({
+    description: '排序值',
+    example: 10
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).default(0).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  }),
+  actionType: z.coerce.number<number>().int().min(0).max(2).default(1).optional().openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});
+
+// 更新广告DTO
+export const UpdateAdvertisementDto = z.object({
+  title: z.string().min(1).max(30).optional().openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().min(1).max(20).optional().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().optional().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  sort: z.coerce.number<number>().int().optional().openapi({
+    description: '排序值',
+    example: 10
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  }),
+  actionType: z.coerce.number<number>().int().min(0).max(2).optional().openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});

+ 9 - 0
packages/server/src/modules/advertisements/advertisement.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { Advertisement } from './advertisement.entity';
+
+export class AdvertisementService extends GenericCrudService<Advertisement> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Advertisement);
+  }
+}

+ 52 - 0
packages/server/src/modules/agent/agent.entity.ts

@@ -0,0 +1,52 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('agent')
+export class Agent {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, nullable: true, comment: '代理商名称' })
+  name!: string | null;
+
+  @Column({ name: 'username', type: 'varchar', length: 20, unique: true, comment: '用户名' })
+  username!: string;
+
+  @Column({ name: 'password', type: 'varchar', length: 255, comment: '密码' })
+  password!: string;
+
+  @Column({ name: 'phone', type: 'char', length: 11, nullable: true, comment: '手机号码' })
+  phone!: string | null;
+
+  @Column({ name: 'realname', type: 'varchar', length: 20, nullable: true, comment: '姓名' })
+  realname!: string | null;
+
+  @Column({ name: 'login_num', type: 'int', unsigned: true, default: 0, comment: '登录次数' })
+  loginNum!: number;
+
+  @Column({ name: 'login_time', type: 'int', unsigned: true, default: 0, comment: '登录时间' })
+  loginTime!: number;
+
+  @Column({ name: 'login_ip', type: 'varchar', length: 15, nullable: true, comment: '登录IP' })
+  loginIp!: string | null;
+
+  @Column({ name: 'last_login_time', type: 'int', unsigned: true, default: 0, comment: '上次登录时间' })
+  lastLoginTime!: number;
+
+  @Column({ name: 'last_login_ip', type: 'varchar', length: 15, nullable: true, comment: '上次登录IP' })
+  lastLoginIp!: string | null;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 2, comment: '状态 1启用 2禁用' })
+  state!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+}

+ 119 - 0
packages/server/src/modules/agent/agent.schema.ts

@@ -0,0 +1,119 @@
+import { z } from '@hono/zod-openapi';
+
+export const AgentSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '代理商ID' }),
+  name: z.string().min(1, '代理商名称不能为空').max(255, '代理商名称最多255个字符').nullable().openapi({
+    description: '代理商名称',
+    example: '代理商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').openapi({
+    description: '用户名',
+    example: 'agent001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '李四'
+  }),
+  loginNum: z.number().int().nonnegative('登录次数必须为非负数').default(0).openapi({
+    description: '登录次数',
+    example: 0
+  }),
+  loginTime: z.number().int().nonnegative('登录时间必须为非负数').default(0).openapi({
+    description: '登录时间',
+    example: 0
+  }),
+  loginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '登录IP',
+    example: '192.168.1.1'
+  }),
+  lastLoginTime: z.number().int().nonnegative('上次登录时间必须为非负数').default(0).openapi({
+    description: '上次登录时间',
+    example: 0
+  }),
+  lastLoginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '上次登录IP',
+    example: '192.168.1.1'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});
+
+export const CreateAgentDto = z.object({
+  name: z.string().min(1, '代理商名称不能为空').max(255, '代理商名称最多255个字符').nullable().optional().openapi({
+    description: '代理商名称',
+    example: '代理商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').openapi({
+    description: '用户名',
+    example: 'agent001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '李四'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  })
+});
+
+export const UpdateAgentDto = z.object({
+  name: z.string().min(1, '代理商名称不能为空').max(255, '代理商名称最多255个字符').nullable().optional().openapi({
+    description: '代理商名称',
+    example: '代理商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').optional().openapi({
+    description: '用户名',
+    example: 'agent001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').optional().openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '李四'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  })
+});

+ 9 - 0
packages/server/src/modules/agent/agent.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { Agent } from './agent.entity';
+
+export class AgentService extends GenericCrudService<Agent> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Agent);
+  }
+}

+ 42 - 0
packages/server/src/modules/card/card.entity.ts

@@ -0,0 +1,42 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { Agent } from '@/server/modules/agent/agent.entity';
+
+@Entity('card')
+export class Card {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'agent_id', type: 'int', unsigned: true, nullable: true, comment: '代理商ID' })
+  agentId!: number | null;
+
+  @Column({ name: 'card_type', type: 'tinyint', unsigned: true, comment: '1盛京通卡 2通用联名电子卡' })
+  cardType!: number;
+
+  @Column({ name: 'card_no', type: 'varchar', length: 20, unique: true, comment: '卡号' })
+  cardNo!: string;
+
+  @Column({ name: 'password', type: 'varchar', length: 255, comment: '密码' })
+  password!: string;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '状态 1绑定 2解绑 通用联名电子卡不可解绑' })
+  state!: number;
+
+  @Column({ name: 'face_value', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0.00, comment: '面值' })
+  faceValue!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+
+  @ManyToOne(() => Agent, { nullable: true })
+  @JoinColumn({ name: 'agent_id', referencedColumnName: 'id' })
+  agent!: Agent | null;
+}

+ 99 - 0
packages/server/src/modules/card/card.schema.ts

@@ -0,0 +1,99 @@
+import { z } from '@hono/zod-openapi';
+import { UserSchema } from '@/server/modules/users/user.schema';
+
+export const CardSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '卡ID' }),
+  agentId: z.number().int().positive().nullable().openapi({
+    description: '代理商ID',
+    example: 1
+  }),
+  cardType: z.number().int().min(1).max(2).openapi({
+    description: '卡类型 1盛京通卡 2通用联名电子卡',
+    example: 1
+  }),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').openapi({
+    description: '卡号',
+    example: '1234567890'
+  }),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符').openapi({
+    description: '密码',
+    example: 'hashed_password'
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1绑定 2解绑 通用联名电子卡不可解绑',
+    example: 1
+  }),
+  faceValue: z.coerce.number().multipleOf(0.01, '面值最多保留两位小数').min(0, '面值不能为负数').default(0).openapi({
+    description: '面值',
+    example: 100.00
+  }),
+  agent: UserSchema.nullable().optional().openapi({
+    description: '代理商信息'
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  }),
+});
+
+export const CreateCardDto = z.object({
+  cardType: z.number().int().min(1, '卡类型必须在1-2之间').max(2, '卡类型必须在1-2之间').openapi({
+    description: '卡类型 1盛京通卡 2通用联名电子卡',
+    example: 1
+  }),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').openapi({
+    description: '卡号',
+    example: '1234567890'
+  }),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  faceValue: z.coerce.number().multipleOf(0.01, '面值最多保留两位小数').min(0, '面值不能为负数').default(0).openapi({
+    description: '面值',
+    example: 100.00
+  }),
+  agentId: z.number().int().positive().nullable().optional().openapi({
+    description: '代理商ID',
+    example: 1
+  })
+});
+
+export const UpdateCardDto = z.object({
+  cardType: z.number().int().min(1).max(2).optional().openapi({
+    description: '卡类型 1盛京通卡 2通用联名电子卡',
+    example: 1
+  }),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').optional().openapi({
+    description: '卡号',
+    example: '1234567890'
+  }),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符').optional().openapi({
+    description: '密码',
+    example: 'hashed_password'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1绑定 2解绑 通用联名电子卡不可解绑',
+    example: 1
+  }),
+  faceValue: z.coerce.number().multipleOf(0.01, '面值最多保留两位小数').min(0, '面值不能为负数').optional().openapi({
+    description: '面值',
+    example: 100.00
+  }),
+  agentId: z.number().int().positive().nullable().optional().openapi({
+    description: '代理商ID',
+    example: 1
+  })
+});

+ 9 - 0
packages/server/src/modules/card/card.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { Card } from './card.entity';
+
+export class CardService extends GenericCrudService<Card> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Card);
+  }
+}

+ 71 - 0
packages/server/src/modules/delivery-address/delivery-address.entity.ts

@@ -0,0 +1,71 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { User } from '@/server/modules/users/user.entity';
+import { City } from '@/server/modules/system/city.entity';
+
+@Entity('delivery_address')
+export class DeliveryAddress {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'user_id', type: 'int', unsigned: true, comment: '用户id' })
+  userId!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, comment: '姓名' })
+  name!: string;
+
+  @Column({ name: 'phone', type: 'varchar', length: 255, comment: '手机号' })
+  phone!: string;
+
+  @Column({ name: 'address', type: 'varchar', length: 255, comment: '详细地址' })
+  address!: string;
+
+  @Column({ name: 'receiver_province', type: 'bigint', unsigned: true, default: 0, comment: '省' })
+  receiverProvince!: number;
+
+  @Column({ name: 'receiver_city', type: 'bigint', unsigned: true, default: 0, comment: '市' })
+  receiverCity!: number;
+
+  @Column({ name: 'receiver_district', type: 'bigint', unsigned: true, default: 0, comment: '区' })
+  receiverDistrict!: number;
+
+  @Column({ name: 'receiver_town', type: 'bigint', unsigned: true, default: 0, comment: '街道' })
+  receiverTown!: number;
+
+  @Column({ name: 'state', type: 'int', unsigned: true, default: 1, comment: '是否可用1正常2禁用3删除(不显示)' })
+  state!: number;
+
+  @Column({ name: 'is_default', type: 'int', unsigned: true, default: 0, comment: '是否常用1是 2否' })
+  isDefault!: number;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+
+  @ManyToOne(() => User)
+  @JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
+  user!: User;
+
+  @ManyToOne(() => City)
+  @JoinColumn({ name: 'receiver_province', referencedColumnName: 'id' })
+  province!: City;
+
+  @ManyToOne(() => City)
+  @JoinColumn({ name: 'receiver_city', referencedColumnName: 'id' })
+  city!: City;
+
+  @ManyToOne(() => City)
+  @JoinColumn({ name: 'receiver_district', referencedColumnName: 'id' })
+  district!: City;
+
+  @ManyToOne(() => City)
+  @JoinColumn({ name: 'receiver_town', referencedColumnName: 'id' })
+  town!: City;
+}

+ 293 - 0
packages/server/src/modules/delivery-address/delivery-address.schema.ts

@@ -0,0 +1,293 @@
+import { z } from '@hono/zod-openapi';
+import { UserSchema } from '@/server/modules/users/user.schema';
+import { CitySchema } from '@/server/modules/system/city.schema';
+
+// 状态枚举
+export const DeliveryAddressStatusEnum = {
+  NORMAL: 1,
+  DISABLED: 2,
+  DELETED: 3
+} as const;
+
+export const IsDefaultEnum = {
+  NOT_DEFAULT: 0,
+  IS_DEFAULT: 1
+} as const;
+
+// 基础实体Schema
+export const DeliveryAddressSchema = z.object({
+  id: z.number()
+    .int('ID必须是整数')
+    .positive('ID必须是正整数')
+    .openapi({
+      description: '收货地址ID',
+      example: 1
+    }),
+  userId: z.number()
+    .int('用户ID必须是整数')
+    .positive('用户ID必须是正整数')
+    .openapi({
+      description: '用户ID',
+      example: 1
+    }),
+  name: z.string()
+    .min(1, '姓名不能为空')
+    .max(255, '姓名最多255个字符')
+    .openapi({
+      description: '收货人姓名',
+      example: '张三'
+    }),
+  phone: z.string()
+    .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号')
+    .openapi({
+      description: '收货人手机号',
+      example: '13800138000'
+    }),
+  address: z.string()
+    .min(1, '详细地址不能为空')
+    .max(255, '详细地址最多255个字符')
+    .openapi({
+      description: '详细地址',
+      example: '北京市朝阳区建国门外大街1号'
+    }),
+  receiverProvince: z.coerce.number<number>()
+    .int('省份ID必须是整数')
+    .positive('省份ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货省份ID',
+      example: 110000
+    }),
+  receiverCity: z.coerce.number<number>()
+    .int('城市ID必须是整数')
+    .positive('城市ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货城市ID',
+      example: 110100
+    }),
+  receiverDistrict: z.coerce.number<number>()
+    .int('区县ID必须是整数')
+    .positive('区县ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货区县ID',
+      example: 110105
+    }),
+  receiverTown: z.coerce.number<number>()
+    .int('街道ID必须是整数')
+    .positive('街道ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货街道ID',
+      example: 110105001
+    }),
+  state: z.coerce.number<number>()
+    .int('状态必须是整数')
+    .min(1, '状态最小值为1')
+    .max(3, '状态最大值为3')
+    .default(1)
+    .openapi({
+      description: '状态:1正常,2禁用,3删除',
+      example: 1
+    }),
+  isDefault: z.coerce.number<number>()
+    .int('是否默认必须是整数')
+    .min(0, '最小值为0')
+    .max(1, '最大值为1')
+    .default(0)
+    .openapi({
+      description: '是否默认地址:0否,1是',
+      example: 0
+    }),
+  createdBy: z.number()
+    .int('创建人ID必须是整数')
+    .positive('创建人ID必须是正整数')
+    .nullable()
+    .openapi({
+      description: '创建用户ID',
+      example: 1
+    }),
+  updatedBy: z.number()
+    .int('更新人ID必须是整数')
+    .positive('更新人ID必须是正整数')
+    .nullable()
+    .openapi({
+      description: '更新用户ID',
+      example: 1
+    }),
+  createdAt: z.coerce.date('创建时间格式不正确')
+    .openapi({
+      description: '创建时间',
+      example: '2024-01-01T12:00:00Z'
+    }),
+  updatedAt: z.coerce.date('更新时间格式不正确')
+    .openapi({
+      description: '更新时间',
+      example: '2024-01-01T12:00:00Z'
+    }),
+  user: UserSchema.optional().openapi({
+    description: '关联用户信息'
+  }),
+  province: CitySchema.optional().openapi({
+    description: '关联省份信息'
+  }),
+  city: CitySchema.optional().openapi({
+    description: '关联城市信息'
+  }),
+  district: CitySchema.optional().openapi({
+    description: '关联区县信息'
+  }),
+  town: CitySchema.optional().openapi({
+    description: '关联街道信息'
+  })
+});
+
+// 创建DTO
+export const CreateDeliveryAddressDto = z.object({
+  userId: z.number()
+    .int('用户ID必须是整数')
+    .positive('用户ID必须是正整数')
+    .openapi({
+      description: '用户ID',
+      example: 1
+    }),
+  name: z.string()
+    .min(1, '收货人姓名不能为空')
+    .max(255, '收货人姓名最多255个字符')
+    .openapi({
+      description: '收货人姓名',
+      example: '张三'
+    }),
+  phone: z.string()
+    .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号')
+    .openapi({
+      description: '收货人手机号',
+      example: '13800138000'
+    }),
+  address: z.string()
+    .min(1, '详细地址不能为空')
+    .max(255, '详细地址最多255个字符')
+    .openapi({
+      description: '详细地址',
+      example: '北京市朝阳区建国门外大街1号'
+    }),
+  receiverProvince: z.coerce.number<number>()
+    .int('省份ID必须是整数')
+    .positive('省份ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货省份ID',
+      example: 110000
+    }),
+  receiverCity: z.coerce.number<number>()
+    .int('城市ID必须是整数')
+    .positive('城市ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货城市ID',
+      example: 110100
+    }),
+  receiverDistrict: z.coerce.number<number>()
+    .int('区县ID必须是整数')
+    .positive('区县ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货区县ID',
+      example: 110105
+    }),
+  receiverTown: z.coerce.number<number>()
+    .int('街道ID必须是整数')
+    .positive('街道ID必须是正整数')
+    .default(0)
+    .openapi({
+      description: '收货街道ID',
+      example: 110105001
+    }),
+  isDefault: z.coerce.number<number>()
+    .int('是否默认必须是整数')
+    .min(0, '最小值为0')
+    .max(1, '最大值为1')
+    .default(0)
+    .openapi({
+      description: '是否默认地址:0否,1是',
+      example: 0
+    })
+});
+
+// 更新DTO
+export const UpdateDeliveryAddressDto = z.object({
+  name: z.string()
+    .min(1, '收货人姓名不能为空')
+    .max(255, '收货人姓名最多255个字符')
+    .optional()
+    .openapi({
+      description: '收货人姓名',
+      example: '张三'
+    }),
+  phone: z.string()
+    .regex(/^1[3-9]\d{9}$/, '请输入正确的手机号')
+    .optional()
+    .openapi({
+      description: '收货人手机号',
+      example: '13800138000'
+    }),
+  address: z.string()
+    .min(1, '详细地址不能为空')
+    .max(255, '详细地址最多255个字符')
+    .optional()
+    .openapi({
+      description: '详细地址',
+      example: '北京市朝阳区建国门外大街1号'
+    }),
+  receiverProvince: z.coerce.number<number>()
+    .int('省份ID必须是整数')
+    .positive('省份ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货省份ID',
+      example: 110000
+    }),
+  receiverCity: z.coerce.number<number>()
+    .int('城市ID必须是整数')
+    .positive('城市ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货城市ID',
+      example: 110100
+    }),
+  receiverDistrict: z.coerce.number<number>()
+    .int('区县ID必须是整数')
+    .positive('区县ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货区县ID',
+      example: 110105
+    }),
+  receiverTown: z.coerce.number<number>()
+    .int('街道ID必须是整数')
+    .positive('街道ID必须是正整数')
+    .optional()
+    .openapi({
+      description: '收货街道ID',
+      example: 110105001
+    }),
+  state: z.coerce.number<number>()
+    .int('状态必须是整数')
+    .min(1, '状态最小值为1')
+    .max(3, '状态最大值为3')
+    .optional()
+    .openapi({
+      description: '状态:1正常,2禁用,3删除',
+      example: 1
+    }),
+  isDefault: z.coerce.number<number>()
+    .int('是否默认必须是整数')
+    .min(0, '最小值为0')
+    .max(1, '最大值为1')
+    .optional()
+    .openapi({
+      description: '是否默认地址:0否,1是',
+      example: 0
+    })
+});

+ 58 - 0
packages/server/src/modules/delivery-address/delivery-address.service.ts

@@ -0,0 +1,58 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { DeliveryAddress } from './delivery-address.entity';
+
+export class DeliveryAddressService extends GenericCrudService<DeliveryAddress> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, DeliveryAddress);
+  }
+
+  /**
+   * 获取用户的收货地址列表
+   * @param userId 用户ID
+   * @returns 收货地址列表
+   */
+  async findByUser(userId: number): Promise<DeliveryAddress[]> {
+    return this.repository.find({
+      where: { userId, state: 1 },
+      relations: ['user', 'province', 'city', 'district', 'town'],
+      order: { isDefault: 'DESC', createdAt: 'DESC' }
+    });
+  }
+
+  /**
+   * 设置默认地址
+   * @param id 地址ID
+   * @param userId 用户ID
+   * @returns 是否设置成功
+   */
+  async setDefault(id: number, userId: number): Promise<boolean> {
+    await this.repository.manager.transaction(async (transactionalEntityManager) => {
+      // 先将该用户的所有地址设为非默认
+      await transactionalEntityManager.update(DeliveryAddress, 
+        { userId }, 
+        { isDefault: 0 }
+      );
+      
+      // 将指定地址设为默认
+      await transactionalEntityManager.update(DeliveryAddress,
+        { id, userId },
+        { isDefault: 1 }
+      );
+    });
+    
+    return true;
+  }
+
+  /**
+   * 获取用户的默认地址
+   * @param userId 用户ID
+   * @returns 默认地址或null
+   */
+  async findDefaultByUser(userId: number): Promise<DeliveryAddress | null> {
+    return this.repository.findOne({
+      where: { userId, isDefault: 1, state: 1 },
+      relations: ['user', 'province', 'city', 'district', 'town']
+    });
+  }
+}

+ 39 - 0
packages/server/src/modules/goods/goods-category.entity.ts

@@ -0,0 +1,39 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
+import { File } from '@/server/modules/files/file.entity';
+
+@Entity('goods_category')
+export class GoodsCategory {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, comment: '类别名称' })
+  name!: string;
+
+  @Column({ name: 'parent_id', type: 'int', unsigned: true, default: 0, comment: '上级id' })
+  parentId!: number;
+
+  @Column({ name: 'image_file_id', type: 'int', unsigned: true, nullable: true, comment: '分类图片文件ID' })
+  imageFileId!: number | null;
+
+  @Column({ name: 'level', type: 'int', unsigned: true, default: 0, comment: '层级' })
+  level!: number;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '状态 1可用 2不可用' })
+  state!: number;
+
+  @Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', comment: '创建时间' })
+  createdAt!: Date;
+
+  @Column({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+
+  @ManyToOne(() => File, { nullable: true })
+  @JoinColumn({ name: 'image_file_id', referencedColumnName: 'id' })
+  imageFile!: File | null;
+}

+ 91 - 0
packages/server/src/modules/goods/goods-category.schema.ts

@@ -0,0 +1,91 @@
+import { z } from '@hono/zod-openapi';
+import { FileSchema } from '@/server/modules/files/file.schema';
+
+export const GoodsCategorySchema = z.object({
+  id: z.number().int().positive().openapi({ description: '类别ID' }),
+  name: z.string().min(1, '类别名称不能为空').max(255, '类别名称最多255个字符').openapi({
+    description: '类别名称',
+    example: '电子产品'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级分类ID',
+    example: 0
+  }),
+  imageFileId: z.number().int().positive().nullable().openapi({
+    description: '分类图片文件ID',
+    example: 1
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').default(0).openapi({
+    description: '分类层级',
+    example: 1
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  }),
+  imageFile: FileSchema.nullable().optional().openapi({
+    description: '分类图片信息'
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});
+
+export const CreateGoodsCategoryDto = z.object({
+  name: z.string().min(1, '类别名称不能为空').max(255, '类别名称最多255个字符').openapi({
+    description: '类别名称',
+    example: '电子产品'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级分类ID',
+    example: 0
+  }),
+  imageFileId: z.number().int().positive().nullable().optional().openapi({
+    description: '分类图片文件ID',
+    example: 1
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').default(0).openapi({
+    description: '分类层级',
+    example: 1
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  })
+});
+
+export const UpdateGoodsCategoryDto = z.object({
+  name: z.string().min(1, '类别名称不能为空').max(255, '类别名称最多255个字符').optional().openapi({
+    description: '类别名称',
+    example: '电子产品'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').optional().openapi({
+    description: '上级分类ID',
+    example: 0
+  }),
+  imageFileId: z.number().int().positive().nullable().optional().openapi({
+    description: '分类图片文件ID',
+    example: 1
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').optional().openapi({
+    description: '分类层级',
+    example: 1
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  })
+});

+ 9 - 0
packages/server/src/modules/goods/goods-category.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { GoodsCategory } from './goods-category.entity';
+
+export class GoodsCategoryService extends GenericCrudService<GoodsCategory> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, GoodsCategory);
+  }
+}

+ 115 - 0
packages/server/src/modules/goods/goods.entity.ts

@@ -0,0 +1,115 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn, ManyToMany, JoinTable } from 'typeorm';
+import { GoodsCategory } from './goods-category.entity';
+import { Supplier } from '@/server/modules/supplier/supplier.entity';
+import { File } from '@/server/modules/files/file.entity';
+import { Merchant } from '@/server/modules/merchant/merchant.entity';
+
+@Entity('goods')
+export class Goods {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, comment: '商品名称' })
+  name!: string;
+
+  @Column({ name: 'price', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '售卖价' })
+  price!: number;
+
+  @Column({ name: 'cost_price', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '成本价' })
+  costPrice!: number;
+
+  @Column({ name: 'sales_num', type: 'bigint', unsigned: true, default: 0, comment: '销售数量' })
+  salesNum!: number;
+
+  @Column({ name: 'click_num', type: 'bigint', unsigned: true, default: 0, comment: '点击次数' })
+  clickNum!: number;
+
+  @Column({ name: 'category_id1', type: 'int', unsigned: true, default: 0, comment: '一级类别id' })
+  categoryId1!: number;
+
+  @Column({ name: 'category_id2', type: 'int', unsigned: true, default: 0, comment: '二级类别id' })
+  categoryId2!: number;
+
+  @Column({ name: 'category_id3', type: 'int', unsigned: true, default: 0, comment: '三级类别id' })
+  categoryId3!: number;
+
+  @Column({ name: 'goods_type', type: 'tinyint', unsigned: true, default: 1, comment: '订单类型 1实物产品 2虚拟产品' })
+  goodsType!: number;
+
+  @Column({ name: 'supplier_id', type: 'int', unsigned: true, nullable: true, comment: '所属供应商id' })
+  supplierId!: number | null;
+
+  @Column({ name: 'merchant_id', type: 'int', unsigned: true, nullable: true, comment: '所属商户id' })
+  merchantId!: number | null;
+
+  @Column({ name: 'image_file_id', type: 'int', unsigned: true, nullable: true, comment: '商品主图文件ID' })
+  imageFileId!: number | null;
+
+  @ManyToMany(() => File)
+  @JoinTable({
+    name: 'goods_slide_images',
+    joinColumn: { name: 'goods_id', referencedColumnName: 'id' },
+    inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' }
+  })
+  slideImages!: File[];
+
+  @Column({ name: 'detail', type: 'text', nullable: true, comment: '商品详情' })
+  detail!: string | null;
+
+  @Column({ name: 'instructions', type: 'varchar', length: 255, nullable: true, comment: '简介' })
+  instructions!: string | null;
+
+  @Column({ name: 'sort', type: 'int', unsigned: true, default: 0, comment: '排序' })
+  sort!: number;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '状态 1可用 2不可用' })
+  state!: number;
+
+  @Column({ name: 'stock', type: 'bigint', unsigned: true, default: 0, comment: '库存' })
+  stock!: number;
+
+  @Column({ name: 'spu_id', type: 'int', unsigned: true, default: 0, comment: '主商品ID' })
+  spuId!: number;
+
+  @Column({ name: 'spu_name', type: 'varchar', length: 255, nullable: true, comment: '主商品名称' })
+  spuName!: string | null;
+
+  @Column({ name: 'lowest_buy', type: 'int', unsigned: true, default: 1, comment: '最小起购量' })
+  lowestBuy!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+
+  @ManyToOne(() => GoodsCategory, { nullable: true })
+  @JoinColumn({ name: 'category_id1', referencedColumnName: 'id' })
+  category1!: GoodsCategory | null;
+
+  @ManyToOne(() => GoodsCategory, { nullable: true })
+  @JoinColumn({ name: 'category_id2', referencedColumnName: 'id' })
+  category2!: GoodsCategory | null;
+
+  @ManyToOne(() => GoodsCategory, { nullable: true })
+  @JoinColumn({ name: 'category_id3', referencedColumnName: 'id' })
+  category3!: GoodsCategory | null;
+
+  @ManyToOne(() => Supplier, { nullable: true })
+  @JoinColumn({ name: 'supplier_id', referencedColumnName: 'id' })
+  supplier!: Supplier | null;
+
+  @ManyToOne(() => File, { nullable: true })
+  @JoinColumn({ name: 'image_file_id', referencedColumnName: 'id' })
+  imageFile!: File | null;
+
+  @ManyToOne(() => Merchant, { nullable: true })
+  @JoinColumn({ name: 'merchant_id', referencedColumnName: 'id' })
+  merchant!: Merchant | null;
+}

+ 291 - 0
packages/server/src/modules/goods/goods.schema.ts

@@ -0,0 +1,291 @@
+import { z } from '@hono/zod-openapi';
+import { GoodsCategorySchema } from './goods-category.schema';
+import { SupplierSchema } from '@/server/modules/supplier/supplier.schema';
+import { FileSchema } from '@/server/modules/files/file.schema';
+import { MerchantSchema } from '@/server/modules/merchant/merchant.schema';
+
+export const GoodsSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '商品ID' }),
+  name: z.string().min(1, '商品名称不能为空').max(255, '商品名称最多255个字符').openapi({
+    description: '商品名称',
+    example: 'iPhone 15'
+  }),
+  price: z.coerce.number().multipleOf(0.01, '价格最多保留两位小数').min(0, '价格不能为负数').default(0).openapi({
+    description: '售卖价',
+    example: 5999.99
+  }),
+  costPrice: z.coerce.number().multipleOf(0.01, '成本价最多保留两位小数').min(0, '成本价不能为负数').default(0).openapi({
+    description: '成本价',
+    example: 4999.99
+  }),
+  salesNum: z.coerce.number().int().nonnegative('销售数量必须为非负数').default(0).openapi({
+    description: '销售数量',
+    example: 100
+  }),
+  clickNum: z.coerce.number().int().nonnegative('点击次数必须为非负数').default(0).openapi({
+    description: '点击次数',
+    example: 1000
+  }),
+  categoryId1: z.number().int().nonnegative('一级类别ID必须为非负数').default(0).openapi({
+    description: '一级类别id',
+    example: 1
+  }),
+  categoryId2: z.number().int().nonnegative('二级类别ID必须为非负数').default(0).openapi({
+    description: '二级类别id',
+    example: 2
+  }),
+  categoryId3: z.number().int().nonnegative('三级类别ID必须为非负数').default(0).openapi({
+    description: '三级类别id',
+    example: 3
+  }),
+  goodsType: z.number().int().min(1).max(2).default(1).openapi({
+    description: '订单类型 1实物产品 2虚拟产品',
+    example: 1
+  }),
+  supplierId: z.number().int().positive().nullable().openapi({
+    description: '所属供应商id',
+    example: 1
+  }),
+  merchantId: z.number().int().positive().nullable().openapi({
+    description: '所属商户id',
+    example: 1
+  }),
+  imageFileId: z.number().int().positive().nullable().openapi({
+    description: '商品主图文件ID',
+    example: 1
+  }),
+  slideImages: z.array(FileSchema).nullable().optional().openapi({
+    description: '商品轮播图文件列表',
+    example: [{
+      id: 1,
+      name: 'image1.jpg',
+      fullUrl: 'https://example.com/image1.jpg',
+      type: 'image/jpeg',
+      size: 102400
+    }]
+  }),
+  detail: z.string().nullable().optional().openapi({
+    description: '商品详情',
+    example: '这是商品详情内容'
+  }),
+  instructions: z.string().max(255, '简介最多255个字符').nullable().optional().openapi({
+    description: '简介',
+    example: '高品质智能手机'
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').default(0).openapi({
+    description: '排序',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  }),
+  stock: z.coerce.number().int().nonnegative('库存必须为非负数').default(0).openapi({
+    description: '库存',
+    example: 100
+  }),
+  spuId: z.number().int().nonnegative('主商品ID必须为非负数').default(0).openapi({
+    description: '主商品ID',
+    example: 0
+  }),
+  spuName: z.string().max(255, '主商品名称最多255个字符').nullable().optional().openapi({
+    description: '主商品名称',
+    example: 'iPhone系列'
+  }),
+  lowestBuy: z.number().int().positive('最小起购量必须为正整数').default(1).openapi({
+    description: '最小起购量',
+    example: 1
+  }),
+  category1: GoodsCategorySchema.nullable().optional().openapi({
+    description: '一级分类信息'
+  }),
+  category2: GoodsCategorySchema.nullable().optional().openapi({
+    description: '二级分类信息'
+  }),
+  category3: GoodsCategorySchema.nullable().optional().openapi({
+    description: '三级分类信息'
+  }),
+  supplier: SupplierSchema.nullable().optional().openapi({
+    description: '供应商信息'
+  }),
+  merchant: MerchantSchema.nullable().optional().openapi({
+    description: '商户信息'
+  }),
+  imageFile: FileSchema.nullable().optional().openapi({
+    description: '商品主图信息'
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  }),
+});
+
+export const CreateGoodsDto = z.object({
+  name: z.string().min(1, '商品名称不能为空').max(255, '商品名称最多255个字符').openapi({
+    description: '商品名称',
+    example: 'iPhone 15'
+  }),
+  price: z.coerce.number<number>().multipleOf(0.01, '价格最多保留两位小数').min(0, '价格不能为负数').default(0).openapi({
+    description: '售卖价',
+    example: 5999.99
+  }),
+  costPrice: z.coerce.number<number>().multipleOf(0.01, '成本价最多保留两位小数').min(0, '成本价不能为负数').default(0).openapi({
+    description: '成本价',
+    example: 4999.99
+  }),
+  categoryId1: z.number().int().nonnegative('一级类别ID必须为非负数').default(0).openapi({
+    description: '一级类别id',
+    example: 1
+  }),
+  categoryId2: z.number().int().nonnegative('二级类别ID必须为非负数').default(0).openapi({
+    description: '二级类别id',
+    example: 2
+  }),
+  categoryId3: z.number().int().nonnegative('三级类别ID必须为非负数').default(0).openapi({
+    description: '三级类别id',
+    example: 3
+  }),
+  goodsType: z.number().int().min(1).max(2).default(1).openapi({
+    description: '订单类型 1实物产品 2虚拟产品',
+    example: 1
+  }),
+  supplierId: z.number().int().positive().nullable().optional().openapi({
+    description: '所属供应商id',
+    example: 1
+  }),
+  merchantId: z.number().int().positive().nullable().optional().openapi({
+    description: '所属商户id',
+    example: 1
+  }),
+  imageFileId: z.number().int().positive().nullable().optional().openapi({
+    description: '商品主图文件ID',
+    example: 1
+  }),
+  slideImageIds: z.array(z.number().int().positive()).nullable().optional().openapi({
+    description: '商品轮播图文件ID数组',
+    example: [1, 2, 3]
+  }),
+  detail: z.string().nullable().optional().openapi({
+    description: '商品详情',
+    example: '这是商品详情内容'
+  }),
+  instructions: z.string().max(255, '简介最多255个字符').nullable().optional().openapi({
+    description: '简介',
+    example: '高品质智能手机'
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').default(0).openapi({
+    description: '排序',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  }),
+  stock: z.coerce.number<number>().int().nonnegative('库存必须为非负数').default(0).openapi({
+    description: '库存',
+    example: 100
+  }),
+  spuId: z.number().int().nonnegative('主商品ID必须为非负数').default(0).openapi({
+    description: '主商品ID',
+    example: 0
+  }),
+  spuName: z.string().max(255, '主商品名称最多255个字符').nullable().optional().openapi({
+    description: '主商品名称',
+    example: 'iPhone系列'
+  }),
+  lowestBuy: z.number().int().positive('最小起购量必须为正整数').default(1).openapi({
+    description: '最小起购量',
+    example: 1
+  })
+});
+
+export const UpdateGoodsDto = z.object({
+  name: z.string().min(1, '商品名称不能为空').max(255, '商品名称最多255个字符').optional().openapi({
+    description: '商品名称',
+    example: 'iPhone 15'
+  }),
+  price: z.coerce.number<number>().multipleOf(0.01, '价格最多保留两位小数').min(0, '价格不能为负数').optional().openapi({
+    description: '售卖价',
+    example: 5999.99
+  }),
+  costPrice: z.coerce.number<number>().multipleOf(0.01, '成本价最多保留两位小数').min(0, '成本价不能为负数').optional().openapi({
+    description: '成本价',
+    example: 4999.99
+  }),
+  categoryId1: z.number().int().nonnegative('一级类别ID必须为非负数').optional().openapi({
+    description: '一级类别id',
+    example: 1
+  }),
+  categoryId2: z.number().int().nonnegative('二级类别ID必须为非负数').optional().openapi({
+    description: '二级类别id',
+    example: 2
+  }),
+  categoryId3: z.number().int().nonnegative('三级类别ID必须为非负数').optional().openapi({
+    description: '三级类别id',
+    example: 3
+  }),
+  goodsType: z.number().int().min(1).max(2).optional().openapi({
+    description: '订单类型 1实物产品 2虚拟产品',
+    example: 1
+  }),
+  supplierId: z.number().int().positive().nullable().optional().openapi({
+    description: '所属供应商id',
+    example: 1
+  }),
+  merchantId: z.number().int().positive().nullable().optional().openapi({
+    description: '所属商户id',
+    example: 1
+  }),
+  imageFileId: z.number().int().positive().nullable().optional().openapi({
+    description: '商品主图文件ID',
+    example: 1
+  }),
+  slideImageIds: z.array(z.number().int().positive()).nullable().optional().openapi({
+    description: '商品轮播图文件ID数组',
+    example: [1, 2, 3]
+  }),
+  detail: z.string().nullable().optional().openapi({
+    description: '商品详情',
+    example: '这是商品详情内容'
+  }),
+  instructions: z.string().max(255, '简介最多255个字符').nullable().optional().openapi({
+    description: '简介',
+    example: '高品质智能手机'
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').optional().openapi({
+    description: '排序',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  }),
+  stock: z.coerce.number<number>().int().nonnegative('库存必须为非负数').optional().openapi({
+    description: '库存',
+    example: 100
+  }),
+  spuId: z.number().int().nonnegative('主商品ID必须为非负数').optional().openapi({
+    description: '主商品ID',
+    example: 0
+  }),
+  spuName: z.string().max(255, '主商品名称最多255个字符').nullable().optional().openapi({
+    description: '主商品名称',
+    example: 'iPhone系列'
+  }),
+  lowestBuy: z.number().int().positive('最小起购量必须为正整数').optional().openapi({
+    description: '最小起购量',
+    example: 1
+  })
+});

+ 9 - 0
packages/server/src/modules/goods/goods.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { Goods } from './goods.entity';
+
+export class GoodsService extends GenericCrudService<Goods> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Goods);
+  }
+}

+ 59 - 0
packages/server/src/modules/goods/schemas/random.schema.ts

@@ -0,0 +1,59 @@
+import { z } from '@hono/zod-openapi';
+import { GoodsSchema } from '@/server/modules/goods/goods.schema';
+
+// 随机商品列表查询参数Schema
+export const RandomGoodsQuerySchema = z.object({
+  limit: z.coerce.number().int().positive().min(1).max(50).default(10).openapi({
+    description: '返回商品数量限制',
+    example: 10
+  }),
+  categoryId: z.coerce.number().int().positive().optional().openapi({
+    description: '指定商品分类ID',
+    example: 1
+  }),
+  includeImages: z.coerce.boolean().default(true).openapi({
+    description: '是否包含商品图片',
+    example: true
+  })
+});
+
+// 随机商品列表响应Schema
+export const RandomGoodsResponseSchema = z.object({
+  data: z.array(GoodsSchema).openapi({
+    description: '随机商品列表',
+    example: [{
+      id: 1,
+      name: 'iPhone 15',
+      price: 5999.99,
+      costPrice: 4999.99,
+      salesNum: 100,
+      clickNum: 1000,
+      categoryId1: 1,
+      categoryId2: 2,
+      categoryId3: 3,
+      goodsType: 1,
+      supplierId: 1,
+      imageFileId: 1,
+      detail: null,
+      instructions: '高品质智能手机',
+      sort: 0,
+      state: 1,
+      stock: 100,
+      spuId: 0,
+      spuName: null,
+      lowestBuy: 1,
+      createdAt: new Date('2024-01-01T12:00:00Z'),
+      updatedAt: new Date('2024-01-01T12:00:00Z'),
+      createdBy: 1,
+      updatedBy: 1
+    }]
+  }),
+  total: z.number().int().nonnegative().openapi({
+    description: '符合条件的商品总数',
+    example: 100
+  })
+});
+
+// 类型定义
+export type RandomGoodsQuery = z.infer<typeof RandomGoodsQuerySchema>;
+export type RandomGoodsResponse = z.infer<typeof RandomGoodsResponseSchema>;

+ 31 - 0
packages/server/src/modules/logistics/express-company.entity.ts

@@ -0,0 +1,31 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('express_company')
+export class ExpressCompany {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 50, comment: '物流公司名称' })
+  name!: string;
+
+  @Column({ name: 'code', type: 'varchar', length: 20, comment: '物流编号' })
+  code!: string;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '使用状态 1可用 2禁用' })
+  state!: number;
+
+  @Column({ name: 'sort', type: 'int', unsigned: true, nullable: true, comment: '优先级 值越大越优先' })
+  sort!: number | null;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+}

+ 75 - 0
packages/server/src/modules/logistics/express-company.schema.ts

@@ -0,0 +1,75 @@
+import { z } from '@hono/zod-openapi';
+
+export const ExpressCompanySchema = z.object({
+  id: z.number().int().positive().openapi({ description: '快递公司ID' }),
+  name: z.string().min(1, '物流公司名称不能为空').max(50, '物流公司名称最多50个字符').openapi({
+    description: '物流公司名称',
+    example: '顺丰速运'
+  }),
+  code: z.string().min(1, '物流编号不能为空').max(20, '物流编号最多20个字符').openapi({
+    description: '物流编号',
+    example: 'SF'
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '使用状态 1可用 2禁用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').nullable().openapi({
+    description: '优先级 值越大越优先',
+    example: 100
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});
+
+export const CreateExpressCompanyDto = z.object({
+  name: z.string().min(1, '物流公司名称不能为空').max(50, '物流公司名称最多50个字符').openapi({
+    description: '物流公司名称',
+    example: '顺丰速运'
+  }),
+  code: z.string().min(1, '物流编号不能为空').max(20, '物流编号最多20个字符').openapi({
+    description: '物流编号',
+    example: 'SF'
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '使用状态 1可用 2禁用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').nullable().optional().openapi({
+    description: '优先级 值越大越优先',
+    example: 100
+  })
+});
+
+export const UpdateExpressCompanyDto = z.object({
+  name: z.string().min(1, '物流公司名称不能为空').max(50, '物流公司名称最多50个字符').optional().openapi({
+    description: '物流公司名称',
+    example: '顺丰速运'
+  }),
+  code: z.string().min(1, '物流编号不能为空').max(20, '物流编号最多20个字符').optional().openapi({
+    description: '物流编号',
+    example: 'SF'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '使用状态 1可用 2禁用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').nullable().optional().openapi({
+    description: '优先级 值越大越优先',
+    example: 100
+  })
+});

+ 9 - 0
packages/server/src/modules/logistics/express-company.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { ExpressCompany } from './express-company.entity';
+
+export class ExpressCompanyService extends GenericCrudService<ExpressCompany> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, ExpressCompany);
+  }
+}

+ 58 - 0
packages/server/src/modules/merchant/merchant.entity.ts

@@ -0,0 +1,58 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('merchant')
+export class Merchant {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, nullable: true, comment: '商户名称' })
+  name!: string | null;
+
+  @Column({ name: 'username', type: 'varchar', length: 20, unique: true, comment: '用户名' })
+  username!: string;
+
+  @Column({ name: 'password', type: 'varchar', length: 255, comment: '密码' })
+  password!: string;
+
+  @Column({ name: 'phone', type: 'char', length: 11, nullable: true, comment: '手机号码' })
+  phone!: string | null;
+
+  @Column({ name: 'realname', type: 'varchar', length: 20, nullable: true, comment: '姓名' })
+  realname!: string | null;
+
+  @Column({ name: 'login_num', type: 'int', unsigned: true, default: 0, comment: '登录次数' })
+  loginNum!: number;
+
+  @Column({ name: 'login_time', type: 'int', unsigned: true, default: 0, comment: '登录时间' })
+  loginTime!: number;
+
+  @Column({ name: 'login_ip', type: 'varchar', length: 15, nullable: true, comment: '登录IP' })
+  loginIp!: string | null;
+
+  @Column({ name: 'last_login_time', type: 'int', unsigned: true, default: 0, comment: '上次登录时间' })
+  lastLoginTime!: number;
+
+  @Column({ name: 'last_login_ip', type: 'varchar', length: 15, nullable: true, comment: '上次登录IP' })
+  lastLoginIp!: string | null;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 2, comment: '状态 1启用 2禁用' })
+  state!: number;
+
+  @Column({ name: 'rsa_public_key', type: 'varchar', length: 2000, nullable: true, comment: '公钥' })
+  rsaPublicKey!: string | null;
+
+  @Column({ name: 'aes_key', type: 'varchar', length: 32, nullable: true, comment: 'aes秘钥' })
+  aesKey!: string | null;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+}

+ 143 - 0
packages/server/src/modules/merchant/merchant.schema.ts

@@ -0,0 +1,143 @@
+import { z } from '@hono/zod-openapi';
+
+export const MerchantSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '商户ID' }),
+  name: z.string().min(1, '商户名称不能为空').max(255, '商户名称最多255个字符').nullable().openapi({
+    description: '商户名称',
+    example: '商户A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').openapi({
+    description: '用户名',
+    example: 'merchant001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '李四'
+  }),
+  loginNum: z.number().int().nonnegative('登录次数必须为非负数').default(0).openapi({
+    description: '登录次数',
+    example: 0
+  }),
+  loginTime: z.number().int().nonnegative('登录时间必须为非负数').default(0).openapi({
+    description: '登录时间',
+    example: 0
+  }),
+  loginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '登录IP',
+    example: '192.168.1.1'
+  }),
+  lastLoginTime: z.number().int().nonnegative('上次登录时间必须为非负数').default(0).openapi({
+    description: '上次登录时间',
+    example: 0
+  }),
+  lastLoginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '上次登录IP',
+    example: '192.168.1.1'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});
+
+export const CreateMerchantDto = z.object({
+  name: z.string().min(1, '商户名称不能为空').max(255, '商户名称最多255个字符').nullable().optional().openapi({
+    description: '商户名称',
+    example: '商户A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').openapi({
+    description: '用户名',
+    example: 'merchant001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '李四'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  })
+});
+
+export const UpdateMerchantDto = z.object({
+  name: z.string().min(1, '商户名称不能为空').max(255, '商户名称最多255个字符').nullable().optional().openapi({
+    description: '商户名称',
+    example: '商户A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').optional().openapi({
+    description: '用户名',
+    example: 'merchant001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').optional().openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '李四'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  }),
+  rsaPublicKey: z.string().max(2000, '公钥最多2000个字符').nullable().optional().openapi({
+    description: '公钥',
+    example: '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n-----END PUBLIC KEY-----'
+  }),
+  aesKey: z.string().max(32, 'aes秘钥最多32个字符').nullable().optional().openapi({
+    description: 'aes秘钥',
+    example: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6'
+  })
+});

+ 9 - 0
packages/server/src/modules/merchant/merchant.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { Merchant } from './merchant.entity';
+
+export class MerchantService extends GenericCrudService<Merchant> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Merchant);
+  }
+}

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

@@ -0,0 +1,82 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { Order } from './order.entity';
+import { Goods } from '@/server/modules/goods/goods.entity';
+import { Supplier } from '@/server/modules/supplier/supplier.entity';
+import { File } from '@/server/modules/files/file.entity';
+
+@Entity('orders_goods')
+export class OrderGoods {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'order_id', type: 'int', unsigned: true, comment: '订单表id' })
+  orderId!: number;
+
+  @Column({ name: 'order_no', type: 'varchar', length: 50, nullable: true, comment: '订单号' })
+  orderNo!: string | null;
+
+  @Column({ name: 'goods_id', type: 'int', unsigned: true, comment: '商品id' })
+  goodsId!: number;
+
+  @Column({ name: 'goods_name', type: 'varchar', length: 255, nullable: true, comment: '商品名称' })
+  goodsName!: string | null;
+
+  @Column({ name: 'goods_type', type: 'int', default: 1, comment: '1实物产品2虚拟订单' })
+  goodsType!: number;
+
+  @Column({ name: 'supplier_id', type: 'int', unsigned: true, default: 0, comment: '供货商id' })
+  supplierId!: number;
+
+  @Column({ name: 'cost_price', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '成本价' })
+  costPrice!: number;
+
+  @Column({ name: 'price', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '售价' })
+  price!: number;
+
+  @Column({ name: 'num', type: 'int', default: 0, comment: '数量' })
+  num!: number;
+
+  @Column({ name: 'freight_amount', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '运费' })
+  freightAmount!: number;
+
+  @Column({ name: 'state', type: 'int', default: 0, comment: '状态(0未发货、1已发货)' })
+  state!: number;
+
+  @Column({ name: 'express_name', type: 'varchar', length: 255, nullable: true, comment: '快递名称' })
+  expressName!: string | null;
+
+  @Column({ name: 'express_no', type: 'int', nullable: true, comment: '单号' })
+  expressNo!: number | null;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建人ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新人ID' })
+  updatedBy!: number | null;
+
+  @Column({ name: 'image_file_id', type: 'int', unsigned: true, nullable: true, comment: '商品图片文件ID' })
+  imageFileId!: number | null;
+
+  // 关联关系
+  @ManyToOne(() => Order)
+  @JoinColumn({ name: 'order_id', referencedColumnName: 'id' })
+  order!: Order;
+
+  @ManyToOne(() => Goods)
+  @JoinColumn({ name: 'goods_id', referencedColumnName: 'id' })
+  goods!: Goods;
+
+  @ManyToOne(() => Supplier)
+  @JoinColumn({ name: 'supplier_id', referencedColumnName: 'id' })
+  supplier!: Supplier;
+
+  @ManyToOne(() => File, { nullable: true })
+  @JoinColumn({ name: 'image_file_id', referencedColumnName: 'id' })
+  imageFile!: File | null;
+}

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

@@ -0,0 +1,246 @@
+import { z } from '@hono/zod-openapi';
+
+// 订单商品状态枚举
+export const OrderGoodsStatus = {
+  UN_SHIPPED: 0, // 未发货
+  SHIPPED: 1, // 已发货
+} as const;
+
+// 商品类型枚举
+export const GoodsType = {
+  PHYSICAL: 1, // 实物产品
+  VIRTUAL: 2, // 虚拟订单
+} as const;
+
+// 订单商品基础Schema
+export const OrderGoodsSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '订单商品ID',
+    example: 1
+  }),
+  orderId: z.number().int().positive().openapi({
+    description: '订单ID',
+    example: 1
+  }),
+  orderNo: z.string().max(50, '订单号最多50个字符').nullable().optional().openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  goodsId: z.number().int().positive().openapi({
+    description: '商品ID',
+    example: 1
+  }),
+  goodsName: z.string().max(255, '商品名称最多255个字符').nullable().optional().openapi({
+    description: '商品名称',
+    example: '苹果手机'
+  }),
+  goodsType: z.coerce.number().int().min(1, '商品类型最小为1').max(2, '商品类型最大为2').default(1).openapi({
+    description: '1实物产品2虚拟订单',
+    example: 1
+  }),
+  supplierId: z.coerce.number().int().positive().default(0).openapi({
+    description: '供货商ID',
+    example: 1
+  }),
+  costPrice: z.coerce.number().min(0, '成本价不能小于0').max(999999.99, '成本价不能超过999999.99').default(0).openapi({
+    description: '成本价',
+    example: 50.00
+  }),
+  price: z.coerce.number().min(0, '售价不能小于0').max(999999.99, '售价不能超过999999.99').default(0).openapi({
+    description: '售价',
+    example: 99.99
+  }),
+  num: z.coerce.number().int().min(0, '数量不能小于0').max(99999, '数量不能超过99999').default(0).openapi({
+    description: '数量',
+    example: 2
+  }),
+  freightAmount: z.coerce.number().min(0, '运费不能小于0').max(999999.99, '运费不能超过999999.99').default(0).optional().openapi({
+    description: '运费',
+    example: 10.00
+  }),
+  state: z.coerce.number().int().min(0, '状态最小为0').max(1, '状态最大为1').default(0).openapi({
+    description: '状态(0未发货、1已发货)',
+    example: 0
+  }),
+  expressName: z.string().max(255, '快递名称最多255个字符').nullable().optional().openapi({
+    description: '快递名称',
+    example: '顺丰快递'
+  }),
+  expressNo: z.coerce.number().int().positive().nullable().optional().openapi({
+    description: '快递单号',
+    example: 1234567890
+  }),
+  createdBy: z.number().int().positive().nullable().optional().openapi({
+    description: '创建人ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().optional().openapi({
+    description: '更新人ID',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  imageFileId: z.number().int().positive().nullable().openapi({
+    example: 1,
+    description: '商品图片文件ID'
+  }),
+  imageFile: z.object({
+    id: z.number().int().positive().openapi({ description: '文件ID' }),
+    name: z.string().max(255).openapi({ description: '文件名', example: 'goods.jpg' }),
+    fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/goods.jpg' }),
+    type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+    size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+  }).nullable().optional().openapi({
+    description: '商品图片文件信息'
+  }),
+  // 关联实体
+  order: z.object({
+    id: z.number().int().positive().openapi({ description: '订单ID' }),
+    orderNo: z.string().openapi({ description: '订单号', example: 'ORD20240101123456' })
+  }).nullable().optional().openapi({
+    description: '订单信息'
+  }),
+  goods: z.object({
+    id: z.number().int().positive().openapi({ description: '商品ID' }),
+    name: z.string().openapi({ description: '商品名称', example: '苹果手机' }),
+    image: z.string().nullable().openapi({ description: '商品图片', example: 'https://example.com/goods.jpg' })
+  }).nullable().optional().openapi({
+    description: '商品信息'
+  }),
+  supplier: z.object({
+    id: z.number().int().positive().openapi({ description: '供货商ID' }),
+    name: z.string().openapi({ description: '供货商名称', example: '供货商A' })
+  }).nullable().optional().openapi({
+    description: '供货商信息'
+  })
+});
+
+// 创建订单商品DTO
+export const CreateOrderGoodsDto = z.object({
+  orderId: z.number().int().positive('订单ID必须是正整数').openapi({
+    description: '订单ID',
+    example: 1
+  }),
+  orderNo: z.string().max(50, '订单号最多50个字符').nullable().optional().openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  goodsId: z.number().int().positive('商品ID必须是正整数').openapi({
+    description: '商品ID',
+    example: 1
+  }),
+  goodsName: z.string().max(255, '商品名称最多255个字符').nullable().optional().openapi({
+    description: '商品名称',
+    example: '苹果手机'
+  }),
+  goodsType: z.coerce.number().int().min(1, '商品类型最小为1').max(2, '商品类型最大为2').default(1).optional().openapi({
+    description: '1实物产品2虚拟订单',
+    example: 1
+  }),
+  supplierId: z.coerce.number().int().positive().default(0).optional().openapi({
+    description: '供货商ID',
+    example: 1
+  }),
+  costPrice: z.coerce.number().min(0, '成本价不能小于0').max(999999.99, '成本价不能超过999999.99').default(0).optional().openapi({
+    description: '成本价',
+    example: 50.00
+  }),
+  price: z.coerce.number().min(0, '售价不能小于0').max(999999.99, '售价不能超过999999.99').default(0).optional().openapi({
+    description: '售价',
+    example: 99.99
+  }),
+  num: z.coerce.number().int().min(0, '数量不能小于0').max(99999, '数量不能超过99999').default(0).optional().openapi({
+    description: '数量',
+    example: 2
+  }),
+  freightAmount: z.coerce.number().min(0, '运费不能小于0').max(999999.99, '运费不能超过999999.99').default(0).optional().openapi({
+    description: '运费',
+    example: 10.00
+  }),
+  state: z.coerce.number().int().min(0, '状态最小为0').max(1, '状态最大为1').default(0).optional().openapi({
+    description: '状态(0未发货、1已发货)',
+    example: 0
+  }),
+  expressName: z.string().max(255, '快递名称最多255个字符').nullable().optional().openapi({
+    description: '快递名称',
+    example: '顺丰快递'
+  }),
+  expressNo: z.coerce.number().int().positive().nullable().optional().openapi({
+    description: '快递单号',
+    example: 1234567890
+  }),
+  imageFileId: z.number().int().positive().nullable().optional().openapi({
+    description: '商品图片文件ID',
+    example: 1
+  })
+});
+
+// 更新订单商品DTO
+export const UpdateOrderGoodsDto = z.object({
+  orderId: z.number().int().positive('订单ID必须是正整数').optional().openapi({
+    description: '订单ID',
+    example: 1
+  }),
+  orderNo: z.string().max(50, '订单号最多50个字符').nullable().optional().openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  goodsId: z.number().int().positive('商品ID必须是正整数').optional().openapi({
+    description: '商品ID',
+    example: 1
+  }),
+  goodsName: z.string().max(255, '商品名称最多255个字符').nullable().optional().openapi({
+    description: '商品名称',
+    example: '苹果手机'
+  }),
+  goodsType: z.coerce.number().int().min(1, '商品类型最小为1').max(2, '商品类型最大为2').optional().openapi({
+    description: '1实物产品2虚拟订单',
+    example: 1
+  }),
+  supplierId: z.coerce.number().int().positive().optional().openapi({
+    description: '供货商ID',
+    example: 1
+  }),
+  costPrice: z.coerce.number().min(0, '成本价不能小于0').max(999999.99, '成本价不能超过999999.99').optional().openapi({
+    description: '成本价',
+    example: 50.00
+  }),
+  price: z.coerce.number().min(0, '售价不能小于0').max(999999.99, '售价不能超过999999.99').optional().openapi({
+    description: '售价',
+    example: 99.99
+  }),
+  num: z.coerce.number().int().min(0, '数量不能小于0').max(99999, '数量不能超过99999').optional().openapi({
+    description: '数量',
+    example: 2
+  }),
+  freightAmount: z.coerce.number().min(0, '运费不能小于0').max(999999.99, '运费不能超过999999.99').optional().openapi({
+    description: '运费',
+    example: 10.00
+  }),
+  state: z.coerce.number().int().min(0, '状态最小为0').max(1, '状态最大为1').optional().openapi({
+    description: '状态(0未发货、1已发货)',
+    example: 0
+  }),
+  expressName: z.string().max(255, '快递名称最多255个字符').nullable().optional().openapi({
+    description: '快递名称',
+    example: '顺丰快递'
+  }),
+  expressNo: z.coerce.number().int().positive().nullable().optional().openapi({
+    description: '快递单号',
+    example: 1234567890
+  }),
+  imageFileId: z.number().int().positive().nullable().optional().openapi({
+    description: '商品图片文件ID',
+    example: 1
+  })
+});
+
+// 订单商品状态类型
+export type OrderGoodsStatusType = typeof OrderGoodsStatus[keyof typeof OrderGoodsStatus];
+export type GoodsTypeType = typeof GoodsType[keyof typeof GoodsType];

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

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { OrderGoods } from './order-goods.entity';
+
+export class OrderGoodsService extends GenericCrudService<OrderGoods> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, OrderGoods);
+  }
+}

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

@@ -0,0 +1,40 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { Order } from './order.entity';
+
+@Entity('orders_refund')
+export class OrderRefund {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'order_no', type: 'varchar', length: 32, nullable: true, comment: '订单号' })
+  orderNo!: string | null;
+
+  @Column({ name: 'refund_order_no', type: 'varchar', length: 32, nullable: true, comment: '退款订单号' })
+  refundOrderNo!: string | null;
+
+  @Column({ name: 'refund_amount', type: 'decimal', precision: 10, scale: 2, nullable: true, comment: '退款金额' })
+  refundAmount!: number | null;
+
+  @Column({ name: 'state', type: 'int', default: 0, comment: '0未退款1退款中2退款成功3退款失败' })
+  state!: number;
+
+  @Column({ name: 'remark', type: 'varchar', length: 255, nullable: true, comment: '备注' })
+  remark!: string | null;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建人ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新人ID' })
+  updatedBy!: number | null;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+
+  // 关联关系
+  @ManyToOne(() => Order)
+  @JoinColumn({ name: 'order_no', referencedColumnName: 'orderNo' })
+  order!: Order;
+}

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

@@ -0,0 +1,128 @@
+import { z } from '@hono/zod-openapi';
+
+// 退款状态枚举
+export const RefundStatus = {
+  PENDING: 0, // 未退款
+  PROCESSING: 1, // 退款中
+  SUCCESS: 2, // 退款成功
+  FAILED: 3, // 退款失败
+} as const;
+
+// 订单退款基础Schema
+export const OrderRefundSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '退款记录ID',
+    example: 1
+  }),
+  orderNo: z.string().max(32, '订单号最多32个字符').nullable().optional().openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  refundOrderNo: z.string().max(32, '退款订单号最多32个字符').nullable().optional().openapi({
+    description: '退款订单号',
+    example: 'REF20240101123456'
+  }),
+  refundAmount: z.coerce.number().min(0, '退款金额不能小于0').max(999999.99, '退款金额不能超过999999.99').nullable().optional().openapi({
+    description: '退款金额',
+    example: 99.99
+  }),
+  state: z.coerce.number().int().min(0, '状态最小为0').max(3, '状态最大为3').default(0).openapi({
+    description: '0未退款1退款中2退款成功3退款失败',
+    example: 0
+  }),
+  remark: z.string().max(255, '备注最多255个字符').nullable().optional().openapi({
+    description: '备注',
+    example: '用户申请退款'
+  }),
+  createdBy: z.number().int().positive().nullable().optional().openapi({
+    description: '创建人ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().optional().openapi({
+    description: '更新人ID',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  // 关联实体
+  order: z.object({
+    id: z.number().int().positive().openapi({ description: '订单ID' }),
+    orderNo: z.string().openapi({ description: '订单号', example: 'ORD20240101123456' }),
+    amount: z.number().openapi({ description: '订单金额', example: 99.99 })
+  }).nullable().optional().openapi({
+    description: '订单信息'
+  })
+});
+
+// 创建订单退款DTO
+export const CreateOrderRefundDto = z.object({
+  orderNo: z.string().max(32, '订单号最多32个字符').nullable().optional().openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  refundOrderNo: z.string().max(32, '退款订单号最多32个字符').nullable().optional().openapi({
+    description: '退款订单号',
+    example: 'REF20240101123456'
+  }),
+  refundAmount: z.coerce.number().min(0, '退款金额不能小于0').max(999999.99, '退款金额不能超过999999.99').nullable().optional().openapi({
+    description: '退款金额',
+    example: 99.99
+  }),
+  state: z.coerce.number().int().min(0, '状态最小为0').max(3, '状态最大为3').default(0).optional().openapi({
+    description: '0未退款1退款中2退款成功3退款失败',
+    example: 0
+  }),
+  remark: z.string().max(255, '备注最多255个字符').nullable().optional().openapi({
+    description: '备注',
+    example: '用户申请退款'
+  })
+});
+
+// 更新订单退款DTO
+export const UpdateOrderRefundDto = z.object({
+  orderNo: z.string().max(32, '订单号最多32个字符').nullable().optional().openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  refundOrderNo: z.string().max(32, '退款订单号最多32个字符').nullable().optional().openapi({
+    description: '退款订单号',
+    example: 'REF20240101123456'
+  }),
+  refundAmount: z.coerce.number().min(0, '退款金额不能小于0').max(999999.99, '退款金额不能超过999999.99').nullable().optional().openapi({
+    description: '退款金额',
+    example: 99.99
+  }),
+  state: z.coerce.number().int().min(0, '状态最小为0').max(3, '状态最大为3').optional().openapi({
+    description: '0未退款1退款中2退款成功3退款失败',
+    example: 2
+  }),
+  remark: z.string().max(255, '备注最多255个字符').nullable().optional().openapi({
+    description: '备注',
+    example: '退款已完成'
+  })
+});
+
+// 订单退款列表响应
+export const OrderRefundListResponse = z.object({
+  data: z.array(OrderRefundSchema),
+  pagination: z.object({
+    total: z.number().openapi({
+      example: 100,
+      description: '总记录数'
+    }),
+    current: z.number().openapi({
+      example: 1,
+      description: '当前页码'
+    }),
+    pageSize: z.number().openapi({
+      example: 10,
+      description: '每页数量'
+    })
+  })
+});

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

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { OrderRefund } from './order-refund.entity';
+
+export class OrderRefundService extends GenericCrudService<OrderRefund> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, OrderRefund);
+  }
+}

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

@@ -0,0 +1,139 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from '@/server/modules/users/user.entity';
+import { Merchant } from '@/server/modules/merchant/merchant.entity';
+import { Supplier } from '@/server/modules/supplier/supplier.entity';
+import { DeliveryAddress } from '@/server/modules/delivery-address/delivery-address.entity';
+
+@Entity('orders')
+export class Order {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'order_no', type: 'varchar', length: 50, unique: true, comment: '订单号' })
+  orderNo!: string;
+
+  @Column({ name: 'user_id', type: 'int', unsigned: true, comment: '用户ID' })
+  userId!: number;
+
+  @Column({ name: 'auth_code', type: 'varchar', length: 32, nullable: true, comment: '付款码' })
+  authCode!: string | null;
+
+  @Column({ name: 'card_no', type: 'varchar', length: 32, nullable: true, comment: '卡号' })
+  cardNo!: string | null;
+
+  @Column({ name: 'sjt_card_no', type: 'varchar', length: 32, nullable: true, comment: '盛京通卡号' })
+  sjtCardNo!: string | null;
+
+  @Column({ name: 'amount', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '订单金额' })
+  amount!: number;
+
+  @Column({ name: 'cost_amount', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '成本金额' })
+  costAmount!: number;
+
+  @Column({ name: 'freight_amount', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '运费' })
+  freightAmount!: number;
+
+  @Column({ name: 'discount_amount', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '优惠金额' })
+  discountAmount!: number;
+
+  @Column({ name: 'pay_amount', type: 'decimal', precision: 10, scale: 2, default: 0.00, comment: '实际支付金额' })
+  payAmount!: number;
+
+  @Column({ name: 'device_no', type: 'varchar', length: 255, nullable: true, comment: '设备编号' })
+  deviceNo!: string | null;
+
+  @Column({ name: 'description', type: 'varchar', length: 255, nullable: true, comment: '订单描述' })
+  description!: string | null;
+
+  @Column({ name: 'goods_detail', type: 'varchar', length: 2000, nullable: true, comment: '订单详情 json' })
+  goodsDetail!: string | null;
+
+  @Column({ name: 'goods_tag', type: 'varchar', length: 255, nullable: true, comment: '订单优惠标记' })
+  goodsTag!: string | null;
+
+  @Column({ name: 'address', type: 'varchar', length: 255, nullable: true, comment: '地址' })
+  address!: string | null;
+
+  @Column({ name: 'order_type', type: 'int', default: 1, comment: '订单类型 1实物订单 2虚拟订单' })
+  orderType!: number;
+
+  @Column({ name: 'pay_type', type: 'int', default: 0, comment: '支付类型1积分2礼券' })
+  payType!: number;
+
+  @Column({ name: 'pay_state', type: 'int', default: 0, comment: '0未支付1支付中2支付成功3已退款4支付失败5订单关闭' })
+  payState!: number;
+
+  @Column({ name: 'state', type: 'int', default: 0, comment: '订单状态0未发货1已发货2收货成功3已退货' })
+  state!: number;
+
+  @Column({ name: 'user_phone', type: 'varchar', length: 50, nullable: true, comment: '用户手机号' })
+  userPhone!: string | null;
+
+  @Column({ name: 'merchant_id', type: 'int', unsigned: true, default: 0, comment: '商户id' })
+  merchantId!: number;
+
+  @Column({ name: 'merchant_no', type: 'int', unsigned: true, nullable: true, comment: '商户号' })
+  merchantNo!: number | null;
+
+  @Column({ name: 'supplier_id', type: 'int', unsigned: true, default: 0, comment: '供货商id' })
+  supplierId!: number;
+
+  @Column({ name: 'address_id', type: 'int', unsigned: true, default: 0, comment: '地址id' })
+  addressId!: number;
+
+  @Column({ name: 'receiver_mobile', type: 'varchar', length: 255, nullable: true, comment: '收货人手机号' })
+  receiverMobile!: string | null;
+
+  @Column({ name: 'recevier_name', type: 'varchar', length: 255, nullable: true, comment: '收货人姓名' })
+  recevierName!: string | null;
+
+  @Column({ name: 'recevier_province', type: 'bigint', default: 0, comment: '收货人所在省' })
+  recevierProvince!: number;
+
+  @Column({ name: 'recevier_city', type: 'bigint', default: 0, comment: '收货人所在市' })
+  recevierCity!: number;
+
+  @Column({ name: 'recevier_district', type: 'bigint', default: 0, comment: '收货人所在区' })
+  recevierDistrict!: number;
+
+  @Column({ name: 'recevier_town', type: 'bigint', default: 0, comment: '收货人所在街道' })
+  recevierTown!: number;
+
+  @Column({ name: 'refund_time', type: 'timestamp', nullable: true, comment: '退款时间' })
+  refundTime!: Date | null;
+
+  @Column({ name: 'close_time', type: 'timestamp', nullable: true, comment: '订单关闭时间' })
+  closeTime!: Date | null;
+
+  @Column({ name: 'remark', type: 'varchar', length: 255, nullable: true, comment: '管理员备注信息' })
+  remark!: string | null;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建人ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新人ID' })
+  updatedBy!: number | null;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+
+  // 关联关系
+  @ManyToOne(() => User)
+  @JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
+  user!: User;
+
+  @ManyToOne(() => Merchant)
+  @JoinColumn({ name: 'merchant_id', referencedColumnName: 'id' })
+  merchant!: Merchant;
+
+  @ManyToOne(() => Supplier)
+  @JoinColumn({ name: 'supplier_id', referencedColumnName: 'id' })
+  supplier!: Supplier;
+
+  @ManyToOne(() => DeliveryAddress)
+  @JoinColumn({ name: 'address_id', referencedColumnName: 'id' })
+  deliveryAddress!: DeliveryAddress;
+}

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

@@ -0,0 +1,497 @@
+import { z } from '@hono/zod-openapi';
+
+// 订单状态枚举
+export const OrderStatus = {
+  PENDING: 0, // 未发货
+  SHIPPED: 1, // 已发货
+  RECEIVED: 2, // 收货成功
+  RETURNED: 3, // 已退货
+} as const;
+
+// 支付状态枚举
+export const PayStatus = {
+  UNPAID: 0, // 未支付
+  PAYING: 1, // 支付中
+  SUCCESS: 2, // 支付成功
+  REFUNDED: 3, // 已退款
+  FAILED: 4, // 支付失败
+  CLOSED: 5, // 订单关闭
+} as const;
+
+// 订单类型枚举
+export const OrderType = {
+  PHYSICAL: 1, // 实物订单
+  VIRTUAL: 2, // 虚拟订单
+} as const;
+
+// 支付类型枚举
+export const PayType = {
+  POINTS: 1, // 积分
+  COUPON: 2, // 礼券
+} as const;
+
+// 订单基础Schema
+export const OrderSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '订单ID',
+    example: 1
+  }),
+  orderNo: z.string().min(1, '订单号不能为空').max(50, '订单号最多50个字符').openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  userId: z.number().int().positive().openapi({
+    description: '用户ID',
+    example: 1
+  }),
+  authCode: z.string().max(32, '付款码最多32个字符').nullable().optional().openapi({
+    description: '付款码',
+    example: '12345678901234567890123456789012'
+  }),
+  cardNo: z.string().max(32, '卡号最多32个字符').nullable().optional().openapi({
+    description: '卡号',
+    example: '6222********1234'
+  }),
+  sjtCardNo: z.string().max(32, '盛京通卡号最多32个字符').nullable().optional().openapi({
+    description: '盛京通卡号',
+    example: 'SJT1234567890'
+  }),
+  amount: z.coerce.number().min(0, '订单金额不能小于0').max(999999.99, '订单金额不能超过999999.99').openapi({
+    description: '订单金额',
+    example: 99.99
+  }),
+  costAmount: z.coerce.number().min(0, '成本金额不能小于0').max(999999.99, '成本金额不能超过999999.99').default(0).openapi({
+    description: '成本金额',
+    example: 50.00
+  }),
+  freightAmount: z.coerce.number().min(0, '运费不能小于0').max(999999.99, '运费不能超过999999.99').default(0).openapi({
+    description: '运费',
+    example: 10.00
+  }),
+  discountAmount: z.coerce.number().min(0, '优惠金额不能小于0').max(999999.99, '优惠金额不能超过999999.99').default(0).openapi({
+    description: '优惠金额',
+    example: 5.00
+  }),
+  payAmount: z.coerce.number().min(0, '实际支付金额不能小于0').max(999999.99, '实际支付金额不能超过999999.99').default(0).openapi({
+    description: '实际支付金额',
+    example: 94.99
+  }),
+  deviceNo: z.string().max(255, '设备编号最多255个字符').nullable().optional().openapi({
+    description: '设备编号',
+    example: 'DEV001234'
+  }),
+  description: z.string().max(255, '订单描述最多255个字符').nullable().optional().openapi({
+    description: '订单描述',
+    example: '购买商品'
+  }),
+  goodsDetail: z.string().max(2000, '订单详情最多2000个字符').nullable().optional().openapi({
+    description: '订单详情(json格式)',
+    example: '[{"goodsId":1,"name":"商品1","price":99.99,"num":1}]'
+  }),
+  goodsTag: z.string().max(255, '订单优惠标记最多255个字符').nullable().optional().openapi({
+    description: '订单优惠标记',
+    example: '满100减5'
+  }),
+  address: z.string().max(255, '地址最多255个字符').nullable().optional().openapi({
+    description: '地址',
+    example: '北京市朝阳区xxx路xxx号'
+  }),
+  orderType: z.coerce.number().int().min(1, '订单类型最小为1').max(2, '订单类型最大为2').default(1).openapi({
+    description: '订单类型 1实物订单 2虚拟订单',
+    example: 1
+  }),
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').default(0).openapi({
+    description: '支付类型1积分2礼券',
+    example: 1
+  }),
+  payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').default(0).openapi({
+    description: '支付状态 0未支付1支付中2支付成功3已退款4支付失败5订单关闭',
+    example: 2
+  }),
+  state: z.coerce.number().int().min(0, '订单状态最小为0').max(3, '订单状态最大为3').default(0).openapi({
+    description: '订单状态 0未发货1已发货2收货成功3已退货',
+    example: 0
+  }),
+  userPhone: z.string().max(50, '用户手机号最多50个字符').nullable().optional().openapi({
+    description: '用户手机号',
+    example: '13800138000'
+  }),
+  merchantId: z.coerce.number().int().positive().default(0).openapi({
+    description: '商户id',
+    example: 1
+  }),
+  merchantNo: z.coerce.number().int().positive().nullable().optional().openapi({
+    description: '商户号',
+    example: 1001
+  }),
+  supplierId: z.coerce.number().int().positive().default(0).openapi({
+    description: '供货商id',
+    example: 1
+  }),
+  addressId: z.coerce.number().int().positive().default(0).openapi({
+    description: '地址id',
+    example: 1
+  }),
+  receiverMobile: z.string().max(255, '收货人手机号最多255个字符').nullable().optional().openapi({
+    description: '收货人手机号',
+    example: '13800138000'
+  }),
+  recevierName: z.string().max(255, '收货人姓名最多255个字符').nullable().optional().openapi({
+    description: '收货人姓名',
+    example: '张三'
+  }),
+  recevierProvince: z.coerce.number().int().positive().default(0).openapi({
+    description: '收货人所在省',
+    example: 110000
+  }),
+  recevierCity: z.coerce.number().int().positive().default(0).openapi({
+    description: '收货人所在市',
+    example: 110100
+  }),
+  recevierDistrict: z.coerce.number().int().positive().default(0).openapi({
+    description: '收货人所在区',
+    example: 110105
+  }),
+  recevierTown: z.coerce.number().int().positive().default(0).openapi({
+    description: '收货人所在街道',
+    example: 110105001
+  }),
+  refundTime: z.coerce.date().nullable().optional().openapi({
+    description: '退款时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  closeTime: z.coerce.date().nullable().optional().openapi({
+    description: '订单关闭时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  remark: z.string().max(255, '管理员备注信息最多255个字符').nullable().optional().openapi({
+    description: '管理员备注信息',
+    example: '请尽快发货'
+  }),
+  createdBy: z.number().int().positive().nullable().optional().openapi({
+    description: '创建人ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().optional().openapi({
+    description: '更新人ID',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  // 关联实体
+  user: z.object({
+    id: z.number().int().positive().openapi({ description: '用户ID' }),
+    username: z.string().openapi({ description: '用户名', example: 'user123' }),
+    phone: z.string().nullable().openapi({ description: '手机号', example: '13800138000' })
+  }).nullable().optional().openapi({
+    description: '用户信息'
+  }),
+  merchant: z.object({
+    id: z.number().int().positive().openapi({ description: '商户ID' }),
+    name: z.string().openapi({ description: '商户名称', example: '商户A' })
+  }).nullable().optional().openapi({
+    description: '商户信息'
+  }),
+  supplier: z.object({
+    id: z.number().int().positive().openapi({ description: '供货商ID' }),
+    name: z.string().openapi({ description: '供货商名称', example: '供货商A' })
+  }).nullable().optional().openapi({
+    description: '供货商信息'
+  }),
+  deliveryAddress: z.object({
+    id: z.number().int().positive().openapi({ description: '地址ID' }),
+    name: z.string().openapi({ description: '收货人姓名', example: '张三' }),
+    phone: z.string().openapi({ description: '收货人电话', example: '13800138000' }),
+    address: z.string().openapi({ description: '详细地址', example: '北京市朝阳区xxx路xxx号' })
+  }).nullable().optional().openapi({
+    description: '收货地址信息'
+  })
+});
+
+// 创建订单DTO
+export const CreateOrderDto = z.object({
+  orderNo: z.string().min(1, '订单号不能为空').max(50, '订单号最多50个字符').openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  userId: z.number().int().positive('用户ID必须是正整数').openapi({
+    description: '用户ID',
+    example: 1
+  }),
+  authCode: z.string().max(32, '付款码最多32个字符').nullable().optional().openapi({
+    description: '付款码',
+    example: '12345678901234567890123456789012'
+  }),
+  cardNo: z.string().max(32, '卡号最多32个字符').nullable().optional().openapi({
+    description: '卡号',
+    example: '6222********1234'
+  }),
+  sjtCardNo: z.string().max(32, '盛京通卡号最多32个字符').nullable().optional().openapi({
+    description: '盛京通卡号',
+    example: 'SJT1234567890'
+  }),
+  amount: z.coerce.number().min(0, '订单金额不能小于0').max(999999.99, '订单金额不能超过999999.99').openapi({
+    description: '订单金额',
+    example: 99.99
+  }),
+  costAmount: z.coerce.number().min(0, '成本金额不能小于0').max(999999.99, '成本金额不能超过999999.99').default(0).optional().openapi({
+    description: '成本金额',
+    example: 50.00
+  }),
+  freightAmount: z.coerce.number().min(0, '运费不能小于0').max(999999.99, '运费不能超过999999.99').default(0).optional().openapi({
+    description: '运费',
+    example: 10.00
+  }),
+  discountAmount: z.coerce.number().min(0, '优惠金额不能小于0').max(999999.99, '优惠金额不能超过999999.99').default(0).optional().openapi({
+    description: '优惠金额',
+    example: 5.00
+  }),
+  payAmount: z.coerce.number().min(0, '实际支付金额不能小于0').max(999999.99, '实际支付金额不能超过999999.99').default(0).optional().openapi({
+    description: '实际支付金额',
+    example: 94.99
+  }),
+  deviceNo: z.string().max(255, '设备编号最多255个字符').nullable().optional().openapi({
+    description: '设备编号',
+    example: 'DEV001234'
+  }),
+  description: z.string().max(255, '订单描述最多255个字符').nullable().optional().openapi({
+    description: '订单描述',
+    example: '购买商品'
+  }),
+  goodsDetail: z.string().max(2000, '订单详情最多2000个字符').nullable().optional().openapi({
+    description: '订单详情(json格式)',
+    example: '[{"goodsId":1,"name":"商品1","price":99.99,"num":1}]'
+  }),
+  goodsTag: z.string().max(255, '订单优惠标记最多255个字符').nullable().optional().openapi({
+    description: '订单优惠标记',
+    example: '满100减5'
+  }),
+  address: z.string().max(255, '地址最多255个字符').nullable().optional().openapi({
+    description: '地址',
+    example: '北京市朝阳区xxx路xxx号'
+  }),
+  orderType: z.coerce.number().int().min(1, '订单类型最小为1').max(2, '订单类型最大为2').default(1).openapi({
+    description: '订单类型 1实物订单 2虚拟订单',
+    example: 1
+  }),
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').default(0).openapi({
+    description: '支付类型1积分2礼券',
+    example: 1
+  }),
+  payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').default(0).openapi({
+    description: '支付状态 0未支付1支付中2支付成功3已退款4支付失败5订单关闭',
+    example: 2
+  }),
+  state: z.coerce.number().int().min(0, '订单状态最小为0').max(3, '订单状态最大为3').default(0).openapi({
+    description: '订单状态 0未发货1已发货2收货成功3已退货',
+    example: 0
+  }),
+  userPhone: z.string().max(50, '用户手机号最多50个字符').nullable().optional().openapi({
+    description: '用户手机号',
+    example: '13800138000'
+  }),
+  merchantId: z.coerce.number().int().positive().default(0).openapi({
+    description: '商户id',
+    example: 1
+  }),
+  merchantNo: z.coerce.number().int().positive().nullable().optional().openapi({
+    description: '商户号',
+    example: 1001
+  }),
+  supplierId: z.coerce.number().int().positive().default(0).openapi({
+    description: '供货商id',
+    example: 1
+  }),
+  addressId: z.coerce.number().int().positive().default(0).openapi({
+    description: '地址id',
+    example: 1
+  }),
+  receiverMobile: z.string().max(255, '收货人手机号最多255个字符').nullable().optional().openapi({
+    description: '收货人手机号',
+    example: '13800138000'
+  }),
+  recevierName: z.string().max(255, '收货人姓名最多255个字符').nullable().optional().openapi({
+    description: '收货人姓名',
+    example: '张三'
+  }),
+  recevierProvince: z.coerce.number().int().positive().default(0).openapi({
+    description: '收货人所在省',
+    example: 110000
+  }),
+  recevierCity: z.coerce.number().int().positive().default(0).openapi({
+    description: '收货人所在市',
+    example: 110100
+  }),
+  recevierDistrict: z.coerce.number().int().positive().default(0).openapi({
+    description: '收货人所在区',
+    example: 110105
+  }),
+  recevierTown: z.coerce.number().int().positive().default(0).openapi({
+    description: '收货人所在街道',
+    example: 110105001
+  }),
+  refundTime: z.coerce.date().nullable().optional().openapi({
+    description: '退款时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  closeTime: z.coerce.date().nullable().optional().openapi({
+    description: '订单关闭时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  remark: z.string().max(255, '管理员备注信息最多255个字符').nullable().optional().openapi({
+    description: '管理员备注信息',
+    example: '请尽快发货'
+  })
+});
+
+// 更新订单DTO
+export const UpdateOrderDto = z.object({
+  orderNo: z.string().min(1, '订单号不能为空').max(50, '订单号最多50个字符').optional().openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  userId: z.number().int().positive('用户ID必须是正整数').optional().openapi({
+    description: '用户ID',
+    example: 1
+  }),
+  authCode: z.string().max(32, '付款码最多32个字符').nullable().optional().openapi({
+    description: '付款码',
+    example: '12345678901234567890123456789012'
+  }),
+  cardNo: z.string().max(32, '卡号最多32个字符').nullable().optional().openapi({
+    description: '卡号',
+    example: '6222********1234'
+  }),
+  sjtCardNo: z.string().max(32, '盛京通卡号最多32个字符').nullable().optional().openapi({
+    description: '盛京通卡号',
+    example: 'SJT1234567890'
+  }),
+  amount: z.coerce.number().min(0, '订单金额不能小于0').max(999999.99, '订单金额不能超过999999.99').optional().openapi({
+    description: '订单金额',
+    example: 99.99
+  }),
+  costAmount: z.coerce.number().min(0, '成本金额不能小于0').max(999999.99, '成本金额不能超过999999.99').optional().openapi({
+    description: '成本金额',
+    example: 50.00
+  }),
+  freightAmount: z.coerce.number().min(0, '运费不能小于0').max(999999.99, '运费不能超过999999.99').optional().openapi({
+    description: '运费',
+    example: 10.00
+  }),
+  discountAmount: z.coerce.number().min(0, '优惠金额不能小于0').max(999999.99, '优惠金额不能超过999999.99').optional().openapi({
+    description: '优惠金额',
+    example: 5.00
+  }),
+  payAmount: z.coerce.number().min(0, '实际支付金额不能小于0').max(999999.99, '实际支付金额不能超过999999.99').optional().openapi({
+    description: '实际支付金额',
+    example: 94.99
+  }),
+  deviceNo: z.string().max(255, '设备编号最多255个字符').nullable().optional().openapi({
+    description: '设备编号',
+    example: 'DEV001234'
+  }),
+  description: z.string().max(255, '订单描述最多255个字符').nullable().optional().openapi({
+    description: '订单描述',
+    example: '购买商品'
+  }),
+  goodsDetail: z.string().max(2000, '订单详情最多2000个字符').nullable().optional().openapi({
+    description: '订单详情(json格式)',
+    example: '[{"goodsId":1,"name":"商品1","price":99.99,"num":1}]'
+  }),
+  goodsTag: z.string().max(255, '订单优惠标记最多255个字符').nullable().optional().openapi({
+    description: '订单优惠标记',
+    example: '满100减5'
+  }),
+  address: z.string().max(255, '地址最多255个字符').nullable().optional().openapi({
+    description: '地址',
+    example: '北京市朝阳区xxx路xxx号'
+  }),
+  orderType: z.coerce.number().int().min(1, '订单类型最小为1').max(2, '订单类型最大为2').optional().openapi({
+    description: '订单类型 1实物订单 2虚拟订单',
+    example: 1
+  }),
+  payType: z.coerce.number().int().min(0, '支付类型最小为0').max(2, '支付类型最大为2').optional().openapi({
+    description: '支付类型1积分2礼券',
+    example: 1
+  }),
+  payState: z.coerce.number().int().min(0, '支付状态最小为0').max(5, '支付状态最大为5').optional().openapi({
+    description: '支付状态 0未支付1支付中2支付成功3已退款4支付失败5订单关闭',
+    example: 2
+  }),
+  state: z.coerce.number().int().min(0, '订单状态最小为0').max(3, '订单状态最大为3').optional().openapi({
+    description: '订单状态 0未发货1已发货2收货成功3已退货',
+    example: 0
+  }),
+  userPhone: z.string().max(50, '用户手机号最多50个字符').nullable().optional().openapi({
+    description: '用户手机号',
+    example: '13800138000'
+  }),
+  merchantId: z.coerce.number().int().positive().optional().openapi({
+    description: '商户id',
+    example: 1
+  }),
+  merchantNo: z.coerce.number().int().positive().nullable().optional().openapi({
+    description: '商户号',
+    example: 1001
+  }),
+  supplierId: z.coerce.number().int().positive().optional().openapi({
+    description: '供货商id',
+    example: 1
+  }),
+  addressId: z.coerce.number().int().positive().optional().openapi({
+    description: '地址id',
+    example: 1
+  }),
+  receiverMobile: z.string().max(255, '收货人手机号最多255个字符').nullable().optional().openapi({
+    description: '收货人手机号',
+    example: '13800138000'
+  }),
+  recevierName: z.string().max(255, '收货人姓名最多255个字符').nullable().optional().openapi({
+    description: '收货人姓名',
+    example: '张三'
+  }),
+  recevierProvince: z.coerce.number().int().positive().optional().openapi({
+    description: '收货人所在省',
+    example: 110000
+  }),
+  recevierCity: z.coerce.number().int().positive().optional().openapi({
+    description: '收货人所在市',
+    example: 110100
+  }),
+  recevierDistrict: z.coerce.number().int().positive().optional().openapi({
+    description: '收货人所在区',
+    example: 110105
+  }),
+  recevierTown: z.coerce.number().int().positive().optional().openapi({
+    description: '收货人所在街道',
+    example: 110105001
+  }),
+  refundTime: z.coerce.date().nullable().optional().openapi({
+    description: '退款时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  closeTime: z.coerce.date().nullable().optional().openapi({
+    description: '订单关闭时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  remark: z.string().max(255, '管理员备注信息最多255个字符').nullable().optional().openapi({
+    description: '管理员备注信息',
+    example: '请尽快发货'
+  })
+});
+
+// 订单列表响应Schema
+export const OrderListResponse = z.object({
+  data: z.array(OrderSchema),
+  pagination: z.object({
+    total: z.number().openapi({ example: 100, description: '总记录数' }),
+    current: z.number().openapi({ example: 1, description: '当前页码' }),
+    pageSize: z.number().openapi({ example: 10, description: '每页数量' })
+  })
+});

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

@@ -0,0 +1,178 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource, Repository } from 'typeorm';
+import { Order } from './order.entity';
+import { OrderGoods } from './order-goods.entity';
+import { Goods } from '@/server/modules/goods/goods.entity';
+import { DeliveryAddress } from '@/server/modules/delivery-address/delivery-address.entity';
+import type { CreateOrderRequest } from './schemas/create-order.schema';
+
+export class OrderService extends GenericCrudService<Order> {
+  private orderGoodsRepository: Repository<OrderGoods>;
+  private goodsRepository: Repository<Goods>;
+  private deliveryAddressRepository: Repository<DeliveryAddress>;
+
+  constructor(dataSource: DataSource) {
+    super(dataSource, Order);
+    this.orderGoodsRepository = dataSource.getRepository(OrderGoods);
+    this.goodsRepository = dataSource.getRepository(Goods);
+    this.deliveryAddressRepository = dataSource.getRepository(DeliveryAddress);
+  }
+
+  /**
+   * 创建订单
+   * @param data 创建订单请求数据
+   * @param userId 用户ID
+   * @returns 创建的订单信息
+   */
+  async createOrder(data: CreateOrderRequest, userId: number): Promise<{
+    orderId: number;
+    orderNo: string;
+    amount: number;
+    payAmount: number;
+  }> {
+    const queryRunner = this.dataSource.createQueryRunner();
+    await queryRunner.connect();
+    await queryRunner.startTransaction();
+
+    try {
+      const { addressId, productOwn, consumeFrom, products } = data;
+
+      // 验证商品信息并计算总价
+      let totalAmount = 0;
+      let totalCostAmount = 0;
+      const goodsInfo = [];
+
+      for (const item of products) {
+        const goods = await this.goodsRepository.findOne({
+          where: { id: item.id }
+        });
+
+        if (!goods) {
+          throw new Error(`商品ID ${item.id} 不存在`);
+        }
+
+        if (goods.stock < item.num) {
+          throw new Error(`商品 ${goods.name} 库存不足`);
+        }
+
+        const itemAmount = goods.price * item.num;
+        const itemCostAmount = goods.costPrice * item.num;
+
+        totalAmount += itemAmount;
+        totalCostAmount += itemCostAmount;
+
+        goodsInfo.push({
+          goods,
+          quantity: item.num,
+          amount: itemAmount,
+          costAmount: itemCostAmount
+        });
+      }
+
+      // 获取收货地址信息
+      let deliveryAddress = null;
+      if (addressId) {
+        deliveryAddress = await this.deliveryAddressRepository.findOne({
+          where: { id: addressId, userId }
+        });
+
+        if (!deliveryAddress) {
+          throw new Error('收货地址不存在');
+        }
+      }
+
+      // 生成订单号
+      const orderNo = this.generateOrderNo();
+
+      // 创建订单
+      const order = this.repository.create({
+        orderNo,
+        userId,
+        amount: totalAmount,
+        costAmount: totalCostAmount,
+        payAmount: totalAmount, // 这里可以根据优惠规则计算实际支付金额
+        orderType: 1, // 实物订单
+        payType: consumeFrom === '积分兑换' ? 1 : 2, // 根据消费来源设置支付类型
+        payState: 0, // 未支付
+        state: 0, // 未发货
+        addressId: addressId || 0,
+        merchantId: goodsInfo[0]?.goods.merchantId || 0,
+        supplierId: goodsInfo[0]?.goods.supplierId || 0,
+        createdBy: userId,
+        updatedBy: userId,
+        // 设置收货地址信息(如果提供)
+        ...(deliveryAddress && {
+          receiverMobile: deliveryAddress.phone,
+          recevierName: deliveryAddress.name,
+          recevierProvince: deliveryAddress.receiverProvince,
+          recevierCity: deliveryAddress.receiverCity,
+          recevierDistrict: deliveryAddress.receiverDistrict,
+          recevierTown: deliveryAddress.receiverTown,
+          address: deliveryAddress.address
+        })
+      });
+
+      const savedOrder = await queryRunner.manager.save(order);
+
+      // 创建订单商品明细
+      const orderGoodsList = goodsInfo.map(info => ({
+        orderId: savedOrder.id,
+        orderNo,
+        goodsId: info.goods.id,
+        goodsName: info.goods.name,
+        imageFileId: info.goods.imageFileId,
+        goodsType: info.goods.goodsType,
+        supplierId: info.goods.supplierId,
+        costPrice: info.goods.costPrice,
+        price: info.goods.price,
+        num: info.quantity,
+        freightAmount: 0,
+        state: 0,
+        createdBy: userId,
+        updatedBy: userId
+      }));
+
+      await queryRunner.manager.save(OrderGoods, orderGoodsList);
+
+      // 更新商品库存
+      for (const item of goodsInfo) {
+        await queryRunner.manager.update(Goods, item.goods.id, {
+          stock: () => `stock - ${item.quantity}`,
+          salesNum: () => `sales_num + ${item.quantity}`
+        });
+      }
+
+      await queryRunner.commitTransaction();
+
+      return {
+        orderId: savedOrder.id,
+        orderNo: savedOrder.orderNo,
+        amount: savedOrder.amount,
+        payAmount: savedOrder.payAmount
+      };
+
+    } catch (error) {
+      await queryRunner.rollbackTransaction();
+      throw error;
+    } finally {
+      await queryRunner.release();
+    }
+  }
+
+  /**
+   * 生成订单号
+   * 格式:年月日时分秒 + 6位随机数
+   */
+  private generateOrderNo(): string {
+    const now = new Date();
+    const year = now.getFullYear();
+    const month = String(now.getMonth() + 1).padStart(2, '0');
+    const day = String(now.getDate()).padStart(2, '0');
+    const hour = String(now.getHours()).padStart(2, '0');
+    const minute = String(now.getMinutes()).padStart(2, '0');
+    const second = String(now.getSeconds()).padStart(2, '0');
+    const random = String(Math.floor(Math.random() * 1000000)).padStart(6, '0');
+    
+    return `ORD${year}${month}${day}${hour}${minute}${second}${random}`;
+  }
+}

+ 66 - 0
packages/server/src/modules/orders/schemas/create-order.schema.ts

@@ -0,0 +1,66 @@
+import { z } from '@hono/zod-openapi';
+
+// 订单商品项DTO
+export const OrderItemDto = z.object({
+  id: z.number().int().positive('商品ID必须是正整数').openapi({
+    description: '商品ID',
+    example: 1
+  }),
+  num: z.number().int().min(1, '购买数量必须大于0').openapi({
+    description: '购买数量',
+    example: 2
+  })
+});
+
+// 创建订单请求DTO
+export const CreateOrderRequestDto = z.object({
+  addressId: z.number().int().positive('收货地址ID必须是正整数').optional().openapi({
+    description: '收货地址ID',
+    example: 1
+  }),
+  productOwn: z.string().min(1, '商品所属方不能为空').max(50, '商品所属方最多50个字符').openapi({
+    description: '商品所属方(如:自营、第三方等)',
+    example: '自营'
+  }),
+  consumeFrom: z.string().min(1, '消费来源不能为空').max(50, '消费来源最多50个字符').openapi({
+    description: '消费来源(如:积分兑换、礼券兑换等)',
+    example: '积分兑换'
+  }),
+  products: z.array(OrderItemDto).min(1, '商品列表不能为空').openapi({
+    description: '商品列表',
+    example: [{ id: 1, num: 2 }, { id: 3, num: 1 }]
+  })
+});
+
+// 创建订单响应DTO
+export const CreateOrderResponseDto = z.object({
+  success: z.boolean().openapi({
+    description: '是否成功',
+    example: true
+  }),
+  orderId: z.number().int().positive().openapi({
+    description: '订单ID',
+    example: 1001
+  }),
+  orderNo: z.string().openapi({
+    description: '订单号',
+    example: 'ORD20240101123456'
+  }),
+  amount: z.number().openapi({
+    description: '订单总金额',
+    example: 199.98
+  }),
+  payAmount: z.number().openapi({
+    description: '实际支付金额',
+    example: 199.98
+  }),
+  message: z.string().openapi({
+    description: '提示信息',
+    example: '订单创建成功'
+  })
+});
+
+// 导出类型
+export type CreateOrderRequest = z.infer<typeof CreateOrderRequestDto>;
+export type CreateOrderResponse = z.infer<typeof CreateOrderResponseDto>;
+export type OrderItem = z.infer<typeof OrderItemDto>;

+ 31 - 0
packages/server/src/modules/organization/organization.entity.ts

@@ -0,0 +1,31 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('organization')
+export class Organization {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, comment: '分行网点名称' })
+  name!: string;
+
+  @Column({ name: 'parent_id', type: 'int', unsigned: true, default: 0, comment: '上级id' })
+  parentId!: number;
+
+  @Column({ name: 'level', type: 'int', unsigned: true, default: 4, comment: '层级' })
+  level!: number;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '状态 1可用 2不可用' })
+  state!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+}

+ 75 - 0
packages/server/src/modules/organization/organization.schema.ts

@@ -0,0 +1,75 @@
+import { z } from '@hono/zod-openapi';
+
+export const OrganizationSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '分行网点ID' }),
+  name: z.string().min(1, '分行网点名称不能为空').max(255, '分行网点名称最多255个字符').openapi({
+    description: '分行网点名称',
+    example: '北京分行'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级网点ID',
+    example: 0
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').default(4).openapi({
+    description: '组织层级',
+    example: 4
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});
+
+export const CreateOrganizationDto = z.object({
+  name: z.string().min(1, '分行网点名称不能为空').max(255, '分行网点名称最多255个字符').openapi({
+    description: '分行网点名称',
+    example: '北京分行'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级网点ID',
+    example: 0
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').default(4).openapi({
+    description: '组织层级',
+    example: 4
+  }),
+  state: z.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  })
+});
+
+export const UpdateOrganizationDto = z.object({
+  name: z.string().min(1, '分行网点名称不能为空').max(255, '分行网点名称最多255个字符').optional().openapi({
+    description: '分行网点名称',
+    example: '北京分行'
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').optional().openapi({
+    description: '上级网点ID',
+    example: 0
+  }),
+  level: z.number().int().nonnegative('层级必须为非负数').optional().openapi({
+    description: '组织层级',
+    example: 4
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1可用 2不可用',
+    example: 1
+  })
+});

+ 9 - 0
packages/server/src/modules/organization/organization.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { Organization } from './organization.entity';
+
+export class OrganizationService extends GenericCrudService<Organization> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Organization);
+  }
+}

+ 52 - 0
packages/server/src/modules/supplier/supplier.entity.ts

@@ -0,0 +1,52 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('supplier')
+export class Supplier {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, nullable: true, comment: '供货商名称' })
+  name!: string | null;
+
+  @Column({ name: 'username', type: 'varchar', length: 20, unique: true, comment: '用户名' })
+  username!: string;
+
+  @Column({ name: 'password', type: 'varchar', length: 255, comment: '密码' })
+  password!: string;
+
+  @Column({ name: 'phone', type: 'char', length: 11, nullable: true, comment: '手机号码' })
+  phone!: string | null;
+
+  @Column({ name: 'realname', type: 'varchar', length: 20, nullable: true, comment: '姓名' })
+  realname!: string | null;
+
+  @Column({ name: 'login_num', type: 'int', unsigned: true, default: 0, comment: '登录次数' })
+  loginNum!: number;
+
+  @Column({ name: 'login_time', type: 'int', unsigned: true, default: 0, comment: '登录时间' })
+  loginTime!: number;
+
+  @Column({ name: 'login_ip', type: 'varchar', length: 15, nullable: true, comment: '登录IP' })
+  loginIp!: string | null;
+
+  @Column({ name: 'last_login_time', type: 'int', unsigned: true, default: 0, comment: '上次登录时间' })
+  lastLoginTime!: number;
+
+  @Column({ name: 'last_login_ip', type: 'varchar', length: 15, nullable: true, comment: '上次登录IP' })
+  lastLoginIp!: string | null;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 2, comment: '状态 1启用 2禁用' })
+  state!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+}

+ 119 - 0
packages/server/src/modules/supplier/supplier.schema.ts

@@ -0,0 +1,119 @@
+import { z } from '@hono/zod-openapi';
+
+export const SupplierSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '供应商ID' }),
+  name: z.string().min(1, '供货商名称不能为空').max(255, '供货商名称最多255个字符').nullable().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  loginNum: z.number().int().nonnegative('登录次数必须为非负数').default(0).openapi({
+    description: '登录次数',
+    example: 0
+  }),
+  loginTime: z.number().int().nonnegative('登录时间必须为非负数').default(0).openapi({
+    description: '登录时间',
+    example: 0
+  }),
+  loginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '登录IP',
+    example: '192.168.1.1'
+  }),
+  lastLoginTime: z.number().int().nonnegative('上次登录时间必须为非负数').default(0).openapi({
+    description: '上次登录时间',
+    example: 0
+  }),
+  lastLoginIp: z.string().max(15, 'IP地址最多15个字符').nullable().optional().openapi({
+    description: '上次登录IP',
+    example: '192.168.1.1'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});
+
+export const CreateSupplierDto = z.object({
+  name: z.string().min(1, '供货商名称不能为空').max(255, '供货商名称最多255个字符').nullable().optional().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  })
+});
+
+export const UpdateSupplierDto = z.object({
+  name: z.string().min(1, '供货商名称不能为空').max(255, '供货商名称最多255个字符').nullable().optional().openapi({
+    description: '供货商名称',
+    example: '供应商A'
+  }),
+  username: z.string().min(1, '用户名不能为空').max(20, '用户名最多20个字符').optional().openapi({
+    description: '用户名',
+    example: 'supplier001'
+  }),
+  password: z.string().min(6, '密码至少6位').max(255, '密码最多255位').optional().openapi({
+    description: '密码',
+    example: 'password123'
+  }),
+  phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入正确的手机号').nullable().optional().openapi({
+    description: '手机号码',
+    example: '13800138000'
+  }),
+  realname: z.string().max(20, '姓名最多20个字符').nullable().optional().openapi({
+    description: '姓名',
+    example: '张三'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1启用 2禁用',
+    example: 1
+  })
+});

+ 9 - 0
packages/server/src/modules/supplier/supplier.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { Supplier } from './supplier.entity';
+
+export class SupplierService extends GenericCrudService<Supplier> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Supplier);
+  }
+}

+ 34 - 0
packages/server/src/modules/system/city.entity.ts

@@ -0,0 +1,34 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('city')
+export class City {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 255, comment: '地区名称' })
+  name!: string;
+
+  @Column({ name: 'level', type: 'int', unsigned: true, default: 0, comment: '层级 省市区县1,2,3,4' })
+  level!: number;
+
+  @Column({ name: 'parent_id', type: 'bigint', unsigned: true, default: 0, comment: '上级id' })
+  parentId!: number;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 1, comment: '状态 1可用' })
+  state!: number;
+
+  @Column({ name: 'sort', type: 'int', unsigned: true, default: 0, comment: '排序数值越大越靠前' })
+  sort!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+}

+ 87 - 0
packages/server/src/modules/system/city.schema.ts

@@ -0,0 +1,87 @@
+import { z } from '@hono/zod-openapi';
+
+export const CitySchema = z.object({
+  id: z.number().int().positive().openapi({ description: '地区ID' }),
+  name: z.string().min(1, '地区名称不能为空').max(255, '地区名称最多255个字符').openapi({
+    description: '地区名称',
+    example: '北京市'
+  }),
+  level: z.number().int().min(1).max(4).default(1).openapi({
+    description: '层级 省市区县1,2,3,4',
+    example: 1
+  }),
+  parentId: z.coerce.number<number>().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级地区ID',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(1).default(1).openapi({
+    description: '状态 1可用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').default(0).openapi({
+    description: '排序数值越大越靠前',
+    example: 0
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});
+
+export const CreateCityDto = z.object({
+  name: z.string().min(1, '地区名称不能为空').max(255, '地区名称最多255个字符').openapi({
+    description: '地区名称',
+    example: '北京市'
+  }),
+  level: z.number().int().min(1, '层级必须在1-4之间').max(4, '层级必须在1-4之间').default(1).openapi({
+    description: '层级 省市区县1,2,3,4',
+    example: 1
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').default(0).openapi({
+    description: '上级地区ID',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(1).default(1).openapi({
+    description: '状态 1可用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').default(0).openapi({
+    description: '排序数值越大越靠前',
+    example: 0
+  })
+});
+
+export const UpdateCityDto = z.object({
+  name: z.string().min(1, '地区名称不能为空').max(255, '地区名称最多255个字符').optional().openapi({
+    description: '地区名称',
+    example: '北京市'
+  }),
+  level: z.number().int().min(1, '层级必须在1-4之间').max(4, '层级必须在1-4之间').optional().openapi({
+    description: '层级 省市区县1,2,3,4',
+    example: 1
+  }),
+  parentId: z.number().int().nonnegative('上级ID必须为非负数').optional().openapi({
+    description: '上级地区ID',
+    example: 0
+  }),
+  state: z.number().int().min(1).max(1).optional().openapi({
+    description: '状态 1可用',
+    example: 1
+  }),
+  sort: z.number().int().nonnegative('排序值必须为非负数').optional().openapi({
+    description: '排序数值越大越靠前',
+    example: 0
+  })
+});

+ 9 - 0
packages/server/src/modules/system/city.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { City } from './city.entity';
+
+export class CityService extends GenericCrudService<City> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, City);
+  }
+}

+ 28 - 0
packages/server/src/modules/system/config.entity.ts

@@ -0,0 +1,28 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('config')
+export class Config {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'key', type: 'varchar', length: 255, comment: '配置键名' })
+  key!: string;
+
+  @Column({ name: 'value', type: 'varchar', length: 255, comment: '配置值' })
+  value!: string;
+
+  @Column({ name: 'state', type: 'tinyint', unsigned: true, default: 2, comment: '状态 1可用 2禁用' })
+  state!: number;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+}

+ 63 - 0
packages/server/src/modules/system/config.schema.ts

@@ -0,0 +1,63 @@
+import { z } from '@hono/zod-openapi';
+
+export const ConfigSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '配置ID' }),
+  key: z.string().min(1, '配置键名不能为空').max(255, '配置键名最多255个字符').openapi({
+    description: '配置键名',
+    example: 'site_name'
+  }),
+  value: z.string().min(1, '配置值不能为空').max(255, '配置值最多255个字符').openapi({
+    description: '配置值',
+    example: '站点名称'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1可用 2禁用',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  })
+});
+
+export const CreateConfigDto = z.object({
+  key: z.string().min(1, '配置键名不能为空').max(255, '配置键名最多255个字符').openapi({
+    description: '配置键名',
+    example: 'site_name'
+  }),
+  value: z.string().min(1, '配置值不能为空').max(255, '配置值最多255个字符').openapi({
+    description: '配置值',
+    example: '站点名称'
+  }),
+  state: z.number().int().min(1).max(2).default(2).openapi({
+    description: '状态 1可用 2禁用',
+    example: 1
+  })
+});
+
+export const UpdateConfigDto = z.object({
+  key: z.string().min(1, '配置键名不能为空').max(255, '配置键名最多255个字符').optional().openapi({
+    description: '配置键名',
+    example: 'site_name'
+  }),
+  value: z.string().min(1, '配置值不能为空').max(255, '配置值最多255个字符').optional().openapi({
+    description: '配置值',
+    example: '站点名称'
+  }),
+  state: z.number().int().min(1).max(2).optional().openapi({
+    description: '状态 1可用 2禁用',
+    example: 1
+  })
+});

+ 9 - 0
packages/server/src/modules/system/config.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { Config } from './config.entity';
+
+export class ConfigService extends GenericCrudService<Config> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, Config);
+  }
+}

+ 53 - 0
packages/server/src/modules/user-card-balance-records/user-card-balance-record.entity.ts

@@ -0,0 +1,53 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from '@/server/modules/users/user.entity';
+import { UserCard } from '@/server/modules/user-cards/user-card.entity';
+
+@Entity('user_card_balance_records')
+export class UserCardBalanceRecord {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'user_id', type: 'int', unsigned: true, comment: '用户ID' })
+  userId!: number;
+
+  @Column({ name: 'card_no', type: 'varchar', length: 20, comment: '卡号' })
+  cardNo!: string;
+
+  @Column({ name: 'amount', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0.00, comment: '变动金额' })
+  amount!: number;
+
+  @Column({ name: 'amount_before', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0.00, comment: '变动前金额' })
+  amountBefore!: number;
+
+  @Column({ name: 'amount_after', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0.00, comment: '变动后金额' })
+  amountAfter!: number;
+
+  @Column({ name: 'order_no', type: 'varchar', length: 32, comment: '订单号' })
+  orderNo!: string;
+
+  @Column({ name: 'type', type: 'int', comment: '类型 1消费 2退款' })
+  type!: number;
+
+  @Column({ name: 'remark', type: 'varchar', length: 32, nullable: true, comment: '备注' })
+  remark!: string | null;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建人ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新人ID' })
+  updatedBy!: number | null;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+
+  @ManyToOne(() => User)
+  @JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
+  user!: User;
+
+  @ManyToOne(() => UserCard)
+  @JoinColumn({ name: 'card_no', referencedColumnName: 'cardNo' })
+  userCard!: UserCard;
+}

+ 125 - 0
packages/server/src/modules/user-card-balance-records/user-card-balance-record.schema.ts

@@ -0,0 +1,125 @@
+import { z } from '@hono/zod-openapi';
+import { UserSchema } from '@/server/modules/users/user.schema';
+import { UserCardSchema } from '@/server/modules/user-cards/user-card.schema';
+
+// 基础用户卡余额记录Schema
+const UserCardBalanceRecordSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '记录ID',
+    example: 1
+  }),
+  userId: z.number().int().positive().openapi({
+    description: '用户ID',
+    example: 1001
+  }),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').openapi({
+    description: '卡号',
+    example: '12345678901234567890'
+  }),
+  amount: z.coerce.number<number>().multipleOf(0.01).min(0).max(999999.99).openapi({
+    description: '变动金额',
+    example: 50.50
+  }),
+  amountBefore: z.coerce.number<number>().multipleOf(0.01).min(0).max(999999.99).openapi({
+    description: '变动前金额',
+    example: 100.00
+  }),
+  amountAfter: z.coerce.number<number>().multipleOf(0.01).min(0).max(999999.99).openapi({
+    description: '变动后金额',
+    example: 150.50
+  }),
+  orderNo: z.string().min(1, '订单号不能为空').max(32, '订单号最多32个字符').openapi({
+    description: '订单号',
+    example: 'ORD20240101120000'
+  }),
+  type: z.coerce.number<number>().int().min(1).max(2).openapi({
+    description: '类型 1消费 2退款',
+    example: 1
+  }),
+  remark: z.string().max(32, '备注最多32个字符').nullable().openapi({
+    description: '备注',
+    example: '消费充值'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建人ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新人ID',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  user: UserSchema.optional(),
+  userCard: UserCardSchema.optional()
+});
+
+// 创建用户卡余额记录DTO
+export const CreateUserCardBalanceRecordDto = UserCardBalanceRecordSchema.pick({
+  userId: true,
+  cardNo: true,
+  amount: true,
+  amountBefore: true,
+  amountAfter: true,
+  orderNo: true,
+  type: true,
+  remark: true
+}).extend({
+  userId: z.coerce.number<number>().int().positive('用户ID必须是正整数'),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符'),
+  amount: z.coerce.number<number>().multipleOf(0.01).min(0).max(999999.99),
+  amountBefore: z.coerce.number<number>().multipleOf(0.01).min(0).max(999999.99),
+  amountAfter: z.coerce.number<number>().multipleOf(0.01).min(0).max(999999.99),
+  orderNo: z.string().min(1, '订单号不能为空').max(32, '订单号最多32个字符'),
+  type: z.coerce.number<number>().int().min(1).max(2),
+  remark: z.string().max(32, '备注最多32个字符').nullable()
+});
+
+// 更新用户卡余额记录DTO
+export const UpdateUserCardBalanceRecordDto = UserCardBalanceRecordSchema.pick({
+  userId: true,
+  cardNo: true,
+  amount: true,
+  amountBefore: true,
+  amountAfter: true,
+  orderNo: true,
+  type: true,
+  remark: true
+}).partial().extend({
+  userId: z.coerce.number<number>().int().positive('用户ID必须是正整数').optional(),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').optional(),
+  amount: z.coerce.number<number>().multipleOf(0.01).min(0).max(999999.99).optional(),
+  amountBefore: z.coerce.number<number>().multipleOf(0.01).min(0).max(999999.99).optional(),
+  amountAfter: z.coerce.number<number>().multipleOf(0.01).min(0).max(999999.99).optional(),
+  orderNo: z.string().min(1, '订单号不能为空').max(32, '订单号最多32个字符').optional(),
+  type: z.coerce.number<number>().int().min(1).max(2).optional(),
+  remark: z.string().max(32, '备注最多32个字符').nullable().optional()
+});
+
+// 响应Schema(包含关联数据)
+export const UserCardBalanceRecordResponseSchema = UserCardBalanceRecordSchema.extend({
+  user: UserSchema.pick({
+    id: true,
+    username: true,
+    name: true,
+    phone: true
+  }).optional(),
+  userCard: UserCardSchema.pick({
+    id: true,
+    cardNo: true,
+    balance: true
+  }).optional()
+});
+
+export type UserCardBalanceRecord = z.infer<typeof UserCardBalanceRecordSchema>;
+export type CreateUserCardBalanceRecordDto = z.infer<typeof CreateUserCardBalanceRecordDto>;
+export type UpdateUserCardBalanceRecordDto = z.infer<typeof UpdateUserCardBalanceRecordDto>;
+export type UserCardBalanceRecordResponse = z.infer<typeof UserCardBalanceRecordResponseSchema>;
+
+export { UserCardBalanceRecordSchema };

+ 9 - 0
packages/server/src/modules/user-card-balance-records/user-card-balance-record.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { UserCardBalanceRecord } from './user-card-balance-record.entity';
+
+export class UserCardBalanceRecordService extends GenericCrudService<UserCardBalanceRecord> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, UserCardBalanceRecord);
+  }
+}

+ 56 - 0
packages/server/src/modules/user-cards/user-card.entity.ts

@@ -0,0 +1,56 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { User } from '@/server/modules/users/user.entity';
+import { Agent } from '@/server/modules/agent/agent.entity';
+
+@Entity('user_cards')
+export class UserCard {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'user_id', type: 'int', unsigned: true, comment: '用户ID' })
+  userId!: number;
+
+  @Column({ name: 'agent_id', type: 'int', unsigned: true, nullable: true, comment: '代理商ID' })
+  agentId!: number | null;
+
+  @Column({ name: 'card_no', type: 'varchar', length: 20, unique: true, comment: '卡号' })
+  cardNo!: string;
+
+  @Column({ name: 'sjt_card_no', type: 'varchar', length: 20, nullable: true, comment: '盛京通卡卡号' })
+  sjtCardNo!: string | null;
+
+  @Column({ name: 'password', type: 'varchar', length: 255, comment: '密码' })
+  password!: string;
+
+  @Column({ name: 'auth_code', type: 'varchar', length: 16, nullable: true, comment: '付款码70_75开头16位随机数' })
+  authCode!: string | null;
+
+  @Column({ name: 'state', type: 'int', unsigned: true, default: 1, comment: '状态 1绑定 2解绑 通用联名电子卡不可解绑' })
+  state!: number;
+
+  @Column({ name: 'balance', type: 'decimal', precision: 10, scale: 2, unsigned: true, default: 0.00, comment: '余额' })
+  balance!: number;
+
+  @Column({ name: 'is_default', type: 'int', default: 2, comment: '默认 1是 2否' })
+  isDefault!: number;
+
+  @Column({ name: 'created_by', type: 'int', unsigned: true, nullable: true, comment: '创建人ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', unsigned: true, nullable: true, comment: '更新人ID' })
+  updatedBy!: number | null;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+
+  @ManyToOne(() => User)
+  @JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
+  user!: User;
+
+  @ManyToOne(() => Agent)
+  @JoinColumn({ name: 'agent_id', referencedColumnName: 'id' })
+  agent!: Agent | null;
+}

+ 132 - 0
packages/server/src/modules/user-cards/user-card.schema.ts

@@ -0,0 +1,132 @@
+import { z } from '@hono/zod-openapi';
+import { UserSchema } from '@/server/modules/users/user.schema';
+import { AgentSchema } from '@/server/modules/agent/agent.schema';
+
+// 基础用户卡Schema
+const UserCardSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '用户卡ID',
+    example: 1
+  }),
+  userId: z.number().int().positive().openapi({
+    description: '用户ID',
+    example: 1001
+  }),
+  agentId: z.coerce.number().int().positive().nullable().openapi({
+    description: '代理商ID',
+    example: 5
+  }),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').openapi({
+    description: '卡号',
+    example: '12345678901234567890'
+  }),
+  sjtCardNo: z.string().max(20, '盛京通卡卡号最多20个字符').nullable().openapi({
+    description: '盛京通卡卡号',
+    example: 'SJT1234567890'
+  }),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符').openapi({
+    description: '密码',
+    example: 'encrypted_password'
+  }),
+  authCode: z.string().max(16, '付款码最多16个字符').nullable().openapi({
+    description: '付款码70_75开头16位随机数',
+    example: '7012345678901234'
+  }),
+  state: z.coerce.number().int().min(1).max(2).default(1).openapi({
+    description: '状态 1绑定 2解绑 通用联名电子卡不可解绑',
+    example: 1
+  }),
+  balance: z.coerce.number().multipleOf(0.01).min(0).max(999999.99).default(0).openapi({
+    description: '余额',
+    example: 100.50
+  }),
+  isDefault: z.coerce.number().int().min(1).max(2).default(2).openapi({
+    description: '默认 1是 2否',
+    example: 1
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建人ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新人ID',
+    example: 1
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T12:00:00Z'
+  }),
+  user: UserSchema.optional(),
+  agent: AgentSchema.nullable().optional()
+});
+
+// 创建用户卡DTO
+export const CreateUserCardDto = UserCardSchema.pick({
+  userId: true,
+  agentId: true,
+  cardNo: true,
+  sjtCardNo: true,
+  password: true,
+  authCode: true,
+  state: true,
+  balance: true,
+  isDefault: true
+}).extend({
+  userId: z.coerce.number<number>().int().positive('用户ID必须是正整数'),
+  agentId: z.coerce.number<number>().int().positive('代理商ID必须是正整数').nullable().optional(),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符'),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符'),
+  sjtCardNo: z.string().max(20, '盛京通卡卡号最多20个字符').nullable().optional(),
+  authCode: z.string().max(16, '付款码最多16个字符').nullable().optional(),
+  state: z.coerce.number<number>().int().min(1).max(2).default(1),
+  balance: z.coerce.number<number>().multipleOf(0.01).min(0).max(999999.99).default(0),
+  isDefault: z.coerce.number<number>().int().min(1).max(2).default(2)
+});
+
+// 更新用户卡DTO
+export const UpdateUserCardDto = UserCardSchema.pick({
+  userId: true,
+  agentId: true,
+  cardNo: true,
+  sjtCardNo: true,
+  password: true,
+  authCode: true,
+  state: true,
+  balance: true,
+  isDefault: true
+}).partial().extend({
+  userId: z.coerce.number<number>().int().positive('用户ID必须是正整数').optional(),
+  agentId: z.coerce.number<number>().int().positive('代理商ID必须是正整数').nullable().optional(),
+  cardNo: z.string().min(1, '卡号不能为空').max(20, '卡号最多20个字符').optional(),
+  password: z.string().min(1, '密码不能为空').max(255, '密码最多255个字符').optional(),
+  sjtCardNo: z.string().max(20, '盛京通卡卡号最多20个字符').nullable().optional(),
+  authCode: z.string().max(16, '付款码最多16个字符').nullable().optional(),
+  state: z.coerce.number<number>().int().min(1).max(2).optional(),
+  balance: z.coerce.number<number>().multipleOf(0.01).min(0).max(999999.99).optional(),
+  isDefault: z.coerce.number<number>().int().min(1).max(2).optional()
+});
+
+// 响应Schema(包含关联数据)
+export const UserCardResponseSchema = UserCardSchema.extend({
+  user: UserSchema.pick({
+    id: true,
+    username: true,
+    name: true,
+    phone: true
+  }).optional(),
+  agent: AgentSchema.pick({
+    id: true,
+    name: true
+  }).nullable().optional()
+});
+
+export type UserCard = z.infer<typeof UserCardSchema>;
+export type CreateUserCardDto = z.infer<typeof CreateUserCardDto>;
+export type UpdateUserCardDto = z.infer<typeof UpdateUserCardDto>;
+export type UserCardResponse = z.infer<typeof UserCardResponseSchema>;
+
+export { UserCardSchema };

+ 9 - 0
packages/server/src/modules/user-cards/user-card.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { UserCard } from './user-card.entity';
+
+export class UserCardService extends GenericCrudService<UserCard> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, UserCard);
+  }
+}