Explorar el Código

✨ feat(payment): 新增微信支付v3集成功能

- 添加微信支付v3 SDK依赖并集成到项目中
- 实现支付创建API,支持小程序JSAPI支付方式
- 实现支付回调处理API,支持支付状态更新
- 新增支付服务模块,包含订单验证、支付创建、回调处理等功能
- 扩展订单状态枚举,新增支付中和已关闭状态
- 添加支付相关环境变量配置支持
- 创建支付集成测试,覆盖支付创建和回调场景
- 更新小程序配置,启用ES6和增强编译功能
- 添加微信支付使用文档,详细说明配置和使用方法

📝 docs(payment): 添加微信支付集成文档

- 创建微信支付v3使用文档,包含SDK介绍和配置说明
- 提供完整的代码示例和使用步骤
- 说明环境变量配置和证书获取方法
- 列出支持的支付接口和版本更新记录
yourname hace 3 meses
padre
commit
c0a50e2664

+ 181 - 0
docs/wxpay.md

@@ -0,0 +1,181 @@
+# 微信支付v3 支持在ts和js中使用
+
+## 欢迎大家加入一起完善这个api
+## 前言
+微信官方在2020-12-25正式开放了[v3](https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml)版本的接口,相比较旧版本[v2](https://pay.weixin.qq.com/wiki/doc/api/index.html)有了不少改变,例如:
+* 遵循统一的Restful的设计风格
+* 使用JSON作为数据交互的格式,不再使用XML
+* 使用基于非对称密钥的SHA256-RSA的数字签名算法,不再使用MD5或HMAC-SHA256
+* 不再要求HTTPS客户端证书
+* 使用AES-256-GCM,对回调中的关键信息进行加密保护
+
+由于官方文档只支持java和php,所以我在这里使用ts简单的封装了一个版本,支持在js或者ts中使用,后续会更加完善这个npm包,谢谢。
+
+## 使用
+`yarn add wechatpay-node-v3@2.1.8`(也可以用npm,请加上版本号,使用正式版本)
+
+```bash
+import WxPay from 'wechatpay-node-v3'; // 支持使用require
+import fs from 'fs';
+import request from 'superagent';
+
+const pay = new WxPay({
+  appid: '直连商户申请的公众号或移动应用appid',
+  mchid: '商户号',
+  publicKey: fs.readFileSync('./apiclient_cert.pem'), // 公钥
+  privateKey: fs.readFileSync('./apiclient_key.pem'), // 秘钥
+});
+
+# 这里以h5支付为例
+try {
+    # 参数介绍请看h5支付文档 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml
+    const params = {
+      appid: '直连商户申请的公众号或移动应用appid',
+      mchid: '商户号',
+      description: '测试',
+      out_trade_no: '订单号',
+      notify_url: '回调url',
+      amount: {
+        total: 1,
+      },
+      scene_info: {
+        payer_client_ip: 'ip',
+        h5_info: {
+          type: 'Wap',
+          app_name: '网页名称 例如 百度',
+          app_url: '网页域名 例如 https://www.baidu.com',
+        },
+      },
+    };
+    const nonce_str = Math.random().toString(36).substr(2, 15), // 随机字符串
+      timestamp = parseInt(+new Date() / 1000 + '').toString(), // 时间戳 秒
+      url = '/v3/pay/transactions/h5';
+
+    # 获取签名
+    const signature = pay.getSignature('POST', nonce_str, timestamp, url, params); # 如果是get 请求 则不需要params 参数拼接在url上 例如 /v3/pay/transactions/id/12177525012014?mchid=1230000109
+    # 获取头部authorization 参数
+    const authorization = pay.getAuthorization(nonce_str, timestamp, signature);
+
+    const result = await request
+      .post('https://api.mch.weixin.qq.com/v3/pay/transactions/h5')
+      .send(params)
+      .set({
+        Accept: 'application/json',
+        'Content-Type': 'application/json',
+        'User-Agent':
+          'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
+        Authorization: authorization,
+      });
+
+    console.log('result==========>', result.body);
+  } catch (error) {
+    console.log(error);
+  }
+```
+如果你使用的是nest框架,请结合`nest-wechatpay-node-v3`一起使用。
+
+## 使用自定义 http 请求
+> import { IPayRequest, Output } from 'wechatpay-node-v3/dist/define';
+
+自己实现 `IPayRequest` 接口,使用如下方法注入
+
+```ts
+pay.createHttp(...);
+```
+
+
+## WxPay 介绍
+`import WxPay from 'wechatpay-node-v3';` 或者 `const WxPay = require('wechatpay-node-v3')`
+
+参数介绍
+|参数名称  |参数介绍  |是否必须|
+|--|--|--|
+|  appid|   直连商户申请的公众号或移动应用appid|是|
+|mchid|商户号|是
+|serial_no|证书序列号|否|
+|publicKey|公钥|是|
+|privateKey|密钥|是|
+|authType|认证类型,目前为WECHATPAY2-SHA256-RSA2048|否|
+|userAgent|自定义User-Agent|否|
+|key|APIv3密钥|否 有验证回调必须|
+
+## 注意
+1. serial_no是证书序列号 请在命令窗口使用 `openssl x509 -in apiclient_cert.pem -noout -serial` 获取
+2. 头部参数需要添加 User-Agent 参数
+3. 需要在商户平台设置APIv3密钥才会有回调,详情参看文档指引http://kf.qq.com/faq/180830E36vyQ180830AZFZvu.html
+
+## 使用介绍
+以下函数是我针对微信相关接口进行进一步封装,可以直接使用。
+| api名称 | 函数名 |
+|--|--|
+| [h5支付](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml) |[transactions_h5](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/transactions_h5.md)  |
+| [native支付](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml) |[transactions_native](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/transactions_native.md)  |
+| [app支付](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_1.shtml) |[transactions_app](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/transactions_app.md)  |
+| [JSAPI支付 或者 小程序支付](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml) |[transactions_jsapi](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/transactions_jsapi.md)  |
+| [查询订单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml) |[query](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/query.md)  |
+| [关闭订单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml) |[close](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/close.md)  |
+| [申请交易账单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_6.shtml) |[tradebill](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/tradebill.md)  |
+| [申请资金账单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_7.shtml) |[fundflowbill](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/fundflowbill.md)  |
+| [下载账单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_8.shtml) |[downloadBill](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/downloadbill.md)  |
+| [回调解密](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml) |[decipher_gcm](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/transactions_h5.md)  |
+|[合单h5支付](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_2.shtml)|[combine_transactions_h5](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/combine.md)|
+|[合单native支付](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_5.shtml)|[combine_transactions_native](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/combine.md)|
+|[合单app支付](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_1.shtml)|[combine_transactions_app](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/combine.md)|
+|[合单JSAPI支付 或者 小程序支付](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_3.shtml)|[combine_transactions_jsapi](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/combine.md)|
+|[查询合单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_11.shtml)|[combine_query](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/combine.md)|
+|[关闭合单](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter5_1_12.shtml)|[combine_close](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/combine.md)|
+|[获取序列号]()|[getSN](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/transactions_h5.md)|
+|[申请退款](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_9.shtml)|[refunds](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/transactions_h5.md)|
+|[查询退款](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_10.shtml)|[find_refunds](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/transactions_h5.md)|
+|[签名验证](https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml)|[verifySign](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/verifySign.md)|
+|[微信提现到零钱](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_1.shtml)|[batches_transfer](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/batches_transfer.md)|
+|[分账](https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter8_1_1.shtml)|[create_profitsharing_orders](https://github.com/klover2/wechatpay-node-v3-ts/blob/master/docs/profitsharing.md)|
+
+
+
+## 版本介绍
+| 版本号 | 版本介绍 |
+|--|--|
+| v0.0.1 | 仅支持签名和获取头部参数Authorization |
+|v1.0.0|增加了支付(不包括合单支付)、查询订单、关闭订单、申请交易账单、申请资金账单、下载账单|
+|v1.2.0|增加了回调解密,合单支付、合单关闭、合单查询|
+|v1.2.1|修改app、jsapi、native支付字段scene_info 改为可选|
+|v1.2.2|增加获取序列号方法|
+|v1.2.3|修改小程序支付签名错误和取消serial_no字段必填|
+|v1.3.0|增加普通订单的退款和查询|
+|v1.3.1|修复APP调起支付时出现“支付验证签名失败“的问题|
+|v1.3.2|增加请求成功后的签名验证|
+|v1.3.3|修复superagent post请求异常 Z_DATA_ERROR|
+|v1.3.4|修复superagent get请求异常 Z_DATA_ERROR|
+|v1.3.5|修复APP支付签名错误|
+|v2.0.0|增加提现到零钱和优化接口参数,规定返回参数格式,其他接口会后续优化|
+|v2.0.1|增加请求头Wechatpay-Serial和完善转账其他接口|
+|v2.0.2|增加敏感信息加密方法(publicEncrypt)|
+|v2.0.3|修复get请求无参时的签名|
+|v2.0.4|修复敏感信息加密方法(publicEncrypt)使用微信平台公钥|
+|v2.0.6|修复发起商家转账零钱参数wx_serial_no(自定义参数,即http请求头Wechatpay-Serial的值)为可选参数|
+|v2.1.0|升级superagent依赖6.1.0到8.0.6|
+|v2.1.1|商家转账API支持场景参数|
+|v2.1.2|基础支付接口支持传appid|
+|v2.1.3|支持分账相关接口|
+|v2.1.4|修复错误原因存在空值bug|
+|v2.1.5|修复动态 appid 签名错误|
+|v2.1.6|Native下单API支持support_fapiao字段|
+|v2.1.7|修复退款接口refunds和find_refunds返回结果中的http status会被业务status覆盖问题|
+|v2.1.8|修复回调签名key错误|
+|v2.2.0|修复回调解密报Unsupported state or unable to authenticate data |
+|v2.2.1|上传图片 |
+
+## 文档
+[v2支付文档](https://pay.weixin.qq.com/wiki/doc/api/index.html)
+[v3支付文档](https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml)
+
+
+## 贡献
+<a href="https://github.com/klover2/wechatpay-node-v3-ts/graphs/contributors">
+  <img src="https://contrib.rocks/image?repo=klover2/wechatpay-node-v3-ts" />
+</a>
+
+欢迎提[存在的Bug或者意见](https://github.com/klover2/wechatpay-node-v3-ts/issues)。
+
+

+ 2 - 2
mini/.env.production

@@ -2,5 +2,5 @@
 # TARO_APP_ID="生产环境下的小程序 AppID"
 
 # API配置
-API_BASE_URL=https://your-domain.com
-API_VERSION=v1
+TARO_APP_API_BASE_URL=https://d8d-ai-vscode-8080-176-162-template-21-group.r.d8d.fun
+TARO_APP_API_VERSION=v1

+ 2 - 2
mini/project.config.json

@@ -5,8 +5,8 @@
   "appid": "wx308524a14e023425",
   "setting": {
     "urlCheck": false,
-    "es6": false,
-    "enhance": false,
+    "es6": true,
+    "enhance": true,
     "compileHotReLoad": false,
     "postcss": false,
     "minified": true

+ 1 - 0
packages/server/package.json

@@ -54,6 +54,7 @@
     "reflect-metadata": "^0.2.2",
     "typeorm": "^0.3.20",
     "uuid": "^11.1.0",
+    "wechatpay-node-v3": "2.1.8",
     "zod": "^4.1.12"
   },
   "devDependencies": {

+ 153 - 0
packages/server/src/api/payment/create.ts

@@ -0,0 +1,153 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { authMiddleware } from '../../middleware/auth.middleware';
+import { PaymentService } from '../../modules/payment/payment.service';
+import { AuthContext } from '../../types/context';
+import { ErrorSchema } from '../../utils/errorHandler';
+
+// 支付创建请求Schema
+const PaymentCreateSchema = z.object({
+  orderId: z.number().int().positive().describe('订单ID'),
+  totalAmount: z.number().int().positive().describe('支付金额(分)'),
+  description: z.string().min(1).max(128).describe('支付描述'),
+  openid: z.string().optional().describe('用户OpenID')
+});
+
+// 支付创建响应Schema
+const PaymentCreateResponseSchema = z.object({
+  paymentId: z.string().describe('支付ID'),
+  timeStamp: z.string().describe('时间戳'),
+  nonceStr: z.string().describe('随机字符串'),
+  package: z.string().describe('预支付ID'),
+  signType: z.string().describe('签名类型'),
+  paySign: z.string().describe('签名')
+});
+
+// 支付回调请求Schema
+const PaymentCallbackSchema = z.object({
+  id: z.string().describe('通知ID'),
+  create_time: z.string().describe('创建时间'),
+  event_type: z.string().describe('事件类型'),
+  resource_type: z.string().describe('资源类型'),
+  resource: z.object({
+    algorithm: z.string().describe('加密算法'),
+    ciphertext: z.string().describe('密文'),
+    associated_data: z.string().optional().describe('附加数据'),
+    nonce: z.string().describe('随机串')
+  }).describe('回调资源'),
+  summary: z.string().describe('摘要')
+});
+
+// 支付创建路由定义
+const createPaymentRoute = createRoute({
+  method: 'post',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: PaymentCreateSchema }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '支付创建成功',
+      content: { 'application/json': { schema: PaymentCreateResponseSchema } }
+    },
+    400: {
+      description: '参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    401: {
+      description: '未授权',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 支付回调路由定义
+const paymentCallbackRoute = createRoute({
+  method: 'post',
+  path: '/callback',
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: PaymentCallbackSchema }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '回调处理成功',
+      content: { 'text/plain': { schema: z.string() } }
+    },
+    400: {
+      description: '回调数据错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'text/plain': { schema: z.string() } }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>()
+  .openapi(createPaymentRoute, async (c) => {
+    try {
+      const user = c.get('user');
+      const paymentData = c.req.valid('json');
+
+      // 创建支付服务实例
+      const paymentService = new PaymentService();
+
+      // 创建支付订单
+      const paymentResult = await paymentService.createPayment(
+        paymentData.orderId,
+        paymentData.totalAmount,
+        paymentData.description,
+        paymentData.openid
+      );
+
+      return c.json(paymentResult, 200);
+    } catch (error) {
+      console.error('支付创建失败:', error);
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '支付创建失败'
+      }, 500);
+    }
+  })
+  .openapi(paymentCallbackRoute, async (c) => {
+    try {
+      const callbackData = c.req.valid('json');
+
+      // 获取微信支付回调头信息
+      const headers = {
+        'wechatpay-timestamp': c.req.header('wechatpay-timestamp') || '',
+        'wechatpay-nonce': c.req.header('wechatpay-nonce') || '',
+        'wechatpay-signature': c.req.header('wechatpay-signature') || '',
+        'wechatpay-serial': c.req.header('wechatpay-serial') || ''
+      };
+
+      // 创建支付服务实例
+      const paymentService = new PaymentService();
+
+      // 处理支付回调
+      await paymentService.handlePaymentCallback(callbackData, headers);
+
+      // 返回成功响应给微信支付
+      return c.text('SUCCESS', 200);
+    } catch (error) {
+      console.error('支付回调处理失败:', error);
+      // 返回失败响应给微信支付
+      return c.text('FAIL', 500);
+    }
+  });
+
+export default app;

+ 7 - 0
packages/server/src/api/payment/index.ts

@@ -0,0 +1,7 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import createPayment from './create';
+
+// 注册支付创建和回调路由
+const app = new OpenAPIHono().route('/', createPayment);
+
+export default app;

+ 3 - 0
packages/server/src/index.ts

@@ -16,6 +16,7 @@ import routesRoutes from './api/routes'
 import areasUserRoutes from './api/areas'
 import locationsUserRoutes from './api/locations'
 import ordersRoutes from './api/orders/index'
+import paymentRoutes from './api/payment/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 import { Hono } from 'hono'
@@ -131,6 +132,7 @@ export const routesRoutesExport = api.route('/api/v1/routes', routesRoutes)
 export const areasUserRoutesExport = api.route('/api/v1/areas', areasUserRoutes)
 export const locationsUserRoutesExport = api.route('/api/v1/locations', locationsUserRoutes)
 export const ordersRoutesExport = api.route('/api/v1/orders', ordersRoutes)
+export const paymentRoutesExport = api.route('/api/v1/payment', paymentRoutes)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
@@ -147,6 +149,7 @@ export type RoutesRoutes = typeof routesRoutesExport
 export type AreasUserRoutes = typeof areasUserRoutesExport
 export type LocationsUserRoutes = typeof locationsUserRoutesExport
 export type OrdersRoutes = typeof ordersRoutesExport
+export type PaymentRoutes = typeof paymentRoutesExport
 
 app.route('/', api)
 export default app

+ 265 - 0
packages/server/src/modules/payment/payment.service.ts

@@ -0,0 +1,265 @@
+import { AppDataSource } from '../../data-source';
+import { Order } from '../orders/order.entity';
+import { PaymentStatus, OrderStatus } from '../../share/order.types';
+import WxPay from 'wechatpay-node-v3';
+import { Buffer } from 'buffer';
+
+/**
+ * 微信支付服务
+ * 使用微信支付v3 SDK,支持小程序支付
+ */
+export class PaymentService {
+  private readonly wxPay: WxPay;
+  private readonly merchantId: string;
+  private readonly appId: string;
+  private readonly v3Key: string;
+  private readonly notifyUrl: string;
+
+  constructor() {
+    // 从环境变量获取支付配置
+    this.merchantId = process.env.WECHAT_MERCHANT_ID || '';
+    this.appId = process.env.WX_MINI_APP_ID || '';
+    this.v3Key = process.env.WECHAT_V3_KEY || '';
+    this.notifyUrl = process.env.WECHAT_PAY_NOTIFY_URL || '';
+    const certSerialNo = process.env.WECHAT_MERCHANT_CERT_SERIAL_NO || '';
+
+    if (!this.merchantId || !this.appId || !this.v3Key || !certSerialNo) {
+      throw new Error('微信支付配置不完整,请检查环境变量');
+    }
+
+    // 处理证书字符串,将 \n 转换为实际换行符
+    const publicKey = (process.env.WECHAT_PUBLIC_KEY || '').replace(/\\n/g, '\n');
+    const privateKey = (process.env.WECHAT_PRIVATE_KEY || '').replace(/\\n/g, '\n');
+
+    // 初始化微信支付SDK
+    this.wxPay = new WxPay({
+      appid: this.appId,
+      mchid: this.merchantId,
+      publicKey: Buffer.from(publicKey),
+      privateKey: Buffer.from(privateKey),
+      key: this.v3Key,
+      serial_no: certSerialNo
+    });
+  }
+
+  /**
+   * 创建微信支付订单
+   * @param orderId 订单ID
+   * @param totalAmount 支付金额(分)
+   * @param description 支付描述
+   * @param openid 用户OpenID
+   */
+  async createPayment(
+    orderId: number,
+    totalAmount: number,
+    description: string,
+    openid?: string
+  ): Promise<{
+    paymentId: string;
+    timeStamp: string;
+    nonceStr: string;
+    package: string;
+    signType: string;
+    paySign: string;
+  }> {
+    // 验证订单是否存在且状态正确
+    const orderRepository = AppDataSource.getRepository(Order);
+    const order = await orderRepository.findOne({
+      where: { id: orderId }
+    });
+
+    if (!order) {
+      throw new Error('订单不存在');
+    }
+
+    if (order.paymentStatus !== PaymentStatus.PENDING) {
+      throw new Error('订单支付状态不正确');
+    }
+
+    // 订单金额是元,支付金额是分,需要转换比较
+    const orderAmountInCents = Math.round(order.totalAmount * 100);
+    if (orderAmountInCents !== totalAmount) {
+      throw new Error('支付金额与订单金额不匹配');
+    }
+
+    if (!openid) {
+      throw new Error('用户OpenID不能为空');
+    }
+
+    try {
+      // 使用微信支付SDK创建JSAPI支付
+      const result = await this.wxPay.transactions_jsapi({
+        appid: this.appId,
+        mchid: this.merchantId,
+        description,
+        out_trade_no: `ORDER_${orderId}_${Date.now()}`,
+        notify_url: this.notifyUrl,
+        amount: {
+          total: totalAmount,
+        },
+        payer: {
+          openid
+        }
+      });
+
+      console.debug('微信支付SDK返回结果:', result);
+
+      // 从 package 字段中提取 prepay_id
+      const prepayId = result.package ? result.package.replace('prepay_id=', '') : undefined;
+
+      // 更新订单支付状态为处理中
+      order.paymentStatus = PaymentStatus.PROCESSING;
+      await orderRepository.save(order);
+
+      // 直接返回微信支付SDK生成的参数
+      return {
+        paymentId: prepayId,
+        timeStamp: result.timeStamp,
+        nonceStr: result.nonceStr,
+        package: result.package,
+        signType: result.signType,
+        paySign: result.paySign
+      };
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '未知错误';
+      throw new Error(`微信支付创建失败: ${errorMessage}`);
+    }
+  }
+
+  /**
+   * 处理支付回调
+   */
+  async handlePaymentCallback(
+    callbackData: {
+      id: string;
+      create_time: string;
+      event_type: string;
+      resource_type: string;
+      resource: {
+        algorithm: string;
+        ciphertext: string;
+        associated_data?: string;
+        nonce: string;
+      };
+      summary: string;
+    },
+    headers: {
+      'wechatpay-timestamp': string;
+      'wechatpay-nonce': string;
+      'wechatpay-signature': string;
+      'wechatpay-serial': string;
+    }
+  ): Promise<void> {
+    console.debug('收到支付回调请求:', {
+      headers,
+      callbackData
+    });
+
+    // 验证回调签名
+    const isValid = await this.wxPay.verifySign({
+      timestamp: headers['wechatpay-timestamp'],
+      nonce: headers['wechatpay-nonce'],
+      body: JSON.stringify(callbackData),
+      serial: headers['wechatpay-serial'],
+      signature: headers['wechatpay-signature']
+    });
+
+    console.debug('回调签名验证结果:', isValid);
+
+    if (!isValid) {
+      throw new Error('回调签名验证失败');
+    }
+
+    // 解密回调数据
+    const decryptedData = this.wxPay.decipher_gcm(
+      callbackData.resource.ciphertext,
+      callbackData.resource.associated_data || '',
+      callbackData.resource.nonce
+    );
+
+    const parsedData = JSON.parse(decryptedData as string);
+
+    const orderRepository = AppDataSource.getRepository(Order);
+    const orderId = parseInt(parsedData.out_trade_no.split('_')[1]);
+    const order = await orderRepository.findOne({
+      where: { id: orderId }
+    });
+
+    if (!order) {
+      throw new Error('订单不存在');
+    }
+
+    // 根据回调结果更新订单状态
+    if (parsedData.trade_state === 'SUCCESS') {
+      order.paymentStatus = PaymentStatus.PAID;
+      order.status = OrderStatus.WAITING_DEPARTURE;
+    } else if (parsedData.trade_state === 'FAIL') {
+      order.paymentStatus = PaymentStatus.FAILED;
+    } else if (parsedData.trade_state === 'REFUND') {
+      order.paymentStatus = PaymentStatus.REFUNDED;
+    }
+
+    await orderRepository.save(order);
+  }
+
+  /**
+   * 生成随机字符串
+   */
+  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;
+  }
+
+
+  /**
+   * 查询支付状态
+   */
+  async getPaymentStatus(orderId: number): Promise<PaymentStatus> {
+    const orderRepository = AppDataSource.getRepository(Order);
+    const order = await orderRepository.findOne({
+      where: { id: orderId }
+    });
+
+    if (!order) {
+      throw new Error('订单不存在');
+    }
+
+    return order.paymentStatus;
+  }
+
+  /**
+   * 生成回调签名(用于测试)
+   */
+  generateCallbackSignature(
+    timestamp: string,
+    nonce: string,
+    callbackData: any
+  ): string {
+    return this.wxPay.getSignature(
+      'POST',
+      nonce,
+      timestamp,
+      '/v3/pay/transactions/jsapi',
+      callbackData
+    );
+  }
+
+  /**
+   * 获取微信支付平台证书(用于测试)
+   */
+  async getPlatformCertificates(): Promise<any> {
+    try {
+      console.debug('开始获取微信支付平台证书...');
+      const certificates = await this.wxPay.get_certificates(this.v3Key);
+      console.debug('获取平台证书成功:', certificates);
+      return certificates;
+    } catch (error) {
+      console.debug('获取平台证书失败:', error);
+      throw error;
+    }
+  }
+}

+ 3 - 1
packages/server/src/share/order.types.ts

@@ -8,9 +8,11 @@ export enum OrderStatus {
 
 export enum PaymentStatus {
   PENDING = '待支付',
+  PROCESSING = '支付中',
   PAID = '已支付',
   FAILED = '支付失败',
-  REFUNDED = '已退款'
+  REFUNDED = '已退款',
+  CLOSED = '已关闭'
 }
 
 import { OrderResponse, OrderStats } from '../modules/orders/order.schema';

+ 111 - 0
pnpm-lock.yaml

@@ -279,6 +279,9 @@ importers:
       uuid:
         specifier: ^11.1.0
         version: 11.1.0
+      wechatpay-node-v3:
+        specifier: 2.1.8
+        version: 2.1.8
       zod:
         specifier: ^4.1.12
         version: 4.1.12
@@ -2099,6 +2102,14 @@ packages:
     resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
+  '@fidm/asn1@1.0.4':
+    resolution: {integrity: sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==}
+    engines: {node: '>= 8'}
+
+  '@fidm/x509@1.2.1':
+    resolution: {integrity: sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==}
+    engines: {node: '>= 8'}
+
   '@floating-ui/core@1.7.3':
     resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
 
@@ -2379,6 +2390,10 @@ packages:
   '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
     resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==}
 
+  '@noble/hashes@1.8.0':
+    resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
+    engines: {node: ^14.21.3 || >=16}
+
   '@nodelib/fs.scandir@2.1.5':
     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
     engines: {node: '>= 8'}
@@ -2391,6 +2406,9 @@ packages:
     resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
     engines: {node: '>= 8'}
 
+  '@paralleldrive/cuid2@2.2.2':
+    resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
+
   '@parcel/watcher-android-arm64@2.5.1':
     resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
     engines: {node: '>= 10.0.0'}
@@ -4810,6 +4828,9 @@ packages:
     resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
     engines: {node: '>= 0.4'}
 
+  asap@2.0.6:
+    resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
+
   assertion-error@2.0.1:
     resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
     engines: {node: '>=12'}
@@ -5306,6 +5327,9 @@ packages:
   compare-func@2.0.0:
     resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
 
+  component-emitter@1.3.1:
+    resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
+
   compressible@2.0.18:
     resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
     engines: {node: '>= 0.6'}
@@ -5383,6 +5407,9 @@ packages:
     resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
     engines: {node: '>=18'}
 
+  cookiejar@2.1.4:
+    resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==}
+
   copy-anything@2.0.6:
     resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==}
 
@@ -5832,6 +5859,9 @@ packages:
     engines: {node: '>= 4.0.0'}
     hasBin: true
 
+  dezalgo@1.0.4:
+    resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
+
   diff-sequences@29.6.3:
     resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -6318,6 +6348,9 @@ packages:
   fast-levenshtein@2.0.6:
     resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
 
+  fast-safe-stringify@2.1.1:
+    resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
+
   fast-uri@3.1.0:
     resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
 
@@ -6476,6 +6509,9 @@ packages:
     resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==}
     engines: {node: '>= 18'}
 
+  formidable@2.1.5:
+    resolution: {integrity: sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==}
+
   forwarded@0.2.0:
     resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
     engines: {node: '>= 0.6'}
@@ -7948,6 +7984,11 @@ packages:
     engines: {node: '>=4.0.0'}
     hasBin: true
 
+  mime@2.6.0:
+    resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
+    engines: {node: '>=4.0.0'}
+    hasBin: true
+
   mimic-fn@2.1.0:
     resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
     engines: {node: '>=6'}
@@ -9895,6 +9936,11 @@ packages:
     engines: {node: '>=16'}
     hasBin: true
 
+  superagent@8.1.2:
+    resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==}
+    engines: {node: '>=6.4.0 <13 || >=14'}
+    deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net
+
   supports-color@7.2.0:
     resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
     engines: {node: '>=8'}
