Преглед изворни кода

✨ feat(wechat-pay): 集成微信支付代金券功能

- 新增微信支付配置、代金券批次、代金券实体和数据库表
- 实现代金券批次创建、激活、查询等API接口
- 实现代金券发放、查询、同步等核心功能
- 集成微信支付V3 API调用和签名验证
- 重构客户端API调用,提取axios适配器到独立文件
- 更新开发文档移除AppError依赖,使用标准Error

📦 build(api): 注册微信支付相关路由

- 添加微信支付配置、代金券批次、代金券、支付接口路由
- 更新API类型定义支持新的微信相关路由
yourname пре 6 месеци
родитељ
комит
f7ec567400

+ 3 - 4
.roo/rules/11-custom-crud.md

@@ -70,7 +70,6 @@
      import { DataSource, Repository } from 'typeorm';
      import { YourEntity } from './your-entity.entity';
      import { CreateYourEntityDto, UpdateYourEntityDto } from './your-entity.entity';
-     import { AppError } from '@/server/utils/errorHandler';
      
      export class YourEntityService {
        private repository: Repository<YourEntity>;
@@ -105,7 +104,7 @@
        async findById(id: number): Promise<YourEntity> {
          const entity = await this.repository.findOneBy({ id });
          if (!entity) {
-           throw new AppError('实体不存在', 404);
+           throw new Error('实体不存在');
          }
          return entity;
        }
@@ -117,7 +116,7 @@
          // 业务规则验证示例
          const existing = await this.repository.findOneBy({ name: data.name });
          if (existing) {
-           throw new AppError('名称已存在', 400);
+           throw new Error('名称已存在');
          }
          
          const entity = this.repository.create(data);
@@ -147,7 +146,7 @@
          
          // 权限检查示例
          if (entity.createdBy !== userId) {
-           throw new AppError('没有删除权限', 403);
+           throw new Error('没有删除权限');
          }
          
          await this.repository.remove(entity);

+ 30 - 65
src/client/api.ts

@@ -1,77 +1,42 @@
-import axios, { isAxiosError } from 'axios';
 import { hc } from 'hono/client'
-import type {
-  AuthRoutes, UserRoutes, RoleRoutes,
-  FileRoutes
-} from '@/server/api';
-
-// 创建 axios 适配器
-const axiosFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
-  const requestHeaders: Record<string, string> = {};
-
-  if (init?.headers instanceof Headers) {
-    init.headers.forEach((value, key) => {
-      requestHeaders[key] = value;
-    })
-  }
-
-  const response = await axios.request({
-    url: url.toString(),
-    method: init?.method || 'GET',
-    headers: requestHeaders,
-    data: init?.body,
-  }).catch((error) => {
-    console.log('axiosFetch error', error)
-
-    if (isAxiosError(error)) {
-      return {
-        status: error.response?.status,
-        statusText: error.response?.statusText,
-        data: error.response?.data,
-        headers: error.response?.headers
-      }
-    }
-    throw error;
-  })
-
-  const responseHeaders = new Headers();
-  if (response.headers) {
-    for (const [key, value] of Object.entries(response.headers)) {
-      responseHeaders.set(key, value);
-    }
-  }
-
+import { axiosFetch } from './utils/axios-fetch'
+import type { AuthRoutes } from '@/server/api'
+import type { UserRoutes } from '@/server/api'
+import type { RoleRoutes } from '@/server/api'
+import type { FileRoutes } from '@/server/api'
+import type { WechatPayConfigRoutes } from '@/server/api'
+import type { WechatCouponStockRoutes } from '@/server/api'
+import type { WechatCouponRoutes } from '@/server/api'
+import type { WechatPayRoutes } from '@/server/api'
+
+export const authClient = hc<AuthRoutes>('/api/v1', {
+  fetch: axiosFetch,
+}).api.v1.auth
 
-  // 处理204 No Content响应,不设置body
-  const body = response.status === 204
-    ? null
-    : responseHeaders.get('content-type')?.includes('application/json')
-      ? JSON.stringify(response.data)
-      : response.data;
+export const userClient = hc<UserRoutes>('/api/v1', {
+  fetch: axiosFetch,
+}).api.v1.users
 
-  return new Response(
-    body,
-    {
-      status: response.status,
-      statusText: response.statusText,
-      headers: responseHeaders
-    }
-  )
-}
+export const roleClient = hc<RoleRoutes>('/api/v1', {
+  fetch: axiosFetch,
+}).api.v1.roles
 
+export const fileClient = hc<FileRoutes>('/api/v1', {
+  fetch: axiosFetch,
+}).api.v1.files
 
-export const authClient = hc<AuthRoutes>('/', {
+export const wechatPayConfigClient = hc<WechatPayConfigRoutes>('/api/v1', {
   fetch: axiosFetch,
-}).api.v1.auth;
+}).api.v1['wechat-pay-config']
 
-export const userClient = hc<UserRoutes>('/', {
+export const wechatCouponStockClient = hc<WechatCouponStockRoutes>('/api/v1', {
   fetch: axiosFetch,
-}).api.v1.users;
+}).api.v1['wechat-coupon-stocks']
 
-export const roleClient = hc<RoleRoutes>('/', {
+export const wechatCouponClient = hc<WechatCouponRoutes>('/api/v1', {
   fetch: axiosFetch,
-}).api.v1.roles;
+}).api.v1['wechat-coupons']
 
-export const fileClient = hc<FileRoutes>('/', {
+export const wechatPayClient = hc<WechatPayRoutes>('/api/v1', {
   fetch: axiosFetch,
-}).api.v1.files;
+}).api.v1['wechat-pay']

+ 54 - 0
src/client/utils/axios-fetch.ts

