商城后端模板
This commit is contained in:
306
services/alipayservice.js
Normal file
306
services/alipayservice.js
Normal file
@@ -0,0 +1,306 @@
|
||||
const { AlipaySdk } = require('alipay-sdk');
|
||||
const { getDB } = require('../database');
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
class AlipayService {
|
||||
constructor() {
|
||||
this.privateKey = null;
|
||||
this.alipayPublicKey = null;
|
||||
this.alipaySdk = null;
|
||||
this.isInitialized = false;
|
||||
|
||||
this.initializeAlipay();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化支付宝服务
|
||||
*/
|
||||
initializeAlipay() {
|
||||
try {
|
||||
// 读取密钥文件
|
||||
const privateKeyPath = this.resolveCertPath('../certs/alipay-private-key.pem');
|
||||
const publicKeyPath = this.resolveCertPath('../certs/alipay-public-key.pem');
|
||||
|
||||
console.log('支付宝私钥路径:', privateKeyPath);
|
||||
console.log('支付宝公钥路径:', publicKeyPath);
|
||||
this.privateKey = fs.readFileSync(privateKeyPath, 'utf8');
|
||||
this.alipayPublicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
||||
this.initializeSDK();
|
||||
|
||||
} catch (error) {
|
||||
console.error('支付宝服务初始化失败:', error.message);
|
||||
console.error('支付宝功能将不可用');
|
||||
// 不抛出错误,允许服务继续运行
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化支付宝SDK
|
||||
*/
|
||||
initializeSDK() {
|
||||
if (!this.privateKey || !this.alipayPublicKey) {
|
||||
console.warn('支付宝密钥未加载,跳过SDK初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
// 支付宝配置
|
||||
this.config = {
|
||||
appId: process.env.ALIPAY_APP_ID || '2021001161683774', // 替换为实际的应用ID
|
||||
privateKey: this.privateKey, // 从文件读取的应用私钥
|
||||
alipayPublicKey: this.alipayPublicKey, // 从文件读取的支付宝公钥
|
||||
gateway: 'https://openapi.alipay.com/gateway.do', // 支付宝网关地址
|
||||
signType: 'RSA2',
|
||||
charset: 'utf-8',
|
||||
version: '1.0',
|
||||
timeout: 5000
|
||||
};
|
||||
|
||||
// 初始化支付宝SDK
|
||||
this.alipaySdk = new AlipaySdk({
|
||||
appId: this.config.appId,
|
||||
privateKey: this.config.privateKey,
|
||||
alipayPublicKey: this.config.alipayPublicKey,
|
||||
gateway: this.config.gateway,
|
||||
signType: this.config.signType,
|
||||
timeout: this.config.timeout
|
||||
});
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('支付宝SDK初始化成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析证书文件路径
|
||||
* @param {string} relativePath - 相对路径
|
||||
* @returns {string} 绝对路径
|
||||
*/
|
||||
resolveCertPath(relativePath) {
|
||||
return path.resolve(__dirname, relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件是否有效
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {boolean} 是否为有效文件
|
||||
*/
|
||||
isValidFile(filePath) {
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
return stats.isFile();
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查支付宝服务是否已初始化
|
||||
* @returns {boolean} 是否已初始化
|
||||
*/
|
||||
isServiceAvailable() {
|
||||
return this.isInitialized && this.alipaySdk !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建注册支付订单
|
||||
* @param {Object} params - 支付参数
|
||||
* @param {string} params.userId - 用户ID
|
||||
* @param {string} params.username - 用户名
|
||||
* @param {string} params.phone - 手机号
|
||||
* @param {string} params.clientIp - 客户端IP
|
||||
* @returns {Promise<Object>} 支付结果
|
||||
*/
|
||||
async createRegistrationPayOrder({ userId, username, phone, clientIp }) {
|
||||
// 检查服务是否可用
|
||||
if (!this.isServiceAvailable()) {
|
||||
throw new Error('支付宝服务未初始化或不可用');
|
||||
}
|
||||
|
||||
try {
|
||||
const db = getDB();
|
||||
|
||||
// 生成订单号
|
||||
const outTradeNo = this.generateOrderNo();
|
||||
const totalFee = 39900; // 399元,单位:分
|
||||
const subject = '用户注册激活费用';
|
||||
const body = `用户${username}(${phone})注册激活费用`;
|
||||
|
||||
// 业务参数
|
||||
const bizContent = {
|
||||
out_trade_no: outTradeNo,
|
||||
total_amount: (totalFee / 100).toFixed(2), // 转换为元
|
||||
subject: subject,
|
||||
body: body,
|
||||
product_code: 'QUICK_WAP_WAY',
|
||||
quit_url: process.env.ALIPAY_QUIT_URL
|
||||
};
|
||||
|
||||
// 使用新版SDK的pageExecute方法生成支付URL
|
||||
const payUrl = this.alipaySdk.pageExecute('alipay.trade.wap.pay', 'GET', {
|
||||
bizContent: bizContent,
|
||||
notifyUrl: process.env.ALIPAY_NOTIFY_URL,
|
||||
returnUrl: process.env.ALIPAY_RETURN_URL
|
||||
});
|
||||
|
||||
// 保存订单到数据库
|
||||
await db.execute(
|
||||
`INSERT INTO payment_orders
|
||||
(user_id, out_trade_no, total_fee, body, trade_type, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW())`,
|
||||
[userId, outTradeNo, totalFee, body, 'ALIPAY_WAP', 'pending']
|
||||
);
|
||||
|
||||
console.log('支付宝支付订单创建成功:', {
|
||||
userId,
|
||||
outTradeNo,
|
||||
totalFee,
|
||||
payUrl
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
outTradeNo,
|
||||
payUrl,
|
||||
paymentType: 'alipay_wap',
|
||||
totalFee
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('创建支付宝支付订单失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '创建支付订单失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付状态
|
||||
* @param {string} outTradeNo - 商户订单号
|
||||
* @returns {Promise<Object>} 查询结果
|
||||
*/
|
||||
async queryPaymentStatus(outTradeNo) {
|
||||
// 检查服务是否可用
|
||||
if (!this.isServiceAvailable()) {
|
||||
throw new Error('支付宝服务未初始化或不可用');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.alipaySdk.exec('alipay.trade.query', {
|
||||
bizContent: {
|
||||
out_trade_no: outTradeNo
|
||||
}
|
||||
});
|
||||
|
||||
if (result.code === '10000') {
|
||||
// 查询成功
|
||||
const tradeStatus = result.tradeStatus;
|
||||
|
||||
// 如果支付成功,更新数据库
|
||||
if (tradeStatus === 'TRADE_SUCCESS') {
|
||||
await this.updatePaymentStatus(outTradeNo, {
|
||||
status: 'paid',
|
||||
transactionId: result.tradeNo,
|
||||
paidAt: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
trade_status: tradeStatus,
|
||||
trade_no: result.tradeNo,
|
||||
total_amount: result.totalAmount,
|
||||
buyer_pay_amount: result.buyerPayAmount,
|
||||
gmt_payment: result.gmtPayment
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: result.msg || '查询支付状态失败'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询支付宝支付状态失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '查询支付状态失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支付状态
|
||||
* @param {string} outTradeNo - 商户订单号
|
||||
* @param {Object} updateData - 更新数据
|
||||
*/
|
||||
async updatePaymentStatus(outTradeNo, updateData) {
|
||||
try {
|
||||
const db = getDB();
|
||||
|
||||
// 更新订单状态
|
||||
await db.execute(
|
||||
`UPDATE payment_orders
|
||||
SET status = ?, transaction_id = ?, paid_at = ?
|
||||
WHERE out_trade_no = ?`,
|
||||
[updateData.status, updateData.transactionId, updateData.paidAt, outTradeNo]
|
||||
);
|
||||
|
||||
// 如果支付成功,更新用户支付状态
|
||||
if (updateData.status === 'paid') {
|
||||
const [orders] = await db.execute(
|
||||
'SELECT user_id FROM payment_orders WHERE out_trade_no = ?',
|
||||
[outTradeNo]
|
||||
);
|
||||
|
||||
if (orders.length > 0) {
|
||||
const userId = orders[0].user_id;
|
||||
await db.execute(
|
||||
'UPDATE users SET payment_status = ? WHERE id = ?',
|
||||
['paid', userId]
|
||||
);
|
||||
|
||||
console.log('用户支付状态更新成功:', { userId, outTradeNo });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新支付状态失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证支付宝回调签名
|
||||
* @param {Object} params - 回调参数
|
||||
* @returns {boolean} 验证结果
|
||||
*/
|
||||
verifyNotifySign(params) {
|
||||
// 检查服务是否可用
|
||||
if (!this.isServiceAvailable()) {
|
||||
console.error('支付宝服务未初始化,无法验证签名');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return this.alipaySdk.checkNotifySign(params);
|
||||
} catch (error) {
|
||||
console.error('验证支付宝回调签名失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成订单号
|
||||
* @returns {string} 订单号
|
||||
*/
|
||||
generateOrderNo() {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
|
||||
return `ALI${timestamp}${random}`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AlipayService;
|
||||
1572
services/matchingService.js
Normal file
1572
services/matchingService.js
Normal file
File diff suppressed because it is too large
Load Diff
293
services/minioService.js
Normal file
293
services/minioService.js
Normal file
@@ -0,0 +1,293 @@
|
||||
const { createMinioClient, minioConfig, getPublicUrl } = require('../config/minio');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* MinIO 文件服务
|
||||
* 提供文件上传、删除、获取等功能
|
||||
*/
|
||||
class MinioService {
|
||||
constructor() {
|
||||
this.client = createMinioClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一文件名
|
||||
* @param {string} originalName - 原始文件名
|
||||
* @returns {string} 唯一文件名
|
||||
*/
|
||||
generateUniqueFileName(originalName) {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const timestamp = Date.now();
|
||||
const randomString = crypto.randomBytes(8).toString('hex');
|
||||
const ext = path.extname(originalName);
|
||||
return `${year}/${month}/${day}/${timestamp}_${randomString}${ext}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件类型获取存储桶名称
|
||||
* @param {string} type - 文件类型 (avatar, product, document)
|
||||
* @returns {string} 存储桶名称
|
||||
*/
|
||||
getBucketName(type = 'document') {
|
||||
const bucketMap = {
|
||||
'avatar': minioConfig.buckets.avatars,
|
||||
'product': minioConfig.buckets.products,
|
||||
'document': minioConfig.buckets.documents
|
||||
};
|
||||
return bucketMap[type] || minioConfig.buckets.documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传单个文件
|
||||
* @param {Buffer} fileBuffer - 文件缓冲区
|
||||
* @param {string} originalName - 原始文件名
|
||||
* @param {string} mimeType - 文件MIME类型
|
||||
* @param {string} type - 文件类型
|
||||
* @returns {Promise<Object>} 上传结果
|
||||
*/
|
||||
async uploadFile(fileBuffer, originalName, mimeType, type = 'document') {
|
||||
try {
|
||||
const bucketName = this.getBucketName(type);
|
||||
const fileName = this.generateUniqueFileName(originalName);
|
||||
|
||||
// 设置文件元数据
|
||||
const metaData = {
|
||||
'Content-Type': mimeType,
|
||||
'Original-Name': encodeURIComponent(originalName),
|
||||
'Upload-Time': new Date().toISOString()
|
||||
};
|
||||
|
||||
// 上传文件到MinIO
|
||||
await this.client.putObject(bucketName, fileName, fileBuffer, fileBuffer.length, metaData);
|
||||
|
||||
// 生成访问URL
|
||||
const url = getPublicUrl(bucketName, fileName);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
filename: fileName,
|
||||
originalname: originalName,
|
||||
mimetype: mimeType,
|
||||
size: fileBuffer.length,
|
||||
bucket: bucketName,
|
||||
path: `${bucketName}/${fileName}`,
|
||||
url: url
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('MinIO文件上传失败:', error);
|
||||
throw new Error(`文件上传失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移专用:上传文件到指定存储桶和路径
|
||||
* @param {string} bucketName - 存储桶名称
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {Buffer} fileBuffer - 文件缓冲区
|
||||
* @param {string} mimeType - 文件MIME类型
|
||||
* @returns {Promise<Object>} 上传结果
|
||||
*/
|
||||
async uploadFileForMigration(bucketName, filePath, fileBuffer, mimeType) {
|
||||
try {
|
||||
// 设置文件元数据
|
||||
const metaData = {
|
||||
'Content-Type': mimeType,
|
||||
'Upload-Time': new Date().toISOString()
|
||||
};
|
||||
|
||||
// 上传文件到MinIO
|
||||
await this.client.putObject(bucketName, filePath, fileBuffer, fileBuffer.length, metaData);
|
||||
|
||||
// 生成访问URL
|
||||
const url = getPublicUrl(bucketName, filePath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
filename: filePath,
|
||||
mimetype: mimeType,
|
||||
size: fileBuffer.length,
|
||||
bucket: bucketName,
|
||||
path: `${bucketName}/${filePath}`,
|
||||
url: url
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('MinIO文件迁移上传失败:', error);
|
||||
throw new Error(`文件迁移上传失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传多个文件
|
||||
* @param {Array} files - 文件数组,每个文件包含 {buffer, originalName, mimeType}
|
||||
* @param {string} type - 文件类型
|
||||
* @returns {Promise<Array>} 上传结果数组
|
||||
*/
|
||||
async uploadMultipleFiles(files, type = 'document') {
|
||||
try {
|
||||
const uploadPromises = files.map(file =>
|
||||
this.uploadFile(file.buffer, file.originalName, file.mimeType, type)
|
||||
);
|
||||
|
||||
const results = await Promise.all(uploadPromises);
|
||||
const uploadedFiles = results.map(result => result.data);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
files: uploadedFiles,
|
||||
urls: uploadedFiles.map(file => file.url),
|
||||
count: uploadedFiles.length
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('MinIO多文件上传失败:', error);
|
||||
throw new Error(`多文件上传失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {string} bucketName - 存储桶名称
|
||||
* @param {string} fileName - 文件名
|
||||
* @returns {Promise<boolean>} 删除结果
|
||||
*/
|
||||
async deleteFile(bucketName, fileName) {
|
||||
try {
|
||||
await this.client.removeObject(bucketName, fileName);
|
||||
console.log(`✅ 文件删除成功: ${bucketName}/${fileName}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('MinIO文件删除失败:', error);
|
||||
throw new Error(`文件删除失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除文件
|
||||
* @param {string} bucketName - 存储桶名称
|
||||
* @param {Array<string>} fileNames - 文件名数组
|
||||
* @returns {Promise<Object>} 删除结果
|
||||
*/
|
||||
async deleteMultipleFiles(bucketName, fileNames) {
|
||||
try {
|
||||
const deletePromises = fileNames.map(fileName =>
|
||||
this.deleteFile(bucketName, fileName)
|
||||
);
|
||||
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
deletedCount: fileNames.length,
|
||||
message: `成功删除${fileNames.length}个文件`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('MinIO批量删除失败:', error);
|
||||
throw new Error(`批量删除失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否存在
|
||||
* @param {string} bucketName - 存储桶名称
|
||||
* @param {string} fileName - 文件名
|
||||
* @returns {Promise<boolean>} 文件是否存在
|
||||
*/
|
||||
async fileExists(bucketName, fileName) {
|
||||
try {
|
||||
await this.client.statObject(bucketName, fileName);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.code === 'NotFound') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件信息
|
||||
* @param {string} bucketName - 存储桶名称
|
||||
* @param {string} fileName - 文件名
|
||||
* @returns {Promise<Object>} 文件信息
|
||||
*/
|
||||
async getFileInfo(bucketName, fileName) {
|
||||
try {
|
||||
const stat = await this.client.statObject(bucketName, fileName);
|
||||
return {
|
||||
size: stat.size,
|
||||
lastModified: stat.lastModified,
|
||||
etag: stat.etag,
|
||||
contentType: stat.metaData['content-type'],
|
||||
originalName: decodeURIComponent(stat.metaData['original-name'] || fileName)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取文件信息失败:', error);
|
||||
throw new Error(`获取文件信息失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成预签名URL(用于临时访问)
|
||||
* @param {string} bucketName - 存储桶名称
|
||||
* @param {string} fileName - 文件名
|
||||
* @param {number} expiry - 过期时间(秒),默认7天
|
||||
* @returns {Promise<string>} 预签名URL
|
||||
*/
|
||||
async getPresignedUrl(bucketName, fileName, expiry = 7 * 24 * 60 * 60) {
|
||||
try {
|
||||
const url = await this.client.presignedGetObject(bucketName, fileName, expiry);
|
||||
return url;
|
||||
} catch (error) {
|
||||
console.error('生成预签名URL失败:', error);
|
||||
throw new Error(`生成预签名URL失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出存储桶中的文件
|
||||
* @param {string} bucketName - 存储桶名称
|
||||
* @param {string} prefix - 文件前缀
|
||||
* @param {number} limit - 限制数量
|
||||
* @returns {Promise<Array>} 文件列表
|
||||
*/
|
||||
async listFiles(bucketName, prefix = '', limit = 100) {
|
||||
try {
|
||||
const files = [];
|
||||
const stream = this.client.listObjects(bucketName, prefix, true);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', (obj) => {
|
||||
if (files.length < limit) {
|
||||
files.push({
|
||||
name: obj.name,
|
||||
size: obj.size,
|
||||
lastModified: obj.lastModified,
|
||||
etag: obj.etag,
|
||||
url: getPublicUrl(bucketName, obj.name)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', () => resolve(files));
|
||||
stream.on('error', reject);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('列出文件失败:', error);
|
||||
throw new Error(`列出文件失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const minioService = new MinioService();
|
||||
|
||||
module.exports = minioService;
|
||||
380
services/timeoutService.js
Normal file
380
services/timeoutService.js
Normal file
@@ -0,0 +1,380 @@
|
||||
const { getDB } = require('../database');
|
||||
const { logger, auditLogger } = require('../config/logger');
|
||||
|
||||
class TimeoutService {
|
||||
/**
|
||||
* 检查转账超时情况
|
||||
* 标记超时转账和风险用户,自动取消超过2.5小时的pending转账
|
||||
*/
|
||||
async checkTransferTimeouts() {
|
||||
const db = getDB();
|
||||
|
||||
try {
|
||||
// 只在调试模式下输出开始检查的日志
|
||||
// console.log('开始检查转账超时情况...');
|
||||
|
||||
// 1. 查找所有超时的转账记录(有deadline_at的)
|
||||
const [overdueTransfers] = await db.execute(
|
||||
`SELECT t.*, u.username, u.real_name
|
||||
FROM transfers t
|
||||
JOIN users u ON t.from_user_id = u.id
|
||||
WHERE t.status = 'pending'
|
||||
AND t.deadline_at IS NOT NULL
|
||||
AND t.deadline_at < NOW()
|
||||
AND t.is_overdue = 0`
|
||||
);
|
||||
|
||||
// 2. 查找所有pending状态超过2.5小时的转账记录
|
||||
const [longPendingTransfers] = await db.execute(
|
||||
`SELECT t.*, u.username, u.real_name
|
||||
FROM transfers t
|
||||
JOIN users u ON t.from_user_id = u.id
|
||||
WHERE t.status = 'pending'
|
||||
AND t.created_at < DATE_SUB(NOW(), INTERVAL 150 MINUTE)`
|
||||
);
|
||||
|
||||
let hasWork = false;
|
||||
|
||||
// 处理有deadline的超时转账
|
||||
if (overdueTransfers.length > 0) {
|
||||
hasWork = true;
|
||||
console.log(`⚠️ 发现 ${overdueTransfers.length} 笔超时转账,开始处理...`);
|
||||
|
||||
for (const transfer of overdueTransfers) {
|
||||
await this.handleOverdueTransfer(transfer);
|
||||
}
|
||||
}
|
||||
// 处理超过2.5小时的pending转账
|
||||
if (longPendingTransfers.length > 0) {
|
||||
hasWork = true;
|
||||
console.log(`⚠️ 发现 ${longPendingTransfers.length} 笔超过2.5小时的pending转账,开始自动取消...`);
|
||||
|
||||
for (const transfer of longPendingTransfers) {
|
||||
await this.handleLongPendingTransfer(transfer);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWork) {
|
||||
console.log('✅ 转账超时检查完成');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查转账超时失败:', error);
|
||||
logger.error('Transfer timeout check failed', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理超时转账
|
||||
* @param {Object} transfer - 转账记录
|
||||
*/
|
||||
async handleOverdueTransfer(transfer) {
|
||||
const db = getDB();
|
||||
|
||||
try {
|
||||
await db.query('START TRANSACTION');
|
||||
|
||||
// 标记转账为超时和坏账
|
||||
await db.execute(
|
||||
'UPDATE transfers SET is_overdue = 1, is_bad_debt = 1, overdue_at = NOW() WHERE id = ?',
|
||||
[transfer.id]
|
||||
);
|
||||
|
||||
// 标记用户为风险用户
|
||||
await db.execute(
|
||||
`UPDATE users SET
|
||||
is_risk_user = 1,
|
||||
risk_reason = CONCAT(IFNULL(risk_reason, ''), '转账超时(转账ID: ', ?, ', 金额: ', ?, '元, 超时时间: ', NOW(), '); ')
|
||||
WHERE id = ?`,
|
||||
[transfer.id, transfer.amount, transfer.from_user_id]
|
||||
);
|
||||
|
||||
await db.query('COMMIT');
|
||||
|
||||
// 记录审计日志
|
||||
auditLogger.info('Transfer marked as overdue and bad debt, user marked as risk', {
|
||||
transferId: transfer.id,
|
||||
userId: transfer.from_user_id,
|
||||
username: transfer.username,
|
||||
amount: transfer.amount,
|
||||
deadlineAt: transfer.deadline_at
|
||||
});
|
||||
|
||||
console.log(`转账 ${transfer.id} 已标记为超时和坏账,用户 ${transfer.username}(ID: ${transfer.from_user_id}) 已标记为风险用户`);
|
||||
|
||||
} catch (error) {
|
||||
await db.query('ROLLBACK');
|
||||
console.error(`处理超时转账 ${transfer.id} 失败:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理超过2.5小时的pending转账
|
||||
* @param {Object} transfer - 转账记录
|
||||
*/
|
||||
async handleLongPendingTransfer(transfer) {
|
||||
const db = getDB();
|
||||
|
||||
try {
|
||||
await db.query('START TRANSACTION');
|
||||
|
||||
// 将转账状态改为cancelled
|
||||
await db.execute(
|
||||
'UPDATE transfers SET status = "cancelled", updated_at = NOW() WHERE id = ?',
|
||||
[transfer.id]
|
||||
);
|
||||
|
||||
// 如果有关联的matching_order_id,检查并更新matching_orders状态
|
||||
if (transfer.matching_order_id) {
|
||||
// 检查该matching_order下是否还有非cancelled状态的transfers
|
||||
const [remainingTransfers] = await db.execute(
|
||||
'SELECT COUNT(*) as count FROM transfers WHERE matching_order_id = ? AND status != "cancelled"',
|
||||
[transfer.matching_order_id]
|
||||
);
|
||||
|
||||
// 如果所有关联的transfers都是cancelled状态,则更新matching_order状态为cancelled
|
||||
if (remainingTransfers[0].count === 0) {
|
||||
await db.execute(
|
||||
'UPDATE matching_orders SET status = "cancelled", updated_at = NOW() WHERE id = ?',
|
||||
[transfer.matching_order_id]
|
||||
);
|
||||
|
||||
console.log(`匹配订单 ${transfer.matching_order_id} 的所有转账都已取消,订单状态已更新为cancelled`);
|
||||
}
|
||||
}
|
||||
|
||||
await db.query('COMMIT');
|
||||
|
||||
// 记录审计日志
|
||||
auditLogger.info('Long pending transfer auto-cancelled', {
|
||||
transferId: transfer.id,
|
||||
userId: transfer.from_user_id,
|
||||
username: transfer.username,
|
||||
amount: transfer.amount,
|
||||
createdAt: transfer.created_at,
|
||||
matchingOrderId: transfer.matching_order_id
|
||||
});
|
||||
|
||||
console.log(`转账 ${transfer.id} 超过2.5小时未处理,已自动取消 (用户: ${transfer.username}, 金额: ${transfer.amount}元)`);
|
||||
|
||||
} catch (error) {
|
||||
await db.query('ROLLBACK');
|
||||
console.error(`处理长时间pending转账 ${transfer.id} 失败:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取风险用户列表
|
||||
* @param {Object} filters - 筛选条件
|
||||
* @param {Object} pagination - 分页参数
|
||||
* @returns {Object} 风险用户列表和分页信息
|
||||
*/
|
||||
async getRiskUsers(filters = {}, pagination = {}) {
|
||||
const db = getDB();
|
||||
const { page = 1, limit = 10 } = pagination;
|
||||
const pageNum = parseInt(page, 10) || 1;
|
||||
const limitNum = parseInt(limit, 10) || 10;
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
let whereClause = 'WHERE is_risk_user = 1';
|
||||
const params = [];
|
||||
|
||||
// 构建查询条件
|
||||
if (filters.is_blacklisted !== undefined) {
|
||||
whereClause += ' AND is_blacklisted = ?';
|
||||
params.push(filters.is_blacklisted);
|
||||
}
|
||||
|
||||
if (filters.username) {
|
||||
whereClause += ' AND username LIKE ?';
|
||||
params.push(`%${filters.username}%`);
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取总数
|
||||
const [countResult] = await db.execute(
|
||||
`SELECT COUNT(*) as total FROM users ${whereClause}`,
|
||||
params
|
||||
);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 获取数据
|
||||
const [users] = await db.execute(
|
||||
`SELECT id, username, real_name, is_risk_user, is_blacklisted,
|
||||
risk_reason, blacklist_reason, blacklisted_at, created_at,phone
|
||||
FROM users
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ${limitNum} OFFSET ${offset}`,
|
||||
params
|
||||
);
|
||||
|
||||
return {
|
||||
users,
|
||||
pagination: {
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
total,
|
||||
pages: Math.ceil(total / limitNum)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Failed to get risk users', { error: error.message, filters });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉黑用户
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {string} reason - 拉黑原因
|
||||
* @param {number} operatorId - 操作员ID
|
||||
*/
|
||||
async blacklistUser(userId, reason, operatorId) {
|
||||
const db = getDB();
|
||||
|
||||
try {
|
||||
// 检查用户是否存在
|
||||
const [users] = await db.execute(
|
||||
'SELECT id, username, phone FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
|
||||
if (user.is_blacklisted) {
|
||||
throw new Error('用户已被拉黑');
|
||||
}
|
||||
|
||||
// 拉黑用户
|
||||
await db.execute(
|
||||
`UPDATE users SET
|
||||
is_blacklisted = 1,
|
||||
blacklist_reason = ?,
|
||||
blacklisted_at = NOW()
|
||||
WHERE id = ?`,
|
||||
[reason, userId]
|
||||
);
|
||||
|
||||
// 记录审计日志
|
||||
auditLogger.info('User blacklisted', {
|
||||
userId,
|
||||
username: user.username,
|
||||
reason,
|
||||
operatorId
|
||||
});
|
||||
|
||||
logger.info('User blacklisted successfully', { userId, operatorId });
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to blacklist user', {
|
||||
error: error.message,
|
||||
userId,
|
||||
operatorId
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除拉黑
|
||||
* @param {number} userId - 用户ID
|
||||
* @param {number} operatorId - 操作员ID
|
||||
*/
|
||||
async unblacklistUser(userId, operatorId) {
|
||||
const db = getDB();
|
||||
|
||||
try {
|
||||
// 检查用户是否存在
|
||||
const [users] = await db.execute(
|
||||
'SELECT id, username, is_blacklisted FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
|
||||
if (!user.is_blacklisted) {
|
||||
throw new Error('用户未被拉黑');
|
||||
}
|
||||
|
||||
// 解除拉黑
|
||||
await db.execute(
|
||||
`UPDATE users SET
|
||||
is_blacklisted = 0,
|
||||
blacklist_reason = NULL,
|
||||
blacklisted_at = NULL
|
||||
WHERE id = ?`,
|
||||
[userId]
|
||||
);
|
||||
|
||||
// 记录审计日志
|
||||
auditLogger.info('User unblacklisted', {
|
||||
userId,
|
||||
username: user.username,
|
||||
operatorId
|
||||
});
|
||||
|
||||
logger.info('User unblacklisted successfully', { userId, operatorId });
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to unblacklist user', {
|
||||
error: error.message,
|
||||
userId,
|
||||
operatorId
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否被拉黑
|
||||
* @param {number} userId - 用户ID
|
||||
* @returns {boolean} 是否被拉黑
|
||||
*/
|
||||
async isUserBlacklisted(userId) {
|
||||
const db = getDB();
|
||||
|
||||
try {
|
||||
const [users] = await db.execute(
|
||||
'SELECT is_blacklisted FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
return users.length > 0 && users[0].is_blacklisted === 1;
|
||||
} catch (error) {
|
||||
logger.error('Failed to check user blacklist status', {
|
||||
error: error.message,
|
||||
userId
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定时检查任务
|
||||
* 每5分钟检查一次转账超时情况
|
||||
*/
|
||||
startTimeoutChecker() {
|
||||
console.log('启动转账超时检查定时任务...');
|
||||
|
||||
// 立即执行一次
|
||||
this.checkTransferTimeouts();
|
||||
|
||||
// 每5分钟执行一次
|
||||
setInterval(() => {
|
||||
this.checkTransferTimeouts();
|
||||
}, 5 * 1000); // 5秒
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TimeoutService();
|
||||
1192
services/transferService.js
Normal file
1192
services/transferService.js
Normal file
File diff suppressed because it is too large
Load Diff
609
services/wechatPayService.js
Normal file
609
services/wechatPayService.js
Normal file
@@ -0,0 +1,609 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user