初次提交

This commit is contained in:
2025-09-25 11:01:00 +08:00
commit aee6cca805
28 changed files with 6971 additions and 0 deletions

68
.env Normal file
View File

@@ -0,0 +1,68 @@
# 数据库配置
DB_HOST=114.55.111.44
DB_USER=maov2
DB_PASSWORD=5fYhw8z6T62b7heS
DB_NAME=maov2
# JWT密钥
JWT_SECRET=NINGBOJURONGkejiyouxiangongsi202
# 阿里云短信服务配置
# 请在阿里云控制台获取以下配置信息:
# 1. AccessKey ID 和 AccessKey Secret在阿里云控制台 -> AccessKey管理中创建
# 2. 短信签名:在阿里云短信服务控制台中申请并审核通过的签名
# 3. 短信模板CODE在阿里云短信服务控制台中申请并审核通过的模板CODE
ALIYUN_ACCESS_KEY_ID=LTAI5tBHymRUu1vvo5tgYpaa
ALIYUN_ACCESS_KEY_SECRET=lNsDZvpUVX2b3pfBQCBawOEyr3dNB9
ALIYUN_SMS_SIGN_NAME=宁波炬融歆创科技
ALIYUN_SMS_TEMPLATE_CODE=SMS_324470054
# 环境配置
NODE_ENV=development
PORT=3005
# 前端地址配置
FRONTEND_URL=https://www.zrbjr.com/frontend
# FRONTEND_URL=http://114.55.111.44:3001/frontend
# MinIO 对象存储配置
# MinIO服务器地址不包含协议
MINIO_ENDPOINT=114.55.111.44
# MinIO服务器端口
MINIO_PORT=9000
# 是否使用SSLtrue/false
MINIO_USE_SSL=false
# MinIO访问密钥
MINIO_ACCESS_KEY=minio
# MinIO秘密密钥
MINIO_SECRET_KEY=CNy6fMCfyfeaEjbE
# MinIO公开访问地址用于生成文件URL
MINIO_PUBLIC_URL=https://minio.zrbjr.com
# MinIO存储桶配置
MINIO_BUCKET_UPLOADS=jurongquan
MINIO_BUCKET_AVATARS=jurongquan
MINIO_BUCKET_PRODUCTS=jurongquan
MINIO_BUCKET_DOCUMENTS=jurongquan
#支付配置
WECHAT_APP_ID=wx3a702dbe13fd2217
WECHAT_MCH_ID=1726377336
WECHAT_API_KEY=NINGBOJURONGkejiyouxiangongsi202
WECHAT_API_V3_KEY=NINGBOJURONGkejiyouxiangongsi202
WECHAT_CERT_PATH=./cert/apiclient_cert.pem
WECHAT_KEY_PATH=./cert/apiclient_key.pem
WECHAT_NOTIFY_URL=https://www.zrbjr.com/api/wechat-pay/notify
# 支付宝配置
# 请在支付宝开放平台获取以下配置信息:
# 1. 应用ID在支付宝开放平台创建应用后获得
# 2. 应用私钥和支付宝公钥现在从文件读取
ALIPAY_APP_ID=2021005188682022
ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify
ALIPAY_RETURN_URL=https://www.zrbjr.com/payment
ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/
#ALIPAY_APP_ID=9021000151699946
#ALIPAY_NOTIFY_URL=https://test.zrbjr.com/api/payment/alipay/notify
#ALIPAY_RETURN_URL=http://192.168.1.124:5173/frontend/payment
#ALIPAY_QUIT_URL=http://192.168.1.124:5173/frontend/payment

64
.env.example Normal file
View File

@@ -0,0 +1,64 @@
# 数据库配置
DB_HOST=114.55.111.44
DB_USER=maov2
DB_PASSWORD=5fYhw8z6T62b7heS
DB_NAME=maov2
# JWT密钥
JWT_SECRET=your_jwt_secret_key
# 阿里云短信服务配置
# 请在阿里云控制台获取以下配置信息:
# 1. AccessKey ID 和 AccessKey Secret在阿里云控制台 -> AccessKey管理中创建
# 2. 短信签名:在阿里云短信服务控制台中申请并审核通过的签名
# 3. 短信模板CODE在阿里云短信服务控制台中申请并审核通过的模板CODE
ALIYUN_ACCESS_KEY_ID=LTAI5tBHymRUu1vvo5tgYpaa
ALIYUN_ACCESS_KEY_SECRET=lNsDZvpUVX2b3pfBQCBawOEyr3dNB9
ALIYUN_SMS_SIGN_NAME=宁波炬融歆创科技
ALIYUN_SMS_TEMPLATE_CODE=SMS_324470054
# 环境配置
NODE_ENV=development
PORT=3000
# 前端地址配置
FRONTEND_URL=https://www.zrbjr.com/frontend
# FRONTEND_URL=http://114.55.111.44:3001/frontend
# MinIO 对象存储配置
# MinIO服务器地址不包含协议
MINIO_ENDPOINT=114.55.111.44
# MinIO服务器端口
MINIO_PORT=9000
# 是否使用SSLtrue/false
MINIO_USE_SSL=false
# MinIO访问密钥
MINIO_ACCESS_KEY=minio
# MinIO秘密密钥
MINIO_SECRET_KEY=CNy6fMCfyfeaEjbE
# MinIO公开访问地址用于生成文件URL
MINIO_PUBLIC_URL=https://minio.zrbjr.com
# MinIO存储桶配置
MINIO_BUCKET_UPLOADS=jurongquan
MINIO_BUCKET_AVATARS=jurongquan
MINIO_BUCKET_PRODUCTS=jurongquan
MINIO_BUCKET_DOCUMENTS=jurongquan
#支付配置
WECHAT_APP_ID=wx3a702dbe13fd2217
WECHAT_MCH_ID=1726377336
WECHAT_API_KEY=NINGBOJURONGkejiyouxiangongsi202
WECHAT_API_V3_KEY=NINGBOJURONGkejiyouxiangongsi202
WECHAT_CERT_PATH=./cert/apiclient_cert.pem
WECHAT_KEY_PATH=./cert/apiclient_key.pem
WECHAT_NOTIFY_URL=https://www.zrbjr.com/api/wechat-pay/notify
# 支付宝配置
# 请在支付宝开放平台获取以下配置信息:
# 1. 应用ID在支付宝开放平台创建应用后获得
# 2. 应用私钥和支付宝公钥现在从文件读取
ALIPAY_APP_ID=2021005188682022
ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify
ALIPAY_RETURN_URL=https://www.zrbjr.com/payment/success
ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/cancel

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/node_modules
/.idea

17
config/config.js Normal file
View File

@@ -0,0 +1,17 @@
const mysql = require('mysql2')
const sql = {
createConnection() {
return mysql.createPool({
connectionLimit: 10,
host: '114.55.111.44',
user: 'test_mao',
password: 'nK2mPbWriBp25BRd',
database: 'test_mao',
charset: 'utf8mb4',
multipleStatements: true
})
}
}
module.exports = sql

70
config/constants.js Normal file
View File

@@ -0,0 +1,70 @@
// 系统常量配置
module.exports = {
// 转账类型
TRANSFER_TYPES: {
USER_TO_USER: 'user_to_user',
SYSTEM_TO_USER: 'system_to_user',
USER_TO_SYSTEM: 'user_to_system'
},
// 转账状态
TRANSFER_STATUS: {
PENDING: 'pending',
CONFIRMED: 'confirmed',
RECEIVED: 'received',
REJECTED: 'rejected',
CANCELLED: 'cancelled',
NOT_RECEIVED: 'not_received',
FAILED: 'failed'
},
// 用户角色
USER_ROLES: {
ADMIN: 'admin',
USER: 'user'
},
// 订单状态
ORDER_STATUS: {
PENDING: 'pending',
PAID: 'paid',
SHIPPED: 'shipped',
DELIVERED: 'delivered',
CANCELLED: 'cancelled'
},
// 错误代码
ERROR_CODES: {
VALIDATION_ERROR: 'VALIDATION_ERROR',
AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR',
AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR',
NOT_FOUND: 'NOT_FOUND',
DUPLICATE_ENTRY: 'DUPLICATE_ENTRY',
DATABASE_ERROR: 'DATABASE_ERROR',
INTERNAL_ERROR: 'INTERNAL_ERROR'
},
// HTTP状态码
HTTP_STATUS: {
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
CONFLICT: 409,
INTERNAL_SERVER_ERROR: 500
},
// 分页默认值
PAGINATION: {
DEFAULT_PAGE: 1,
DEFAULT_LIMIT: 10,
MAX_LIMIT: 100
},
// JWT配置
JWT: {
EXPIRES_IN: '24h'
}
};

26
config/database-init.js Normal file
View File

@@ -0,0 +1,26 @@
const { initDB, } = require('../database');
/**
* 数据库初始化函数
* 创建所有必要的表结构和初始数据
*/
async function initDatabase() {
try {
// 初始化数据库连接池
await initDB();
console.log('数据库连接池初始化成功');
} catch (error) {
console.error('数据库初始化失败:', error);
throw error;
}
}
module.exports = {
initDatabase
};

363
config/dbv2.js Normal file
View File

