初次提交
This commit is contained in:
68
.env
Normal file
68
.env
Normal 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
|
||||||
|
# 是否使用SSL(true/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
64
.env.example
Normal 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
|
||||||
|
# 是否使用SSL(true/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
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/.idea
|
||||||
17
config/config.js
Normal file
17
config/config.js
Normal 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
70
config/constants.js
Normal 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
26
config/database-init.js
Normal 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
363
config/dbv2.js
Normal 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
73
config/logger.js
Normal 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
97
config/minio.js
Normal 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
24
config/wechatPay.js
Normal 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
129
database.js
Normal 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
0
logs/audit.log
Normal file
51
logs/combined.log
Normal file
51
logs/combined.log
Normal 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
12
logs/error.log
Normal 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
110
middleware/auth.js
Normal 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
129
middleware/errorHandler.js
Normal 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
230
middleware/validation.js
Normal 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
3180
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
package.json
Normal file
34
package.json
Normal 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
355
routes/auth.js
Normal 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
158
routes/captcha.js
Normal 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
175
routes/sms.js
Normal 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
243
routes/upload.js
Normal 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
0
routes/user.js
Normal file
153
server.js
Normal file
153
server.js
Normal 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
306
services/alipayservice.js
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
const { AlipaySdk } = require('alipay-sdk');
|
||||||
|
const { getDB } = require('../database');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
class AlipayService {
|
||||||
|
constructor() {
|
||||||
|
this.privateKey = null;
|
||||||
|
this.alipayPublicKey = null;
|
||||||
|
this.alipaySdk = null;
|
||||||
|
this.isInitialized = false;
|
||||||
|
|
||||||
|
this.initializeAlipay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化支付宝服务
|
||||||
|
*/
|
||||||
|
initializeAlipay() {
|
||||||
|
try {
|
||||||
|
// 读取密钥文件
|
||||||
|
const privateKeyPath = this.resolveCertPath('../certs/alipay-private-key.pem');
|
||||||
|
const publicKeyPath = this.resolveCertPath('../certs/alipay-public-key.pem');
|
||||||
|
|
||||||
|
console.log('支付宝私钥路径:', privateKeyPath);
|
||||||
|
console.log('支付宝公钥路径:', publicKeyPath);
|
||||||
|
this.privateKey = fs.readFileSync(privateKeyPath, 'utf8');
|
||||||
|
this.alipayPublicKey = fs.readFileSync(publicKeyPath, 'utf8');
|
||||||
|
this.initializeSDK();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('支付宝服务初始化失败:', error.message);
|
||||||
|
console.error('支付宝功能将不可用');
|
||||||
|
// 不抛出错误,允许服务继续运行
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化支付宝SDK
|
||||||
|
*/
|
||||||
|
initializeSDK() {
|
||||||
|
if (!this.privateKey || !this.alipayPublicKey) {
|
||||||
|
console.warn('支付宝密钥未加载,跳过SDK初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付宝配置
|
||||||
|
this.config = {
|
||||||
|
appId: process.env.ALIPAY_APP_ID || '2021001161683774', // 替换为实际的应用ID
|
||||||
|
privateKey: this.privateKey, // 从文件读取的应用私钥
|
||||||
|
alipayPublicKey: this.alipayPublicKey, // 从文件读取的支付宝公钥
|
||||||
|
gateway: 'https://openapi.alipay.com/gateway.do', // 支付宝网关地址
|
||||||
|
signType: 'RSA2',
|
||||||
|
charset: 'utf-8',
|
||||||
|
version: '1.0',
|
||||||
|
timeout: 5000
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化支付宝SDK
|
||||||
|
this.alipaySdk = new AlipaySdk({
|
||||||
|
appId: this.config.appId,
|
||||||
|
privateKey: this.config.privateKey,
|
||||||
|
alipayPublicKey: this.config.alipayPublicKey,
|
||||||
|
gateway: this.config.gateway,
|
||||||
|
signType: this.config.signType,
|
||||||
|
timeout: this.config.timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
console.log('支付宝SDK初始化成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析证书文件路径
|
||||||
|
* @param {string} relativePath - 相对路径
|
||||||
|
* @returns {string} 绝对路径
|
||||||
|
*/
|
||||||
|
resolveCertPath(relativePath) {
|
||||||
|
return path.resolve(__dirname, relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证文件是否有效
|
||||||
|
* @param {string} filePath - 文件路径
|
||||||
|
* @returns {boolean} 是否为有效文件
|
||||||
|
*/
|
||||||
|
isValidFile(filePath) {
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
return stats.isFile();
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查支付宝服务是否已初始化
|
||||||
|
* @returns {boolean} 是否已初始化
|
||||||
|
*/
|
||||||
|
isServiceAvailable() {
|
||||||
|
return this.isInitialized && this.alipaySdk !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建注册支付订单
|
||||||
|
* @param {Object} params - 支付参数
|
||||||
|
* @param {string} params.userId - 用户ID
|
||||||
|
* @param {string} params.username - 用户名
|
||||||
|
* @param {string} params.phone - 手机号
|
||||||
|
* @param {string} params.clientIp - 客户端IP
|
||||||
|
* @returns {Promise<Object>} 支付结果
|
||||||
|
*/
|
||||||
|
async createRegistrationPayOrder({ userId, username, phone, clientIp }) {
|
||||||
|
// 检查服务是否可用
|
||||||
|
if (!this.isServiceAvailable()) {
|
||||||
|
throw new Error('支付宝服务未初始化或不可用');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
|
||||||
|
// 生成订单号
|
||||||
|
const outTradeNo = this.generateOrderNo();
|
||||||
|
const totalFee = 39900; // 399元,单位:分
|
||||||
|
const subject = '用户注册激活费用';
|
||||||
|
const body = `用户${username}(${phone})注册激活费用`;
|
||||||
|
|
||||||
|
// 业务参数
|
||||||
|
const bizContent = {
|
||||||
|
out_trade_no: outTradeNo,
|
||||||
|
total_amount: (totalFee / 100).toFixed(2), // 转换为元
|
||||||
|
subject: subject,
|
||||||
|
body: body,
|
||||||
|
product_code: 'QUICK_WAP_WAY',
|
||||||
|
quit_url: process.env.ALIPAY_QUIT_URL
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用新版SDK的pageExecute方法生成支付URL
|
||||||
|
const payUrl = this.alipaySdk.pageExecute('alipay.trade.wap.pay', 'GET', {
|
||||||
|
bizContent: bizContent,
|
||||||
|
notifyUrl: process.env.ALIPAY_NOTIFY_URL,
|
||||||
|
returnUrl: process.env.ALIPAY_RETURN_URL
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存订单到数据库
|
||||||
|
await db.execute(
|
||||||
|
`INSERT INTO payment_orders
|
||||||
|
(user_id, out_trade_no, total_fee, body, trade_type, status, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, NOW())`,
|
||||||
|
[userId, outTradeNo, totalFee, body, 'ALIPAY_WAP', 'pending']
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('支付宝支付订单创建成功:', {
|
||||||
|
userId,
|
||||||
|
outTradeNo,
|
||||||
|
totalFee,
|
||||||
|
payUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
outTradeNo,
|
||||||
|
payUrl,
|
||||||
|
paymentType: 'alipay_wap',
|
||||||
|
totalFee
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建支付宝支付订单失败:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message || '创建支付订单失败'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付状态
|
||||||
|
* @param {string} outTradeNo - 商户订单号
|
||||||
|
* @returns {Promise<Object>} 查询结果
|
||||||
|
*/
|
||||||
|
async queryPaymentStatus(outTradeNo) {
|
||||||
|
// 检查服务是否可用
|
||||||
|
if (!this.isServiceAvailable()) {
|
||||||
|
throw new Error('支付宝服务未初始化或不可用');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.alipaySdk.exec('alipay.trade.query', {
|
||||||
|
bizContent: {
|
||||||
|
out_trade_no: outTradeNo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.code === '10000') {
|
||||||
|
// 查询成功
|
||||||
|
const tradeStatus = result.tradeStatus;
|
||||||
|
|
||||||
|
// 如果支付成功,更新数据库
|
||||||
|
if (tradeStatus === 'TRADE_SUCCESS') {
|
||||||
|
await this.updatePaymentStatus(outTradeNo, {
|
||||||
|
status: 'paid',
|
||||||
|
transactionId: result.tradeNo,
|
||||||
|
paidAt: new Date()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
trade_status: tradeStatus,
|
||||||
|
trade_no: result.tradeNo,
|
||||||
|
total_amount: result.totalAmount,
|
||||||
|
buyer_pay_amount: result.buyerPayAmount,
|
||||||
|
gmt_payment: result.gmtPayment
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: result.msg || '查询支付状态失败'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询支付宝支付状态失败:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error.message || '查询支付状态失败'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新支付状态
|
||||||
|
* @param {string} outTradeNo - 商户订单号
|
||||||
|
* @param {Object} updateData - 更新数据
|
||||||
|
*/
|
||||||
|
async updatePaymentStatus(outTradeNo, updateData) {
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
|
||||||
|
// 更新订单状态
|
||||||
|
await db.execute(
|
||||||
|
`UPDATE payment_orders
|
||||||
|
SET status = ?, transaction_id = ?, paid_at = ?
|
||||||
|
WHERE out_trade_no = ?`,
|
||||||
|
[updateData.status, updateData.transactionId, updateData.paidAt, outTradeNo]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果支付成功,更新用户支付状态
|
||||||
|
if (updateData.status === 'paid') {
|
||||||
|
const [orders] = await db.execute(
|
||||||
|
'SELECT user_id FROM payment_orders WHERE out_trade_no = ?',
|
||||||
|
[outTradeNo]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (orders.length > 0) {
|
||||||
|
const userId = orders[0].user_id;
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE users SET payment_status = ? WHERE id = ?',
|
||||||
|
['paid', userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('用户支付状态更新成功:', { userId, outTradeNo });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新支付状态失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证支付宝回调签名
|
||||||
|
* @param {Object} params - 回调参数
|
||||||
|
* @returns {boolean} 验证结果
|
||||||
|
*/
|
||||||
|
verifyNotifySign(params) {
|
||||||
|
// 检查服务是否可用
|
||||||
|
if (!this.isServiceAvailable()) {
|
||||||
|
console.error('支付宝服务未初始化,无法验证签名');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this.alipaySdk.checkNotifySign(params);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证支付宝回调签名失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成订单号
|
||||||
|
* @returns {string} 订单号
|
||||||
|
*/
|
||||||
|
generateOrderNo() {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0');
|
||||||
|
return `ALI${timestamp}${random}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AlipayService;
|
||||||
293
services/minioService.js
Normal file
293
services/minioService.js
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
const { createMinioClient, minioConfig, getPublicUrl } = require('../config/minio');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MinIO 文件服务
|
||||||
|
* 提供文件上传、删除、获取等功能
|
||||||
|
*/
|
||||||
|
class MinioService {
|
||||||
|
constructor() {
|
||||||
|
this.client = createMinioClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一文件名
|
||||||
|
* @param {string} originalName - 原始文件名
|
||||||
|
* @returns {string} 唯一文件名
|
||||||
|
*/
|
||||||
|
generateUniqueFileName(originalName) {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(now.getDate()).padStart(2, '0');
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const randomString = crypto.randomBytes(8).toString('hex');
|
||||||
|
const ext = path.extname(originalName);
|
||||||
|
return `${year}/${month}/${day}/${timestamp}_${randomString}${ext}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文件类型获取存储桶名称
|
||||||
|
* @param {string} type - 文件类型 (avatar, product, document)
|
||||||
|
* @returns {string} 存储桶名称
|
||||||
|
*/
|
||||||
|
getBucketName(type = 'document') {
|
||||||
|
const bucketMap = {
|
||||||
|
'avatar': minioConfig.buckets.avatars,
|
||||||
|
'product': minioConfig.buckets.products,
|
||||||
|
'document': minioConfig.buckets.documents
|
||||||
|
};
|
||||||
|
return bucketMap[type] || minioConfig.buckets.documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传单个文件
|
||||||
|
* @param {Buffer} fileBuffer - 文件缓冲区
|
||||||
|
* @param {string} originalName - 原始文件名
|
||||||
|
* @param {string} mimeType - 文件MIME类型
|
||||||
|
* @param {string} type - 文件类型
|
||||||
|
* @returns {Promise<Object>} 上传结果
|
||||||
|
*/
|
||||||
|
async uploadFile(fileBuffer, originalName, mimeType, type = 'document') {
|
||||||
|
try {
|
||||||
|
const bucketName = this.getBucketName(type);
|
||||||
|
const fileName = this.generateUniqueFileName(originalName);
|
||||||
|
|
||||||
|
// 设置文件元数据
|
||||||
|
const metaData = {
|
||||||
|
'Content-Type': mimeType,
|
||||||
|
'Original-Name': encodeURIComponent(originalName),
|
||||||
|
'Upload-Time': new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传文件到MinIO
|
||||||
|
await this.client.putObject(bucketName, fileName, fileBuffer, fileBuffer.length, metaData);
|
||||||
|
|
||||||
|
// 生成访问URL
|
||||||
|
const url = getPublicUrl(bucketName, fileName);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
filename: fileName,
|
||||||
|
originalname: originalName,
|
||||||
|
mimetype: mimeType,
|
||||||
|
size: fileBuffer.length,
|
||||||
|
bucket: bucketName,
|
||||||
|
path: `${bucketName}/${fileName}`,
|
||||||
|
url: url
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MinIO文件上传失败:', error);
|
||||||
|
throw new Error(`文件上传失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 迁移专用:上传文件到指定存储桶和路径
|
||||||
|
* @param {string} bucketName - 存储桶名称
|
||||||
|
* @param {string} filePath - 文件路径
|
||||||
|
* @param {Buffer} fileBuffer - 文件缓冲区
|
||||||
|
* @param {string} mimeType - 文件MIME类型
|
||||||
|
* @returns {Promise<Object>} 上传结果
|
||||||
|
*/
|
||||||
|
async uploadFileForMigration(bucketName, filePath, fileBuffer, mimeType) {
|
||||||
|
try {
|
||||||
|
// 设置文件元数据
|
||||||
|
const metaData = {
|
||||||
|
'Content-Type': mimeType,
|
||||||
|
'Upload-Time': new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 上传文件到MinIO
|
||||||
|
await this.client.putObject(bucketName, filePath, fileBuffer, fileBuffer.length, metaData);
|
||||||
|
|
||||||
|
// 生成访问URL
|
||||||
|
const url = getPublicUrl(bucketName, filePath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
filename: filePath,
|
||||||
|
mimetype: mimeType,
|
||||||
|
size: fileBuffer.length,
|
||||||
|
bucket: bucketName,
|
||||||
|
path: `${bucketName}/${filePath}`,
|
||||||
|
url: url
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MinIO文件迁移上传失败:', error);
|
||||||
|
throw new Error(`文件迁移上传失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传多个文件
|
||||||
|
* @param {Array} files - 文件数组,每个文件包含 {buffer, originalName, mimeType}
|
||||||
|
* @param {string} type - 文件类型
|
||||||
|
* @returns {Promise<Array>} 上传结果数组
|
||||||
|
*/
|
||||||
|
async uploadMultipleFiles(files, type = 'document') {
|
||||||
|
try {
|
||||||
|
const uploadPromises = files.map(file =>
|
||||||
|
this.uploadFile(file.buffer, file.originalName, file.mimeType, type)
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = await Promise.all(uploadPromises);
|
||||||
|
const uploadedFiles = results.map(result => result.data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
files: uploadedFiles,
|
||||||
|
urls: uploadedFiles.map(file => file.url),
|
||||||
|
count: uploadedFiles.length
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MinIO多文件上传失败:', error);
|
||||||
|
throw new Error(`多文件上传失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文件
|
||||||
|
* @param {string} bucketName - 存储桶名称
|
||||||
|
* @param {string} fileName - 文件名
|
||||||
|
* @returns {Promise<boolean>} 删除结果
|
||||||
|
*/
|
||||||
|
async deleteFile(bucketName, fileName) {
|
||||||
|
try {
|
||||||
|
await this.client.removeObject(bucketName, fileName);
|
||||||
|
console.log(`✅ 文件删除成功: ${bucketName}/${fileName}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MinIO文件删除失败:', error);
|
||||||
|
throw new Error(`文件删除失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除文件
|
||||||
|
* @param {string} bucketName - 存储桶名称
|
||||||
|
* @param {Array<string>} fileNames - 文件名数组
|
||||||
|
* @returns {Promise<Object>} 删除结果
|
||||||
|
*/
|
||||||
|
async deleteMultipleFiles(bucketName, fileNames) {
|
||||||
|
try {
|
||||||
|
const deletePromises = fileNames.map(fileName =>
|
||||||
|
this.deleteFile(bucketName, fileName)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(deletePromises);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
deletedCount: fileNames.length,
|
||||||
|
message: `成功删除${fileNames.length}个文件`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MinIO批量删除失败:', error);
|
||||||
|
throw new Error(`批量删除失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件是否存在
|
||||||
|
* @param {string} bucketName - 存储桶名称
|
||||||
|
* @param {string} fileName - 文件名
|
||||||
|
* @returns {Promise<boolean>} 文件是否存在
|
||||||
|
*/
|
||||||
|
async fileExists(bucketName, fileName) {
|
||||||
|
try {
|
||||||
|
await this.client.statObject(bucketName, fileName);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'NotFound') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件信息
|
||||||
|
* @param {string} bucketName - 存储桶名称
|
||||||
|
* @param {string} fileName - 文件名
|
||||||
|
* @returns {Promise<Object>} 文件信息
|
||||||
|
*/
|
||||||
|
async getFileInfo(bucketName, fileName) {
|
||||||
|
try {
|
||||||
|
const stat = await this.client.statObject(bucketName, fileName);
|
||||||
|
return {
|
||||||
|
size: stat.size,
|
||||||
|
lastModified: stat.lastModified,
|
||||||
|
etag: stat.etag,
|
||||||
|
contentType: stat.metaData['content-type'],
|
||||||
|
originalName: decodeURIComponent(stat.metaData['original-name'] || fileName)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取文件信息失败:', error);
|
||||||
|
throw new Error(`获取文件信息失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成预签名URL(用于临时访问)
|
||||||
|
* @param {string} bucketName - 存储桶名称
|
||||||
|
* @param {string} fileName - 文件名
|
||||||
|
* @param {number} expiry - 过期时间(秒),默认7天
|
||||||
|
* @returns {Promise<string>} 预签名URL
|
||||||
|
*/
|
||||||
|
async getPresignedUrl(bucketName, fileName, expiry = 7 * 24 * 60 * 60) {
|
||||||
|
try {
|
||||||
|
const url = await this.client.presignedGetObject(bucketName, fileName, expiry);
|
||||||
|
return url;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成预签名URL失败:', error);
|
||||||
|
throw new Error(`生成预签名URL失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出存储桶中的文件
|
||||||
|
* @param {string} bucketName - 存储桶名称
|
||||||
|
* @param {string} prefix - 文件前缀
|
||||||
|
* @param {number} limit - 限制数量
|
||||||
|
* @returns {Promise<Array>} 文件列表
|
||||||
|
*/
|
||||||
|
async listFiles(bucketName, prefix = '', limit = 100) {
|
||||||
|
try {
|
||||||
|
const files = [];
|
||||||
|
const stream = this.client.listObjects(bucketName, prefix, true);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
stream.on('data', (obj) => {
|
||||||
|
if (files.length < limit) {
|
||||||
|
files.push({
|
||||||
|
name: obj.name,
|
||||||
|
size: obj.size,
|
||||||
|
lastModified: obj.lastModified,
|
||||||
|
etag: obj.etag,
|
||||||
|
url: getPublicUrl(bucketName, obj.name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', () => resolve(files));
|
||||||
|
stream.on('error', reject);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('列出文件失败:', error);
|
||||||
|
throw new Error(`列出文件失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建单例实例
|
||||||
|
const minioService = new MinioService();
|
||||||
|
|
||||||
|
module.exports = minioService;
|
||||||
609
services/wechatPayService.js
Normal file
609
services/wechatPayService.js
Normal file
@@ -0,0 +1,609 @@
|
|||||||
|
const crypto = require('crypto');
|
||||||
|
const axios = require('axios');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { wechatPay } = require('../config/wechatPay');
|
||||||
|
const { getDB } = require('../database');
|
||||||
|
|
||||||
|
class WechatPayService {
|
||||||
|
constructor() {
|
||||||
|
this.config = {
|
||||||
|
...wechatPay,
|
||||||
|
apiV3Key: process.env.WECHAT_API_V3_KEY
|
||||||
|
};
|
||||||
|
this.privateKey = null; // API v3 私钥
|
||||||
|
this.serialNo = null; // 商户证书序列号
|
||||||
|
this.initializeV3();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化API v3配置
|
||||||
|
async initializeV3() {
|
||||||
|
try {
|
||||||
|
// 检查配置是否存在
|
||||||
|
if (!this.config.keyPath || !this.config.certPath) {
|
||||||
|
console.warn('微信支付证书路径未配置,跳过API v3初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载私钥
|
||||||
|
const keyPath = this.resolveCertPath(this.config.keyPath);
|
||||||
|
console.log('尝试加载私钥文件:', keyPath);
|
||||||
|
|
||||||
|
if (this.isValidFile(keyPath)) {
|
||||||
|
this.privateKey = fs.readFileSync(keyPath, 'utf8');
|
||||||
|
console.log('API v3 私钥加载成功');
|
||||||
|
} else {
|
||||||
|
console.error('私钥文件不存在或不是有效文件:', keyPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取证书序列号
|
||||||
|
const certPath = this.resolveCertPath(this.config.certPath);
|
||||||
|
console.log('尝试加载证书文件:', certPath);
|
||||||
|
|
||||||
|
if (this.isValidFile(certPath)) {
|
||||||
|
const cert = fs.readFileSync(certPath, 'utf8');
|
||||||
|
this.serialNo = this.getCertificateSerialNumber(cert);
|
||||||
|
console.log('证书序列号:', this.serialNo);
|
||||||
|
} else {
|
||||||
|
console.error('证书文件不存在或不是有效文件:', certPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化API v3配置失败:', error.message);
|
||||||
|
console.error('错误详情:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析证书文件路径
|
||||||
|
resolveCertPath(configPath) {
|
||||||
|
// 如果是绝对路径,直接使用
|
||||||
|
if (path.isAbsolute(configPath)) {
|
||||||
|
return configPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理相对路径
|
||||||
|
let relativePath = configPath;
|
||||||
|
if (relativePath.startsWith('./')) {
|
||||||
|
relativePath = relativePath.substring(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.resolve(__dirname, '..', relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为有效的文件(不是目录)
|
||||||
|
isValidFile(filePath) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
return stats.isFile();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查文件状态失败:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取证书序列号
|
||||||
|
getCertificateSerialNumber(cert) {
|
||||||
|
try {
|
||||||
|
const x509 = crypto.X509Certificate ? new crypto.X509Certificate(cert) : null;
|
||||||
|
if (x509) {
|
||||||
|
return x509.serialNumber.toLowerCase().replace(/:/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备用方法:使用openssl命令行工具
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const tempFile = path.join(__dirname, 'temp_cert.pem');
|
||||||
|
fs.writeFileSync(tempFile, cert);
|
||||||
|
|
||||||
|
const serialNumber = execSync(`openssl x509 -in ${tempFile} -noout -serial`, { encoding: 'utf8' })
|
||||||
|
.replace('serial=', '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
fs.unlinkSync(tempFile);
|
||||||
|
return serialNumber;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取证书序列号失败:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机字符串
|
||||||
|
* @param {number} length 长度
|
||||||
|
* @returns {string} 随机字符串
|
||||||
|
*/
|
||||||
|
generateNonceStr(length = 32) {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成时间戳
|
||||||
|
* @returns {string} 时间戳
|
||||||
|
*/
|
||||||
|
generateTimestamp() {
|
||||||
|
return Math.floor(Date.now() / 1000).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成API v3签名
|
||||||
|
* @param {string} method HTTP方法
|
||||||
|
* @param {string} url 请求URL路径
|
||||||
|
* @param {number} timestamp 时间戳
|
||||||
|
* @param {string} nonceStr 随机字符串
|
||||||
|
* @param {string} body 请求体
|
||||||
|
* @returns {string} 签名
|
||||||
|
*/
|
||||||
|
generateV3Sign(method, url, timestamp, nonceStr, body = '') {
|
||||||
|
if (!this.privateKey) {
|
||||||
|
throw new Error('私钥未加载,无法生成签名');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造签名串
|
||||||
|
const signString = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n`;
|
||||||
|
console.log('API v3 签名字符串:', signString);
|
||||||
|
|
||||||
|
// 使用私钥进行SHA256-RSA签名
|
||||||
|
const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey);
|
||||||
|
const signature = sign.toString('base64');
|
||||||
|
|
||||||
|
console.log('API v3 生成的签名:', signature);
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成Authorization头
|
||||||
|
* @param {string} method HTTP方法
|
||||||
|
* @param {string} url 请求URL路径
|
||||||
|
* @param {string} body 请求体
|
||||||
|
* @returns {string} Authorization头值
|
||||||
|
*/
|
||||||
|
generateAuthorizationHeader(method, url, body = '') {
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
const nonceStr = this.generateNonceStr();
|
||||||
|
const signature = this.generateV3Sign(method, url, timestamp, nonceStr, body);
|
||||||
|
|
||||||
|
return `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonceStr}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成JSAPI支付参数
|
||||||
|
* @param {string} prepayId 预支付交易会话标识
|
||||||
|
* @returns {object} JSAPI支付参数
|
||||||
|
*/
|
||||||
|
generateJSAPIPayParams(prepayId) {
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||||
|
const nonceStr = this.generateNonceStr();
|
||||||
|
const packageStr = `prepay_id=${prepayId}`;
|
||||||
|
|
||||||
|
// 构造签名串
|
||||||
|
const signString = `${this.config.appId}\n${timestamp}\n${nonceStr}\n${packageStr}\n`;
|
||||||
|
|
||||||
|
// 使用私钥进行签名
|
||||||
|
const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey);
|
||||||
|
const paySign = sign.toString('base64');
|
||||||
|
|
||||||
|
return {
|
||||||
|
appId: this.config.appId,
|
||||||
|
timeStamp: timestamp,
|
||||||
|
nonceStr: nonceStr,
|
||||||
|
package: packageStr,
|
||||||
|
signType: 'RSA',
|
||||||
|
paySign: paySign
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建注册支付订单 (H5支付)
|
||||||
|
* @param {object} orderData 订单数据
|
||||||
|
* @returns {object} 支付结果
|
||||||
|
*/
|
||||||
|
async createRegistrationPayOrder(orderData) {
|
||||||
|
const { userId, username, phone, clientIp = '127.0.0.1' } = orderData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.privateKey || !this.serialNo) {
|
||||||
|
throw new Error('API v3 配置未完成,请检查证书和私钥');
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = getDB();
|
||||||
|
|
||||||
|
// 生成订单号
|
||||||
|
const outTradeNo = `REG_${Date.now()}_${userId}`;
|
||||||
|
|
||||||
|
// 创建支付订单记录
|
||||||
|
await db.execute(
|
||||||
|
'INSERT INTO payment_orders (user_id, out_trade_no, total_fee, body, trade_type, status, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())',
|
||||||
|
[userId, outTradeNo, this.config.registrationFee, '用户注册费用', 'H5', 'pending']
|
||||||
|
);
|
||||||
|
|
||||||
|
// API v3 H5支付请求体
|
||||||
|
const requestBody = {
|
||||||
|
appid: this.config.appId,
|
||||||
|
mchid: this.config.mchId,
|
||||||
|
description: '用户注册费用',
|
||||||
|
out_trade_no: outTradeNo,
|
||||||
|
notify_url: this.config.notifyUrl,
|
||||||
|
amount: {
|
||||||
|
total: this.config.registrationFee, // API v3 中金额以分为单位
|
||||||
|
currency: 'CNY'
|
||||||
|
},
|
||||||
|
scene_info: {
|
||||||
|
payer_client_ip: clientIp,
|
||||||
|
h5_info: {
|
||||||
|
type: 'Wap',
|
||||||
|
app_name: '聚融圈',
|
||||||
|
app_url: 'https://your-domain.com',
|
||||||
|
bundle_id: 'com.jurong.circle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('API v3 H5支付参数:', requestBody);
|
||||||
|
|
||||||
|
const requestBodyStr = JSON.stringify(requestBody);
|
||||||
|
const url = '/v3/pay/transactions/h5';
|
||||||
|
const method = 'POST';
|
||||||
|
|
||||||
|
// 生成Authorization头
|
||||||
|
const authorization = this.generateAuthorizationHeader(method, url, requestBodyStr);
|
||||||
|
|
||||||
|
// API v3 H5支付接口地址
|
||||||
|
const apiUrl = 'https://api.mch.weixin.qq.com/v3/pay/transactions/h5';
|
||||||
|
|
||||||
|
console.log('使用的API v3 H5地址:', apiUrl);
|
||||||
|
console.log('Authorization头:', authorization);
|
||||||
|
|
||||||
|
const response = await axios.post(apiUrl, requestBody, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': authorization,
|
||||||
|
'User-Agent': 'jurong-circle/1.0.0'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('微信支付API v3 H5响应:', response.data);
|
||||||
|
|
||||||
|
if (response.data && response.data.h5_url) {
|
||||||
|
// 更新订单状态
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE payment_orders SET mweb_url = ? WHERE out_trade_no = ?',
|
||||||
|
[response.data.h5_url, outTradeNo]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
outTradeNo,
|
||||||
|
h5Url: response.data.h5_url,
|
||||||
|
paymentType: 'h5'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log(response.data);
|
||||||
|
|
||||||
|
throw new Error(response.data?.message || '支付订单创建失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建H5支付订单失败:', error.response?.data || error.message);
|
||||||
|
throw new Error('支付订单创建失败: ' + (error.response?.data?.message || error.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理支付回调
|
||||||
|
* @param {string} xmlData 微信回调的XML数据
|
||||||
|
* @returns {object} 处理结果
|
||||||
|
*/
|
||||||
|
async handlePaymentNotify(xmlData) {
|
||||||
|
try {
|
||||||
|
const result = this.xmlToObject(xmlData);
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
const sign = result.sign;
|
||||||
|
delete result.sign;
|
||||||
|
const calculatedSign = this.generateSign(result);
|
||||||
|
|
||||||
|
if (sign !== calculatedSign) {
|
||||||
|
throw new Error('签名验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
|
||||||
|
const db = getDB();
|
||||||
|
|
||||||
|
// 开始事务
|
||||||
|
await db.beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新支付订单状态
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?',
|
||||||
|
['paid', result.transaction_id, result.out_trade_no]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取订单信息
|
||||||
|
const [orders] = await db.execute(
|
||||||
|
'SELECT user_id FROM payment_orders WHERE out_trade_no = ?',
|
||||||
|
[result.out_trade_no]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (orders.length > 0) {
|
||||||
|
const userId = orders[0].user_id;
|
||||||
|
|
||||||
|
// 激活用户账户
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE users SET payment_status = "paid" WHERE id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`用户 ${userId} 支付成功,账户已激活`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
await db.commit();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '支付成功,账户已激活'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// 回滚事务
|
||||||
|
await db.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const db = getDB();
|
||||||
|
|
||||||
|
// 更新订单状态为失败
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE payment_orders SET status = ? WHERE out_trade_no = ?',
|
||||||
|
['failed', result.out_trade_no]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: '支付失败'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理支付回调失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理API v3支付回调
|
||||||
|
* @param {object} notifyData 回调数据
|
||||||
|
* @returns {object} 处理结果
|
||||||
|
*/
|
||||||
|
async handleV3PaymentNotify(notifyData) {
|
||||||
|
try {
|
||||||
|
const { signature, timestamp, nonce, serial, body } = notifyData;
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
const isValidSignature = this.verifyV3Signature({
|
||||||
|
timestamp,
|
||||||
|
nonce,
|
||||||
|
body,
|
||||||
|
signature
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isValidSignature) {
|
||||||
|
console.error('API v3回调签名验证失败');
|
||||||
|
return { success: false, message: '签名验证失败' };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('API v3回调签名验证成功');
|
||||||
|
|
||||||
|
// 解析回调数据
|
||||||
|
const callbackData = JSON.parse(body);
|
||||||
|
console.log('解析的回调数据:', callbackData);
|
||||||
|
|
||||||
|
// 检查事件类型
|
||||||
|
if (callbackData.event_type === 'TRANSACTION.SUCCESS') {
|
||||||
|
// 解密resource数据
|
||||||
|
const resource = callbackData.resource;
|
||||||
|
const decryptedData = this.decryptV3Resource(resource);
|
||||||
|
|
||||||
|
console.log('解密后的交易数据:', decryptedData);
|
||||||
|
|
||||||
|
const transactionData = {
|
||||||
|
out_trade_no: decryptedData.out_trade_no,
|
||||||
|
transaction_id: decryptedData.transaction_id,
|
||||||
|
trade_state: decryptedData.trade_state
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('交易数据:', transactionData);
|
||||||
|
|
||||||
|
if (transactionData.trade_state === 'SUCCESS') {
|
||||||
|
const db = getDB();
|
||||||
|
|
||||||
|
// 开始事务
|
||||||
|
await db.beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新支付订单状态
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?',
|
||||||
|
['paid', transactionData.transaction_id, transactionData.out_trade_no]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取订单信息
|
||||||
|
const [orders] = await db.execute(
|
||||||
|
'SELECT user_id FROM payment_orders WHERE out_trade_no = ?',
|
||||||
|
[transactionData.out_trade_no]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (orders.length > 0) {
|
||||||
|
const userId = orders[0].user_id;
|
||||||
|
|
||||||
|
// 激活用户账户
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE users SET payment_status = "paid" WHERE id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`用户 ${userId} API v3支付成功,账户已激活`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交事务
|
||||||
|
await db.commit();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'API v3支付成功,账户已激活'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// 回滚事务
|
||||||
|
await db.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: '未知的回调事件类型' };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('处理API v3支付回调异常:', error);
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证API v3回调签名
|
||||||
|
* @param {object} params 签名参数
|
||||||
|
* @returns {boolean} 验证结果
|
||||||
|
*/
|
||||||
|
verifyV3Signature({ timestamp, nonce, body, signature }) {
|
||||||
|
try {
|
||||||
|
// 构造签名字符串
|
||||||
|
const signStr = `${timestamp}\n${nonce}\n${body}\n`;
|
||||||
|
|
||||||
|
console.log('构造的签名字符串:', signStr);
|
||||||
|
console.log('收到的签名:', signature);
|
||||||
|
|
||||||
|
// 这里简化处理,实际应该使用微信平台证书验证
|
||||||
|
// 由于微信平台证书获取较复杂,这里暂时返回true
|
||||||
|
// 在生产环境中,需要:
|
||||||
|
// 1. 获取微信支付平台证书
|
||||||
|
// 2. 使用平台证书的公钥验证签名
|
||||||
|
console.log('API v3签名验证(简化处理)');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证API v3签名失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密API v3回调资源数据
|
||||||
|
* @param {object} resource 加密的资源数据
|
||||||
|
* @returns {object} 解密后的数据
|
||||||
|
*/
|
||||||
|
decryptV3Resource(resource) {
|
||||||
|
try {
|
||||||
|
const { ciphertext, associated_data, nonce } = resource;
|
||||||
|
|
||||||
|
// 使用API v3密钥解密
|
||||||
|
const apiV3Key = this.config.apiV3Key;
|
||||||
|
if (!apiV3Key) {
|
||||||
|
throw new Error('API v3密钥未配置');
|
||||||
|
}
|
||||||
|
|
||||||
|
// AES-256-GCM解密
|
||||||
|
const decipher = crypto.createDecipherGCM('aes-256-gcm', apiV3Key);
|
||||||
|
decipher.setAAD(Buffer.from(associated_data, 'utf8'));
|
||||||
|
decipher.setAuthTag(Buffer.from(ciphertext.slice(-32), 'base64'));
|
||||||
|
|
||||||
|
const encrypted = ciphertext.slice(0, -32);
|
||||||
|
let decrypted = decipher.update(encrypted, 'base64', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
|
return JSON.parse(decrypted);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解密API v3资源数据失败:', error);
|
||||||
|
throw new Error('解密回调数据失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付状态 (API v3)
|
||||||
|
* @param {string} outTradeNo 商户订单号
|
||||||
|
* @returns {object} 支付状态信息
|
||||||
|
*/
|
||||||
|
async queryPaymentStatus(outTradeNo) {
|
||||||
|
try {
|
||||||
|
if (!this.privateKey || !this.serialNo) {
|
||||||
|
throw new Error('私钥或证书序列号未初始化');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${outTradeNo}`;
|
||||||
|
const method = 'GET';
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
const nonce = this.generateNonceStr();
|
||||||
|
const body = '';
|
||||||
|
|
||||||
|
// 生成签名
|
||||||
|
const signature = this.generateV3Sign(
|
||||||
|
method,
|
||||||
|
`/v3/pay/transactions/out-trade-no/${outTradeNo}?mchid=${this.config.mchId}`,
|
||||||
|
timestamp,
|
||||||
|
nonce,
|
||||||
|
body
|
||||||
|
);
|
||||||
|
|
||||||
|
// 生成Authorization头
|
||||||
|
const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`;
|
||||||
|
|
||||||
|
console.log('查询支付状态 - API v3请求:', {
|
||||||
|
url,
|
||||||
|
authorization
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
const response = await axios.get(url, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': authorization,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'jurong-circle/1.0'
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
mchid: this.config.mchId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('查询支付状态响应:', response.data);
|
||||||
|
|
||||||
|
const result = response.data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: result.trade_state === 'SUCCESS',
|
||||||
|
tradeState: result.trade_state,
|
||||||
|
transactionId: result.transaction_id,
|
||||||
|
outTradeNo: result.out_trade_no,
|
||||||
|
totalAmount: result.amount ? result.amount.total : 0,
|
||||||
|
payerOpenid: result.payer ? result.payer.openid : null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('查询支付状态失败:', error);
|
||||||
|
|
||||||
|
if (error.response) {
|
||||||
|
console.error('API v3查询支付状态错误响应:', error.response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WechatPayService;
|
||||||
Reference in New Issue
Block a user