609 lines
18 KiB
JavaScript
609 lines
18 KiB
JavaScript
const crypto = require('crypto');
|
||
const axios = require('axios');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { wechatPay } = require('../config/wechatPay');
|
||
const { getDB } = require('../database');
|
||
|
||
class WechatPayService {
|
||
constructor() {
|
||
this.config = {
|
||
...wechatPay,
|
||
apiV3Key: process.env.WECHAT_API_V3_KEY
|
||
};
|
||
this.privateKey = null; // API v3 私钥
|
||
this.serialNo = null; // 商户证书序列号
|
||
this.initializeV3();
|
||
}
|
||
|
||
// 初始化API v3配置
|
||
async initializeV3() {
|
||
try {
|
||
// 检查配置是否存在
|
||
if (!this.config.keyPath || !this.config.certPath) {
|
||
console.warn('微信支付证书路径未配置,跳过API v3初始化');
|
||
return;
|
||
}
|
||
|
||
// 加载私钥
|
||
const keyPath = this.resolveCertPath(this.config.keyPath);
|
||
console.log('尝试加载私钥文件:', keyPath);
|
||
|
||
if (this.isValidFile(keyPath)) {
|
||
this.privateKey = fs.readFileSync(keyPath, 'utf8');
|
||
console.log('API v3 私钥加载成功');
|
||
} else {
|
||
console.error('私钥文件不存在或不是有效文件:', keyPath);
|
||
return;
|
||
}
|
||
|
||
// 获取证书序列号
|
||
const certPath = this.resolveCertPath(this.config.certPath);
|
||
console.log('尝试加载证书文件:', certPath);
|
||
|
||
if (this.isValidFile(certPath)) {
|
||
const cert = fs.readFileSync(certPath, 'utf8');
|
||
this.serialNo = this.getCertificateSerialNumber(cert);
|
||
console.log('证书序列号:', this.serialNo);
|
||
} else {
|
||
console.error('证书文件不存在或不是有效文件:', certPath);
|
||
}
|
||
} catch (error) {
|
||
console.error('初始化API v3配置失败:', error.message);
|
||
console.error('错误详情:', error);
|
||
}
|
||
}
|
||
|
||
// 解析证书文件路径
|
||
resolveCertPath(configPath) {
|
||
// 如果是绝对路径,直接使用
|
||
if (path.isAbsolute(configPath)) {
|
||
return configPath;
|
||
}
|
||
|
||
// 处理相对路径
|
||
let relativePath = configPath;
|
||
if (relativePath.startsWith('./')) {
|
||
relativePath = relativePath.substring(2);
|
||
}
|
||
|
||
return path.resolve(__dirname, '..', relativePath);
|
||
}
|
||
|
||
// 检查是否为有效的文件(不是目录)
|
||
isValidFile(filePath) {
|
||
try {
|
||
if (!fs.existsSync(filePath)) {
|
||
return false;
|
||
}
|
||
|
||
const stats = fs.statSync(filePath);
|
||
return stats.isFile();
|
||
} catch (error) {
|
||
console.error('检查文件状态失败:', error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 获取证书序列号
|
||
getCertificateSerialNumber(cert) {
|
||
try {
|
||
const x509 = crypto.X509Certificate ? new crypto.X509Certificate(cert) : null;
|
||
if (x509) {
|
||
return x509.serialNumber.toLowerCase().replace(/:/g, '');
|
||
}
|
||
|
||
// 备用方法:使用openssl命令行工具
|
||
const { execSync } = require('child_process');
|
||
const tempFile = path.join(__dirname, 'temp_cert.pem');
|
||
fs.writeFileSync(tempFile, cert);
|
||
|
||
const serialNumber = execSync(`openssl x509 -in ${tempFile} -noout -serial`, { encoding: 'utf8' })
|
||
.replace('serial=', '')
|
||
.trim()
|
||
.toLowerCase();
|
||
|
||
fs.unlinkSync(tempFile);
|
||
return serialNumber;
|
||
} catch (error) {
|
||
console.error('获取证书序列号失败:', error.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成随机字符串
|
||
* @param {number} length 长度
|
||
* @returns {string} 随机字符串
|
||
*/
|
||
generateNonceStr(length = 32) {
|
||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||
let result = '';
|
||
for (let i = 0; i < length; i++) {
|
||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 生成时间戳
|
||
* @returns {string} 时间戳
|
||
*/
|
||
generateTimestamp() {
|
||
return Math.floor(Date.now() / 1000).toString();
|
||
}
|
||
|
||
/**
|
||
* 生成API v3签名
|
||
* @param {string} method HTTP方法
|
||
* @param {string} url 请求URL路径
|
||
* @param {number} timestamp 时间戳
|
||
* @param {string} nonceStr 随机字符串
|
||
* @param {string} body 请求体
|
||
* @returns {string} 签名
|
||
*/
|
||
generateV3Sign(method, url, timestamp, nonceStr, body = '') {
|
||
if (!this.privateKey) {
|
||
throw new Error('私钥未加载,无法生成签名');
|
||
}
|
||
|
||
// 构造签名串
|
||
const signString = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n`;
|
||
console.log('API v3 签名字符串:', signString);
|
||
|
||
// 使用私钥进行SHA256-RSA签名
|
||
const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey);
|
||
const signature = sign.toString('base64');
|
||
|
||
console.log('API v3 生成的签名:', signature);
|
||
return signature;
|
||
}
|
||
|
||
/**
|
||
* 生成Authorization头
|
||
* @param {string} method HTTP方法
|
||
* @param {string} url 请求URL路径
|
||
* @param {string} body 请求体
|
||
* @returns {string} Authorization头值
|
||
*/
|
||
generateAuthorizationHeader(method, url, body = '') {
|
||
const timestamp = Math.floor(Date.now() / 1000);
|
||
const nonceStr = this.generateNonceStr();
|
||
const signature = this.generateV3Sign(method, url, timestamp, nonceStr, body);
|
||
|
||
return `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonceStr}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`;
|
||
}
|
||
|
||
/**
|
||
* 生成JSAPI支付参数
|
||
* @param {string} prepayId 预支付交易会话标识
|
||
* @returns {object} JSAPI支付参数
|
||
*/
|
||
generateJSAPIPayParams(prepayId) {
|
||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||
const nonceStr = this.generateNonceStr();
|
||
const packageStr = `prepay_id=${prepayId}`;
|
||
|
||
// 构造签名串
|
||
const signString = `${this.config.appId}\n${timestamp}\n${nonceStr}\n${packageStr}\n`;
|
||
|
||
// 使用私钥进行签名
|
||
const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey);
|
||
const paySign = sign.toString('base64');
|
||
|
||
return {
|
||
appId: this.config.appId,
|
||
timeStamp: timestamp,
|
||
nonceStr: nonceStr,
|
||
package: packageStr,
|
||
signType: 'RSA',
|
||
paySign: paySign
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 创建注册支付订单 (H5支付)
|
||
* @param {object} orderData 订单数据
|
||
* @returns {object} 支付结果
|
||
*/
|
||
async createRegistrationPayOrder(orderData) {
|
||
const { userId, username, phone, clientIp = '127.0.0.1' } = orderData;
|
||
|
||
try {
|
||
if (!this.privateKey || !this.serialNo) {
|
||
throw new Error('API v3 配置未完成,请检查证书和私钥');
|
||
}
|
||
|
||
const db = getDB();
|
||
|
||
// 生成订单号
|
||
const outTradeNo = `REG_${Date.now()}_${userId}`;
|
||
|
||
// 创建支付订单记录
|
||
await db.execute(
|
||
'INSERT INTO payment_orders (user_id, out_trade_no, total_fee, body, trade_type, status, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())',
|
||
[userId, outTradeNo, this.config.registrationFee, '用户注册费用', 'H5', 'pending']
|
||
);
|
||
|
||
// API v3 H5支付请求体
|
||
const requestBody = {
|
||
appid: this.config.appId,
|
||
mchid: this.config.mchId,
|
||
description: '用户注册费用',
|
||
out_trade_no: outTradeNo,
|
||
notify_url: this.config.notifyUrl,
|
||
amount: {
|
||
total: this.config.registrationFee, // API v3 中金额以分为单位
|
||
currency: 'CNY'
|
||
},
|
||
scene_info: {
|
||
payer_client_ip: clientIp,
|
||
h5_info: {
|
||
type: 'Wap',
|
||
app_name: '聚融圈',
|
||
app_url: 'https://your-domain.com',
|
||
bundle_id: 'com.jurong.circle'
|
||
}
|
||
}
|
||
};
|
||
|
||
console.log('API v3 H5支付参数:', requestBody);
|
||
|
||
const requestBodyStr = JSON.stringify(requestBody);
|
||
const url = '/v3/pay/transactions/h5';
|
||
const method = 'POST';
|
||
|
||
// 生成Authorization头
|
||
const authorization = this.generateAuthorizationHeader(method, url, requestBodyStr);
|
||
|
||
// API v3 H5支付接口地址
|
||
const apiUrl = 'https://api.mch.weixin.qq.com/v3/pay/transactions/h5';
|
||
|
||
console.log('使用的API v3 H5地址:', apiUrl);
|
||
console.log('Authorization头:', authorization);
|
||
|
||
const response = await axios.post(apiUrl, requestBody, {
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json',
|
||
'Authorization': authorization,
|
||
'User-Agent': 'jurong-circle/1.0.0'
|
||
}
|
||
});
|
||
|
||
console.log('微信支付API v3 H5响应:', response.data);
|
||
|
||
if (response.data && response.data.h5_url) {
|
||
// 更新订单状态
|
||
await db.execute(
|
||
'UPDATE payment_orders SET mweb_url = ? WHERE out_trade_no = ?',
|
||
[response.data.h5_url, outTradeNo]
|
||
);
|
||
|
||
return {
|
||
success: true,
|
||
data: {
|
||
outTradeNo,
|
||
h5Url: response.data.h5_url,
|
||
paymentType: 'h5'
|
||
}
|
||
};
|
||
} else {
|
||
console.log(response.data);
|
||
|
||
throw new Error(response.data?.message || '支付订单创建失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('创建H5支付订单失败:', error.response?.data || error.message);
|
||
throw new Error('支付订单创建失败: ' + (error.response?.data?.message || error.message));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理支付回调
|
||
* @param {string} xmlData 微信回调的XML数据
|
||
* @returns {object} 处理结果
|
||
*/
|
||
async handlePaymentNotify(xmlData) {
|
||
try {
|
||
const result = this.xmlToObject(xmlData);
|
||
|
||
// 验证签名
|
||
const sign = result.sign;
|
||
delete result.sign;
|
||
const calculatedSign = this.generateSign(result);
|
||
|
||
if (sign !== calculatedSign) {
|
||
throw new Error('签名验证失败');
|
||
}
|
||
|
||
if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
|
||
const db = getDB();
|
||
|
||
// 开始事务
|
||
await db.beginTransaction();
|
||
|
||
try {
|
||
// 更新支付订单状态
|
||
await db.execute(
|
||
'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?',
|
||
['paid', result.transaction_id, result.out_trade_no]
|
||
);
|
||
|
||
// 获取订单信息
|
||
const [orders] = await db.execute(
|
||
'SELECT user_id FROM payment_orders WHERE out_trade_no = ?',
|
||
[result.out_trade_no]
|
||
);
|
||
|
||
if (orders.length > 0) {
|
||
const userId = orders[0].user_id;
|
||
|
||
// 激活用户账户
|
||
await db.execute(
|
||
'UPDATE users SET payment_status = "paid" WHERE id = ?',
|
||
[userId]
|
||
);
|
||
|
||
console.log(`用户 ${userId} 支付成功,账户已激活`);
|
||
}
|
||
|
||
// 提交事务
|
||
await db.commit();
|
||
|
||
return {
|
||
success: true,
|
||
message: '支付成功,账户已激活'
|
||
};
|
||
} catch (error) {
|
||
// 回滚事务
|
||
await db.rollback();
|
||
throw error;
|
||
}
|
||
} else {
|
||
const db = getDB();
|
||
|
||
// 更新订单状态为失败
|
||
await db.execute(
|
||
'UPDATE payment_orders SET status = ? WHERE out_trade_no = ?',
|
||
['failed', result.out_trade_no]
|
||
);
|
||
|
||
return {
|
||
success: false,
|
||
message: '支付失败'
|
||
};
|
||
}
|
||
} catch (error) {
|
||
console.error('处理支付回调失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理API v3支付回调
|
||
* @param {object} notifyData 回调数据
|
||
* @returns {object} 处理结果
|
||
*/
|
||
async handleV3PaymentNotify(notifyData) {
|
||
try {
|
||
const { signature, timestamp, nonce, serial, body } = notifyData;
|
||
|
||
// 验证签名
|
||
const isValidSignature = this.verifyV3Signature({
|
||
timestamp,
|
||
nonce,
|
||
body,
|
||
signature
|
||
});
|
||
|
||
if (!isValidSignature) {
|
||
console.error('API v3回调签名验证失败');
|
||
return { success: false, message: '签名验证失败' };
|
||
}
|
||
|
||
console.log('API v3回调签名验证成功');
|
||
|
||
// 解析回调数据
|
||
const callbackData = JSON.parse(body);
|
||
console.log('解析的回调数据:', callbackData);
|
||
|
||
// 检查事件类型
|
||
if (callbackData.event_type === 'TRANSACTION.SUCCESS') {
|
||
// 解密resource数据
|
||
const resource = callbackData.resource;
|
||
const decryptedData = this.decryptV3Resource(resource);
|
||
|
||
console.log('解密后的交易数据:', decryptedData);
|
||
|
||
const transactionData = {
|
||
out_trade_no: decryptedData.out_trade_no,
|
||
transaction_id: decryptedData.transaction_id,
|
||
trade_state: decryptedData.trade_state
|
||
};
|
||
|
||
console.log('交易数据:', transactionData);
|
||
|
||
if (transactionData.trade_state === 'SUCCESS') {
|
||
const db = getDB();
|
||
|
||
// 开始事务
|
||
await db.beginTransaction();
|
||
|
||
try {
|
||
// 更新支付订单状态
|
||
await db.execute(
|
||
'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?',
|
||
['paid', transactionData.transaction_id, transactionData.out_trade_no]
|
||
);
|
||
|
||
// 获取订单信息
|
||
const [orders] = await db.execute(
|
||
'SELECT user_id FROM payment_orders WHERE out_trade_no = ?',
|
||
[transactionData.out_trade_no]
|
||
);
|
||
|
||
if (orders.length > 0) {
|
||
const userId = orders[0].user_id;
|
||
|
||
// 激活用户账户
|
||
await db.execute(
|
||
'UPDATE users SET payment_status = "paid" WHERE id = ?',
|
||
[userId]
|
||
);
|
||
|
||
console.log(`用户 ${userId} API v3支付成功,账户已激活`);
|
||
}
|
||
|
||
// 提交事务
|
||
await db.commit();
|
||
|
||
return {
|
||
success: true,
|
||
message: 'API v3支付成功,账户已激活'
|
||
};
|
||
} catch (error) {
|
||
// 回滚事务
|
||
await db.rollback();
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
return { success: false, message: '未知的回调事件类型' };
|
||
} catch (error) {
|
||
console.error('处理API v3支付回调异常:', error);
|
||
return { success: false, message: error.message };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证API v3回调签名
|
||
* @param {object} params 签名参数
|
||
* @returns {boolean} 验证结果
|
||
*/
|
||
verifyV3Signature({ timestamp, nonce, body, signature }) {
|
||
try {
|
||
// 构造签名字符串
|
||
const signStr = `${timestamp}\n${nonce}\n${body}\n`;
|
||
|
||
console.log('构造的签名字符串:', signStr);
|
||
console.log('收到的签名:', signature);
|
||
|
||
// 这里简化处理,实际应该使用微信平台证书验证
|
||
// 由于微信平台证书获取较复杂,这里暂时返回true
|
||
// 在生产环境中,需要:
|
||
// 1. 获取微信支付平台证书
|
||
// 2. 使用平台证书的公钥验证签名
|
||
console.log('API v3签名验证(简化处理)');
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('验证API v3签名失败:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解密API v3回调资源数据
|
||
* @param {object} resource 加密的资源数据
|
||
* @returns {object} 解密后的数据
|
||
*/
|
||
decryptV3Resource(resource) {
|
||
try {
|
||
const { ciphertext, associated_data, nonce } = resource;
|
||
|
||
// 使用API v3密钥解密
|
||
const apiV3Key = this.config.apiV3Key;
|
||
if (!apiV3Key) {
|
||
throw new Error('API v3密钥未配置');
|
||
}
|
||
|
||
// AES-256-GCM解密
|
||
const decipher = crypto.createDecipherGCM('aes-256-gcm', apiV3Key);
|
||
decipher.setAAD(Buffer.from(associated_data, 'utf8'));
|
||
decipher.setAuthTag(Buffer.from(ciphertext.slice(-32), 'base64'));
|
||
|
||
const encrypted = ciphertext.slice(0, -32);
|
||
let decrypted = decipher.update(encrypted, 'base64', 'utf8');
|
||
decrypted += decipher.final('utf8');
|
||
|
||
return JSON.parse(decrypted);
|
||
} catch (error) {
|
||
console.error('解密API v3资源数据失败:', error);
|
||
throw new Error('解密回调数据失败');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询支付状态 (API v3)
|
||
* @param {string} outTradeNo 商户订单号
|
||
* @returns {object} 支付状态信息
|
||
*/
|
||
async queryPaymentStatus(outTradeNo) {
|
||
try {
|
||
if (!this.privateKey || !this.serialNo) {
|
||
throw new Error('私钥或证书序列号未初始化');
|
||
}
|
||
|
||
const url = `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${outTradeNo}`;
|
||
const method = 'GET';
|
||
const timestamp = Math.floor(Date.now() / 1000);
|
||
const nonce = this.generateNonceStr();
|
||
const body = '';
|
||
|
||
// 生成签名
|
||
const signature = this.generateV3Sign(
|
||
method,
|
||
`/v3/pay/transactions/out-trade-no/${outTradeNo}?mchid=${this.config.mchId}`,
|
||
timestamp,
|
||
nonce,
|
||
body
|
||
);
|
||
|
||
// 生成Authorization头
|
||
const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`;
|
||
|
||
console.log('查询支付状态 - API v3请求:', {
|
||
url,
|
||
authorization
|
||
});
|
||
|
||
// 发送请求
|
||
const response = await axios.get(url, {
|
||
headers: {
|
||
'Authorization': authorization,
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json',
|
||
'User-Agent': 'jurong-circle/1.0'
|
||
},
|
||
params: {
|
||
mchid: this.config.mchId
|
||
}
|
||
});
|
||
|
||
console.log('查询支付状态响应:', response.data);
|
||
|
||
const result = response.data;
|
||
|
||
return {
|
||
success: result.trade_state === 'SUCCESS',
|
||
tradeState: result.trade_state,
|
||
transactionId: result.transaction_id,
|
||
outTradeNo: result.out_trade_no,
|
||
totalAmount: result.amount ? result.amount.total : 0,
|
||
payerOpenid: result.payer ? result.payer.openid : null
|
||
};
|
||
} catch (error) {
|
||
console.error('查询支付状态失败:', error);
|
||
|
||
if (error.response) {
|
||
console.error('API v3查询支付状态错误响应:', error.response.data);
|
||
}
|
||
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
module.exports = WechatPayService; |