@@ -0,0 +1,363 @@
class QueryBuilder {
constructor() {
this.conditions = {};
this.limit = null;
this.offset = null;
this.groupBy = null;
}
where(condition, ...params) {
this.conditions[condition] = params;
return this;
}
setLimit(limit) {
this.limit = limit;
return this;
}
setOffset(offset) {
this.offset = offset;
return this;
}
setGroupBy(groupBy) {
this.groupBy = groupBy;
return this;
}
sqdata(sql, params) {
return new Promise((resolve, reject) => {
global.sqlReq.query(sql, params, (err, result) => {
if (err) {
reject(err);
}
resolve(result);
});
});
}
getParams() {
return Object.values(this.conditions).flat();
}
buildConditions() {
return Object.keys(this.conditions).map(condition => `${condition}`).join(' AND ');
}
}
class SelectBuilder extends QueryBuilder {
constructor() {
super();
this.selectFields = [];
this.tables = [];
this.orderByField = '';
this.orderByDirection = 'ASC';
this.subQueries = []; // 用于存储子查询
this.unions = []; // 存储UNION查询
}
// 添加UNION查询
union(queryBuilder, type = 'UNION') {
this.unions.push({queryBuilder, type});
return this;
}
// 添加UNION ALL查询
unionAll(queryBuilder) {
this.union(queryBuilder, 'UNION ALL');
return this;
}
// 构建主查询部分不含ORDER BY/LIMIT/OFFSET
buildMainQuery() {
const subQuerySQL = this.subQueries.map(({alias, subQuery}) => `(${subQuery}) AS ${alias}`);
const selectClause = this.selectFields.concat(subQuerySQL).join(', ');
let sql = `SELECT ${selectClause}
FROM ${this.tables.join(' ')}`;
const conditionClauses = this.buildConditions();
if (conditionClauses) {
sql += ` WHERE ${conditionClauses}`;
}
if (this.groupBy) {
sql += ` GROUP BY ${this.groupBy}`;
}
const params = this.getParams();
return {sql, params};
}
// 供UNION查询调用的构建方法
buildForUnion() {
return this.buildMainQuery();
}
select(fields) {
this.selectFields = fields.split(',').map(field => field.trim());
return this;
}
// 添加子查询
addSubQuery(alias, subQuery) {
this.subQueries.push({alias, subQuery});
return this;
}
whereLike(fields, keyword) {
const likeConditions = fields.map(field => `${field} LIKE ?`).join(' OR ');
this.conditions[likeConditions] = fields.map(() => `%${keyword}%`);
return this;
}
from(table) {
this.tables.push(table);
return this;
}
leftJoin(table, condition) {
this.tables.push(`LEFT JOIN ${table} ON ${condition}`);
return this;
}
orderBy(field, direction = 'ASC') {
this.orderByField = field;
this.orderByDirection = direction.toUpperCase();
return this;
}
paginate(page, pageSize) {
if (page <= 0 || pageSize <= 0) {
throw new Error('分页参数必须大于0');
}
this.limit = pageSize;
this.offset = (page - 1) * pageSize;
return this;
}
async chidBuild() {
let sql = `SELECT ${this.selectFields.join(', ')}
FROM ${this.tables.join(' ')}`;
let conditionClauses = this.buildConditions();
if (conditionClauses) {
sql += ` WHERE ${conditionClauses}`;
}
if (this.orderByField) {
sql += ` ORDER BY ${this.orderByField} ${this.orderByDirection}`;
}
if (this.limit !== null) {
sql += ` LIMIT ${this.limit}`;
}
if (this.offset !== null) {
sql += ` OFFSET ${this.offset}`;
}
return sql;
}
async build() {
const main = this.buildMainQuery();
let fullSql = `(${main.sql})`;
const allParams = [...main.params];
// 处理UNION部分
for (const union of this.unions) {
const unionBuilder = union.queryBuilder;
if (!(unionBuilder instanceof SelectBuilder)) {
throw new Error('UNION query must be a SelectBuilder instance');
}
const unionResult = unionBuilder.buildForUnion();
fullSql += ` ${union.type} (${unionResult.sql})`;
allParams.push(...unionResult.params);
}
// 添加ORDER BY、LIMIT、OFFSET
if (this.orderByField) {
fullSql += ` ORDER BY ${this.orderByField} ${this.orderByDirection}`;
}
if (this.limit !== null) {
fullSql += ` LIMIT ${this.limit}`;
}
if (this.offset !== null) {
fullSql += ` OFFSET ${this.offset}`;
}
console.log(fullSql,allParams);
return await this.sqdata(fullSql, allParams);
}
}
class UpdateBuilder extends QueryBuilder {
constructor() {
super();
this.table = '';
this.updateFields = {};
}
update(table) {
this.table = table;
return this;
}
set(field, value) {
if (value && value.increment && typeof value === 'object' ) {
this.updateFields[field] = {increment: value.increment};
} else {
this.updateFields[field] = value;
}
return this;
}
async build() {
let sql = `UPDATE ${this.table}
SET `;
let updateClauses = Object.keys(this.updateFields).map(field => {
const value = this.updateFields[field];
if (value && value.increment && typeof value === 'object' ) {
return `${field} = ${field} + ?`;
}
return `${field} = ?`;
}).join(', ');
sql += updateClauses;
let conditionClauses = this.buildConditions();
if (conditionClauses) {
sql += ` WHERE ${conditionClauses}`;
}
// 处理参数,确保自增字段也传入增量值
const params = [
...Object.values(this.updateFields).map(value =>
(value && value.increment && typeof value === 'object' ) ? value.increment : value
),
...this.getParams()
];
return await this.sqdata(sql, params);
}
}
class InsertBuilder extends QueryBuilder {
constructor() {
super();
this.table = '';
this.insertValues = [];
this.updateValues = {};
}
insertInto(table) {
this.table = table;
return this;
}
// 仍然保留单条记录的插入
values(values) {
if (Array.isArray(values)) {
this.insertValues = values;
} else {
this.insertValues = [values]; // 将单条记录包装成数组
}
return this;
}
// 新增方法,支持一次插入多条记录
valuesMultiple(records) {
if (!Array.isArray(records) || records.length === 0) {
throw new Error('Values must be a non-empty array');
}
// 确保每一条记录都是对象
records.forEach(record => {
if (typeof record !== 'object') {
throw new Error('Each record must be an object');
}
});
this.insertValues = records;
return this;
}
// 新增 upsert 方法,支持更新或插入
upsert(values, updateFields) {
// values: 要插入的记录
// updateFields: 如果记录存在时,需要更新的字段
if (!Array.isArray(values) || values.length === 0) {
throw new Error('Values must be a non-empty array');
}
// 检查每条记录是否是对象
values.forEach(record => {
if (typeof record !== 'object') {
throw new Error('Each record must be an object');
}
});
this.insertValues = values;
this.updateValues = updateFields || {};
return this;
}
async build() {
if (this.insertValues.length === 0) {
throw new Error("No values to insert");
}
// 获取表单列名,假设所有记录有相同的字段
const columns = Object.keys(this.insertValues[0]);
// 构建 VALUES 子句,支持批量插入
const valuePlaceholders = this.insertValues.map(() =>
`(${columns.map(() => '?').join(', ')})`
).join(', ');
// 展平所有的插入值
const params = this.insertValues.flatMap(record =>
columns.map(column => record[column])
);
// 如果有 updateFields构建 ON DUPLICATE KEY UPDATE 子句
let updateClause = '';
if (Object.keys(this.updateValues).length > 0) {
updateClause = ' ON DUPLICATE KEY UPDATE ' +
Object.keys(this.updateValues).map(field => {
return `${field} = VALUES(${field})`;
}).join(', ');
}
// 生成 SQL 语句
const sql = `INSERT INTO ${this.table} (${columns.join(', ')})
VALUES ${valuePlaceholders} ${updateClause}`;
// 执行查询
return await this.sqdata(sql, params);
}
}
class DeleteBuilder extends QueryBuilder {
constructor() {
super();
this.table = '';
}
deleteFrom(table) {
this.table = table;
return this;
}
async build() {
let sql = `DELETE
FROM ${this.table}`;
let conditionClauses = this.buildConditions();
if (conditionClauses) {
sql += ` WHERE ${conditionClauses}`;
}
return await this.sqdata(sql, this.getParams());
}
}
module.exports = {
SelectBuilder,
UpdateBuilder,
InsertBuilder,
DeleteBuilder,
};

73
config/logger.js Normal file
View File

