Files
bw-mini-app-server/src/payment/adapters/wechat-payment.adapter.ts
imeepos 96ae77471c feat: 实现完整的在线支付模块
- 添加支付模块架构,支持微信支付和抖音支付
- 创建支付订单、交易记录、退款记录数据表和实体
- 实现基于适配器模式的多平台支付集成
- 添加统一支付服务,集成会员订阅、积分购买、模板解锁业务
- 完善支付回调处理和退款功能
- 添加完整的DTO验证和API文档
- 集成到主应用模块,完成依赖注入配置
2025-09-25 20:28:30 +08:00

693 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<PaymentOrderEntity>,
@InjectRepository(PaymentTransactionEntity)
paymentTransactionRepository: Repository<PaymentTransactionEntity>,
@InjectRepository(RefundRecordEntity)
refundRecordRepository: Repository<RefundRecordEntity>,
) {
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<PaymentOrderResult> {
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<PaymentOrderQueryResult> {
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<PaymentCallbackResult> {
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<RefundResult> {
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<RefundQueryResult> {
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<boolean> {
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<BillData> {
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;
}
}