@@ -0,0 +1,54 @@
+import axios, { isAxiosError } from 'axios';
+// 创建 axios 适配器
+export const axiosFetch = async (url: RequestInfo | URL, init?: RequestInit) => {
+    const requestHeaders: Record<string, string> = {};
+  
+    if (init?.headers instanceof Headers) {
+      init.headers.forEach((value, key) => {
+        requestHeaders[key] = value;
+      })
+    }
+  
+    const response = await axios.request({
+      url: url.toString(),
+      method: init?.method || 'GET',
+      headers: requestHeaders,
+      data: init?.body,
+    }).catch((error) => {
+      console.log('axiosFetch error', error)
+  
+      if (isAxiosError(error)) {
+        return {
+          status: error.response?.status,
+          statusText: error.response?.statusText,
+          data: error.response?.data,
+          headers: error.response?.headers
+        }
+      }
+      throw error;
+    })
+  
+    const responseHeaders = new Headers();
+    if (response.headers) {
+      for (const [key, value] of Object.entries(response.headers)) {
+        responseHeaders.set(key, value);
+      }
+    }
+  
+  
+    // 处理204 No Content响应,不设置body
+    const body = response.status === 204
+      ? null
+      : responseHeaders.get('content-type')?.includes('application/json')
+        ? JSON.stringify(response.data)
+        : response.data;
+  
+    return new Response(
+      body,
+      {
+        status: response.status,
+        statusText: response.statusText,
+        headers: responseHeaders
+      }
+    )
+  }

+ 12 - 0
src/server/api.ts

@@ -5,6 +5,10 @@ import usersRouter from './api/users/index'
 import authRoute from './api/auth/index'
 import rolesRoute from './api/roles/index'
 import fileRoutes from './api/files/index'
+import wechatPayConfigRoutes from './api/wechat-pay-config/index'
+import wechatCouponStockRoutes from './api/wechat-coupon-stocks/index'
+import wechatCouponRoutes from './api/wechat-coupons/index'
+import wechatPayRoutes from './api/wechat-pay/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 import { Hono } from 'hono'
@@ -103,11 +107,19 @@ const userRoutes = api.route('/api/v1/users', usersRouter)
 const authRoutes = api.route('/api/v1/auth', authRoute)
 const roleRoutes = api.route('/api/v1/roles', rolesRoute)
 const fileApiRoutes = api.route('/api/v1/files', fileRoutes)
+const wechatPayConfigApiRoutes = api.route('/api/v1/wechat-pay-config', wechatPayConfigRoutes)
+const wechatCouponStockApiRoutes = api.route('/api/v1/wechat-coupon-stocks', wechatCouponStockRoutes)
+const wechatCouponApiRoutes = api.route('/api/v1/wechat-coupons', wechatCouponRoutes)
+const wechatPayApiRoutes = api.route('/api/v1/wechat-pay', wechatPayRoutes)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
 export type RoleRoutes = typeof roleRoutes
 export type FileRoutes = typeof fileApiRoutes
+export type WechatPayConfigRoutes = typeof wechatPayConfigApiRoutes
+export type WechatCouponStockRoutes = typeof wechatCouponStockApiRoutes
+export type WechatCouponRoutes = typeof wechatCouponApiRoutes
+export type WechatPayRoutes = typeof wechatPayApiRoutes
 
 app.route('/', api)
 export default app