@@ -0,0 +1,73 @@
const winston = require('winston');
const path = require('path');
// 创建日志目录
const logDir = path.join(__dirname, '../logs');
// 日志格式配置
const logFormat = winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.errors({ stack: true }),
winston.format.json()
);
// 控制台日志格式
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''}`;
})
);
// 创建logger实例
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
defaultMeta: { service: 'integrated-system' },
transports: [
// 错误日志文件
new winston.transports.File({
filename: path.join(logDir, 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// 所有日志文件
new winston.transports.File({
filename: path.join(logDir, 'combined.log'),
maxsize: 5242880, // 5MB
maxFiles: 5
})
]
});
// 开发环境添加控制台输出
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: consoleFormat
}));
}
// 审计日志记录器
const auditLogger = winston.createLogger({
level: 'info',
format: logFormat,
defaultMeta: { service: 'audit' },
transports: [
new winston.transports.File({
filename: path.join(logDir, 'audit.log'),
maxsize: 5242880, // 5MB
maxFiles: 10
})
]
});
module.exports = {
logger,
auditLogger
};

97
config/minio.js Normal file
View File

@@ -0,0 +1,97 @@
const Minio = require('minio');
require('dotenv').config();
/**
* MinIO 配置
* 用于对象存储服务配置
*/
const minioConfig = {
// MinIO 服务器配置
endPoint: process.env.MINIO_ENDPOINT || 'localhost',
port: parseInt(process.env.MINIO_PORT) || 9000,
useSSL: process.env.MINIO_USE_SSL === 'true' || false,
accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
// 存储桶配置
buckets: {
uploads: process.env.MINIO_BUCKET_UPLOADS || 'uploads',
avatars: process.env.MINIO_BUCKET_AVATARS || 'avatars',
products: process.env.MINIO_BUCKET_PRODUCTS || 'products',
documents: process.env.MINIO_BUCKET_DOCUMENTS || 'documents'
},
// 文件访问配置
publicUrl: process.env.MINIO_PUBLIC_URL || `http://localhost:9000`
};
/**
* 创建 MinIO 客户端实例
*/
const createMinioClient = () => {
return new Minio.Client({
endPoint: minioConfig.endPoint,
port: minioConfig.port,
useSSL: minioConfig.useSSL,
accessKey: minioConfig.accessKey,
secretKey: minioConfig.secretKey
});
};
/**
* 初始化存储桶
* 确保所有需要的存储桶都存在
*/
const initializeBuckets = async () => {
const minioClient = createMinioClient();
try {
// 检查并创建存储桶
for (const [key, bucketName] of Object.entries(minioConfig.buckets)) {
const exists = await minioClient.bucketExists(bucketName);
if (!exists) {
await minioClient.makeBucket(bucketName, 'us-east-1');
console.log(`✅ 存储桶 '${bucketName}' 创建成功`);
// 设置存储桶策略为公开读取(可选)
const policy = {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: { AWS: ['*'] },
Action: ['s3:GetObject'],
Resource: [`arn:aws:s3:::${bucketName}/*`]
}
]
};
try {
await minioClient.setBucketPolicy(bucketName, JSON.stringify(policy));
console.log(`✅ 存储桶 '${bucketName}' 策略设置成功`);
} catch (policyError) {
console.warn(`⚠️ 存储桶 '${bucketName}' 策略设置失败:`, policyError.message);
}
} else {
console.log(`✅ 存储桶 '${bucketName}' 已存在`);
}
}
} catch (error) {
console.error('❌ 初始化存储桶失败:', error);
throw error;
}
};
/**
* 获取文件的公开访问URL
*/
const getPublicUrl = (bucketName, objectName) => {
return `${minioConfig.publicUrl}/${bucketName}/${objectName}`;
};
module.exports = {
minioConfig,
createMinioClient,
initializeBuckets,
getPublicUrl
};

24
config/wechatPay.js Normal file
View File

@@ -0,0 +1,24 @@
// 微信支付配置
module.exports = {
// 微信支付配置
wechatPay: {
appId: process.env.WECHAT_APP_ID || '', // 微信公众号AppID
mchId: process.env.WECHAT_MCH_ID || '', // 商户号
apiKey: process.env.WECHAT_API_KEY || '', // API密钥
apiV3Key: process.env.WECHAT_API_V3_KEY || '', // APIv3密钥
notifyUrl: process.env.WECHAT_NOTIFY_URL || 'https://your-domain.com/api/wechat/notify', // 支付回调地址
// 证书路径(生产环境需要配置)
certPath: process.env.WECHAT_CERT_PATH || '',
keyPath: process.env.WECHAT_KEY_PATH || '',
// 支付相关配置
tradeType: {
h5: 'MWEB', // H5支付
jsapi: 'JSAPI' // 公众号支付
},
// 注册费用配置(单位:分)
registrationFee: 100 // 1元注册费
}
};

129
database.js Normal file
View File

