|
|
@@ -0,0 +1,262 @@
|
|
|
+import { DataSource } from 'typeorm';
|
|
|
+import WxPay from 'wechatpay-node-v3';
|
|
|
+import { Buffer } from 'buffer';
|
|
|
+import { PaymentEntity } from '../entities/payment.entity.js';
|
|
|
+import { PaymentStatus } from '../entities/payment.types.js';
|
|
|
+import { PaymentCreateResponse } from '../entities/payment.types.js';
|
|
|
+
|
|
|
+/**
|
|
|
+ * 微信支付服务
|
|
|
+ * 使用微信支付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(
|
|
|
+ private readonly dataSource: DataSource
|
|
|
+ ) {
|
|
|
+ // 从环境变量获取支付配置
|
|
|
+ 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 externalOrderId 外部订单ID
|
|
|
+ * @param userId 用户ID
|
|
|
+ * @param totalAmount 支付金额(分)
|
|
|
+ * @param description 支付描述
|
|
|
+ * @param openid 用户OpenID
|
|
|
+ */
|
|
|
+ async createPayment(
|
|
|
+ externalOrderId: number,
|
|
|
+ userId: number,
|
|
|
+ totalAmount: number,
|
|
|
+ description: string,
|
|
|
+ openid: string
|
|
|
+ ): Promise<PaymentCreateResponse> {
|
|
|
+ // 检查是否已存在相同外部订单ID的支付记录
|
|
|
+ const paymentRepository = this.dataSource.getRepository(PaymentEntity);
|
|
|
+ const existingPayment = await paymentRepository.findOne({
|
|
|
+ where: { externalOrderId }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (existingPayment) {
|
|
|
+ if (existingPayment.paymentStatus !== PaymentStatus.PENDING) {
|
|
|
+ throw new Error('该订单已存在支付记录且状态不正确');
|
|
|
+ }
|
|
|
+ // 如果存在待支付的记录,可以更新或重新创建,这里选择重新创建
|
|
|
+ await paymentRepository.remove(existingPayment);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!openid) {
|
|
|
+ throw new Error('用户OpenID不能为空');
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 创建商户订单号
|
|
|
+ const outTradeNo = `PAYMENT_${externalOrderId}_${Date.now()}`;
|
|
|
+
|
|
|
+ // 使用微信支付SDK创建JSAPI支付
|
|
|
+ const result = await this.wxPay.transactions_jsapi({
|
|
|
+ appid: this.appId,
|
|
|
+ mchid: this.merchantId,
|
|
|
+ description,
|
|
|
+ out_trade_no: outTradeNo,
|
|
|
+ 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;
|
|
|
+
|
|
|
+ // 创建支付记录
|
|
|
+ const payment = new PaymentEntity();
|
|
|
+ payment.externalOrderId = externalOrderId;
|
|
|
+ payment.userId = userId;
|
|
|
+ payment.totalAmount = totalAmount;
|
|
|
+ payment.description = description;
|
|
|
+ payment.paymentStatus = PaymentStatus.PROCESSING;
|
|
|
+ payment.outTradeNo = outTradeNo;
|
|
|
+ payment.openid = openid;
|
|
|
+
|
|
|
+ await paymentRepository.save(payment);
|
|
|
+
|
|
|
+ // 直接返回微信支付SDK生成的参数
|
|
|
+ return {
|
|
|
+ paymentId: prepayId,
|
|
|
+ timeStamp: result.timeStamp,
|
|
|
+ nonceStr: result.nonceStr,
|
|
|
+ package: result.package,
|
|
|
+ signType: result.signType,
|
|
|
+ paySign: result.paySign,
|
|
|
+ totalAmount: totalAmount // 添加金额字段用于前端验证
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error instanceof Error ? error.message : '未知错误';
|
|
|
+ throw new Error(`微信支付创建失败: ${errorMessage}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理支付回调
|
|
|
+ */
|
|
|
+ async handlePaymentCallback(
|
|
|
+ callbackData: any,
|
|
|
+ headers: any,
|
|
|
+ rawBody: string // 添加原始请求体参数
|
|
|
+ ): Promise<void> {
|
|
|
+ console.debug('收到支付回调请求:', {
|
|
|
+ headers,
|
|
|
+ callbackData,
|
|
|
+ rawBody
|
|
|
+ });
|
|
|
+
|
|
|
+ // 验证回调签名
|
|
|
+ const isValid = await this.wxPay.verifySign({
|
|
|
+ timestamp: headers['wechatpay-timestamp'],
|
|
|
+ nonce: headers['wechatpay-nonce'],
|
|
|
+ body: rawBody, // 优先使用原始请求体
|
|
|
+ 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
|
|
|
+ );
|
|
|
+
|
|
|
+ console.log('解密回调数据', decryptedData)
|
|
|
+ console.log('解密回调数据类型:', typeof decryptedData)
|
|
|
+
|
|
|
+ // 处理解密后的数据,可能是字符串或对象
|
|
|
+ let parsedData;
|
|
|
+ if (typeof decryptedData === 'string') {
|
|
|
+ parsedData = JSON.parse(decryptedData);
|
|
|
+ } else {
|
|
|
+ parsedData = decryptedData;
|
|
|
+ }
|
|
|
+
|
|
|
+ const paymentRepository = this.dataSource.getRepository(PaymentEntity);
|
|
|
+ const outTradeNo = parsedData.out_trade_no;
|
|
|
+ const payment = await paymentRepository.findOne({
|
|
|
+ where: { outTradeNo }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!payment) {
|
|
|
+ throw new Error('支付记录不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据回调结果更新支付状态
|
|
|
+ if (parsedData.trade_state === 'SUCCESS') {
|
|
|
+ payment.paymentStatus = PaymentStatus.PAID;
|
|
|
+ payment.wechatTransactionId = parsedData.transaction_id;
|
|
|
+ } else if (parsedData.trade_state === 'FAIL') {
|
|
|
+ payment.paymentStatus = PaymentStatus.FAILED;
|
|
|
+ } else if (parsedData.trade_state === 'REFUND') {
|
|
|
+ payment.paymentStatus = PaymentStatus.REFUNDED;
|
|
|
+ }
|
|
|
+
|
|
|
+ await paymentRepository.save(payment);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询支付状态
|
|
|
+ */
|
|
|
+ async getPaymentStatus(externalOrderId: number): Promise<PaymentStatus> {
|
|
|
+ const paymentRepository = this.dataSource.getRepository(PaymentEntity);
|
|
|
+ const payment = await paymentRepository.findOne({
|
|
|
+ where: { externalOrderId }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!payment) {
|
|
|
+ throw new Error('支付记录不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ return payment.paymentStatus;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成随机字符串
|
|
|
+ */
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成回调签名(用于测试)
|
|
|
+ */
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|