@@ -10189,6 +10235,9 @@ packages:
   tw-animate-css@1.4.0:
     resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
 
+  tweetnacl@1.0.3:
+    resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==}
+
   type-check@0.4.0:
     resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
     engines: {node: '>= 0.8.0'}
@@ -10667,6 +10716,9 @@ packages:
     resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
     engines: {node: '>=0.8.0'}
 
+  wechatpay-node-v3@2.1.8:
+    resolution: {integrity: sha512-WXooVphNskM8CqPpg3rP5E+E0nHIHRUzMLrY9tGLXHZSJVQEVMGVJnJBGes4buhYphjZ7Wo3NO8KJpIP6pacrw==}
+
   whatwg-encoding@2.0.0:
     resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
     engines: {node: '>=12'}
@@ -12470,6 +12522,13 @@ snapshots:
       '@eslint/core': 0.16.0
       levn: 0.4.1
 
+  '@fidm/asn1@1.0.4': {}
+
+  '@fidm/x509@1.2.1':
+    dependencies:
+      '@fidm/asn1': 1.0.4
+      tweetnacl: 1.0.3
+
   '@floating-ui/core@1.7.3':
     dependencies:
       '@floating-ui/utils': 0.2.10
@@ -12883,6 +12942,8 @@ snapshots:
     dependencies:
       eslint-scope: 5.1.1
 