@@ -0,0 +1,129 @@
const mysql = require('mysql2/promise');
// 数据库配置
const dbConfig = {
// host: process.env.DB_HOST || '114.55.111.44',
// user: process.env.DB_USER || 'maov2',
// password: process.env.DB_PASSWORD || '5fYhw8z6T62b7heS',
// database: process.env.DB_NAME || 'maov2',
host: '114.55.111.44',
user: 'test_mao',
password: 'nK2mPbWriBp25BRd',
database: 'test_mao',
charset: 'utf8mb4',
dateStrings: true,
// 连接池配置
connectionLimit: 20, // 连接池最大连接数
queueLimit: 0, // 排队等待连接的最大数量0表示无限制
// 确保参数正确处理
supportBigNumbers: true,
bigNumberStrings: false
};
// 创建数据库连接池
let pool;
/**
* 初始化数据库连接池
* @returns {Promise<mysql.Pool>} 数据库连接池
*/
async function initDB() {
if (!pool) {
try {
pool = mysql.createPool(dbConfig);
// 添加连接池事件监听
pool.on('connection', function (connection) {
console.log('新的数据库连接建立:', connection.threadId);
});
pool.on('error', function (err) {
console.error('数据库连接池错误:', err);
if (err.code === 'PROTOCOL_CONNECTION_LOST') {
console.log('数据库连接丢失,尝试重新连接...');
} else if (err.code === 'ECONNRESET') {
console.log('数据库连接被重置,尝试重新连接...');
} else if (err.code === 'ETIMEDOUT') {
console.log('数据库连接超时,尝试重新连接...');
}
});
// 测试连接
const connection = await pool.getConnection();
console.log('数据库连接池初始化成功');
connection.release();
} catch (error) {
console.error('数据库连接池初始化失败:', error);
throw error;
}
}
return pool;
}
/**
* 获取数据库连接池
* @returns {mysql.Pool} 数据库连接池
*/
function getDB() {
if (!pool) {
throw new Error('数据库连接池未初始化,请先调用 initDB()');
}
return pool;
}
/**
* 执行数据库查询(带重试机制)
* @param {string} sql SQL查询语句
* @param {Array} params 查询参数
* @param {number} retries 重试次数
* @returns {Promise<Array>} 查询结果
*/
async function executeQuery(sql, params = [], retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const connection = await pool.getConnection();
try {
const [results] = await connection.execute(sql, params);
connection.release();
return results;
} catch (error) {
connection.release();
throw error;
}
} catch (error) {
console.error(`数据库查询失败 (尝试 ${i + 1}/${retries}):`, error.message);
if (i === retries - 1) {
throw error;
}
// 如果是连接相关错误,等待后重试
if (error.code === 'PROTOCOL_CONNECTION_LOST' ||
error.code === 'ECONNRESET' ||
error.code === 'ETIMEDOUT') {
console.log(`等待 ${(i + 1) * 1000}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, (i + 1) * 1000));
} else {
throw error;
}
}
}
}
/**
* 关闭数据库连接池
*/
async function closeDB() {
if (pool) {
await pool.end();
pool = null;
console.log('数据库连接池已关闭');
}
}
module.exports = {
initDB,
getDB,
closeDB,
dbConfig
};

0
logs/audit.log Normal file
View File

51
logs/combined.log Normal file
View File

@@ -0,0 +1,51 @@
{"level":"info","message":"Server starting","port":"3005","service":"integrated-system","timestamp":"2025-09-24 10:51:48"}
{"environment":"development","level":"info","message":"Server started successfully","port":"3005","service":"integrated-system","timestamp":"2025-09-24 10:51:49"}
{"duration":"33ms","ip":"::ffff:192.168.0.12","level":"info","message":"HTTP Request","method":"POST","service":"integrated-system","statusCode":400,"timestamp":"2025-09-24 11:31:39","url":"/auth/login","userAgent":"Apifox/1.0.0 (https://apifox.com)"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"integrated-system","timestamp":"2025-09-24 11:32:55"}
{"level":"info","message":"Server starting","port":"3005","service":"integrated-system","timestamp":"2025-09-24 11:32:56"}
{"environment":"development","level":"info","message":"Server started successfully","port":"3005","service":"integrated-system","timestamp":"2025-09-24 11:32:56"}
{"ip":"::ffff:192.168.0.15","level":"error","message":"Error occurred: 路径 /api/captcha/generate 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /api/captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:899:7\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:782:5","timestamp":"2025-09-24 11:37:32","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"duration":"7ms","ip":"::ffff:192.168.0.15","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-24 11:37:32","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"ip":"::ffff:192.168.0.15","level":"error","message":"Error occurred: 路径 /api/captcha/generate 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /api/captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:899:7\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:782:5","timestamp":"2025-09-24 11:38:02","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"duration":"19ms","ip":"::ffff:192.168.0.15","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-24 11:38:02","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"ip":"::ffff:192.168.0.15","level":"error","message":"Error occurred: 路径 /api/captcha/generate 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /api/captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:899:7\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:782:5","timestamp":"2025-09-24 11:38:03","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"duration":"11ms","ip":"::ffff:192.168.0.15","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-24 11:38:03","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"ip":"::ffff:192.168.0.15","level":"error","message":"Error occurred: 路径 /api/captcha/generate 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /api/captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:899:7\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:782:5","timestamp":"2025-09-24 11:38:05","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"duration":"16ms","ip":"::ffff:192.168.0.15","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-24 11:38:05","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"ip":"::ffff:192.168.0.15","level":"error","message":"Error occurred: 路径 /api/captcha/generate 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /api/captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:899:7\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:782:5","timestamp":"2025-09-24 11:38:05","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"duration":"3ms","ip":"::ffff:192.168.0.15","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-24 11:38:05","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"duration":"51ms","ip":"::ffff:192.168.0.15","level":"info","message":"HTTP Request","method":"POST","service":"integrated-system","statusCode":401,"timestamp":"2025-09-24 11:40:18","url":"/auth/login","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /auth/login 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /auth/login 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 14:36:11","url":"/auth/login","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"duration":"7ms","ip":"::ffff:192.168.0.11","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-24 14:36:11","url":"/auth/login","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /auth/login 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /auth/login 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 14:36:17","url":"/auth/login","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"duration":"3ms","ip":"::ffff:192.168.0.11","level":"info","message":"HTTP Request","method":"GET","service":"integrated-system","statusCode":404,"timestamp":"2025-09-24 14:36:17","url":"/auth/login","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"duration":"1ms","ip":"::ffff:192.168.0.11","level":"info","message":"HTTP Request","method":"POST","service":"integrated-system","statusCode":400,"timestamp":"2025-09-24 14:36:32","url":"/auth/login","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /captcha/generate 未找到","method":"POST","service":"integrated-system","stack":"Error: 路径 /captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 15:40:19","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"duration":"9ms","ip":"::ffff:192.168.0.11","level":"info","message":"HTTP Request","method":"POST","service":"integrated-system","statusCode":404,"timestamp":"2025-09-24 15:40:19","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /captcha/generate 未找到","method":"POST","service":"integrated-system","stack":"Error: 路径 /captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 15:40:23","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"duration":"3ms","ip":"::ffff:192.168.0.11","level":"info","message":"HTTP Request","method":"POST","service":"integrated-system","statusCode":404,"timestamp":"2025-09-24 15:40:23","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /captcha/generate 未找到","method":"POST","service":"integrated-system","stack":"Error: 路径 /captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 15:40:24","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"duration":"5ms","ip":"::ffff:192.168.0.11","level":"info","message":"HTTP Request","method":"POST","service":"integrated-system","statusCode":404,"timestamp":"2025-09-24 15:40:24","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /captcha/generate 未找到","method":"POST","service":"integrated-system","stack":"Error: 路径 /captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 15:40:25","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"duration":"5ms","ip":"::ffff:192.168.0.11","level":"info","message":"HTTP Request","method":"POST","service":"integrated-system","statusCode":404,"timestamp":"2025-09-24 15:40:25","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"duration":"2ms","ip":"::ffff:192.168.0.11","level":"info","message":"HTTP Request","method":"POST","service":"integrated-system","statusCode":400,"timestamp":"2025-09-24 15:50:02","url":"/auth/login","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"duration":"1ms","ip":"::ffff:192.168.0.11","level":"info","message":"HTTP Request","method":"POST","service":"integrated-system","statusCode":400,"timestamp":"2025-09-24 16:17:05","url":"/auth/login","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"integrated-system","timestamp":"2025-09-24 16:17:53"}
{"level":"info","message":"Server starting","port":"3005","service":"integrated-system","timestamp":"2025-09-24 16:17:54"}
{"environment":"development","level":"info","message":"Server started successfully","port":"3005","service":"integrated-system","timestamp":"2025-09-24 16:17:54"}
{"duration":"125ms","ip":"::ffff:192.168.0.11","level":"info","message":"HTTP Request","method":"POST","service":"integrated-system","statusCode":401,"timestamp":"2025-09-24 16:22:40","url":"/auth/login","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"integrated-system","timestamp":"2025-09-25 05:37:03"}
{"level":"info","message":"Server starting","port":"3005","service":"integrated-system","timestamp":"2025-09-25 09:17:08"}
{"environment":"development","level":"info","message":"Server started successfully","port":"3005","service":"integrated-system","timestamp":"2025-09-25 09:17:08"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"integrated-system","timestamp":"2025-09-25 09:27:53"}
{"level":"info","message":"Server starting","port":"3005","service":"integrated-system","timestamp":"2025-09-25 09:27:53"}
{"environment":"development","level":"info","message":"Server started successfully","port":"3005","service":"integrated-system","timestamp":"2025-09-25 09:27:53"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"integrated-system","timestamp":"2025-09-25 09:39:33"}
{"level":"info","message":"Server starting","port":"3005","service":"integrated-system","timestamp":"2025-09-25 09:39:48"}
{"environment":"development","level":"info","message":"Server started successfully","port":"3005","service":"integrated-system","timestamp":"2025-09-25 09:39:48"}
{"error":"key.startsWith is not a function","level":"error","message":"Uncaught Exception","service":"integrated-system","stack":"TypeError: key.startsWith is not a function\n at Timeout._onTimeout (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\routes\\sms.js:165:13)\n at listOnTimeout (node:internal/timers:581:17)\n at process.processTimers (node:internal/timers:519:7)","timestamp":"2025-09-25 09:41:48"}
{"level":"info","message":"Server starting","port":"3005","service":"integrated-system","timestamp":"2025-09-25 10:00:22"}
{"environment":"development","level":"info","message":"Server started successfully","port":"3005","service":"integrated-system","timestamp":"2025-09-25 10:00:23"}
{"level":"info","message":"SIGINT received, shutting down gracefully","service":"integrated-system","timestamp":"2025-09-25 10:20:43"}
{"level":"info","message":"Server starting","port":"3005","service":"integrated-system","timestamp":"2025-09-25 10:20:50"}
{"environment":"development","level":"info","message":"Server started successfully","port":"3005","service":"integrated-system","timestamp":"2025-09-25 10:20:50"}

12
logs/error.log Normal file
View File

@@ -0,0 +1,12 @@
{"ip":"::ffff:192.168.0.15","level":"error","message":"Error occurred: 路径 /api/captcha/generate 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /api/captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:899:7\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:782:5","timestamp":"2025-09-24 11:37:32","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"ip":"::ffff:192.168.0.15","level":"error","message":"Error occurred: 路径 /api/captcha/generate 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /api/captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:899:7\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:782:5","timestamp":"2025-09-24 11:38:02","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"ip":"::ffff:192.168.0.15","level":"error","message":"Error occurred: 路径 /api/captcha/generate 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /api/captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:899:7\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:782:5","timestamp":"2025-09-24 11:38:03","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"ip":"::ffff:192.168.0.15","level":"error","message":"Error occurred: 路径 /api/captcha/generate 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /api/captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:899:7\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:782:5","timestamp":"2025-09-24 11:38:05","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"ip":"::ffff:192.168.0.15","level":"error","message":"Error occurred: 路径 /api/captcha/generate 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /api/captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:899:7\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\express-rate-limit\\dist\\index.cjs:782:5","timestamp":"2025-09-24 11:38:05","url":"/api/captcha/generate","userAgent":"Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /auth/login 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /auth/login 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 14:36:11","url":"/auth/login","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /auth/login 未找到","method":"GET","service":"integrated-system","stack":"Error: 路径 /auth/login 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 14:36:17","url":"/auth/login","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /captcha/generate 未找到","method":"POST","service":"integrated-system","stack":"Error: 路径 /captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 15:40:19","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /captcha/generate 未找到","method":"POST","service":"integrated-system","stack":"Error: 路径 /captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 15:40:23","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /captcha/generate 未找到","method":"POST","service":"integrated-system","stack":"Error: 路径 /captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 15:40:24","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"ip":"::ffff:192.168.0.11","level":"error","message":"Error occurred: 路径 /captcha/generate 未找到","method":"POST","service":"integrated-system","stack":"Error: 路径 /captcha/generate 未找到\n at notFound (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\middleware\\errorHandler.js:107:17)\n at Layer.handleRequest (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\lib\\layer.js:152:17)\n at trimPrefix (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:342:13)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:297:9\n at processParams (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:582:12)\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:291:5)\n at D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:688:15\n at next (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:276:14)\n at Function.handle (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:186:3)\n at router (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\node_modules\\router\\index.js:60:12)","timestamp":"2025-09-24 15:40:25","url":"/captcha/generate","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 Edg/92.0.902.67"}
{"error":"key.startsWith is not a function","level":"error","message":"Uncaught Exception","service":"integrated-system","stack":"TypeError: key.startsWith is not a function\n at Timeout._onTimeout (D:\\work\\客户\\毛总\\code\\jurong_Intermediate\\routes\\sms.js:165:13)\n at listOnTimeout (node:internal/timers:581:17)\n at process.processTimers (node:internal/timers:519:7)","timestamp":"2025-09-25 09:41:48"}

110
middleware/auth.js Normal file
View File

@@ -0,0 +1,110 @@
const jwt = require('jsonwebtoken');
const { getDB } = require('../database');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; // 在生产环境中应该使用环境变量
/**
* 用户认证中间件
* 验证JWT令牌并检查用户状态包括是否被拉黑
*/
const auth = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ success: false, message: '未提供认证令牌' });
}
const decoded = jwt.verify(token, JWT_SECRET);
const db = getDB();
const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]);
if (users.length === 0) {
return res.status(401).json({ success: false, message: '用户不存在' });
}
const user = users[0];
// 检查用户是否被拉黑
if (user.is_blacklisted) {
return res.status(403).json({
success: false,
message: '账户已被拉黑,请联系管理员',
code: 'USER_BLACKLISTED'
});
}
// 检查支付状态(管理员除外)
if (user.role !== 'admin' && user.payment_status === 'unpaid') {
return res.status(403).json({
success: false,
message: '您的账户尚未激活,请完成支付后再使用',
code: 'PAYMENT_REQUIRED',
needPayment: true,
userId: user.id
});
}
req.user = user;
next();
} catch (error) {
res.status(401).json({ success: false, message: '无效的认证令牌' });
}
};
// 管理员认证中间件
const adminAuth = (req, res, next) => {
if (req.user.role !== 'admin') {
return res.status(403).json({ success: false, message: '需要管理员权限' });
}
next();
};
/**
* 支付认证中间件
* 只验证JWT令牌和用户状态不检查支付状态
* 用于支付相关接口,允许未支付用户创建支付订单
*/
const paymentAuth = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ success: false, message: '未提供认证令牌' });
}
const decoded = jwt.verify(token, JWT_SECRET);
const db = getDB();
const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]);
if (users.length === 0) {
return res.status(401).json({ success: false, message: '用户不存在' });
}
const user = users[0];
// 检查用户是否被拉黑
if (user.is_blacklisted) {
return res.status(403).json({
success: false,
message: '账户已被拉黑,请联系管理员',
code: 'USER_BLACKLISTED'
});
}
// 注意:这里不检查支付状态,允许未支付用户创建支付订单
req.user = user;
next();
} catch (error) {
console.error('支付认证失败:', error);
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ success: false, message: '无效的认证令牌' });
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ success: false, message: '认证令牌已过期' });
}
return res.status(500).json({ success: false, message: '认证失败' });
}
};
module.exports = { auth, adminAuth, paymentAuth, JWT_SECRET };

129
middleware/errorHandler.js Normal file
View File

@@ -0,0 +1,129 @@
const { logger } = require('../config/logger');
const { ERROR_CODES, HTTP_STATUS } = require('../config/constants');
// 全局错误处理中间件
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
// 记录错误日志
logger.error('Error occurred:', {
message: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.user?.id
});
// MySQL错误处理
if (err.code) {
switch (err.code) {
case 'ER_DUP_ENTRY':
error.message = '数据已存在';
error.statusCode = HTTP_STATUS.CONFLICT;
error.errorCode = ERROR_CODES.DUPLICATE_ENTRY;
break;
case 'ER_NO_REFERENCED_ROW_2':
error.message = '关联数据不存在';
error.statusCode = HTTP_STATUS.BAD_REQUEST;
error.errorCode = ERROR_CODES.VALIDATION_ERROR;
break;
case 'ER_ROW_IS_REFERENCED_2':
error.message = '数据正在被使用,无法删除';
error.statusCode = HTTP_STATUS.CONFLICT;
error.errorCode = ERROR_CODES.VALIDATION_ERROR;
break;
case 'ECONNREFUSED':
error.message = '数据库连接失败';
error.statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR;
error.errorCode = ERROR_CODES.DATABASE_ERROR;
break;
default:
error.message = '数据库操作失败';
error.statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR;
error.errorCode = ERROR_CODES.DATABASE_ERROR;
}
}
// JWT错误处理
if (err.name === 'JsonWebTokenError') {
error.message = '无效的访问令牌';
error.statusCode = HTTP_STATUS.UNAUTHORIZED;
error.errorCode = ERROR_CODES.AUTHENTICATION_ERROR;
}
if (err.name === 'TokenExpiredError') {
error.message = '访问令牌已过期';
error.statusCode = HTTP_STATUS.UNAUTHORIZED;
error.errorCode = ERROR_CODES.AUTHENTICATION_ERROR;
}
// 参数验证错误
if (err.name === 'ValidationError' || err.isJoi) {
const message = err.details ? err.details.map(detail => detail.message).join(', ') : err.message;
error.message = `参数验证失败: ${message}`;
error.statusCode = HTTP_STATUS.BAD_REQUEST;
error.errorCode = ERROR_CODES.VALIDATION_ERROR;
}
// 业务逻辑错误处理
if (err.message === '余额不足') {
error.message = '用户积分余额不足,无法完成转账操作。请先为用户充值积分或选择其他用户。';
error.statusCode = HTTP_STATUS.BAD_REQUEST;
error.errorCode = ERROR_CODES.VALIDATION_ERROR;
}
if (err.message === '用户不存在') {
error.message = '指定的用户不存在,请检查用户信息后重试。';
error.statusCode = HTTP_STATUS.BAD_REQUEST;
error.errorCode = ERROR_CODES.VALIDATION_ERROR;
}
// 自定义错误
if (err.statusCode) {
error.statusCode = err.statusCode;
error.errorCode = err.errorCode || ERROR_CODES.INTERNAL_ERROR;
}
// 默认错误
const statusCode = error.statusCode || HTTP_STATUS.INTERNAL_SERVER_ERROR;
const errorCode = error.errorCode || ERROR_CODES.INTERNAL_ERROR;
const message = error.message || '服务器内部错误';
res.status(statusCode).json({
success: false,
error: {
code: errorCode,
message: message
},
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
// 404错误处理
const notFound = (req, res, next) => {
const error = new Error(`路径 ${req.originalUrl} 未找到`);
error.statusCode = HTTP_STATUS.NOT_FOUND;
error.errorCode = ERROR_CODES.NOT_FOUND;
next(error);
};
// 自定义错误类
class AppError extends Error {
constructor(message, statusCode, errorCode) {
super(message);
this.statusCode = statusCode;
this.errorCode = errorCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = {
errorHandler,
notFound,
AppError
};

230
middleware/validation.js Normal file
View File

@@ -0,0 +1,230 @@
const Joi = require('joi');
const { AppError } = require('./errorHandler');
const { ERROR_CODES, HTTP_STATUS } = require('../config/constants');
// 验证中间件工厂函数
const validate = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body, { abortEarly: false });
if (error) {
const errorMessage = error.details.map(detail => detail.message).join(', ');
return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR));
}
next();
};
};
// 查询参数验证中间件
const validateQuery = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.query, { abortEarly: false });
if (error) {
const errorMessage = error.details.map(detail => detail.message).join(', ');
return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR));
}
next();
};
};
// 路径参数验证中间件
const validateParams = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.params, { abortEarly: false });
if (error) {
const errorMessage = error.details.map(detail => detail.message).join(', ');
return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR));
}
next();
};
};
// 通用验证规则
const commonSchemas = {
// ID验证
id: Joi.number().integer().positive().required().messages({
'number.base': 'ID必须是数字',
'number.integer': 'ID必须是整数',
'number.positive': 'ID必须是正数',
'any.required': 'ID是必需的'
}),
// 分页验证
pagination: Joi.object({
page: Joi.number().integer().min(1).default(1).messages({
'number.base': '页码必须是数字',
'number.integer': '页码必须是整数',
'number.min': '页码必须大于0'
}),
limit: Joi.number().integer().min(1).max(100).default(10).messages({
'number.base': '每页数量必须是数字',
'number.integer': '每页数量必须是整数',
'number.min': '每页数量必须大于0',
'number.max': '每页数量不能超过100'
})
})
};
// 用户相关验证规则
const userSchemas = {
// 用户注册
register: Joi.object({
username: Joi.string().alphanum().min(3).max(30).required().messages({
'string.base': '用户名必须是字符串',
'string.alphanum': '用户名只能包含字母和数字',
'string.min': '用户名至少3个字符',
'string.max': '用户名最多30个字符',
'any.required': '用户名是必需的'
}),
password: Joi.string().min(6).max(128).required().messages({
'string.base': '密码必须是字符串',
'string.min': '密码至少6个字符',
'string.max': '密码最多128个字符',
'any.required': '密码是必需的'
}),
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required().messages({
'string.pattern.base': '手机号格式不正确',
'any.required': '手机号是必需的'
}),
// 可选字段,注册时不需要填写
real_name: Joi.string().max(50).allow('').optional().messages({
'string.max': '真实姓名最多50个字符'
}),
role: Joi.string().valid('admin', 'user').default('user').messages({
'any.only': '角色只能是admin或user'
})
}),
// 用户登录
login: Joi.object({
username: Joi.string().required().messages({
'any.required': '用户名是必需的'
}),
password: Joi.string().required().messages({
'any.required': '密码是必需的'
})
})
};
// 转账相关验证规则
const transferSchemas = {
// 转账查询参数
query: Joi.object({
page: Joi.number().integer().min(1).default(1).messages({
'number.base': '页码必须是数字',
'number.integer': '页码必须是整数',
'number.min': '页码必须大于0'
}),
limit: Joi.number().integer().min(1).max(100).default(10).messages({
'number.base': '每页数量必须是数字',
'number.integer': '每页数量必须是整数',
'number.min': '每页数量必须大于0',
'number.max': '每页数量不能超过100'
}),
status: Joi.string().valid('pending', 'confirmed', 'rejected', 'cancelled').allow('').messages({
'any.only': '状态值无效'
}),
type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').allow('').messages({
'any.only': '转账类型无效'
}),
search: Joi.string().allow('').max(100).messages({
'string.max': '搜索关键词最多100个字符'
}),
transfer_type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').allow('').messages({
'any.only': '转账类型无效'
}),
start_date: Joi.date().iso().allow('').messages({
'date.format': '开始日期格式不正确'
}),
end_date: Joi.date().iso().allow('').messages({
'date.format': '结束日期格式不正确'
}),
sort: Joi.string().valid('id', 'amount', 'created_at', 'updated_at', 'status').allow('').messages({
'any.only': '排序字段无效,只支持: id, amount, created_at, updated_at, status'
}),
order: Joi.string().valid('asc', 'desc').allow('').messages({
'any.only': '排序方向无效,只支持: asc, desc'
}),
// 优先显示待处理转账参数
show_pending: Joi.alternatives().try(
Joi.boolean(),
Joi.string().valid('true', 'false', '')
).allow('').messages({
'alternatives.match': 'show_pending参数只能是布尔值或字符串true/false'
})
}),
// 创建转账
create: Joi.object({
to_user_id: Joi.number().integer().positive().required().messages({
'number.base': '收款用户ID必须是数字',
'number.integer': '收款用户ID必须是整数',
'number.positive': '收款用户ID必须是正数',
'any.required': '收款用户ID是必需的'
}),
amount: Joi.number().positive().precision(2).required().messages({
'number.base': '金额必须是数字',
'number.positive': '金额必须是正数',
'any.required': '金额是必需的'
}),
transfer_type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').required().messages({
'any.only': '转账类型无效',
'any.required': '转账类型是必需的'
}),
description: Joi.string().max(500).allow('').messages({
'string.max': '描述最多500个字符'
}),
voucher_url: Joi.string().uri().allow('').messages({
'string.uri': '凭证URL格式不正确'
})
}),
// 确认转账
confirm: Joi.object({
transfer_id: Joi.number().integer().positive().required().messages({
'number.base': '转账ID必须是数字',
'number.integer': '转账ID必须是整数',
'number.positive': '转账ID必须是正数',
'any.required': '转账ID是必需的'
}),
note: Joi.string().max(500).allow('').messages({
'string.max': '备注最多500个字符'
})
}),
// 拒绝转账
reject: Joi.object({
transfer_id: Joi.number().integer().positive().required().messages({
'number.base': '转账ID必须是数字',
'number.integer': '转账ID必须是整数',
'number.positive': '转账ID必须是正数',
'any.required': '转账ID是必需的'
}),
note: Joi.string().max(500).allow('').messages({
'string.max': '备注最多500个字符'
})
})
};
// 系统设置相关验证规则
const systemSchemas = {
updateSettings: Joi.object({
site_name: Joi.string().max(100).optional(),
site_description: Joi.string().max(500).optional(),
contact_phone: Joi.string().max(20).optional(),
maintenance_mode: Joi.boolean().optional(),
max_transfer_amount: Joi.number().positive().optional(),
min_transfer_amount: Joi.number().positive().optional(),
transfer_fee_rate: Joi.number().min(0).max(1).optional()
})
};
// 导出所有验证规则
module.exports = {
validate,
validateQuery,
validateParams,
commonSchemas,
userSchemas,
transferSchemas,
systemSchemas
};

3180
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
package.json Normal file
View File

@@ -0,0 +1,34 @@
{
"dependencies": {
"@alicloud/dysmsapi20170525": "^4.2.0",
"@alicloud/openapi-client": "^0.4.15",
"alipay-sdk": "^4.14.0",
"bcryptjs": "^3.0.2",
"body-parser": "^2.2.0",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dayjs": "^1.11.18",
"dotenv": "^17.2.2",
"express": "^5.1.0",
"express-rate-limit": "^8.1.0",
"express-validator": "^7.2.1",
"jsonwebtoken": "^9.0.2",
"minio": "^8.0.6",
"multer": "^2.0.2",
"mysql2": "^3.15.0",
"node-cron": "^4.2.1",
"node-rsa": "^1.1.1",
"qrcode": "^1.5.4",
"winston": "^3.17.0"
},
"name": "jurong_intermediate",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

355
routes/auth.js Normal file
View File

@@ -0,0 +1,355 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const {getDB} = require('../database');
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
router.post('/register', async (req, res) => {
try {
const db = getDB();
await db.query('START TRANSACTION');
const {
username,
phone,
password,
city,
district_id: district,
province,
inviter = null,
captchaId,
captchaText,
smsCode, // 短信验证码
role = 'user'
} = req.body;
if (!username || !phone || !password || !city || !district || !province) {
return res.status(400).json({success: false, message: '用户名、手机号、密码、城市和区域不能为空'});
}
if (!captchaId || !captchaText) {
return res.status(400).json({success: false, message: '图形验证码不能为空'});
}
const storedCaptcha = global.captchaStore.get(captchaId);
console.log(storedCaptcha);
if (!storedCaptcha) {
return res.status(400).json({
success: false,
message: '验证码不存在或已过期'
});
}
// 检查是否过期
if (Date.now() > storedCaptcha.expires) {
global.captchaStore.delete(captchaId);
return res.status(400).json({
success: false,
message: '验证码已过期'
});
}
// 验证验证码(不区分大小写)
const isValid = storedCaptcha.text === captchaText.toLowerCase();
// 删除已验证的验证码
global.captchaStore.delete(captchaId);
if (!isValid) {
return res.status(400).json({
success: false,
message: '验证码错误'
});
}
if (!smsCode) {
return res.status(400).json({success: false, message: '短信验证码不能为空'});
}
// 验证短信验证码
const smsAPI = require('./sms');
const smsValid = smsAPI.verifySMSCode(phone, smsCode);
if (!smsValid) {
return res.status(400).json({success: false, message: '短信验证码错误或已过期'});
}
// 验证手机号格式
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(phone)) {
return res.status(400).json({success: false, message: '手机号格式不正确'});
}
// 检查用户是否已存在
const [existingUsers] = await db.execute(
'SELECT id, payment_status FROM users WHERE username = ? OR phone = ?',
[username, phone]
);
if (existingUsers.length > 0) {
return res.status(400).json({success: false, message: '用户名或手机号已存在'});
}
// 加密密码
const hashedPassword = await bcrypt.hash(password, 10);
// 创建用户(初始状态为未支付)
const [result] = await db.execute(
'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id, payment_status, province, inviter) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid", ?, ?)',
[username, phone, hashedPassword, role, 0, 'pending', city, district, province, inviter]
);
const userId = result.insertId;
await db.query('COMMIT');
// 生成JWT token用于支付流程
const token = jwt.sign(
{userId: userId, username, role},
JWT_SECRET,
{expiresIn: '24h'}
);
res.status(201).json({
success: true,
message: '用户信息创建成功,请完成支付以激活账户',
token,
user: {
id: userId,
username,
phone,
role,
points: 0,
audit_status: 'pending',
city,
district,
paymentStatus: 'unpaid'
},
needPayment: true
});
} catch (error) {
try {
await getDB().query('ROLLBACK');
} catch (rollbackError) {
console.error('回滚错误:', rollbackError);
}
console.error('注册错误详情:', error);
console.error('错误堆栈:', error.stack);
res.status(500).json({
success: false,
message: '注册失败',
error: process.env.NODE_ENV === 'development' ? error.message : undefined
});
}
});
router.post('/login', async (req, res) => {
try {
const db = getDB();
const {username, password, captchaId, captchaText,type} = req.body;
if (!username || !password) {
return res.status(400).json({success: false, message: '用户名和密码不能为空'});
}
if (!captchaId || !captchaText) {
return res.status(400).json({success: false, message: '验证码不能为空'});
}
// 获取存储的验证码
const storedCaptcha = global.captchaStore.get(captchaId);
console.log(storedCaptcha);
if (!storedCaptcha) {
return res.status(400).json({
success: false,
message: '验证码不存在或已过期'
});
}
// 检查是否过期
if (Date.now() > storedCaptcha.expires) {
global.captchaStore.delete(captchaId);
return res.status(400).json({
success: false,
message: '验证码已过期'
});
}
// 验证验证码(不区分大小写)
const isValid = storedCaptcha.text === captchaText.toLowerCase();
// 删除已验证的验证码
global.captchaStore.delete(captchaId);
if (!isValid) {
return res.status(400).json({
success: false,
message: '验证码错误'
});
}
// 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证
// 查找用户(包含支付状态)
console.log('登录尝试 - 用户名:', username);
const [users] = await db.execute(
'SELECT * FROM users WHERE username = ?',
[username]
);
console.log('查找到的用户数量:', users.length);
if (users.length === 0) {
console.log('用户不存在:', username);
return res.status(401).json({success: false, message: '用户名或密码错误'});
}
const user = users[0];
console.log('找到用户:', user.username, '密码长度:', user.password ? user.password.length : 'null');
// 验证密码
console.log('验证密码 - 输入密码:', password, '数据库密码前10位:', user.password ? user.password.substring(0, 10) : 'null');
const isValidPassword = await bcrypt.compare(password, user.password);
console.log('密码验证结果:', isValidPassword);
if (!isValidPassword) {
console.log('密码验证失败');
return res.status(401).json({success: false, message: '用户名或密码错误'});
}
// 检查支付状态(管理员除外)
if (user.role !== 'admin' && user.payment_status === 'unpaid' && type!== 'app') {
const token = jwt.sign(
{userId: user.id, username: user.username, role: user.role},
JWT_SECRET,
{expiresIn: '5m'}
);
return res.status(200).json({
success: false,
message: '您的账户尚未激活,请完成支付后再登录',
needPayment: true,
user: user[0],
token
});
}
// 检查用户审核状态(管理员除外,只阻止被拒绝的用户)
if (user.role !== 'admin' && user.audit_status === 'rejected') {
return res.status(403).json({success: false, message: '您的账户审核未通过,请联系管理员'});
}
// 待审核用户可以正常登录使用系统,但匹配功能会有限制
// 生成JWT token
let token;
if(type === 'app') {
token = jwt.sign(
{userId: user.id, username: user.username, role: user.role},
JWT_SECRET,
{expiresIn: '999999h'}
);
}else {
token = jwt.sign(
{userId: user.id, username: user.username, role: user.role},
JWT_SECRET,
{expiresIn: '24h'}
);
}
const [is_distribution] = await db.execute(`
SELECT *
FROM distribution
WHERE user_id = ?`, [user.id]);
user.distribution = is_distribution.length > 0;
res.json({
success: true,
message: '登录成功',
token,
user
});
} catch (error) {
console.error('登录错误:', error);
res.status(500).json({success: false, message: '登录失败'});
}
});
// 验证token中间件
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({success: false, message: '访问令牌缺失'});
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({success: false, message: '访问令牌无效'});
}
req.user = user;
next();
});
};
// 获取当前用户信息
router.get('/me', authenticateToken, async (req, res) => {
try {
const db = getDB();
const [users] = await db.execute(
'SELECT * FROM users WHERE id = ?',
[req.user.userId]
);
if (users.length === 0) {
return res.status(404).json({success: false, message: '用户不存在'});
}
res.json({success: true, user: users[0]});
} catch (error) {
console.error('获取用户信息错误:', error);
res.status(500).json({success: false, message: '获取用户信息失败'});
}
});
// 修改密码
router.put('/change-password', authenticateToken, async (req, res) => {
try {
const db = getDB();
const {currentPassword, newPassword} = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({success: false, message: '旧密码和新密码不能为空'});
}
// 获取用户当前密码
const [users] = await db.execute(
'SELECT password FROM users WHERE id = ?',
[req.user.userId]
);
if (users.length === 0) {
return res.status(404).json({success: false, message: '用户不存在'});
}
// 验证旧密码
const isValidPassword = await bcrypt.compare(currentPassword, users[0].password);
if (!isValidPassword) {
return res.status(400).json({success: false, message: '旧密码错误'});
}
// 加密新密码
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
// 更新密码
await db.execute(
'UPDATE users SET password = ? WHERE id = ?',
[hashedNewPassword, req.user.userId]
);
res.json({success: true, message: '密码修改成功'});
} catch (error) {
console.error('修改密码错误:', error);
res.status(500).json({success: false, message: '修改密码失败'});
}
});
module.exports = router;
module.exports.authenticateToken = authenticateToken;

158
routes/captcha.js Normal file
View File

@@ -0,0 +1,158 @@
const express = require('express');
const crypto = require('crypto');
const router = express.Router();
/**
* 生成随机验证码字符串
* @param {number} length 验证码长度
* @returns {string} 验证码字符串
*/
function generateCaptchaText(length = 4) {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* 生成SVG验证码图片
* @param {string} text 验证码文本
* @returns {string} SVG字符串
*/
function generateCaptchaSVG(text) {
const width = 120;
const height = 40;
const fontSize = 18;
// 生成随机颜色
const getRandomColor = () => {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8'];
return colors[Math.floor(Math.random() * colors.length)];
};
// 生成干扰线
const generateNoise = () => {
let noise = '';
for (let i = 0; i < 3; i++) {
const x1 = Math.random() * width;
const y1 = Math.random() * height;
const x2 = Math.random() * width;
const y2 = Math.random() * height;
noise += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${getRandomColor()}" stroke-width="1" opacity="0.3"/>`;
}
return noise;
};
// 生成干扰点
const generateDots = () => {
let dots = '';
for (let i = 0; i < 20; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
const r = Math.random() * 2 + 1;
dots += `<circle cx="${x}" cy="${y}" r="${r}" fill="${getRandomColor()}" opacity="0.4"/>`;
}
return dots;
};
// 生成文字
let textElements = '';
const charWidth = width / text.length;
for (let i = 0; i < text.length; i++) {
const char = text[i];
const x = charWidth * i + charWidth / 2;
const y = height / 2 + fontSize / 3;
const rotation = (Math.random() - 0.5) * 30; // 随机旋转角度
const color = getRandomColor();
textElements += `
<text x="${x}" y="${y}"
font-family="Arial, sans-serif"
font-size="${fontSize}"
font-weight="bold"
fill="${color}"
text-anchor="middle"
transform="rotate(${rotation} ${x} ${y})">
${char}
</text>`;
}
const svg = `
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#f8f9fa;stop-opacity:1" />
<stop offset="100%" style="stop-color:#e9ecef;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="${width}" height="${height}" fill="url(#bg)" stroke="#dee2e6" stroke-width="1"/>
${generateNoise()}
${generateDots()}
${textElements}
</svg>`;
return svg;
}
router.get('/generate', (req, res) => {
try {
// 生成验证码文本
const captchaText = generateCaptchaText();
// 生成唯一ID
const captchaId = crypto.randomUUID();
// 存储验证码5分钟过期
global.captchaStore.set(captchaId, {
text: captchaText.toLowerCase(), // 存储小写用于比较
expires: Date.now() + 5 * 60 * 1000 // 5分钟过期
});
// 生成SVG图片
const svgImage = generateCaptchaSVG(captchaText);
res.json({
success: true,
data: {
captchaId,
image: `data:image/svg+xml;base64,${Buffer.from(svgImage).toString('base64')}`
}
});
} catch (error) {
console.error('生成验证码失败:', error);
res.status(500).json({
success: false,
message: '生成验证码失败'
});
}
});
// 清理过期验证码的定时任务
setInterval(() => {
const now = Date.now();
for (const [id, captcha] of global.captchaStore.entries()) {
if (now > captcha.expires) {
global.captchaStore.delete(id);
}
}
}, 60 * 1000); // 每分钟清理一次
// 导出验证函数供其他模块使用
module.exports = router;
module.exports.verifyCaptcha = (captchaId, captchaText) => {
const captcha = global.captchaStore.get(captchaId);
if (!captcha) {
return false; // 验证码不存在或已过期
}
if (captcha.text.toLowerCase() !== captchaText.toLowerCase()) {
return false; // 验证码错误
}
// 验证成功后删除验证码(一次性使用)
global.captchaStore.delete(captchaId);
return true;
};