+ 21 - 0
src/server/api/wechat-coupon-stocks/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { WechatCouponStock } from '@/server/modules/wechat-pay/wechat-coupon-stock.entity';
+import { WechatCouponStockSchema, CreateWechatCouponStockDto, UpdateWechatCouponStockDto } from '@/server/modules/wechat-pay/wechat-coupon-stock.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const wechatCouponStockRoutes = createCrudRoutes({
+  entity: WechatCouponStock,
+  createSchema: CreateWechatCouponStockDto,
+  updateSchema: UpdateWechatCouponStockDto,
+  getSchema: WechatCouponStockSchema,
+  listSchema: WechatCouponStockSchema,
+  searchFields: ['stockName', 'stockId'],
+  relations: ['config'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default wechatCouponStockRoutes;

+ 21 - 0
src/server/api/wechat-coupons/index.ts

@@ -0,0 +1,21 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { WechatCoupon } from '@/server/modules/wechat-pay/wechat-coupon.entity';
+import { WechatCouponSchema, CreateWechatCouponDto, UpdateWechatCouponDto } from '@/server/modules/wechat-pay/wechat-coupon.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const wechatCouponRoutes = createCrudRoutes({
+  entity: WechatCoupon,
+  createSchema: CreateWechatCouponDto,
+  updateSchema: UpdateWechatCouponDto,
+  getSchema: WechatCouponSchema,
+  listSchema: WechatCouponSchema,
+  searchFields: ['openid', 'stockId', 'couponId'],
+  relations: ['stock', 'stock.config'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default wechatCouponRoutes;

+ 20 - 0
src/server/api/wechat-pay-config/index.ts

@@ -0,0 +1,20 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { WechatPayConfig } from '@/server/modules/wechat-pay/wechat-pay-config.entity';
+import { WechatPayConfigSchema, CreateWechatPayConfigDto, UpdateWechatPayConfigDto } from '@/server/modules/wechat-pay/wechat-pay-config.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const wechatPayConfigRoutes = createCrudRoutes({
+  entity: WechatPayConfig,
+  createSchema: CreateWechatPayConfigDto,
+  updateSchema: UpdateWechatPayConfigDto,
+  getSchema: WechatPayConfigSchema,
+  listSchema: WechatPayConfigSchema,
+  searchFields: ['merchantId', 'appId'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});
+
+export default wechatPayConfigRoutes;

+ 66 - 0
src/server/api/wechat-pay/actions/activate-stock.ts

@@ -0,0 +1,66 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { WechatPayService } from '@/server/modules/wechat-pay/wechat-pay.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const ActivateStockParams = z.object({
+  stockId: z.string().max(32).openapi({
+    param: { name: 'stockId', in: 'path' },
+    example: '1234567890',
+    description: '批次号'
+  })
+});
+
+const ActivateStockResponse = z.object({
+  success: z.boolean().openapi({ description: '激活结果', example: true }),
+  message: z.string().openapi({ description: '结果消息', example: '批次激活成功' })
+});
+
+const routeDef = createRoute({
+  method: 'post',
+  path: '/stocks/{stockId}/activate',
+  middleware: [authMiddleware],
+  request: {
+    params: ActivateStockParams
+  },
+  responses: {
+    200: {
+      description: '批次激活成功',
+      content: { 'application/json': { schema: ActivateStockResponse } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '批次不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { stockId } = c.req.valid('param');
+    const service = new WechatPayService(AppDataSource);
+    
+    await service.activateStock(stockId);
+    
+    return c.json({
+      success: true,
+      message: '批次激活成功'
+    }, 200);
+  } catch (error) {
+    const { code = 500, message = '激活批次失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 85 - 0
src/server/api/wechat-pay/actions/post.ts

@@ -0,0 +1,85 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { WechatPayService } from '@/server/modules/wechat-pay/wechat-pay.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const SendCouponRequest = z.object({
+  openid: z.string().max(64).openapi({
+    description: '用户openid',
+    example: 'oP-y45xbf7hrhC6yeabpjXzIhUWg'
+  }),
+  stockId: z.string().max(32).openapi({
+    description: '批次号',
+    example: '1234567890'
+  }),
+  outRequestNo: z.string().max(64).openapi({
+    description: '商户请求单号',
+    example: 'ORDER123456789'
+  }),
+  stockCreatorMchid: z.string().max(32).openapi({
+    description: '创建批次的商户号',
+    example: '1234567890'
+  }),
+  configId: z.coerce.number().int().positive().openapi({
+    description: '微信支付配置ID',
+    example: 1
+  })
+});
+
+const SendCouponResponse = z.object({
+  couponId: z.string().openapi({ description: '代金券ID', example: '1234567890' }),
+  stockId: z.string().openapi({ description: '批次号', example: '1234567890' }),
+  openid: z.string().openapi({ description: '用户openid', example: 'oP-y45xbf7hrhC6yeabpjXzIhUWg' }),
+  outRequestNo: z.string().openapi({ description: '商户请求单号', example: 'ORDER123456789' })
+});
+
+const routeDef = createRoute({
+  method: 'post',
+  path: '/send-coupon',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: SendCouponRequest }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '代金券发放成功',
+      content: { 'application/json': { schema: SendCouponResponse } }
+    },
+    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 body = await c.req.json();
+    const service = new WechatPayService(AppDataSource);
+    
+    const coupon = await service.sendCoupon(body, body.configId);
+    
+    return c.json({
+      couponId: coupon.couponId,
+      stockId: coupon.stockId,
+      openid: coupon.openid,
+      outRequestNo: coupon.outRequestNo
+    }, 200);
+  } catch (error) {
+    const { code = 500, message = '发放代金券失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 87 - 0
src/server/api/wechat-pay/actions/query-coupon.ts

@@ -0,0 +1,87 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { WechatPayService } from '@/server/modules/wechat-pay/wechat-pay.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const QueryCouponParams = z.object({
+  couponId: z.string().max(32).openapi({
+    param: { name: 'couponId', in: 'path' },
+    example: '1234567890',
+    description: '代金券ID'
+  }),
+  openid: z.string().max(64).openapi({
+    param: { name: 'openid', in: 'query' },
+    example: 'oP-y45xbf7hrhC6yeabpjXzIhUWg'
+  })
+});
+
+const QueryCouponResponse = z.object({
+  couponId: z.string().openapi({ description: '代金券ID', example: '1234567890' }),
+  stockId: z.string().openapi({ description: '批次号', example: '1234567890' }),
+  openid: z.string().openapi({ description: '用户openid', example: 'oP-y45xbf7hrhC6yeabpjXzIhUWg' }),
+  status: z.string().openapi({ description: '代金券状态', example: 'SENDED' }),
+  amount: z.number().openapi({ description: '代金券面额(分)', example: 100 }),
+  availableStartTime: z.string().openapi({ description: '可用开始时间', example: '2024-01-01T00:00:00Z' }),
+  availableEndTime: z.string().openapi({ description: '可用结束时间', example: '2024-12-31T23:59:59Z' })
+});
+
+const routeDef = createRoute({
+  method: 'get',
+  path: '/coupons/{couponId}',
+  middleware: [authMiddleware],
+  request: {
+    params: QueryCouponParams,
+    query: z.object({
+      openid: z.string().max(64).openapi({
+        description: '用户openid',
+        example: 'oP-y45xbf7hrhC6yeabpjXzIhUWg'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '代金券详情查询成功',
+      content: { 'application/json': { schema: QueryCouponResponse } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '代金券不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { couponId } = c.req.valid('param');
+    const { openid } = c.req.valid('query');
+    
+    const service = new WechatPayService(AppDataSource);
+    const couponDetail = await service.getCouponDetail(couponId, openid);
+    
+    return c.json({
+      couponId: couponDetail.coupon_id,
+      stockId: couponDetail.stock_id,
+      openid: couponDetail.openid,
+      status: couponDetail.status,
+      amount: couponDetail.amount,
+      availableStartTime: couponDetail.available_start_time,
+      availableEndTime: couponDetail.available_end_time
+    }, 200);
+  } catch (error) {
+    const { code = 500, message = '查询代金券详情失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 94 - 0
src/server/api/wechat-pay/actions/query-stocks.ts

@@ -0,0 +1,94 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { WechatPayService } from '@/server/modules/wechat-pay/wechat-pay.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const QueryStocksQuery = z.object({
+  offset: z.coerce.number().int().min(0).default(0).openapi({
+    description: '偏移量',
+    example: 0
+  }),
+  limit: z.coerce.number().int().min(1).max(100).default(10).openapi({
+    description: '返回数量',
+    example: 10
+  }),
+  stockCreatorMchid: z.string().max(32).openapi({
+    description: '创建批次的商户号',
+    example: '1234567890'
+  }),
+  status: z.string().max(20).optional().openapi({
+    description: '批次状态',
+    example: 'RUNNING'
+  })
+});
+
+const StockItem = z.object({
+  stockId: z.string().openapi({ description: '批次号', example: '1234567890' }),
+  stockName: z.string().openapi({ description: '批次名称', example: '春节代金券' }),
+  status: z.string().openapi({ description: '批次状态', example: 'RUNNING' }),
+  couponAmount: z.number().openapi({ description: '代金券面额(分)', example: 100 }),
+  couponQuantity: z.number().openapi({ description: '代金券数量', example: 1000 }),
+  availableQuantity: z.number().openapi({ description: '可用数量', example: 800 }),
+  distributedQuantity: z.number().openapi({ description: '已发放数量', example: 200 }),
+  availableBeginTime: z.string().openapi({ description: '可用开始时间', example: '2024-01-01T00:00:00Z' }),
+  availableEndTime: z.string().openapi({ description: '可用结束时间', example: '2024-12-31T23:59:59Z' })
+});
+
+const QueryStocksResponse = z.object({
+  data: z.array(StockItem),
+  offset: z.number().openapi({ description: '偏移量', example: 0 }),
+  limit: z.number().openapi({ description: '返回数量', example: 10 }),
+  total: z.number().openapi({ description: '总数量', example: 100 })
+});
+
+const routeDef = createRoute({
+  method: 'get',
+  path: '/stocks',
+  middleware: [authMiddleware],
+  request: {
+    query: QueryStocksQuery
+  },
+  responses: {
+    200: {
+      description: '批次列表查询成功',
+      content: { 'application/json': { schema: QueryStocksResponse } }
+    },
+    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 service = new WechatPayService(AppDataSource);
+    
+    const stocks = await service.getStockList(
+      query.offset,
+      query.limit,
+      query.stockCreatorMchid,
+      query.status
+    );
+    
+    return c.json({
+      data: stocks.data || [],
+      offset: query.offset,
+      limit: query.limit,
+      total: stocks.total_count || 0
+    }, 200);
+  } catch (error) {
+    const { code = 500, message = '查询批次列表失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 13 - 0
src/server/api/wechat-pay/index.ts

@@ -0,0 +1,13 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import sendCouponRoute from './actions/post';
+import activateStockRoute from './actions/activate-stock';
+import queryCouponRoute from './actions/query-coupon';
+import queryStocksRoute from './actions/query-stocks';
+
+const app = new OpenAPIHono()
+  .route('/', sendCouponRoute)
+  .route('/', activateStockRoute)
+  .route('/', queryCouponRoute)
+  .route('/', queryStocksRoute);
+
+export default app;

+ 4 - 1
src/server/data-source.ts

@@ -6,6 +6,9 @@ import process from 'node:process'
 import { UserEntity as User } from "./modules/users/user.entity"
 import { Role } from "./modules/users/role.entity"
 import { File } from "./modules/files/file.entity"
+import { WechatPayConfig } from "./modules/wechat-pay/wechat-pay-config.entity"
+import { WechatCouponStock } from "./modules/wechat-pay/wechat-coupon-stock.entity"
+import { WechatCoupon } from "./modules/wechat-pay/wechat-coupon.entity"
 
 export const AppDataSource = new DataSource({
   type: "mysql",
@@ -15,7 +18,7 @@ export const AppDataSource = new DataSource({
   password: process.env.DB_PASSWORD || "",
   database: process.env.DB_DATABASE || "d8dai",
   entities: [
-    User, Role, File,
+    User, Role, File, WechatPayConfig, WechatCouponStock, WechatCoupon,
   ],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 0 - 1
src/server/modules/files/file.service.ts

@@ -2,7 +2,6 @@ import { GenericCrudService } from '@/server/utils/generic-crud.service';
 import { DataSource } from 'typeorm';
 import { File } from './file.entity';
 import { MinioService } from './minio.service';
-// import { AppError } from '@/server/utils/errorHandler';
 import { v4 as uuidv4 } from 'uuid';
 import { logger } from '@/server/utils/logger';
 

+ 117 - 0
src/server/modules/wechat-pay/wechat-coupon-stock.entity.ts

@@ -0,0 +1,117 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+import { WechatPayConfig } from './wechat-pay-config.entity';
+
+@Entity('wechat_coupon_stocks')
+export class WechatCouponStock {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'stock_id', type: 'varchar', length: 32, unique: true, comment: '微信支付批次号' })
+  stockId!: string;
+
+  @Column({ name: 'stock_name', type: 'varchar', length: 64, comment: '批次名称' })
+  stockName!: string;
+
+  @Column({ name: 'stock_creator_mchid', type: 'varchar', length: 32, comment: '创建批次的商户号' })
+  stockCreatorMchid!: string;
+
+  @Column({ name: 'coupon_type', type: 'varchar', length: 20, comment: '代金券类型:NORMAL-满减券,CUT_TO-减至券' })
+  couponType!: string;
+
+  @Column({ name: 'coupon_use_rule', type: 'json', comment: '代金券使用规则' })
+  couponUseRule!: any;
+
+  @Column({ name: 'stock_send_rule', type: 'json', comment: '批次发放规则' })
+  stockSendRule!: any;
+
+  @Column({ name: 'coupon_amount', type: 'int', comment: '代金券面额(分)' })
+  couponAmount!: number;
+
+  @Column({ name: 'coupon_quantity', type: 'int', comment: '代金券数量' })
+  couponQuantity!: number;
+
+  @Column({ name: 'available_quantity', type: 'int', comment: '可用数量' })
+  availableQuantity!: number;
+
+  @Column({ name: 'distributed_quantity', type: 'int', default: 0, comment: '已发放数量' })
+  distributedQuantity!: number;
+
+  @Column({ name: 'status', type: 'varchar', length: 20, default: 'CREATED', comment: '批次状态:CREATED-已创建,PROCESSING-发放中,RUNNING-运行中,STOPED-已停止,PAUSED-暂停,FINISHED-已结束' })
+  status!: string;
+
+  @Column({ name: 'start_time', type: 'datetime', comment: '开始时间' })
+  startTime!: Date;
+
+  @Column({ name: 'end_time', type: 'datetime', comment: '结束时间' })
+  endTime!: Date;
+
+  @Column({ name: 'config_id', type: 'int', comment: '微信支付配置ID' })
+  configId!: number;
+
+  @ManyToOne(() => WechatPayConfig)
+  @JoinColumn({ name: 'config_id' })
+  config!: WechatPayConfig;
+
+  @Column({ name: 'created_by', type: 'int', nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+
+  @CreateDateColumn({ name: 'created_at' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at' })
+  updatedAt!: Date;
+}
+
+// Zod Schema定义
+export const WechatCouponStockSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '批次ID' }),
+  stockId: z.string().max(32).openapi({ description: '微信支付批次号', example: '1234567890' }),
+  stockName: z.string().max(64).openapi({ description: '批次名称', example: '春节代金券' }),
+  stockCreatorMchid: z.string().max(32).openapi({ description: '创建批次的商户号', example: '1234567890' }),
+  couponType: z.string().max(20).openapi({ description: '代金券类型', example: 'NORMAL' }),
+  couponUseRule: z.any().openapi({ description: '代金券使用规则' }),
+  stockSendRule: z.any().openapi({ description: '批次发放规则' }),
+  couponAmount: z.number().int().positive().openapi({ description: '代金券面额(分)', example: 100 }),
+  couponQuantity: z.number().int().positive().openapi({ description: '代金券数量', example: 1000 }),
+  availableQuantity: z.number().int().min(0).openapi({ description: '可用数量', example: 1000 }),
+  distributedQuantity: z.number().int().min(0).default(0).openapi({ description: '已发放数量', example: 0 }),
+  status: z.string().max(20).default('CREATED').openapi({ description: '批次状态', example: 'CREATED' }),
+  startTime: z.date().openapi({ description: '开始时间', example: '2024-01-01T00:00:00Z' }),
+  endTime: z.date().openapi({ description: '结束时间', example: '2024-12-31T23:59:59Z' }),
+  configId: z.number().int().positive().openapi({ description: '微信支付配置ID', 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.date().openapi({ description: '创建时间', example: '2024-01-01T00:00:00Z' }),
+  updatedAt: z.date().openapi({ description: '更新时间', example: '2024-01-01T00:00:00Z' })
+});
+
+export const CreateWechatCouponStockDto = z.object({
+  stockName: z.string().max(64).openapi({ description: '批次名称', example: '春节代金券' }),
+  stockCreatorMchid: z.string().max(32).openapi({ description: '创建批次的商户号', example: '1234567890' }),
+  couponType: z.string().max(20).openapi({ description: '代金券类型', example: 'NORMAL' }),
+  couponUseRule: z.any().openapi({ description: '代金券使用规则' }),
+  stockSendRule: z.any().openapi({ description: '批次发放规则' }),
+  couponAmount: z.number().int().positive().openapi({ description: '代金券面额(分)', example: 100 }),
+  couponQuantity: z.number().int().positive().openapi({ description: '代金券数量', example: 1000 }),
+  startTime: z.coerce.date().openapi({ description: '开始时间', example: '2024-01-01T00:00:00Z' }),
+  endTime: z.coerce.date().openapi({ description: '结束时间', example: '2024-12-31T23:59:59Z' }),
+  configId: z.number().int().positive().openapi({ description: '微信支付配置ID', example: 1 })
+});
+
+export const UpdateWechatCouponStockDto = z.object({
+  stockName: z.string().max(64).optional().openapi({ description: '批次名称', example: '春节代金券' }),
+  couponUseRule: z.any().optional().openapi({ description: '代金券使用规则' }),
+  stockSendRule: z.any().optional().openapi({ description: '批次发放规则' }),
+  couponAmount: z.number().int().positive().optional().openapi({ description: '代金券面额(分)', example: 100 }),
+  couponQuantity: z.number().int().positive().optional().openapi({ description: '代金券数量', example: 1000 }),
+  availableQuantity: z.number().int().min(0).optional().openapi({ description: '可用数量', example: 1000 }),
+  distributedQuantity: z.number().int().min(0).optional().openapi({ description: '已发放数量', example: 0 }),
+  status: z.string().max(20).optional().openapi({ description: '批次状态', example: 'RUNNING' }),
+  startTime: z.coerce.date().optional().openapi({ description: '开始时间', example: '2024-01-01T00:00:00Z' }),
+  endTime: z.coerce.date().optional().openapi({ description: '结束时间', example: '2024-12-31T23:59:59Z' }),
+  configId: z.number().int().positive().optional().openapi({ description: '微信支付配置ID', example: 1 })
+});

+ 97 - 0
src/server/modules/wechat-pay/wechat-coupon.entity.ts

@@ -0,0 +1,97 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+import { WechatCouponStock } from './wechat-coupon-stock.entity';
+
+@Entity('wechat_coupons')
+export class WechatCoupon {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'coupon_id', type: 'varchar', length: 32, unique: true, comment: '微信支付代金券ID' })
+  couponId!: string;
+
+  @Column({ name: 'stock_id', type: 'varchar', length: 32, comment: '批次号' })
+  stockId!: string;
+
+  @Column({ name: 'openid', type: 'varchar', length: 64, comment: '用户openid' })
+  openid!: string;
+
+  @Column({ name: 'out_request_no', type: 'varchar', length: 64, comment: '商户请求单号' })
+  outRequestNo!: string;
+
+  @Column({ name: 'coupon_status', type: 'varchar', length: 20, default: 'SENDED', comment: '代金券状态:SENDED-已发放,USED-已使用,EXPIRED-已过期' })
+  couponStatus!: string;
+
+  @Column({ name: 'amount', type: 'int', comment: '代金券面额(分)' })
+  amount!: number;
+
+  @Column({ name: 'available_start_time', type: 'datetime', comment: '可用开始时间' })
+  availableStartTime!: Date;
+
+  @Column({ name: 'available_end_time', type: 'datetime', comment: '可用结束时间' })
+  availableEndTime!: Date;
+
+  @Column({ name: 'used_time', type: 'datetime', nullable: true, comment: '使用时间' })
+  usedTime!: Date | null;
+
+  @Column({ name: 'transaction_id', type: 'varchar', length: 64, nullable: true, comment: '微信支付订单号' })
+  transactionId!: string | null;
+
+  @Column({ name: 'stock_id_ref', type: 'int', comment: '批次ID' })
+  stockIdRef!: number;
+
+  @ManyToOne(() => WechatCouponStock)
+  @JoinColumn({ name: 'stock_id_ref' })
+  stock!: WechatCouponStock;
+
+  @Column({ name: 'created_by', type: 'int', nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+
+  @CreateDateColumn({ name: 'created_at' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at' })
+  updatedAt!: Date;
+}
+
+// Zod Schema定义
+export const WechatCouponSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '代金券ID' }),
+  couponId: z.string().max(32).openapi({ description: '微信支付代金券ID', example: '1234567890' }),
+  stockId: z.string().max(32).openapi({ description: '批次号', example: '1234567890' }),
+  openid: z.string().max(64).openapi({ description: '用户openid', example: 'oP-y45xbf7hrhC6yeabpjXzIhUWg' }),
+  outRequestNo: z.string().max(64).openapi({ description: '商户请求单号', example: 'ORDER123456789' }),
+  couponStatus: z.string().max(20).default('SENDED').openapi({ description: '代金券状态', example: 'SENDED' }),
+  amount: z.number().int().positive().openapi({ description: '代金券面额(分)', example: 100 }),
+  availableStartTime: z.date().openapi({ description: '可用开始时间', example: '2024-01-01T00:00:00Z' }),
+  availableEndTime: z.date().openapi({ description: '可用结束时间', example: '2024-12-31T23:59:59Z' }),
+  usedTime: z.date().nullable().openapi({ description: '使用时间', example: null }),
+  transactionId: z.string().max(64).nullable().openapi({ description: '微信支付订单号', example: null }),
+  stockIdRef: z.number().int().positive().openapi({ description: '批次ID', 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.date().openapi({ description: '创建时间', example: '2024-01-01T00:00:00Z' }),
+  updatedAt: z.date().openapi({ description: '更新时间', example: '2024-01-01T00:00:00Z' })
+});
+
+export const CreateWechatCouponDto = z.object({
+  couponId: z.string().max(32).openapi({ description: '微信支付代金券ID', example: '1234567890' }),
+  stockId: z.string().max(32).openapi({ description: '批次号', example: '1234567890' }),
+  openid: z.string().max(64).openapi({ description: '用户openid', example: 'oP-y45xbf7hrhC6yeabpjXzIhUWg' }),
+  outRequestNo: z.string().max(64).openapi({ description: '商户请求单号', example: 'ORDER123456789' }),
+  couponStatus: z.string().max(20).default('SENDED').openapi({ description: '代金券状态', example: 'SENDED' }),
+  amount: z.number().int().positive().openapi({ description: '代金券面额(分)', example: 100 }),
+  availableStartTime: z.coerce.date().openapi({ description: '可用开始时间', example: '2024-01-01T00:00:00Z' }),
+  availableEndTime: z.coerce.date().openapi({ description: '可用结束时间', example: '2024-12-31T23:59:59Z' }),
+  stockIdRef: z.number().int().positive().openapi({ description: '批次ID', example: 1 })
+});
+
+export const UpdateWechatCouponDto = z.object({
+  couponStatus: z.string().max(20).optional().openapi({ description: '代金券状态', example: 'USED' }),
+  usedTime: z.coerce.date().optional().openapi({ description: '使用时间', example: '2024-01-01T12:00:00Z' }),
+  transactionId: z.string().max(64).optional().openapi({ description: '微信支付订单号', example: '4200000123456789' }),
+  stockIdRef: z.number().int().positive().optional().openapi({ description: '批次ID', example: 1 })
+});

+ 71 - 0
src/server/modules/wechat-pay/wechat-pay-config.entity.ts

@@ -0,0 +1,71 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+
+@Entity('wechat_pay_config')
+export class WechatPayConfig {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'merchant_id', type: 'varchar', length: 32, comment: '商户号' })
+  merchantId!: string;
+
+  @Column({ name: 'app_id', type: 'varchar', length: 32, comment: '应用ID' })
+  appId!: string;
+
+  @Column({ name: 'private_key', type: 'text', comment: '商户私钥' })
+  privateKey!: string;
+
+  @Column({ name: 'certificate_serial_no', type: 'varchar', length: 40, comment: '证书序列号' })
+  certificateSerialNo!: string;
+
+  @Column({ name: 'api_v3_key', type: 'varchar', length: 32, comment: 'APIv3密钥' })
+  apiV3Key!: string;
+
+  @Column({ name: 'is_active', type: 'tinyint', default: 1, comment: '是否启用:0-禁用,1-启用' })
+  isActive!: number;
+
+  @Column({ name: 'created_by', type: 'int', nullable: true, comment: '创建用户ID' })
+  createdBy!: number | null;
+
+  @Column({ name: 'updated_by', type: 'int', nullable: true, comment: '更新用户ID' })
+  updatedBy!: number | null;
+
+  @CreateDateColumn({ name: 'created_at' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at' })
+  updatedAt!: Date;
+}
+
+// Zod Schema定义
+export const WechatPayConfigSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '配置ID' }),
+  merchantId: z.string().max(32).openapi({ description: '商户号', example: '1234567890' }),
+  appId: z.string().max(32).openapi({ description: '应用ID', example: 'wx1234567890abcdef' }),
+  privateKey: z.string().openapi({ description: '商户私钥', example: '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASC...' }),
+  certificateSerialNo: z.string().max(40).openapi({ description: '证书序列号', example: '1234567890ABCDEF1234567890ABCDEF12345678' }),
+  apiV3Key: z.string().max(32).openapi({ description: 'APIv3密钥', example: 'abcdefghijklmnopqrstuvwxyz012345' }),
+  isActive: z.number().int().min(0).max(1).openapi({ description: '是否启用:0-禁用,1-启用', 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.date().openapi({ description: '创建时间', example: '2024-01-01T00:00:00Z' }),
+  updatedAt: z.date().openapi({ description: '更新时间', example: '2024-01-01T00:00:00Z' })
+});
+
+export const CreateWechatPayConfigDto = z.object({
+  merchantId: z.string().max(32).openapi({ description: '商户号', example: '1234567890' }),
+  appId: z.string().max(32).openapi({ description: '应用ID', example: 'wx1234567890abcdef' }),
+  privateKey: z.string().openapi({ description: '商户私钥', example: '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASC...' }),
+  certificateSerialNo: z.string().max(40).openapi({ description: '证书序列号', example: '1234567890ABCDEF1234567890ABCDEF12345678' }),
+  apiV3Key: z.string().max(32).openapi({ description: 'APIv3密钥', example: 'abcdefghijklmnopqrstuvwxyz012345' }),
+  isActive: z.number().int().min(0).max(1).default(1).openapi({ description: '是否启用:0-禁用,1-启用', example: 1 })
+});
+
+export const UpdateWechatPayConfigDto = z.object({
+  merchantId: z.string().max(32).optional().openapi({ description: '商户号', example: '1234567890' }),
+  appId: z.string().max(32).optional().openapi({ description: '应用ID', example: 'wx1234567890abcdef' }),
+  privateKey: z.string().optional().openapi({ description: '商户私钥', example: '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASC...' }),
+  certificateSerialNo: z.string().max(40).optional().openapi({ description: '证书序列号', example: '1234567890ABCDEF1234567890ABCDEF12345678' }),
+  apiV3Key: z.string().max(32).optional().openapi({ description: 'APIv3密钥', example: 'abcdefghijklmnopqrstuvwxyz012345' }),
+  isActive: z.number().int().min(0).max(1).optional().openapi({ description: '是否启用:0-禁用,1-启用', example: 1 })
+});

+ 347 - 0
src/server/modules/wechat-pay/wechat-pay.service.ts

@@ -0,0 +1,347 @@
+import { DataSource, Repository } from 'typeorm';
+import { WechatPayConfig } from './wechat-pay-config.entity';
+import { WechatCouponStock } from './wechat-coupon-stock.entity';
+import { WechatCoupon } from './wechat-coupon.entity';
+import crypto from 'crypto';
+import axios from 'axios';
+
+interface WechatPayConfigData {
+  merchantId: string;
+  appId: string;
+  privateKey: string;
+  certificateSerialNo: string;
+  apiV3Key: string;
+}
+
+interface CreateStockData {
+  stockName: string;
+  stockCreatorMchid: string;
+  couponType: string;
+  couponUseRule: any;
+  stockSendRule: any;
+  couponAmount: number;
+  couponQuantity: number;
+  startTime: Date;
+  endTime: Date;
+}
+
+interface SendCouponData {
+  openid: string;
+  stockId: string;
+  outRequestNo: string;
+  stockCreatorMchid: string;
+}
+
+export class WechatPayService {
+  private configRepository: Repository<WechatPayConfig>;
+  private stockRepository: Repository<WechatCouponStock>;
+  private couponRepository: Repository<WechatCoupon>;
+
+  constructor(private dataSource: DataSource) {
+    this.configRepository = dataSource.getRepository(WechatPayConfig);
+    this.stockRepository = dataSource.getRepository(WechatCouponStock);
+    this.couponRepository = dataSource.getRepository(WechatCoupon);
+  }
+
+  // 获取微信支付配置
+  async getActiveConfig(): Promise<WechatPayConfig> {
+    const config = await this.configRepository.findOne({ where: { isActive: 1 } });
+    if (!config) {
+      throw new Error('未找到有效的微信支付配置');
+    }
+    return config;
+  }
+
+  // 生成V3签名
+  private generateV3Signature(
+    httpMethod: string,
+    url: string,
+    body: string,
+    config: WechatPayConfigData
+  ): string {
+    const timestamp = Math.floor(Date.now() / 1000);
+    const nonceStr = this.generateNonceStr(32);
+    
+    const urlParts = new URL(url);
+    const canonicalUrl = urlParts.pathname + (urlParts.search || '');
+    
+    const message = `${httpMethod}\n${canonicalUrl}\n${timestamp}\n${nonceStr}\n${body}\n`;
+    
+    const sign = crypto.createSign('RSA-SHA256');
+    sign.write(message);
+    sign.end();
+    
+    const privateKey = `-----BEGIN PRIVATE KEY-----\n${config.privateKey}\n-----END PRIVATE KEY-----`;
+    const signature = sign.sign(privateKey, 'base64');
+    
+    const token = `WECHATPAY2-SHA256-RSA2048 mchid="${config.merchantId}",nonce_str="${nonceStr}",timestamp="${timestamp}",serial_no="${config.certificateSerialNo}",signature="${signature}"`;
+    
+    return token;
+  }
+
+  // 生成随机字符串
+  private generateNonceStr(length: number = 32): string {
+    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    let result = '';
+    for (let i = 0; i < length; i++) {
+      result += chars.charAt(Math.floor(Math.random() * chars.length));
+    }
+    return result;
+  }
+
+  // 发起微信支付API请求
+  private async wechatPayRequest(
+    method: string,
+    endpoint: string,
+    data?: any,
+    config?: WechatPayConfig
+  ): Promise<any> {
+    const activeConfig = config || await this.getActiveConfig();
+    
+    const configData: WechatPayConfigData = {
+      merchantId: activeConfig.merchantId,
+      appId: activeConfig.appId,
+      privateKey: activeConfig.privateKey,
+      certificateSerialNo: activeConfig.certificateSerialNo,
+      apiV3Key: activeConfig.apiV3Key
+    };
+
+    const url = `https://api.mch.weixin.qq.com${endpoint}`;
+    const body = data ? JSON.stringify(data) : '';
+    const signature = this.generateV3Signature(method, url, body, configData);
+
+    const response = await axios({
+      method,
+      url,
+      data,
+      headers: {
+        'Authorization': signature,
+        'Content-Type': 'application/json',
+        'Accept': 'application/json',
+        'User-Agent': 'WeChatPay/1.0'
+      }
+    });
+
+    return response.data;
+  }
+
+  // 创建代金券批次
+  async createStock(data: CreateStockData, configId: number): Promise<WechatCouponStock> {
+    const config = await this.configRepository.findOne({ where: { id: configId } });
+    if (!config) {
+      throw new Error('微信支付配置不存在');
+    }
+
+    const stockData = {
+      stock_name: data.stockName,
+      stock_creator_mchid: data.stockCreatorMchid,
+      coupon_use_rule: data.couponUseRule,
+      stock_send_rule: data.stockSendRule,
+      coupon_type: data.couponType,
+      coupon_amount: data.couponAmount,
+      coupon_quantity: data.couponQuantity,
+      available_begin_time: data.startTime.toISOString(),
+      available_end_time: data.endTime.toISOString()
+    };
+
+    const response = await this.wechatPayRequest('POST', '/v3/marketing/favor/coupon-stocks', stockData, config);
+
+    const stock = this.stockRepository.create({
+      stockId: response.stock_id,
+      stockName: data.stockName,
+      stockCreatorMchid: data.stockCreatorMchid,
+      couponType: data.couponType,
+      couponUseRule: data.couponUseRule,
+      stockSendRule: data.stockSendRule,
+      couponAmount: data.couponAmount,
+      couponQuantity: data.couponQuantity,
+      availableQuantity: data.couponQuantity,
+      distributedQuantity: 0,
+      status: 'CREATED',
+      startTime: data.startTime,
+      endTime: data.endTime,
+      configId
+    });
+
+    return this.stockRepository.save(stock);
+  }
+
+  // 激活代金券批次
+  async activateStock(stockId: string): Promise<void> {
+    const stock = await this.stockRepository.findOne({ where: { stockId } });
+    if (!stock) {
+      throw new Error('代金券批次不存在');
+    }
+
+    const config = await this.configRepository.findOne({ where: { id: stock.configId } });
+    if (!config) {
+      throw new Error('微信支付配置不存在');
+    }
+
+    await this.wechatPayRequest('POST', `/v3/marketing/favor/stocks/${stockId}/start`, {
+      stock_creator_mchid: stock.stockCreatorMchid
+    }, config);
+
+    stock.status = 'RUNNING';
+    await this.stockRepository.save(stock);
+  }
+
+  // 发放代金券
+  async sendCoupon(data: SendCouponData, configId: number): Promise<WechatCoupon> {
+    const stock = await this.stockRepository.findOne({ 
+      where: { stockId: data.stockId, stockCreatorMchid: data.stockCreatorMchid } 
+    });
+    if (!stock) {
+      throw new Error('代金券批次不存在');
+    }
+
+    if (stock.availableQuantity <= 0) {
+      throw new Error('代金券库存不足');
+    }
+
+    const config = await this.configRepository.findOne({ where: { id: configId } });
+    if (!config) {
+      throw new Error('微信支付配置不存在');
+    }
+
+    const couponData = {
+      stock_id: data.stockId,
+      out_request_no: data.outRequestNo,
+      appid: config.appId,
+      stock_creator_mchid: data.stockCreatorMchid
+    };
+
+    const response = await this.wechatPayRequest('POST', `/v3/marketing/favor/users/${data.openid}/coupons`, couponData, config);
+
+    const coupon = this.couponRepository.create({
+      couponId: response.coupon_id,
+      stockId: data.stockId,
+      openid: data.openid,
+      outRequestNo: data.outRequestNo,
+      couponStatus: 'SENDED',
+      amount: stock.couponAmount,
+      availableStartTime: new Date(response.available_start_time),
+      availableEndTime: new Date(response.available_end_time),
+      usedTime: null,
+      transactionId: null,
+      stockIdRef: stock.id
+    });
+
+    // 更新批次可用数量
+    stock.availableQuantity--;
+    stock.distributedQuantity++;
+    await this.stockRepository.save(stock);
+
+    return this.couponRepository.save(coupon);
+  }
+
+  // 查询代金券详情
+  async getCouponDetail(couponId: string, openid: string): Promise<any> {
+    const coupon = await this.couponRepository.findOne({ 
+      where: { couponId, openid },
+      relations: ['stock', 'stock.config']
+    });
+    if (!coupon) {
+      throw new Error('代金券不存在');
+    }
+
+    const response = await this.wechatPayRequest(
+      'GET', 
+      `/v3/marketing/favor/users/${openid}/coupons/${couponId}?appid=${coupon.stock.config.appId}`
+    );
+
+    return response;
+  }
+
+  // 查询代金券批次列表
+  async getStockList(
+    offset: number,
+    limit: number,
+    stockCreatorMchid: string,
+    status?: string
+  ): Promise<any> {
+    const config = await this.getActiveConfig();
+    
+    let url = `/v3/marketing/favor/stocks?offset=${offset}&limit=${limit}&stock_creator_mchid=${stockCreatorMchid}`;
+    if (status) {
+      url += `&status=${status}`;
+    }
+
+    const response = await this.wechatPayRequest('GET', url, undefined, config);
+    return response;
+  }
+
+  // 查询代金券批次详情
+  async getStockDetail(stockId: string, stockCreatorMchid: string): Promise<any> {
+    const response = await this.wechatPayRequest(
+      'GET', 
+      `/v3/marketing/favor/stocks/${stockId}?stock_creator_mchid=${stockCreatorMchid}`
+    );
+    return response;
+  }
+
+  // 查询本地代金券批次列表
+  async getLocalStockList(
+    page: number = 1,
+    pageSize: number = 10,
+    keyword?: string,
+    status?: string
+  ): Promise<[WechatCouponStock[], number]> {
+    const query = this.stockRepository.createQueryBuilder('stock')
+      .leftJoinAndSelect('stock.config', 'config')
+      .orderBy('stock.createdAt', 'DESC');
+
+    if (keyword) {
+      query.andWhere('stock.stockName LIKE :keyword', { keyword: `%${keyword}%` });
+    }
+
+    if (status) {
+      query.andWhere('stock.status = :status', { status });
+    }
+
+    return query.skip((page - 1) * pageSize).take(pageSize).getManyAndCount();
+  }
+
+  // 查询本地代金券列表
+  async getLocalCouponList(
+    page: number = 1,
+    pageSize: number = 10,
+    openid?: string,
+    stockId?: string,
+    status?: string
+  ): Promise<[WechatCoupon[], number]> {
+    const query = this.couponRepository.createQueryBuilder('coupon')
+      .leftJoinAndSelect('coupon.stock', 'stock')
+      .orderBy('coupon.createdAt', 'DESC');
+
+    if (openid) {
+      query.andWhere('coupon.openid = :openid', { openid });
+    }
+
+    if (stockId) {
+      query.andWhere('coupon.stockId = :stockId', { stockId });
+    }
+
+    if (status) {
+      query.andWhere('coupon.couponStatus = :status', { status });
+    }
+
+    return query.skip((page - 1) * pageSize).take(pageSize).getManyAndCount();
+  }
+
+  // 同步批次状态
+  async syncStockStatus(stockId: string): Promise<void> {
+    const stock = await this.stockRepository.findOne({ where: { stockId } });
+    if (!stock) {
+      throw new Error('代金券批次不存在');
+    }
+
+    const response = await this.getStockDetail(stockId, stock.stockCreatorMchid);
+    
+    stock.status = response.status;
+    stock.availableQuantity = response.available_quantity;
+    stock.distributedQuantity = response.distributed_quantity;
+    
+    await this.stockRepository.save(stock);
+  }
+}