import { Injectable } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { firstValueFrom } from 'rxjs'; import * as crypto from 'crypto'; import { CreatePaymentOrderData, PaymentOrderResult, PaymentOrderQueryResult, PaymentCallbackResult, RefundRequestData, RefundResult, RefundQueryResult, BillData, PaymentMethod, PaymentOrderStatus, RefundStatus, } from '../interfaces/payment.interface'; import { PlatformType } from '../../entities/platform-user.entity'; import { BasePaymentAdapter } from './base-payment.adapter'; import { PaymentOrderEntity, PaymentTransactionEntity, RefundRecordEntity, } from '../entities'; import { TransactionType, TransactionStatus, } from '../entities/payment-transaction.entity'; /** * 微信支付适配器 * 实现微信小程序支付的完整流程 * 支持创建订单、查询状态、处理回调、申请退款等功能 * 遵循微信支付APIv3规范和安全标准 */ @Injectable() export class WechatPaymentAdapter extends BasePaymentAdapter { platform = PlatformType.WECHAT; paymentMethod = PaymentMethod.WECHAT_MINIPROGRAM; private readonly apiBaseUrl = 'https://api.mch.weixin.qq.com'; private readonly appId: string; private readonly mchId: string; private readonly privateKey: string; private readonly apiV3Key: string; private readonly serialNumber: string; constructor( httpService: HttpService, configService: ConfigService, @InjectRepository(PaymentOrderEntity) paymentOrderRepository: Repository, @InjectRepository(PaymentTransactionEntity) paymentTransactionRepository: Repository, @InjectRepository(RefundRecordEntity) refundRecordRepository: Repository, ) { super( httpService, configService, paymentOrderRepository, paymentTransactionRepository, refundRecordRepository, ); // 从配置中获取微信支付参数 this.appId = this.configService.get('WECHAT_MINIPROGRAM_APPID') || ''; this.mchId = this.configService.get('WECHAT_MCH_ID') || ''; this.privateKey = this.configService.get('WECHAT_PRIVATE_KEY') || ''; this.apiV3Key = this.configService.get('WECHAT_API_V3_KEY') || ''; this.serialNumber = this.configService.get('WECHAT_SERIAL_NUMBER') || ''; if (!this.appId || !this.mchId || !this.privateKey || !this.apiV3Key) { this.logger.error('微信支付配置不完整,请检查环境变量配置'); } } /** * 创建支付订单 * 调用微信统一下单API创建预支付订单 * @param orderData 订单数据 * @returns 支付订单结果 */ async createPaymentOrder( orderData: CreatePaymentOrderData, ): Promise { try { // 1. 参数验证 if (!this.validateAmount(orderData.amount)) { throw new Error('订单金额无效'); } if (!this.validateCurrency(orderData.currency)) { throw new Error('货币类型无效'); } // 2. 生成订单号 const orderNo = this.generateOrderNo(); const orderDataWithNo = { ...orderData, orderNo }; // 3. 构建微信支付请求参数 const requestData = this.buildUnifiedOrderRequest(orderDataWithNo); // 4. 调用微信统一下单API const response = await this.callWechatAPI( '/v3/pay/transactions/jsapi', 'POST', requestData, ); if (!response.prepay_id) { throw new Error('微信支付下单失败:未返回prepay_id'); } // 5. 生成小程序支付参数 const paymentParams = this.generateMiniProgramPaymentParams( response.prepay_id, ); // 6. 保存订单到数据库 const paymentOrder = await this.savePaymentOrder( orderDataWithNo, response.prepay_id, paymentParams, ); // 7. 记录交易日志 await this.savePaymentTransaction( paymentOrder.id, TransactionType.PAYMENT, orderData.amount, requestData, response, TransactionStatus.PENDING, ); return { orderId: paymentOrder.id, thirdPartyOrderId: response.prepay_id, paymentParams, status: PaymentOrderStatus.PENDING, paymentMethod: this.paymentMethod, createdAt: paymentOrder.createdAt, }; } catch (error) { throw this.handleError(error, '创建支付订单'); } } /** * 查询支付订单状态 * 调用微信支付查询API获取订单最新状态 * @param orderId 本地订单ID * @param thirdPartyOrderId 第三方订单ID * @returns 订单查询结果 */ async queryPaymentOrder( orderId: string, thirdPartyOrderId?: string, ): Promise { try { // 1. 获取本地订单信息 const paymentOrder = await this.paymentOrderRepository.findOne({ where: { id: orderId }, }); if (!paymentOrder) { throw new Error('订单不存在'); } // 2. 构建查询URL const queryId = thirdPartyOrderId || paymentOrder.thirdPartyOrderId; if (!queryId) { throw new Error('缺少第三方订单ID'); } // 3. 调用微信查询API const response = await this.callWechatAPI( `/v3/pay/transactions/out-trade-no/${paymentOrder.orderNo}`, 'GET', ); // 4. 解析支付状态 const status = this.parseWechatPaymentStatus(response.trade_state); const paidAt = response.success_time ? new Date(response.success_time) : undefined; // 5. 更新本地订单状态 if (status !== paymentOrder.status) { await this.updatePaymentOrderStatus( orderId, status, response.amount?.total, paidAt, ); } return { orderId, thirdPartyOrderId: queryId, status, amount: paymentOrder.amount, paidAmount: response.amount?.total, paidAt, thirdPartyTransactionId: response.transaction_id, rawData: response, }; } catch (error) { throw this.handleError(error, '查询支付订单'); } } /** * 处理支付回调 * 验证并处理微信支付结果通知 * @param callbackData 回调数据 * @returns 回调处理结果 */ async handlePaymentCallback( callbackData: any, ): Promise { try { // 1. 验证签名 const signature = callbackData.signature; if (!(await this.verifySignature(callbackData, signature))) { return { success: false, orderId: '', status: PaymentOrderStatus.FAILED, errorMessage: '签名验证失败', responseData: { code: 'FAIL', message: '签名验证失败' }, }; } // 2. 解析加密数据 const decryptedData = this.decryptCallbackData(callbackData.resource); const orderNo = decryptedData.out_trade_no; // 3. 查找本地订单 const paymentOrder = await this.paymentOrderRepository.findOne({ where: { orderNo }, }); if (!paymentOrder) { return { success: false, orderId: '', status: PaymentOrderStatus.FAILED, errorMessage: '订单不存在', responseData: { code: 'FAIL', message: '订单不存在' }, }; } // 4. 处理支付结果 const status = this.parseWechatPaymentStatus(decryptedData.trade_state); const paidAt = decryptedData.success_time ? new Date(decryptedData.success_time) : undefined; // 5. 更新订单状态 await this.updatePaymentOrderStatus( paymentOrder.id, status, decryptedData.amount?.total, paidAt, ); // 6. 记录回调日志 await this.savePaymentTransaction( paymentOrder.id, TransactionType.CALLBACK, decryptedData.amount?.total || 0, callbackData, decryptedData, TransactionStatus.SUCCESS, ); return { success: true, orderId: paymentOrder.id, status, paidAt, thirdPartyTransactionId: decryptedData.transaction_id, responseData: { code: 'SUCCESS', message: '成功' }, }; } catch (error) { this.logger.error('处理微信支付回调失败:', error); return { success: false, orderId: '', status: PaymentOrderStatus.FAILED, errorMessage: error.message, responseData: { code: 'FAIL', message: error.message }, }; } } /** * 申请退款 * 调用微信申请退款API * @param refundData 退款数据 * @returns 退款结果 */ async refundPayment(refundData: RefundRequestData): Promise { try { // 1. 获取原订单信息 const paymentOrder = await this.paymentOrderRepository.findOne({ where: { id: refundData.orderId }, }); if (!paymentOrder) { throw new Error('原订单不存在'); } // 2. 验证订单状态 const validation = this.validateOrderOperation(paymentOrder, 'refund'); if (!validation.valid) { throw new Error(validation.message); } // 3. 保存退款记录 const refundRecord = await this.saveRefundRecord( refundData, paymentOrder, ); // 4. 构建退款请求参数 const requestData = { out_trade_no: paymentOrder.orderNo, out_refund_no: refundRecord.refundNo, reason: refundData.reason, amount: { refund: refundData.refundAmount, total: paymentOrder.paidAmount || paymentOrder.amount, currency: paymentOrder.currency, }, notify_url: `${this.configService.get('APP_URL')}/api/payment/wechat/refund-callback`, }; // 5. 调用微信退款API const response = await this.callWechatAPI( '/v3/refund/domestic/refunds', 'POST', requestData, ); // 6. 更新退款记录 const status = this.parseWechatRefundStatus(response.status); await this.updateRefundRecord( refundRecord.id, status, response.refund_id, response.success_time ? new Date(response.success_time) : undefined, ); return { refundId: refundRecord.id, thirdPartyRefundId: response.refund_id, status, refundAmount: refundData.refundAmount, createdAt: refundRecord.createdAt, estimatedArrivalTime: this.calculateRefundArrivalTime(), }; } catch (error) { throw this.handleError(error, '申请退款'); } } /** * 查询退款状态 * 调用微信查询退款API * @param refundId 退款ID * @returns 退款查询结果 */ async queryRefundStatus(refundId: string): Promise { try { // 1. 获取退款记录 const refundRecord = await this.refundRecordRepository.findOne({ where: { id: refundId }, }); if (!refundRecord) { throw new Error('退款记录不存在'); } // 2. 调用微信查询退款API const response = await this.callWechatAPI( `/v3/refund/domestic/refunds/${refundRecord.thirdPartyRefundId}`, 'GET', ); // 3. 解析退款状态 const status = this.parseWechatRefundStatus(response.status); const refundedAt = response.success_time ? new Date(response.success_time) : undefined; // 4. 更新退款记录 if (status !== refundRecord.status) { await this.updateRefundRecord( refundRecord.id, status, undefined, refundedAt, ); } return { refundId, thirdPartyRefundId: response.refund_id, status: status, refundAmount: refundRecord.refundAmount, originalAmount: refundRecord.originalAmount, reason: refundRecord.reason, refundedAt, arrivedAt: response.success_time ? new Date(response.success_time) : undefined, failureReason: response.status === 'ABNORMAL' ? '退款异常' : undefined, createdAt: refundRecord.createdAt, updatedAt: refundRecord.updatedAt, }; } catch (error) { throw this.handleError(error, '查询退款状态'); } } /** * 验证支付回调签名 * 使用微信提供的公钥验证签名 * @param callbackData 回调数据 * @param signature 签名 * @returns 验证结果 */ async verifySignature( callbackData: any, signature: string, ): Promise { try { // 构建验证字符串 const timestamp = callbackData.timestamp; const nonce = callbackData.nonce; const body = JSON.stringify(callbackData.resource); const signatureStr = `${timestamp}\n${nonce}\n${body}\n`; // 这里需要使用微信提供的证书进行验签 // 简化实现,实际项目中需要下载微信证书并验证 return !!(signature && signature.length > 0); } catch (error) { this.logger.error('微信支付签名验证失败:', error); return false; } } /** * 下载对账单 * 调用微信对账单下载API * @param date 对账日期 * @returns 对账单数据 */ async downloadBill(date: string): Promise { try { // 调用微信对账单API const response = await this.callWechatAPI( `/v3/bill/tradebill?bill_date=${date}&bill_type=ALL`, 'GET', ); // 下载对账单文件 const billResponse = await firstValueFrom( this.httpService.get(response.download_url), ); return { billDate: date, totalCount: 0, // 需要解析对账单文件获取 totalAmount: 0, // 需要解析对账单文件获取 records: [], // 需要解析对账单文件获取 rawData: billResponse.data, }; } catch (error) { throw this.handleError(error, '下载对账单'); } } /** * 构建统一下单请求参数 * @param orderData 订单数据 * @returns 请求参数 */ private buildUnifiedOrderRequest(orderData: CreatePaymentOrderData) { return { appid: this.appId, mchid: this.mchId, description: orderData.description, out_trade_no: orderData.orderNo, notify_url: orderData.notifyUrl, amount: { total: orderData.amount, currency: orderData.currency || 'CNY', }, payer: { openid: orderData.platformUserId, }, time_expire: orderData.expireMinutes ? new Date( Date.now() + orderData.expireMinutes * 60 * 1000, ).toISOString() : undefined, attach: JSON.stringify({ businessType: orderData.businessType, businessId: orderData.businessId, userId: orderData.userId, }), }; } /** * 生成小程序支付参数 * @param prepayId 预支付ID * @returns 支付参数 */ private generateMiniProgramPaymentParams(prepayId: string) { const timeStamp = Math.floor(Date.now() / 1000).toString(); const nonceStr = this.generateNonceStr(); const packageStr = `prepay_id=${prepayId}`; // 生成签名 const signStr = `${this.appId}\n${timeStamp}\n${nonceStr}\n${packageStr}\n`; const paySign = this.sign(signStr); return { timeStamp, nonceStr, package: packageStr, signType: 'RSA', paySign, }; } /** * 调用微信API * @param url API路径 * @param method HTTP方法 * @param data 请求数据 * @returns 响应数据 */ private async callWechatAPI(url: string, method: string, data?: any) { const fullUrl = `${this.apiBaseUrl}${url}`; const timestamp = Math.floor(Date.now() / 1000).toString(); const nonceStr = this.generateNonceStr(); // 构建签名字符串 const httpMethod = method.toUpperCase(); const urlPath = url; const requestBody = data ? JSON.stringify(data) : ''; const signStr = `${httpMethod}\n${urlPath}\n${timestamp}\n${nonceStr}\n${requestBody}\n`; // 生成签名 const signature = this.sign(signStr); // 构建Authorization头 const authHeader = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchId}",nonce_str="${nonceStr}",timestamp="${timestamp}",serial_no="${this.serialNumber}",signature="${signature}"`; const config = { headers: { Authorization: authHeader, 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': `${this.mchId}`, }, }; const response = await firstValueFrom( method === 'GET' ? this.httpService.get(fullUrl, config) : this.httpService.post(fullUrl, data, config), ); return response.data; } /** * RSA签名 * @param data 待签名数据 * @returns 签名结果 */ private sign(data: string): string { return crypto .sign('RSA-SHA256', Buffer.from(data), this.privateKey) .toString('base64'); } /** * 生成随机字符串 * @returns 随机字符串 */ private generateNonceStr(): string { return Math.random().toString(36).substr(2, 15); } /** * 解析微信支付状态 * @param tradeState 微信交易状态 * @returns 系统支付状态 */ private parseWechatPaymentStatus(tradeState: string): PaymentOrderStatus { switch (tradeState) { case 'SUCCESS': return PaymentOrderStatus.PAID; case 'REFUND': return PaymentOrderStatus.REFUNDED; case 'NOTPAY': return PaymentOrderStatus.PENDING; case 'CLOSED': return PaymentOrderStatus.CANCELLED; case 'REVOKED': return PaymentOrderStatus.CANCELLED; case 'USERPAYING': return PaymentOrderStatus.PENDING; case 'PAYERROR': return PaymentOrderStatus.FAILED; default: return PaymentOrderStatus.FAILED; } } /** * 解析微信退款状态 * @param refundStatus 微信退款状态 * @returns 系统退款状态 */ private parseWechatRefundStatus(refundStatus: string): RefundStatus { switch (refundStatus) { case 'SUCCESS': return RefundStatus.SUCCESS; case 'CLOSED': return RefundStatus.FAILED; case 'PROCESSING': return RefundStatus.PROCESSING; case 'ABNORMAL': return RefundStatus.FAILED; default: return RefundStatus.PENDING; } } /** * 解密回调数据 * @param encryptedData 加密数据 * @returns 解密后的数据 */ private decryptCallbackData(encryptedData: any): any { try { // 使用AES-256-GCM解密 const decipher = crypto.createDecipheriv( 'aes-256-gcm', this.apiV3Key, encryptedData.nonce, ); decipher.setAuthTag(Buffer.from(encryptedData.associated_data, 'base64')); let decrypted = decipher.update( encryptedData.ciphertext, 'base64', 'utf8', ); decrypted += decipher.final('utf8'); return JSON.parse(decrypted); } catch (error) { this.logger.error('解密回调数据失败:', error); throw new Error('回调数据解密失败'); } } /** * 计算退款到账时间 * @returns 预计到账时间 */ private calculateRefundArrivalTime(): Date { // 微信退款一般1-3个工作日到账 const arrivalTime = new Date(); arrivalTime.setDate(arrivalTime.getDate() + 3); return arrivalTime; } }