175
routes/sms.js Normal file
View File

@@ -0,0 +1,175 @@
const express = require('express')
const router = express.Router()
const { getDB } = require('../database')
const Dysmsapi20170525 = require('@alicloud/dysmsapi20170525')
const OpenApi = require('@alicloud/openapi-client')
const { Config } = require('@alicloud/openapi-client')
// 阿里云短信配置
const config = new Config({
// 您的AccessKey ID
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || 'your_access_key_id',
// 您的AccessKey Secret
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || 'your_access_key_secret',
// 访问的域名
endpoint: 'dysmsapi.aliyuncs.com'
})
// 创建短信客户端
const client = new Dysmsapi20170525.default(config)
// 短信模板配置
const SMS_CONFIG = {
signName: process.env.ALIYUN_SMS_SIGN_NAME || '您的签名', // 短信签名
templateCode: process.env.ALIYUN_SMS_TEMPLATE_CODE || 'SMS_XXXXXX', // 短信模板CODE
// 开发环境标识
isDevelopment: process.env.NODE_ENV !== 'production'
}
// 存储验证码的内存对象生产环境建议使用Redis
const smsCodeStore = new Map()
// 验证码有效期5分钟
const CODE_EXPIRE_TIME = 5 * 60 * 1000
// 最大尝试次数
const MAX_ATTEMPTS = 3
// 发送频率限制60秒
const SEND_INTERVAL = 60 * 1000
/**
* 生成6位数字验证码
* @returns {string} 验证码
*/
function generateSMSCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
router.post('/send', async (req, res) => {
try {
const { phone } = req.body
console.log(phone)
// 验证手机号格式
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(phone)) {
return res.json({
success: false,
message: '手机号格式不正确'
})
}
// 检查发送频率限制
const lastSendTime = smsCodeStore.get(`last_send_${phone}`)
if (lastSendTime && Date.now() - lastSendTime < SEND_INTERVAL) {
const remainingTime = Math.ceil((SEND_INTERVAL - (Date.now() - lastSendTime)) / 1000)
return res.json({
success: false,
message: `请等待${remainingTime}秒后再发送`
})
}
// 生成6位数字验证码
const code = Math.random().toString().slice(-6)
// 存储验证码信息
smsCodeStore.set(phone, {
code,
timestamp: Date.now(),
attempts: 0
})
// 记录发送时间
smsCodeStore.set(`last_send_${phone}`, Date.now())
// 生产环境发送真实短信
try {
console.log(code);
const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({
phoneNumbers: phone,
signName: SMS_CONFIG.signName,
templateCode: SMS_CONFIG.templateCode,
templateParam: JSON.stringify({ code })
})
const response = await client.sendSms(sendSmsRequest)
console.log(response.body);
if (response.body.code === 'OK') {
res.json({
success: true,
message: '验证码发送成功'
})
} else {
console.error('阿里云短信发送失败:', response.body)
res.json({
success: false,
message: '发送失败,请稍后重试'
})
}
} catch (smsError) {
console.error('阿里云短信API调用失败:', smsError)
res.json({
success: false,
message: '发送失败,请稍后重试'
})
}
} catch (error) {
console.error('发送短信验证码失败:', error)
res.status(500).json({
success: false,
message: '发送失败,请稍后重试'
})
}
});
/**
* 导出验证手机号的函数供其他模块使用
* @param {string} phone 手机号
* @param {string} code 验证码
* @returns {boolean} 验证结果
*/
function verifySMSCode(phone, code) {
const storedData = smsCodeStore.get(phone);
if (!storedData) {
return false;
}
// 检查是否过期
if (Date.now() - storedData.timestamp > 300000) {
smsCodeStore.delete(phone);
return false;
}
// 检查尝试次数
if (storedData.attempts >= 3) {
smsCodeStore.delete(phone);
return false;
}
// 验证验证码
if (storedData.code === code) {
smsCodeStore.delete(phone);
smsCodeStore.delete(`time_${phone}`);
return true;
}
return false;
}
// 清理过期验证码的定时任务
setInterval(() => {
const now = Date.now();
for (const [key, value] of smsCodeStore.entries()) {
if (key.startsWith('time_')) continue;
if (value.timestamp && now - value.timestamp > 300000) {
smsCodeStore.delete(key);
smsCodeStore.delete(`time_${key}`);
}
}
}, 60000); // 每分钟清理一次
module.exports = router;
module.exports.verifySMSCode = verifySMSCode;