+  '@noble/hashes@1.8.0': {}
+
   '@nodelib/fs.scandir@2.1.5':
     dependencies:
       '@nodelib/fs.stat': 2.0.5
@@ -12895,6 +12956,10 @@ snapshots:
       '@nodelib/fs.scandir': 2.1.5
       fastq: 1.19.1
 
+  '@paralleldrive/cuid2@2.2.2':
+    dependencies:
+      '@noble/hashes': 1.8.0
+
   '@parcel/watcher-android-arm64@2.5.1':
     optional: true
 
@@ -15550,6 +15615,8 @@ snapshots:
       get-intrinsic: 1.3.0
       is-array-buffer: 3.0.5
 
+  asap@2.0.6: {}
+
   assertion-error@2.0.1: {}
 
   ast-v8-to-istanbul@0.3.7:
@@ -16155,6 +16222,8 @@ snapshots:
       array-ify: 1.0.0
       dot-prop: 5.3.0
 
+  component-emitter@1.3.1: {}
+
   compressible@2.0.18:
     dependencies:
       mime-db: 1.54.0
@@ -16234,6 +16303,8 @@ snapshots:
 
   cookie@1.0.2: {}
 
+  cookiejar@2.1.4: {}
+
   copy-anything@2.0.6:
     dependencies:
       is-what: 3.14.1
