# PaySim **Repository Path**: guyongqi/pay-sim ## Basic Information - **Project Name**: PaySim - **Description**: 一个模拟商城支付的项目 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-04 - **Last Updated**: 2026-03-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 你想了解如何在 NestJS 框架中接入支付宝和微信支付的 SDK,并与 Vue3 前端配合实现完整的支付功能,这是基于 Node.js 生态的主流支付开发方案。 ### 一、整体实现思路 NestJS 作为后端框架,核心负责: 1. 集成支付 SDK 并封装支付相关接口 2. 处理支付参数生成、签名验证、异步通知 3. 维护订单状态,保证支付逻辑的安全性和幂等性 Vue3 前端负责: 1. 触发支付请求,传递订单信息 2. 接收后端返回的支付参数,唤起支付控件 3. 展示支付结果,跳转对应页面 ### 二、前置准备 1. **支付宝**:获取 `APP_ID`、商户私钥、支付宝公钥,安装 SDK:`npm i alipay-sdk` 2. **微信支付**:获取 `APP_ID`、`MCH_ID`(商户号)、`MCH_KEY`(商户密钥),安装 SDK:`npm i wechatpay-api-v3` 3. **NestJS 基础环境**:已搭建 NestJS 项目,安装必要依赖:`npm i axios crypto-js xml2js` 4. **Vue3 环境**:安装 `axios`(请求后端)、微信支付需安装 `weixin-js-sdk`(H5 支付) ### 三、NestJS 后端实现 #### 1. 支付宝支付集成 ##### (1)创建支付宝配置模块(alipay.config.ts) ```typescript // src/pay/config/alipay.config.ts export const ALIPAY_CONFIG = { appId: '你的支付宝APPID', privateKey: '你的商户私钥(PKCS8格式)', alipayPublicKey: '支付宝公钥', gateway: 'https://openapi.alipaydev.com/gateway.do', // 沙箱环境,生产用 https://openapi.alipay.com/gateway.do returnUrl: 'http://localhost:3000/pay/alipay/success', // 同步回调地址(前端) notifyUrl: 'http://你的公网域名/pay/alipay/notify', // 异步通知地址(后端) }; ``` ##### (2)封装支付宝服务(alipay.service.ts) ```typescript // src/pay/services/alipay.service.ts import { Injectable } from '@nestjs/common'; import AlipaySdk from 'alipay-sdk'; import { ALIPAY_CONFIG } from '../config/alipay.config'; @Injectable() export class AlipayService { private alipaySdk: AlipaySdk; constructor() { // 初始化支付宝 SDK this.alipaySdk = new AlipaySdk({ appId: ALIPAY_CONFIG.appId, privateKey: ALIPAY_CONFIG.privateKey, alipayPublicKey: ALIPAY_CONFIG.alipayPublicKey, gateway: ALIPAY_CONFIG.gateway, signType: 'RSA2', }); } /** * 生成支付宝电脑网站支付参数 * @param outTradeNo 商户订单号 * @param totalAmount 订单金额(元,保留2位小数) * @param subject 订单标题 */ async createPagePayParams( outTradeNo: string, totalAmount: string, subject: string, ) { try { const result = await this.alipaySdk.exec( 'alipay.trade.page.pay', { bizContent: { out_trade_no: outTradeNo, total_amount: totalAmount, subject: subject, product_code: 'FAST_INSTANT_TRADE_PAY', }, returnUrl: ALIPAY_CONFIG.returnUrl, notifyUrl: ALIPAY_CONFIG.notifyUrl, }, { formData: true, // 返回 HTML 表单,前端直接渲染 }, ); return result; } catch (error) { throw new Error(`生成支付宝支付参数失败:${error.message}`); } } /** * 验证支付宝异步通知的签名 * @param params 支付宝通知参数 */ verifyNotifySign(params: Record): boolean { try { return this.alipaySdk.checkNotifySign(params); } catch (error) { return false; } } } ``` ##### (3)支付宝控制器(alipay.controller.ts) ```typescript // src/pay/controllers/alipay.controller.ts import { Controller, Post, Get, Body, Query, Req } from '@nestjs/common'; import { AlipayService } from '../services/alipay.service'; @Controller('pay/alipay') export class AlipayController { constructor(private readonly alipayService: AlipayService) {} // 1. 生成支付宝支付订单 @Post('create-order') async createOrder( @Body() body: { outTradeNo: string; totalAmount: string; subject: string }, ) { const { outTradeNo, totalAmount, subject } = body; const payForm = await this.alipayService.createPagePayParams( outTradeNo, totalAmount, subject, ); return { code: 200, data: payForm, msg: '生成支付订单成功' }; } // 2. 支付宝异步通知(核心:验证支付结果) @Post('notify') async notify(@Req() req: any) { // 解析通知参数 const notifyParams = req.body; // 1. 验证签名 const isSignValid = this.alipayService.verifyNotifySign(notifyParams); if (!isSignValid) { return 'fail'; // 验签失败,返回fail,支付宝会重试 } // 2. 判断支付状态 if (notifyParams.trade_status === 'TRADE_SUCCESS') { const { out_trade_no, total_amount } = notifyParams; // 3. 处理业务逻辑:更新订单状态、记录支付日志(保证幂等性) console.log(`订单 ${out_trade_no} 支付成功,金额:${total_amount}元`); } // 4. 返回success,告知支付宝通知已处理 return 'success'; } // 3. 同步回调(仅展示结果,不处理业务) @Get('success') success(@Query() query) { // 前端跳转页面,返回成功提示 return { code: 200, msg: '支付成功', data: query }; } } ``` #### 2. 微信支付集成 ##### (1)微信支付配置(wxpay.config.ts) ```typescript // src/pay/config/wxpay.config.ts export const WXPAY_CONFIG = { appId: '你的微信APPID', mchId: '你的商户号', mchKey: '你的商户密钥', notifyUrl: 'http://你的公网域名/pay/wxpay/notify', // 异步通知地址 tradeType: 'JSAPI', // H5支付用JSAPI,扫码用NATIVE }; ``` ##### (2)微信支付服务(wxpay.service.ts) ```typescript // src/pay/services/wxpay.service.ts import { Injectable } from '@nestjs/common'; import axios from 'axios'; import * as crypto from 'crypto'; import * as xml2js from 'xml2js'; import { WXPAY_CONFIG } from '../config/wxpay.config'; @Injectable() export class WxPayService { // 生成随机字符串 private createNonceStr(): string { return Math.random().toString(36).substr(2, 15); } // 生成时间戳 private createTimeStamp(): string { return Math.floor(Date.now() / 1000).toString(); } // 生成签名 private createSign(params: Record): string { // 1. 排序参数 const sortedKeys = Object.keys(params).sort(); let signStr = ''; sortedKeys.forEach((key) => { if (params[key] && key !== 'sign') { signStr += `${key}=${params[key]}&`; } }); // 2. 拼接商户密钥 signStr += `key=${WXPAY_CONFIG.mchKey}`; // 3. MD5加密并转大写 return crypto.createHash('md5').update(signStr).digest('hex').toUpperCase(); } // XML转JSON private async xmlToJson(xml: string): Promise> { const parser = new xml2js.Parser({ explicitArray: false, trim: true }); return new Promise((resolve, reject) => { parser.parseString(xml, (err, result) => { if (err) reject(err); else resolve(result.xml); }); }); } // JSON转XML private jsonToXml(json: Record): string { let xml = ''; Object.keys(json).forEach((key) => { xml += `<${key}>${json[key]}`; }); xml += ''; return xml; } /** * 统一下单接口,生成预支付参数 * @param outTradeNo 商户订单号 * @param totalFee 金额(分) * @param openid 用户openid(JSAPI支付必填) * @param spbillCreateIp 客户端IP */ async createUnifiedOrder( outTradeNo: string, totalFee: string, openid: string, spbillCreateIp: string, ) { try { // 1. 构造统一下单参数 const params = { appid: WXPAY_CONFIG.appId, mch_id: WXPAY_CONFIG.mchId, nonce_str: this.createNonceStr(), body: '测试商品', out_trade_no: outTradeNo, total_fee: totalFee, spbill_create_ip: spbillCreateIp, notify_url: WXPAY_CONFIG.notifyUrl, trade_type: WXPAY_CONFIG.tradeType, openid: openid, }; // 2. 生成签名 params.sign = this.createSign(params); // 3. 转XML const xmlParams = this.jsonToXml(params); // 4. 调用微信统一下单接口 const response = await axios.post( 'https://api.mch.weixin.qq.com/pay/unifiedorder', xmlParams, { headers: { 'Content-Type': 'text/xml' } }, ); // 5. 解析返回结果 const result = await this.xmlToJson(response.data); if (result.return_code !== 'SUCCESS' || result.result_code !== 'SUCCESS') { throw new Error(`微信统一下单失败:${result.return_msg || result.err_code_des}`); } // 6. 生成前端唤起支付的参数 const payParams = { appId: WXPAY_CONFIG.appId, timeStamp: this.createTimeStamp(), nonceStr: this.createNonceStr(), package: `prepay_id=${result.prepay_id}`, signType: 'MD5', }; payParams['paySign'] = this.createSign(payParams); return payParams; } catch (error) { throw new Error(`生成微信支付参数失败:${error.message}`); } } /** * 验证微信异步通知签名 * @param params 通知参数 */ verifyNotifySign(params: Record): boolean { const sign = params.sign; delete params.sign; const newSign = this.createSign(params); return newSign === sign; } } ``` ##### (3)微信支付控制器(wxpay.controller.ts) ```typescript // src/pay/controllers/wxpay.controller.ts import { Controller, Post, Body, Req } from '@nestjs/common'; import { WxPayService } from '../services/wxpay.service'; import * as xml2js from 'xml2js'; @Controller('pay/wxpay') export class WxPayController { constructor(private readonly wxPayService: WxPayService) {} // 1. 生成微信支付订单 @Post('create-order') async createOrder( @Body() body: { outTradeNo: string; totalFee: string; openid: string; spbillCreateIp: string }, ) { const { outTradeNo, totalFee, openid, spbillCreateIp } = body; const payParams = await this.wxPayService.createUnifiedOrder( outTradeNo, totalFee, openid, spbillCreateIp, ); return { code: 200, data: payParams, msg: '生成支付订单成功' }; } // 2. 微信异步通知 @Post('notify') async notify(@Req() req: any) { // 解析XML参数 const buffer = []; for await (const chunk of req) { buffer.push(chunk); } const xmlData = Buffer.concat(buffer).toString('utf-8'); const result = await this.wxPayService['xmlToJson'](xmlData); // 1. 验证签名 const isSignValid = this.wxPayService.verifyNotifySign(result); if (!isSignValid) { return this.wxPayService['jsonToXml']({ return_code: 'FAIL', return_msg: '签名失败', }); } // 2. 判断支付结果 if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') { const { out_trade_no, total_fee } = result; // 3. 处理业务逻辑:更新订单状态(保证幂等性) console.log(`订单 ${out_trade_no} 支付成功,金额:${total_fee}分`); } // 4. 返回成功响应 return this.wxPayService['jsonToXml']({ return_code: 'SUCCESS', return_msg: 'OK', }); } } ``` ##### (4)注册支付模块(pay.module.ts) ```typescript // src/pay/pay.module.ts import { Module } from '@nestjs/common'; import { AlipayController } from './controllers/alipay.controller'; import { AlipayService } from './services/alipay.service'; import { WxPayController } from './controllers/wxpay.controller'; import { WxPayService } from './services/wxpay.service'; @Module({ controllers: [AlipayController, WxPayController], providers: [AlipayService, WxPayService], }) export class PayModule {} ``` 最后在 `app.module.ts` 中导入 `PayModule` 即可。 ### 四、Vue3 前端实现 #### 1. 安装依赖 ```bash npm i axios weixin-js-sdk ``` #### 2. 支付宝支付组件(AlipayPay.vue) ```vue ``` #### 3. 微信支付组件(WxPay.vue) ```vue ``` ### 五、关键注意事项 1. **跨域处理**:NestJS 需配置 CORS,在 `main.ts` 中添加: ```typescript app.enableCors({ origin: 'http://localhost:3000', // 前端域名 credentials: true, }); ``` 2. **公网可访问**:支付平台的异步通知地址必须是公网可访问的,开发时可使用内网穿透工具(如 ngrok)。 3. **幂等性**:处理异步通知时,需通过订单号加锁或状态判断,避免重复更新订单。 4. **环境切换**:上线前需将支付宝/微信的网关、配置参数切换到生产环境,并完成商户认证。 ### 总结 1. **NestJS 后端核心**:封装支付 SDK,提供生成支付参数的接口,处理支付平台的异步通知(验证签名、更新订单状态),是支付逻辑的核心。 2. **Vue3 前端核心**:调用后端接口获取支付参数,渲染支付宝表单/唤起微信支付控件,展示支付结果(最终结果以后端异步通知为准)。 3. **安全要点**:必须验证支付平台的签名,异步通知地址需公网可访问,处理订单时保证幂等性,区分测试/生产环境。