243
routes/upload.js Normal file
View File

@@ -0,0 +1,243 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const { auth } = require('../middleware/auth');
const { authenticateToken } = require('./auth');
const minioService = require('../services/minioService');
const { initializeBuckets } = require('../config/minio');
const router = express.Router();
// 配置multer内存存储用于MinIO上传
const storage = multer.memoryStorage();
// 文件过滤器 - 支持图片和视频
const fileFilter = (req, file, cb) => {
// 允许图片和视频文件
if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) {
cb(null, true);
} else {
cb(new Error('只能上传图片或视频文件'), false);
}
};
// 单文件上传配置
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 1 // 一次只能上传一个文件
}
});
// 多文件上传配置
const multiUpload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB (视频文件更大)
files: 10 // 最多10个文件
}
});
router.post('/image', authenticateToken, (req, res) => {
upload.single('file')(req, res, async (err) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
success: false,
message: '文件大小不能超过 5MB'
});
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({
success: false,
message: '一次只能上传一个文件'
});
}
return res.status(400).json({
success: false,
message: '文件上传失败:' + err.message
});
} else if (err) {
return res.status(400).json({
success: false,
message: err.message
});
}
if (!req.file) {
return res.status(400).json({
success: false,
message: '请选择要上传的文件'
});
}
try {
// 使用MinIO服务上传文件
const type = req.body.type || 'document';
const result = await minioService.uploadFile(
req.file.buffer,
req.file.originalname,
req.file.mimetype,
type
);
res.json({
success: true,
message: '文件上传成功',
data: result.data
});
} catch (error) {
console.error('文件上传到MinIO失败:', error);
res.status(500).json({
success: false,
message: error.message || '文件上传失败'
});
}
});
});
router.post('/', authenticateToken, (req, res) => {
multiUpload.array('file', 10)(req, res, async (err) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
success: false,
message: '文件大小不能超过 10MB'
});
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({
success: false,
message: '一次最多只能上传10个文件'
});
}
return res.status(400).json({
success: false,
message: '文件上传失败:' + err.message
});
} else if (err) {
return res.status(400).json({
success: false,
message: err.message
});
}
if (!req.files || req.files.length === 0) {
return res.status(400).json({
success: false,
message: '请选择要上传的文件'
});
}
try {
// 使用MinIO服务上传多个文件
const type = req.body.type || 'document';
const files = req.files.map(file => ({
buffer: file.buffer,
originalName: file.originalname,
mimeType: file.mimetype
}));
const result = await minioService.uploadMultipleFiles(files, type);
// 如果只上传了一个文件,返回单文件格式以保持兼容性
if (result.data.files.length === 1) {
result.data.files.forEach(element => {
element.path = '/' + element.path
});
res.json({
success: true,
message: '文件上传成功',
data: {
...result.data.files[0],
urls: result.data.urls // 同时提供urls数组格式
}
});
} else {
// 多文件返回数组格式
res.json({
success: true,
message: `成功上传${result.data.files.length}个文件`,
data: result.data
});
}
} catch (error) {
console.error('文件上传到MinIO失败:', error);
res.status(500).json({
success: false,
message: error.message || '文件上传失败'
});
}
});
});
router.post('/single', auth, (req, res) => {
upload.single('file')(req, res, async (err) => {
if (err instanceof multer.MulterError) {
return res.status(400).json({
success: false,
message: '文件上传失败:' + err.message
});
} else if (err) {
return res.status(400).json({
success: false,
message: err.message
});
}
if (!req.file) {
return res.status(400).json({ success: false, message: '没有上传文件' });
}
try {
// 使用MinIO服务上传文件
const type = req.body.type || 'document';
const result = await minioService.uploadFile(
req.file.buffer,
req.file.originalname,
req.file.mimetype,
type
);
res.json({
success: true,
message: '文件上传成功',
url: result.data.url,
filename: result.data.filename,
originalname: result.data.originalname,
size: result.data.size
});
} catch (error) {
console.error('文件上传到MinIO失败:', error);
res.status(500).json({
success: false,
message: error.message || '文件上传失败'
});
}
});
});
// 错误处理中间件
router.use((error, req, res, next) => {
if (error instanceof multer.MulterError) {
if (error.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ success: false, message: '文件大小不能超过10MB' });
}
if (error.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ success: false, message: '一次最多只能上传10个文件' });
}
}
if (error.message === '只能上传图片或视频文件') {
return res.status(400).json({ success: false, message: error.message });
}
res.status(500).json({ success: false, message: '上传失败' });
});
module.exports = router;