@@ -16658,6 +16729,11 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  dezalgo@1.0.4:
+    dependencies:
+      asap: 2.0.6
+      wrappy: 1.0.2
+
   diff-sequences@29.6.3: {}
 
   dingtalk-jsapi@2.15.6:
@@ -17438,6 +17514,8 @@ snapshots:
 
   fast-levenshtein@2.0.6: {}
 
+  fast-safe-stringify@2.1.1: {}
+
   fast-uri@3.1.0: {}
 
   fast-xml-parser@4.5.3:
@@ -17594,6 +17672,13 @@ snapshots:
 
   formdata-node@6.0.3: {}
 
+  formidable@2.1.5:
+    dependencies:
+      '@paralleldrive/cuid2': 2.2.2
+      dezalgo: 1.0.4
+      once: 1.4.0
+      qs: 6.13.0
+
   forwarded@0.2.0: {}
 
   frac@1.1.2: {}
@@ -19359,6 +19444,8 @@ snapshots:
 
   mime@2.5.2: {}
 
+  mime@2.6.0: {}
+
   mimic-fn@2.1.0: {}
 
   mimic-function@5.0.1: {}
@@ -21462,6 +21549,21 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  superagent@8.1.2:
+    dependencies:
+      component-emitter: 1.3.1
+      cookiejar: 2.1.4
+      debug: 4.4.3
+      fast-safe-stringify: 2.1.1
+      form-data: 4.0.4
+      formidable: 2.1.5
+      methods: 1.1.2
+      mime: 2.6.0
+      qs: 6.13.0
+      semver: 7.7.3
+    transitivePeerDependencies:
+      - supports-color
+
   supports-color@7.2.0:
     dependencies:
       has-flag: 4.0.0