0
routes/user.js Normal file
View File

153
server.js Normal file
View File

@@ -0,0 +1,153 @@
// 加载环境变量配置
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const path = require('path');
const rateLimit = require('express-rate-limit');
const { logger } = require('./config/logger');
const { errorHandler, notFound } = require('./middleware/errorHandler');
const fs = require('fs');
const app = express();
const PORT = process.env.PORT || 3000;
// 确保日志目录存在
const logDir = path.join(__dirname, 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
// 中间件配置
// CORS配置 - 允许前端访问静态资源
app.use(cors({
origin: [
'http://localhost:5173',
'http://localhost:5176',
'http://localhost:5175',
'http://localhost:5174',
'http://localhost:3001',
'https://www.zrbjr.com',
'https://zrbjr.com'
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
}));
app.use(bodyParser.json({ limit: '10mb' }));
app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' }));
// 请求日志中间件
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
// 只记录非正常状态码的请求日志过滤掉200、304等正常返回
if (res.statusCode >= 400 || res.statusCode < 200) {
logger.info('HTTP Request', {
method: req.method,
url: req.originalUrl,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('User-Agent')
});
}
});
next();
});
// 限流中间件
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 1000, // 限制每个IP 15分钟内最多100个请求
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: '请求过于频繁,请稍后再试'
}
}
});
app.use('/', limiter);
// 引入数据库初始化模块
const { initDatabase } = require('./config/database-init');
// API路由
//用户相关
app.use('/auth', require('./routes/auth'));
//获取验证码
app.use('/captcha', require('./routes/captcha'));
//手机验证码
app.use('/sms', require('./routes/sms'));
//文件上传
app.use('/upload', require('./routes/upload'));
// 404处理
app.use(notFound);
// 全局错误处理中间件
app.use(errorHandler);
// 启动服务器
app.listen(PORT, async () => {
try {
logger.info('Server starting', { port: PORT });
console.log(`服务器运行在 http://localhost:${PORT}`);
await initDatabase();
global.captchaStore = new Map();
logger.info('Server started successfully', {
port: PORT,
environment: process.env.NODE_ENV || 'development'
});
} catch (error) {
logger.error('Failed to start server', { error: error.message });
process.exit(1);
}
});
// 优雅关闭
process.on('SIGTERM', async () => {
logger.info('SIGTERM received, shutting down gracefully');
try {
const { closeDB } = require('./database');
await closeDB();
} catch (error) {
logger.error('Error closing database', { error: error.message });
}
process.exit(0);
});
process.on('SIGINT', async () => {
logger.info('SIGINT received, shutting down gracefully');
try {
const { closeDB } = require('./database');
await closeDB();
} catch (error) {
logger.error('Error closing database', { error: error.message });
}
process.exit(0);
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection', { reason, promise });
});
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception', { error: error.message, stack: error.stack });
process.exit(1);
});

306
services/alipayservice.js Normal file
View 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;

293
services/minioService.js Normal file
View 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;

View 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;