@@ -21763,6 +21865,8 @@ snapshots:
 
   tw-animate-css@1.4.0: {}
 
+  tweetnacl@1.0.3: {}
+
   type-check@0.4.0:
     dependencies:
       prelude-ls: 1.2.1
@@ -22323,6 +22427,13 @@ snapshots:
 
   websocket-extensions@0.1.4: {}
 
+  wechatpay-node-v3@2.1.8:
+    dependencies:
+      '@fidm/x509': 1.2.1
+      superagent: 8.1.2
+    transitivePeerDependencies:
+      - supports-color
+
   whatwg-encoding@2.0.0:
     dependencies:
       iconv-lite: 0.6.3

+ 352 - 0
web/tests/integration/server/payment.integration.test.ts

@@ -0,0 +1,352 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { testClient } from 'hono/testing';
+import {
+  IntegrationTestDatabase,
+  setupIntegrationDatabaseHooks,
+  TestDataFactory
+} from '~/utils/server/integration-test-db';
+import { paymentRoutesExport } from '@d8d/server/api';
+import { AuthService } from '@d8d/server/modules/auth/auth.service';
+import { UserService } from '@d8d/server/modules/users/user.service';
+import { OrderStatus, PaymentStatus } from '@d8d/server/share/order.types';
+import { config } from 'dotenv';
+import { resolve } from 'path';
+// 导入微信支付SDK用于模拟
+import WxPay from 'wechatpay-node-v3';
+
+// 在测试环境中加载环境变量
+config({ path: resolve(process.cwd(), '.env') });
+
+vi.mock('wechatpay-node-v3')
+
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooks()
+
+describe('支付API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof paymentRoutesExport>>['api']['v1'];
+  let testToken: string;
+  let testUser: any;
+  let testRoute: any;
+  let testOrder: any;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(paymentRoutesExport).api.v1;
+
+    // 创建测试用户并生成token
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    const userService = new UserService(dataSource);
+    const authService = new AuthService(userService);
+
+    // 创建测试用户
+    testUser = await TestDataFactory.createTestUser(dataSource);
+
+    // 生成测试用户的token
+    testToken = authService.generateToken(testUser);
+
+    // 创建测试路线
+    testRoute = await TestDataFactory.createTestRoute(dataSource);
+
+    // 创建测试订单
+    testOrder = await TestDataFactory.createTestOrder(dataSource, {
+      userId: testUser.id,
+      routeId: testRoute.id,
+      passengerCount: 2,
+      totalAmount: 200.00,
+      status: OrderStatus.PENDING_PAYMENT,
+      paymentStatus: PaymentStatus.PENDING
+    });
+
+    // 设置微信支付SDK的全局mock
+    const mockWxPay = {
+      transactions_jsapi: vi.fn().mockResolvedValue({
+        package: 'prepay_id=wx_test_prepay_id_123456',
+        timeStamp: Math.floor(Date.now() / 1000).toString(),
+        nonceStr: 'test_nonce_string',
+        signType: 'RSA',
+        paySign: 'test_pay_sign'
+      }),
+      verifySign: vi.fn().mockResolvedValue(true),
+      decipher_gcm: vi.fn().mockReturnValue(JSON.stringify({
+        out_trade_no: `ORDER_${testOrder.id}_${Date.now()}`,
+        trade_state: 'SUCCESS',
+        transaction_id: 'test_transaction_id',
+        amount: {
+          total: 20000
+        }
+      })),
+      getSignature: vi.fn().mockReturnValue('mock_signature')
+    };
+
+    // 模拟PaymentService的wxPay实例
+    vi.mocked(WxPay).mockImplementation(() => mockWxPay as any);
+
+  });
+
+  describe('POST /payment - 创建支付', () => {
+    it('应该成功创建支付订单', async () => {
+      const response = await client.payment.$post({
+        json: {
+          orderId: testOrder.id,
+          totalAmount: 20000, // 200元,单位分
+          description: '测试支付订单',
+          openid: 'oJy1-16IIG18XZLl7G32k1hHMUFg'
+        },
+      },
+        {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const result = await response.json();
+
+        console.debug('支付创建返回结果:', result);
+
+        expect(result).toHaveProperty('paymentId');
+        expect(result).toHaveProperty('timeStamp');
+        expect(result).toHaveProperty('nonceStr');
+        expect(result).toHaveProperty('package');
+        expect(result).toHaveProperty('signType');
+        expect(result).toHaveProperty('paySign');
+        expect(result.paymentId).toBeDefined();
+        expect(result.paymentId).not.toBe('undefined');
+      }
+    });
+
+    it('应该拒绝未认证的请求', async () => {
+      const response = await client.payment.$post({
+        json: {
+          orderId: testOrder.id,
+          totalAmount: 20000,
+          description: '测试支付订单'
+        }
+      });
+
+      expect(response.status).toBe(401);
+    });
+
+    it('应该验证订单存在性', async () => {
+      const response = await client.payment.$post({
+        json: {
+          orderId: 99999, // 不存在的订单ID
+          totalAmount: 20000,
+          description: '测试支付订单'
+        },
+      },
+        {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        });
+
+      expect(response.status).toBe(500);
+      if (response.status === 500) {
+        const result = await response.json();
+        expect(result.message).toContain('订单不存在');
+      }
+    });
+
+    it('应该验证支付金额匹配', async () => {
+      const response = await client.payment.$post({
+        json: {
+          orderId: testOrder.id,
+          totalAmount: 30000, // 金额不匹配
+          description: '测试支付订单'
+        },
+      },
+        {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        });
+
+      expect(response.status).toBe(500);
+      if (response.status === 500) {
+        const result = await response.json();
+        expect(result.message).toContain('支付金额与订单金额不匹配');
+      }
+    });
+
+    it('应该验证订单支付状态', async () => {
+      // 更新订单状态为已支付
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const orderRepository = dataSource.getRepository('Order');
+      await orderRepository.update(testOrder.id, {
+        paymentStatus: PaymentStatus.PAID
+      });
+
+      const response = await client.payment.$post({
+        json: {
+          orderId: testOrder.id,
+          totalAmount: 20000,
+          description: '测试支付订单'
+        },
+      },
+        {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        });
+
+      expect(response.status).toBe(500);
+      if (response.status === 500) {
+        const result = await response.json();
+        expect(result.message).toContain('订单支付状态不正确');
+      }
+    });
+  });
+
+  describe('POST /payment/callback - 支付回调', () => {
+    it('应该成功处理支付成功回调', async () => {
+      const timestamp = Math.floor(Date.now() / 1000).toString();
+      const nonce = Math.random().toString(36).substring(2, 15);
+
+      const callbackData = {
+        id: 'EV-201802251122332345',
+        create_time: '2018-06-08T10:34:56+08:00',
+        event_type: 'TRANSACTION.SUCCESS',
+        resource_type: 'encrypt-resource',
+        resource: {
+          algorithm: 'AEAD_AES_256_GCM',
+          ciphertext: 'encrypted_data',
+          nonce: 'random_nonce',
+          associated_data: 'associated_data'
+        },
+        summary: 'payment_success'
+      };
+
+      const response = await client.payment.callback.$post({
+        json: callbackData
+      }, {
+        headers: {
+          'wechatpay-timestamp': timestamp,
+          'wechatpay-nonce': nonce,
+          'wechatpay-signature': 'mock_signature_for_test',
+          'wechatpay-serial': process.env.WECHAT_PLATFORM_CERT_SERIAL_NO || ''
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const result = await response.text();
+        expect(result).toBe('SUCCESS');
+      }
+    });
+
+    it('应该处理支付失败回调', async () => {
+      const timestamp = Math.floor(Date.now() / 1000).toString();
+      const nonce = Math.random().toString(36).substring(2, 15);
+
+      const callbackData = {
+        id: 'EV-201802251122332346',
+        create_time: '2018-06-08T10:34:56+08:00',
+        event_type: 'TRANSACTION.FAIL',
+        resource_type: 'encrypt-resource',
+        resource: {
+          algorithm: 'AEAD_AES_256_GCM',
+          ciphertext: 'encrypted_data',
+          nonce: 'random_nonce',
+          associated_data: 'associated_data'
+        },
+        summary: 'payment_failed'
+      };
+
+      const response = await client.payment.callback.$post({
+        json: callbackData
+      }, {
+        headers: {
+          'wechatpay-timestamp': timestamp,
+          'wechatpay-nonce': nonce,
+          'wechatpay-signature': 'mock_signature_for_test',
+          'wechatpay-serial': process.env.WECHAT_PLATFORM_CERT_SERIAL_NO || ''
+        }
+      });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const result = await response.text();
+        expect(result).toBe('SUCCESS');
+      }
+    });
+
+    it('应该处理无效的回调数据', async () => {
+      const response = await client.payment.callback.$post({
+        json: { invalid: 'data' } as any
+      }, {
+        headers: {
+          'wechatpay-timestamp': '1622456896',
+          'wechatpay-nonce': 'random_nonce_string',
+          'wechatpay-signature': 'signature_data',
+          'wechatpay-serial': process.env.WECHAT_PLATFORM_CERT_SERIAL_NO || ''
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+  });
+
+  describe('支付状态流转测试', () => {
+    it('应该正确更新订单支付状态', async () => {
+      // 创建支付
+      const createResponse = await client.payment.$post({
+        json: {
+          orderId: testOrder.id,
+          totalAmount: 20000,
+          description: '测试支付订单',
+          openid: 'oJy1-16IIG18XZLl7G32k1hHMUFg'
+        },
+      },
+        {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        });
+
+      expect(createResponse.status).toBe(200);
+
+      // 验证订单状态已更新为处理中
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const orderRepository = dataSource.getRepository('Order');
+      const updatedOrder = await orderRepository.findOne({
+        where: { id: testOrder.id }
+      });
+
+      expect(updatedOrder?.paymentStatus).toBe(PaymentStatus.PROCESSING);
+    });
+  });
+
+  describe('微信支付JSAPI参数生成测试', () => {
+    it('应该生成正确的支付参数格式', async () => {
+      const response = await client.payment.$post({
+        json: {
+          orderId: testOrder.id,
+          totalAmount: 20000,
+          description: '测试支付订单',
+          openid: 'oJy1-16IIG18XZLl7G32k1hHMUFg'
+        },
+      },
+        {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        });
+
+      expect(response.status).toBe(200);
+      if (response.status === 200) {
+        const result = await response.json();
+
+        // 验证返回参数格式
+        expect(result.timeStamp).toMatch(/^\d+$/); // 时间戳应该是数字字符串
+        expect(result.nonceStr).toBeTruthy(); // 随机字符串应该存在
+        expect(result.package).toContain('prepay_id=');
+        expect(result.signType).toBe('RSA');
+        expect(result.paySign).toBeTruthy(); // 签名应该存在
+      }
+    });
+  });
+});

+ 51 - 0
web/tests/utils/server/integration-test-db.ts

@@ -9,6 +9,8 @@ import { AreaEntity } from '@d8d/server/modules/areas/area.entity';
 import { VehicleType } from '@d8d/server/modules/routes/route.schema';
 import { Passenger } from '@d8d/server/modules/passengers/passenger.entity';
 import { IdType } from '@d8d/server/modules/passengers/passenger.schema';
+import { Order } from '@d8d/server/modules/orders/order.entity';
+import { OrderStatus, PaymentStatus } from '@d8d/server/share/order.types';
 import { AppDataSource } from '@d8d/server/data-source';
 
 /**
@@ -300,6 +302,55 @@ export class TestDataFactory {
     const passenger = passengerRepository.create(passengerData);
     return await passengerRepository.save(passenger);
   }
+
+  /**
+   * 创建测试订单数据
+   */
+  static createOrderData(overrides: Partial<Order> = {}): Partial<Order> {
+    const timestamp = Date.now();
+    return {
+      userId: 0, // 将在创建时自动设置
+      routeId: 0, // 将在创建时自动设置
+      passengerCount: 2,
+      totalAmount: 200.00,
+      status: OrderStatus.PENDING_PAYMENT,
+      paymentStatus: PaymentStatus.PENDING,
+      passengerSnapshots: [
+        { name: '测试乘客1', idType: '身份证', idNumber: '110101199001011234' },
+        { name: '测试乘客2', idType: '身份证', idNumber: '110101199001011235' }
+      ],
+      routeSnapshot: {
+        name: '测试路线快照',
+        description: '测试路线描述',
+        price: 100.00,
+        departureTime: new Date(Date.now() + 24 * 60 * 60 * 1000)
+      },
+      ...overrides
+    };
+  }
+
+  /**
+   * 在数据库中创建测试订单
+   */
+  static async createTestOrder(dataSource: DataSource, overrides: Partial<Order> = {}): Promise<Order> {
+    const orderData = this.createOrderData(overrides);
+    const orderRepository = dataSource.getRepository(Order);
+
+    // 如果没有提供userId,自动创建一个测试用户
+    if (!orderData.userId || orderData.userId === 0) {
+      const testUser = await this.createTestUser(dataSource);
+      orderData.userId = testUser.id;
+    }
+
+    // 如果没有提供routeId,自动创建一个测试路线
+    if (!orderData.routeId || orderData.routeId === 0) {
+      const testRoute = await this.createTestRoute(dataSource);
+      orderData.routeId = testRoute.id;
+    }
+
+    const order = orderRepository.create(orderData);
+    return await orderRepository.save(order);
+  }
 }
 
 /**